homelabby/internal/usb/mock_port_test.go
Mikkel Georgsen 82eaf6bed7 feat(04-01): USB Manager goroutine-per-device, poll loop, reconnect, leak-safe teardown
- 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)
2026-04-10 06:45:26 +00:00

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
}