391 lines
8.3 KiB
Go
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)
|
|
}
|
|
}
|