- Manager with injectable enumerateFunc and serialOpener for test isolation - goroutine-per-device model: deviceLoop owns one serial port per device - context+done-channel teardown: inner read goroutine exits on ctx.Done() - Read buffer capped at 4096 bytes (T-04-01 threat mitigation) - Poll loop reconciles prev/current snapshots for connect/disconnect events - ErrDeviceNotConnected returned by Send() when device absent - All 5 manager tests pass with -race flag; goroutine count stable across 5 replug cycles - mockPort test helper blocks Read until Close() unblocks it (realistic behavior)
50 lines
1.1 KiB
Go
50 lines
1.1 KiB
Go
package usb
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
)
|
|
|
|
// mockPort implements serialPort for testing without hardware.
|
|
// Reads block until Close() is called (simulating a hardware device that
|
|
// stays connected until explicitly disconnected).
|
|
type mockPort struct {
|
|
mu sync.Mutex
|
|
closed bool
|
|
closeCh chan struct{}
|
|
}
|
|
|
|
func newMockPort() *mockPort {
|
|
return &mockPort{
|
|
closeCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Read blocks until the port is closed, then returns io.EOF.
|
|
// This mirrors the behaviour of a real serial port: Read blocks until data
|
|
// arrives or the port is closed.
|
|
func (p *mockPort) Read(buf []byte) (int, error) {
|
|
<-p.closeCh
|
|
return 0, io.EOF
|
|
}
|
|
|
|
// Write is a no-op for the mock (no hardware to write to).
|
|
func (p *mockPort) Write(data []byte) (int, error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.closed {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
return len(data), nil
|
|
}
|
|
|
|
// Close signals the mock port as closed, unblocking any pending Read.
|
|
func (p *mockPort) Close() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if !p.closed {
|
|
p.closed = true
|
|
close(p.closeCh)
|
|
}
|
|
return nil
|
|
}
|