rs2322tcp/internal/serial/serial_test.go
2026-08-10 10:38:14 +02:00

391 lines
8.3 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: serial_test.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Tests für die serielle Schnittstellenabstraktion.
* Zusätzlich wird unter Linux mit socat ein virtuelles serielles Portpaar
* erzeugt, um Read, Write und Close ohne reale RS232-Hardware zu testen.
* ============================================================================
*/
package serial
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"git.lang-dieter.de/rs2322tcp/internal/config"
)
///////////////////////////////////////////////////////////////////////////////
// Test helpers
///////////////////////////////////////////////////////////////////////////////
func testDevice() config.DeviceConfig {
return config.DeviceConfig{
Name: "Testgerät",
SerialPort: "/dev/ttyTEST",
BaudRate: 9600,
DataBits: 8,
Parity: "none",
StopBits: 1,
}
}
///////////////////////////////////////////////////////////////////////////////
// Mode tests
///////////////////////////////////////////////////////////////////////////////
func TestCreateMode(t *testing.T) {
device := testDevice()
mode, err := createMode(device)
if err != nil {
t.Fatalf("createMode() failed: %v", err)
}
if mode.BaudRate != 9600 {
t.Fatalf("BaudRate = %d, want 9600", mode.BaudRate)
}
if mode.DataBits != 8 {
t.Fatalf("DataBits = %d, want 8", mode.DataBits)
}
}
func TestCreateModeParity(t *testing.T) {
tests := []struct {
name string
parity string
}{
{"none", "none"},
{"odd", "odd"},
{"even", "even"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
device := testDevice()
device.Parity = tt.parity
if _, err := createMode(device); err != nil {
t.Fatalf(
"createMode() with parity %q failed: %v",
tt.parity,
err,
)
}
})
}
}
func TestCreateModeStopBits(t *testing.T) {
for _, stopBits := range []int{1, 2} {
t.Run(fmt.Sprintf("stopbits_%d", stopBits), func(t *testing.T) {
device := testDevice()
device.StopBits = stopBits
if _, err := createMode(device); err != nil {
t.Fatalf(
"createMode() with stop bits %d failed: %v",
stopBits,
err,
)
}
})
}
}
///////////////////////////////////////////////////////////////////////////////
// Invalid configuration
///////////////////////////////////////////////////////////////////////////////
func TestCreateModeRejectsInvalidParity(t *testing.T) {
device := testDevice()
device.Parity = "invalid"
if _, err := createMode(device); err == nil {
t.Fatal("createMode() succeeded with invalid parity")
}
}
func TestCreateModeRejectsInvalidStopBits(t *testing.T) {
device := testDevice()
device.StopBits = 3
if _, err := createMode(device); err == nil {
t.Fatal("createMode() succeeded with invalid stop bits")
}
}
func TestCreateModeRejectsInvalidDataBits(t *testing.T) {
device := testDevice()
device.DataBits = 9
if _, err := createMode(device); err == nil {
t.Fatal("createMode() succeeded with invalid data bits")
}
}
func TestCreateModeRejectsInvalidBaudRate(t *testing.T) {
device := testDevice()
device.BaudRate = 0
if _, err := createMode(device); err == nil {
t.Fatal("createMode() succeeded with invalid baud rate")
}
}
///////////////////////////////////////////////////////////////////////////////
// Open
///////////////////////////////////////////////////////////////////////////////
func TestOpenInvalidPort(t *testing.T) {
device := testDevice()
_, err := Open(device)
if err == nil {
t.Fatal("Open() succeeded with nonexistent serial port")
}
}
///////////////////////////////////////////////////////////////////////////////
// Nil connection
///////////////////////////////////////////////////////////////////////////////
func TestNilConnectionRead(t *testing.T) {
var connection *Connection
buffer := make([]byte, 1)
_, err := connection.Read(buffer)
if err == nil {
t.Fatal("Read() succeeded on nil connection")
}
}
func TestNilConnectionWrite(t *testing.T) {
var connection *Connection
_, err := connection.Write([]byte("test"))
if err == nil {
t.Fatal("Write() succeeded on nil connection")
}
}
func TestNilConnectionClose(t *testing.T) {
var connection *Connection
if err := connection.Close(); err != nil {
t.Fatalf("Close() failed: %v", err)
}
}
///////////////////////////////////////////////////////////////////////////////
// Virtual serial port
///////////////////////////////////////////////////////////////////////////////
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("failed to start socat: %v", err)
}
cleanup := func() {
if cmd.Process != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}
}
deadline := time.Now().Add(2 * time.Second)
for {
if _, errA := os.Stat(portA); errA == nil {
if _, errB := os.Stat(portB); errB == nil {
break
}
}
if time.Now().After(deadline) {
cleanup()
t.Fatal("timeout waiting for virtual serial ports")
}
time.Sleep(10 * time.Millisecond)
}
return portA, portB, cleanup
}
///////////////////////////////////////////////////////////////////////////////
// Read / Write integration test
///////////////////////////////////////////////////////////////////////////////
func TestVirtualSerialReadWrite(t *testing.T) {
portA, portB, cleanup := startVirtualSerialPair(t)
defer cleanup()
device := testDevice()
device.SerialPort = portA
connection, err := Open(device)
if err != nil {
t.Fatalf("Open() failed: %v", err)
}
defer connection.Close()
peer, err := os.OpenFile(
portB,
os.O_RDWR,
0,
)
if err != nil {
t.Fatalf("opening peer port failed: %v", err)
}
defer peer.Close()
////////////////////////////////////////////////////////////////////////////
// Serial -> peer
////////////////////////////////////////////////////////////////////////////
outgoing := []byte("hello from serial")
n, err := connection.Write(outgoing)
if err != nil {
t.Fatalf("serial Write() failed: %v", err)
}
if n != len(outgoing) {
t.Fatalf(
"serial Write() wrote %d bytes, want %d",
n,
len(outgoing),
)
}
received := make([]byte, len(outgoing))
if err := readWithTimeout(
peer,
received,
2*time.Second,
); err != nil {
t.Fatalf("peer Read() failed: %v", err)
}
if string(received) != string(outgoing) {
t.Fatalf(
"peer received %q, want %q",
string(received),
string(outgoing),
)
}
////////////////////////////////////////////////////////////////////////////
// peer -> Serial
////////////////////////////////////////////////////////////////////////////
incoming := []byte("hello from peer")
n, err = peer.Write(incoming)
if err != nil {
t.Fatalf("peer Write() failed: %v", err)
}
if n != len(incoming) {
t.Fatalf(
"peer Write() wrote %d bytes, want %d",
n,
len(incoming),
)
}
received = make([]byte, len(incoming))
if err := readWithTimeout(
connection,
received,
2*time.Second,
); err != nil {
t.Fatalf("serial Read() failed: %v", err)
}
if string(received) != string(incoming) {
t.Fatalf(
"serial received %q, want %q",
string(received),
string(incoming),
)
}
}
///////////////////////////////////////////////////////////////////////////////
// Timeout helper
///////////////////////////////////////////////////////////////////////////////
type reader interface {
Read([]byte) (int, error)
}
func readWithTimeout(
r reader,
buffer []byte,
timeout time.Duration,
) error {
result := make(chan error, 1)
go func() {
n, err := r.Read(buffer)
if err != nil {
result <- err
return
}
if n != len(buffer) {
result <- fmt.Errorf(
"read %d bytes, want %d",
n,
len(buffer),
)
return
}
result <- nil
}()
select {
case err := <-result:
return err
case <-time.After(timeout):
return fmt.Errorf("read timeout after %s", timeout)
}
}