rs2322tcp/internal/client/integration_test.go
2026-08-10 11:15:46 +02:00

308 lines
6.6 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: integration_test.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Integrationstest für die Client-/Server-Kommunikation einschließlich
* serieller Datenübertragung über virtuelle serielle Schnittstellen.
* ============================================================================
*/
package client_test
import (
"fmt"
"io"
"net"
"os"
"os/exec"
"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"
)
func startVirtualSerialPair(t *testing.T) (string, string, func()) {
t.Helper()
if _, err := exec.LookPath("socat"); err != nil {
t.Skip("socat not installed")
}
dir := t.TempDir()
portA := filepath.Join(dir, "ttyA")
portB := filepath.Join(dir, "ttyB")
cmd := exec.Command(
"socat",
"-d",
"-d",
fmt.Sprintf("pty,raw,echo=0,link=%s", portA),
fmt.Sprintf("pty,raw,echo=0,link=%s", portB),
)
if err := cmd.Start(); err != nil {
t.Fatalf("start socat: %v", err)
}
cleanup := func() {
if cmd.Process != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}
}
deadline := time.Now().Add(2 * time.Second)
for {
_, errA := os.Stat(portA)
_, errB := os.Stat(portB)
if errA == nil && errB == nil {
return portA, portB, cleanup
}
if time.Now().After(deadline) {
cleanup()
t.Fatal("timeout waiting for virtual serial ports")
}
time.Sleep(10 * time.Millisecond)
}
}
func readExactWithTimeout(
t *testing.T,
reader io.Reader,
buffer []byte,
timeout time.Duration,
) {
t.Helper()
done := make(chan error, 1)
go func() {
_, err := io.ReadFull(reader, buffer)
done <- err
}()
select {
case err := <-done:
if err != nil {
t.Fatalf("read failed: %v", err)
}
case <-time.After(timeout):
t.Fatalf("read timeout after %s", timeout)
}
}
func TestClientServerSerialIntegration(t *testing.T) {
serialA, serialB, cleanup := startVirtualSerialPair(t)
defer cleanup()
////////////////////////////////////////////////////////////////////////////
// Server
////////////////////////////////////////////////////////////////////////////
controlProbe, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("find free control port: %v", err)
}
controlAddr := controlProbe.Addr().(*net.TCPAddr)
controlPort := controlAddr.Port
if err := controlProbe.Close(); err != nil {
t.Fatalf("close control probe: %v", err)
}
cfg := &config.ServerConfig{
Listen: config.ListenConfig{
Address: "127.0.0.1",
Port: controlPort,
},
Devices: []config.DeviceConfig{
{
ID: "radio",
Name: "Funkgerät",
SerialPort: serialA,
BaudRate: 9600,
DataBits: 8,
Parity: "none",
StopBits: 1,
},
},
}
srv, err := server.NewControlServer(cfg)
if err != nil {
t.Fatalf("NewControlServer() failed: %v", err)
}
////////////////////////////////////////////////////////////////////////////
// Control server
////////////////////////////////////////////////////////////////////////////
if err := srv.Listen(); err != nil {
t.Fatalf("server Listen() failed: %v", err)
}
serveDone := make(chan error, 1)
go func() {
serveDone <- srv.Serve()
}()
address := srv.Addr().String()
////////////////////////////////////////////////////////////////////////////
// Client
////////////////////////////////////////////////////////////////////////////
c, err := client.New(address)
if err != nil {
t.Fatalf("client.New() failed: %v", err)
}
defer c.Close()
devices, err := c.GetDevices()
if err != nil {
t.Fatalf("GetDevices() failed: %v", err)
}
if len(devices) != 1 {
t.Fatalf(
"len(devices) = %d, want 1",
len(devices),
)
}
device := devices[0]
if device.ID != "radio" {
t.Fatalf(
"device ID = %q, want %q",
device.ID,
"radio",
)
}
if device.DataPort == 0 {
t.Fatal("server returned data port 0")
}
////////////////////////////////////////////////////////////////////////////
// Open data connection
////////////////////////////////////////////////////////////////////////////
dataConn, err := c.OpenDataConnection(device)
if err != nil {
t.Fatalf("OpenDataConnection() failed: %v", err)
}
defer dataConn.Close()
////////////////////////////////////////////////////////////////////////////
// Open serial peer
////////////////////////////////////////////////////////////////////////////
serialPeer, err := os.OpenFile(
serialB,
os.O_RDWR,
0,
)
if err != nil {
t.Fatalf("open serial peer: %v", err)
}
defer serialPeer.Close()
////////////////////////////////////////////////////////////////////////////
// Client -> TCP -> Server -> Serial
////////////////////////////////////////////////////////////////////////////
clientMessage := []byte("hello from client")
if _, err := dataConn.Write(clientMessage); err != nil {
t.Fatalf("client data Write() failed: %v", err)
}
serialReceived := make([]byte, len(clientMessage))
readExactWithTimeout(
t,
serialPeer,
serialReceived,
2*time.Second,
)
if string(serialReceived) != string(clientMessage) {
t.Fatalf(
"serial received %q, want %q",
string(serialReceived),
string(clientMessage),
)
}
////////////////////////////////////////////////////////////////////////////
// Serial -> Server -> TCP -> Client
////////////////////////////////////////////////////////////////////////////
serialMessage := []byte("hello from serial")
if _, err := serialPeer.Write(serialMessage); err != nil {
t.Fatalf("serial Write() failed: %v", err)
}
clientReceived := make([]byte, len(serialMessage))
readExactWithTimeout(
t,
dataConn,
clientReceived,
2*time.Second,
)
if string(clientReceived) != string(serialMessage) {
t.Fatalf(
"client received %q, want %q",
string(clientReceived),
string(serialMessage),
)
}
////////////////////////////////////////////////////////////////////////////
// Cleanup
////////////////////////////////////////////////////////////////////////////
if err := dataConn.Close(); err != nil {
t.Fatalf("close data connection: %v", err)
}
if err := c.Close(); err != nil {
t.Fatalf("close client: %v", err)
}
// Give the server goroutines a moment to perform their cleanup.
time.Sleep(20 * time.Millisecond)
if err := srv.Close(); err != nil {
t.Fatalf("close server: %v", err)
}
select {
case err := <-serveDone:
if err != nil {
t.Fatalf("server Serve() failed: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for server Serve() to stop")
}
}