/* * ============================================================================ * 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()") } }