package serial import ( "context" "encoding/json" "errors" "net" "path/filepath" "testing" "time" ) type socketProbeController struct { UnavailableController result ProbeResult } type socketLinkController struct{ UnavailableController } type socketCaptureController struct{ configs []CaptureConfig } type socketCRProbeController struct{ states []CRProbeState } type socketBusyController struct{ UnavailableController } func (socketBusyController) Apply(context.Context, []ChannelConfig) ([]State, error) { return nil, ErrStreamLeaseBusy } func (c socketCRProbeController) CRProbeStates(context.Context) ([]CRProbeState, error) { return append([]CRProbeState(nil), c.states...), nil } func (c *socketCaptureController) ApplyCapture(_ context.Context, configs []CaptureConfig) error { c.configs = append([]CaptureConfig(nil), configs...) return nil } func (socketLinkController) LinkStates(context.Context) ([]LinkState, error) { return []LinkState{{Channel: "CH01", State: "up", Signal: "dcd", Available: true}}, nil } func (c socketProbeController) Probe(context.Context, ChannelConfig) (ProbeResult, error) { return c.result, nil } func TestSocketClientPreservesFailClosedSerialUnavailableResult(t *testing.T) { socketPath := filepath.Join(t.TempDir(), "serial-ports.sock") listener, err := net.Listen("unix", socketPath) if err != nil { t.Skipf("unix sockets unavailable: %v", err) } defer listener.Close() server := SocketServer{Controller: UnavailableController{Reason: "validated mapping is not installed"}} done := make(chan error, 1) go func() { done <- server.Serve(listener) }() client := NewSocketClient(socketPath) states, err := client.States(context.Background()) if err != nil || len(states) != ChannelCount || states[0].Available { t.Fatalf("states = %#v, %v", states, err) } _, err = client.Apply(context.Background(), []ChannelConfig{{Channel: "CH01", BaudRate: 115200}}) if !IsUnavailable(err) { t.Fatalf("apply error = %v, want unavailable", err) } if err := listener.Close(); err != nil { t.Fatal(err) } select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(time.Second): t.Fatal("socket server did not stop") } } func TestSocketClientPreservesStreamLeaseBusyResult(t *testing.T) { socketPath := filepath.Join(t.TempDir(), "serial-busy.sock") listener, err := net.Listen("unix", socketPath) if err != nil { t.Skipf("unix sockets unavailable: %v", err) } done := make(chan error, 1) go func() { done <- (SocketServer{Controller: socketBusyController{}}).Serve(listener) }() client := NewSocketClient(socketPath) _, err = client.Apply(context.Background(), []ChannelConfig{{Channel: "CH16", BaudRate: 9600}}) if !errors.Is(err, ErrStreamLeaseBusy) { t.Fatalf("apply error = %v, want stream lease busy", err) } _ = listener.Close() <-done } func TestSocketStatesRequestDoesNotWaitForClientEOF(t *testing.T) { socketPath := filepath.Join(t.TempDir(), "serial-ports.sock") listener, err := net.Listen("unix", socketPath) if err != nil { t.Skipf("unix sockets unavailable: %v", err) } defer listener.Close() server := SocketServer{Controller: UnavailableController{Reason: "read-only test"}} done := make(chan error, 1) go func() { done <- server.Serve(listener) }() connection, err := net.Dial("unix", socketPath) if err != nil { t.Fatal(err) } defer connection.Close() if err := connection.SetDeadline(time.Now().Add(time.Second)); err != nil { t.Fatal(err) } if err := json.NewEncoder(connection).Encode(socketRequest{Action: "states"}); err != nil { t.Fatal(err) } var response socketResponse if err := json.NewDecoder(connection).Decode(&response); err != nil { t.Fatalf("decode response without closing request side: %v", err) } if len(response.States) != ChannelCount { t.Fatalf("states = %d, want %d", len(response.States), ChannelCount) } if err := listener.Close(); err != nil { t.Fatal(err) } select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(time.Second): t.Fatal("socket server did not stop") } } func TestSocketClientReadsBoundedModemLinkState(t *testing.T) { socketPath := filepath.Join(t.TempDir(), "serial-links.sock") listener, err := net.Listen("unix", socketPath) if err != nil { t.Skipf("unix sockets unavailable: %v", err) } done := make(chan error, 1) go func() { done <- (SocketServer{Controller: socketLinkController{}}).Serve(listener) }() client := NewSocketClient(socketPath) states, err := client.LinkStates(context.Background()) if err != nil || len(states) != 1 || !states[0].Available || states[0].Signal != "dcd" { t.Fatalf("LinkStates = %+v, %v", states, err) } _ = listener.Close() <-done } func TestSocketClientReadsCachedCRProbeState(t *testing.T) { socketPath := filepath.Join(t.TempDir(), "serial-cr-probe.sock") listener, err := net.Listen("unix", socketPath) if err != nil { t.Skipf("unix sockets unavailable: %v", err) } want := []CRProbeState{{Channel: "CH08", Enabled: true, State: CRProbeStateResponded, Method: "cr", ResponseBytes: 7}} done := make(chan error, 1) go func() { done <- (SocketServer{ Controller: UnavailableController{Reason: "line control unused"}, CRProbe: socketCRProbeController{states: want}, }).Serve(listener) }() client := NewSocketClient(socketPath) got, err := client.CRProbeStates(context.Background()) if err != nil || len(got) != 1 || got[0].Channel != "CH08" || got[0].State != CRProbeStateResponded { t.Fatalf("CRProbeStates = %+v, %v", got, err) } _ = listener.Close() <-done } func TestSocketClientProbeRoundTrip(t *testing.T) { socketPath := filepath.Join(t.TempDir(), "serial-probe.sock") listener, err := net.Listen("unix", socketPath) if err != nil { t.Skipf("unix sockets unavailable: %v", err) } defer listener.Close() want := ProbeResult{ Channel: "CH01", Device: "/dev/ttyWCH4", Previous: LineSettings{BaudRate: 9600, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none"}, Applied: LineSettings{BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none"}, Restored: true, } server := SocketServer{Controller: socketProbeController{ UnavailableController: UnavailableController{Reason: "states unused"}, result: want, }} done := make(chan error, 1) go func() { done <- server.Serve(listener) }() client := NewSocketClient(socketPath) got, err := client.Probe(context.Background(), ChannelConfig{ Channel: "CH01", BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none", }) if err != nil { t.Fatalf("probe: %v", err) } if got != want { t.Fatalf("probe = %#v, want %#v", got, want) } if err := listener.Close(); err != nil { t.Fatal(err) } select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(time.Second): t.Fatal("socket server did not stop") } } func TestSocketClientAppliesPayloadCaptureWithoutTermios(t *testing.T) { socketPath := filepath.Join(t.TempDir(), "serial-capture.sock") listener, err := net.Listen("unix", socketPath) if err != nil { t.Skipf("unix sockets unavailable: %v", err) } capture := &socketCaptureController{} done := make(chan error, 1) go func() { done <- (SocketServer{Controller: UnavailableController{Reason: "line control unused"}, Capture: capture}).Serve(listener) }() client := NewSocketClient(socketPath) want := []CaptureConfig{{Channel: "CH01", Mode: CaptureModeRXTX}, {Channel: "CH02", Mode: CaptureModeOff}} if err := client.ApplyCapture(context.Background(), want); err != nil { t.Fatal(err) } if len(capture.configs) != len(want) || capture.configs[0] != want[0] || capture.configs[1] != want[1] { t.Fatalf("capture configs = %#v", capture.configs) } _ = listener.Close() <-done }