308 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|