rs2322tcp/internal/client/runtime_integration_test.go
2026-08-11 15:19:36 +02:00

281 lines
6.4 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/client/runtime_integration_test.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Integrationstest für die clientseitige Runtime mit einem echten
* rs2322tcp-Server und einem virtuellen seriellen Client-Port.
*
* Der Test verwendet die vorhandene virtuelle serielle Test-Schnittstelle
* aus integration_test.go und prüft den vollständigen Datenweg:
*
* /dev/ttyUSB100
* ↕
* Bridge
* ↕
* TCP Data Connection
* ↕
* rs2322tcp Server
* ↕
* serielle PTY
*
* Beide Übertragungsrichtungen werden geprüft.
* ============================================================================
*/
package client_test
import (
"net"
"os"
"path/filepath"
"testing"
"time"
"git.lang-dieter.de/rs2322tcp/internal/client"
"git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/server"
)
///////////////////////////////////////////////////////////////////////////////
// Tests
///////////////////////////////////////////////////////////////////////////////
func TestClientRuntimeSerialIntegration(t *testing.T) {
serialA, serialB, cleanup := startVirtualSerialPair(t)
defer cleanup()
const virtualPortPath = "/dev/ttyUSB100"
home, err := os.UserHomeDir()
if err != nil {
t.Fatalf("get user home directory: %v", err)
}
internalPortPath := filepath.Join(
home,
".rs2322tcp",
"virtual",
"ttyUSB100",
)
////////////////////////////////////////////////////////////////////////////
// Server
////////////////////////////////////////////////////////////////////////////
controlProbe, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("find free control port: %v", err)
}
controlPort := controlProbe.Addr().(*net.TCPAddr).Port
if err := controlProbe.Close(); err != nil {
t.Fatalf("close control probe: %v", err)
}
serverConfig := &config.ServerConfig{
Listen: config.ListenConfig{
Address: "127.0.0.1",
Port: controlPort,
},
HardwareErrorResponse: testHardwareErrorResponse,
Devices: []config.DeviceConfig{
{
ID: "radio",
Name: "Funkgerät",
SerialPort: serialA,
BaudRate: 9600,
DataBits: 8,
Parity: "none",
StopBits: 1,
},
},
}
srv, err := server.NewControlServer(serverConfig)
if err != nil {
t.Fatalf("NewControlServer() failed: %v", err)
}
if err := srv.Listen(); err != nil {
t.Fatalf("server Listen() failed: %v", err)
}
defer srv.Close()
go func() {
_ = srv.Serve()
}()
////////////////////////////////////////////////////////////////////////////
// Client and Runtime
////////////////////////////////////////////////////////////////////////////
c, err := client.New(srv.Addr().String())
if err != nil {
t.Fatalf("client.New() failed: %v", err)
}
defer c.Close()
manager := client.NewVirtualPortManager(
config.VirtualPortRangeConfig{
First: 100,
Last: 199,
},
)
runtime, err := client.NewRuntime(c, manager)
if err != nil {
t.Fatalf("NewRuntime() failed: %v", err)
}
defer runtime.Close()
clientConfig := config.ClientConfig{
Server: config.ServerConnectionConfig{
Address: "127.0.0.1",
Port: controlPort,
},
VirtualPorts: []config.VirtualPortConfig{
{
Port: virtualPortPath,
RemoteDevice: "radio",
},
},
}
////////////////////////////////////////////////////////////////////////////
// Start Runtime
////////////////////////////////////////////////////////////////////////////
runtimeDone := make(chan error, 1)
go func() {
runtimeDone <- runtime.Run(clientConfig)
}()
////////////////////////////////////////////////////////////////////////////
// Open client-side virtual serial endpoint
////////////////////////////////////////////////////////////////////////////
var virtualSerial *os.File
deadline := time.Now().Add(2 * time.Second)
for {
virtualSerial, err = os.OpenFile(
internalPortPath,
os.O_RDWR,
0,
)
if err == nil {
break
}
if time.Now().After(deadline) {
t.Fatalf(
"timeout opening virtual serial port %s: %v",
internalPortPath,
err,
)
}
time.Sleep(10 * time.Millisecond)
}
defer virtualSerial.Close()
////////////////////////////////////////////////////////////////////////////
// Client virtual serial -> Bridge -> Server -> serial
////////////////////////////////////////////////////////////////////////////
clientMessage := []byte("hello from runtime")
if _, err := virtualSerial.Write(clientMessage); err != nil {
t.Fatalf(
"write to virtual serial port failed: %v",
err,
)
}
serialReceived := make([]byte, len(clientMessage))
serialPeer, err := os.OpenFile(
serialB,
os.O_RDWR,
0,
)
if err != nil {
t.Fatalf(
"open server serial peer: %v",
err,
)
}
defer serialPeer.Close()
readExactWithTimeout(
t,
serialPeer,
serialReceived,
2*time.Second,
)
if string(serialReceived) != string(clientMessage) {
t.Fatalf(
"serial received %q, want %q",
string(serialReceived),
string(clientMessage),
)
}
////////////////////////////////////////////////////////////////////////////
// Server serial -> Bridge -> Client virtual serial
////////////////////////////////////////////////////////////////////////////
serverMessage := []byte("hello from server")
if _, err := serialPeer.Write(serverMessage); err != nil {
t.Fatalf(
"write to server serial peer failed: %v",
err,
)
}
clientReceived := make([]byte, len(serverMessage))
readExactWithTimeout(
t,
virtualSerial,
clientReceived,
2*time.Second,
)
if string(clientReceived) != string(serverMessage) {
t.Fatalf(
"virtual serial received %q, want %q",
string(clientReceived),
string(serverMessage),
)
}
///////////////////////////////////////////////////////////////////////////////
// Terminate Runtime
///////////////////////////////////////////////////////////////////////////////
if err := virtualSerial.Close(); err != nil {
t.Fatalf("close virtual serial port: %v", err)
}
if err := runtime.Close(); err != nil {
t.Fatalf("close runtime: %v", err)
}
select {
case <-runtimeDone:
// Runtime termination is expected after Runtime.Close().
case <-time.After(2 * time.Second):
t.Fatal("runtime did not terminate after Runtime.Close()")
}
}