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