You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
7.7 KiB
251 lines
7.7 KiB
|
2 months ago
|
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
|
||
|
|
}
|