10 "github.com/stretchr/testify/require"
15 func TestNewRejectsInvalidAssemblySettings(t *testing.T) {
16 testCases := []struct {
18 change func(*Settings)
20 {name: "rejects noncanonical ID", change: func(s *Settings) {
23 {name: "requires host and port", change: func(s *Settings) {
24 s.Address = "example.com"
26 {name: "requires a host", change: func(s *Settings) {
29 {name: "requires numeric port", change: func(s *Settings) {
30 s.Address = "example.com:bad"
32 {name: "rejects port zero", change: func(s *Settings) {
33 s.Address = "example.com:0"
35 {name: "rejects excess port", change: func(s *Settings) {
36 s.Address = "example.com:65536"
38 {name: "rejects broadcast unit", change: func(s *Settings) {
41 {name: "rejects reserved unit", change: func(s *Settings) {
44 {name: "rejects fast polling", change: func(s *Settings) {
45 s.PollInterval = time.Millisecond
47 {name: "rejects slow polling", change: func(s *Settings) {
48 s.PollInterval = time.Minute + time.Nanosecond
50 {name: "requires valid modules", change: func(s *Settings) {
55 for _, tc := range testCases {
56 t.Run(tc.name, func(t *testing.T) {
57 d, client, _ := testAssembly(t)
59 DeviceID: testDeviceID,
60 Address: "example.com:502",
62 PollInterval: 50 * time.Millisecond,
63 Modules: []Module{{Model: HostModel}},
67 _, err := New(settings, d.publisher, client)
73 func TestDriverReconnectsWithConfigurationAndFreshBaseline(t *testing.T) {
74 synctest.Test(t, func(t *testing.T) {
75 d, client, transport := testAssembly(t)
76 client.connectErrors = []error{errors.New("unavailable")}
77 client.inputs[1] = true
78 ctx, cancel := context.WithCancel(t.Context())
81 done := make(chan error, 1)
88 client.withLock(func(client *fakeClient) {
89 require.Len(t, client.attempts, 1)
90 require.Empty(t, client.registerWrites)
92 require.Empty(t, transport.ofKind(protocol.Event))
94 time.Sleep(time.Second)
96 client.withLock(func(client *fakeClient) {
97 require.Len(t, client.attempts, 2)
98 require.Len(t, client.registerWrites, 33)
99 require.EqualValues(t, 200, client.registers[pulseWidthBase+2])
100 require.EqualValues(t, 1, client.registers[outputModeBase+2])
102 require.Empty(t, transport.ofKind(protocol.Event))
104 client.withLock(func(client *fakeClient) {
105 client.inputs[1] = false
108 time.Sleep(50 * time.Millisecond)
110 require.Len(t, transport.ofKind(protocol.Event), 1)
112 client.withLock(func(client *fakeClient) {
113 client.inputErr = errors.New("connection lost")
116 time.Sleep(50 * time.Millisecond)
119 availability, valid := transport.ofKind(protocol.Availability)[2].Payload.(protocol.AvailabilityPayload)
120 require.True(t, valid)
121 require.Equal(t, "offline", availability.Status)
123 client.withLock(func(client *fakeClient) {
124 client.inputErr = nil
125 client.inputs[1] = true
128 time.Sleep(time.Second)
130 client.withLock(func(client *fakeClient) {
131 require.Len(t, client.attempts, 3)
132 require.Len(t, client.registerWrites, 66)
134 require.Len(t, transport.ofKind(protocol.Event), 1)
136 require.ErrorIs(t, <-done, context.Canceled)
137 client.withLock(func(client *fakeClient) {
138 require.Equal(t, 3, client.closes)
143 func TestDriverBoundsReconnectBackoff(t *testing.T) {
144 synctest.Test(t, func(t *testing.T) {
145 d, client, _ := testAssembly(t)
147 client.withLock(func(client *fakeClient) {
148 client.connectErrors = append(client.connectErrors, errors.New("offline"))
152 ctx, cancel := context.WithCancel(t.Context())
155 done := make(chan error, 1)
163 for _, delay := range []time.Duration{1, 2, 4, 8, 16, 30, 30} {
164 time.Sleep(delay * time.Second)
168 client.withLock(func(client *fakeClient) {
169 require.Len(t, client.attempts, 8)
170 require.Equal(t, 30*time.Second, client.attempts[7].Sub(client.attempts[6]))
173 require.ErrorIs(t, <-done, context.Canceled)
177 func TestDriverReturnsPublicationAndCleanupFailures(t *testing.T) {
178 testCases := []struct {
183 {name: "returns descriptor failure", kind: protocol.Descriptor},
184 {name: "returns online failure", kind: protocol.Availability},
185 {name: "returns baseline failure", kind: protocol.Reported},
186 {name: "returns cleanup failure", closeFailure: true},
189 for _, tc := range testCases {
190 t.Run(tc.name, func(t *testing.T) {
191 d, client, transport := testAssembly(t)
192 failure := errors.New("independent failure")
194 client.closeErr = failure
195 client.connectErrors = []error{errors.New("device unavailable")}
197 transport.failKind, transport.err = tc.kind, failure
200 require.ErrorIs(t, d.Run(t.Context()), failure)
201 require.LessOrEqual(t, len(client.attempts), 1)
206 func TestDriverRefreshDoesNotRepeatCommandsOrEvents(t *testing.T) {
207 synctest.Test(t, func(t *testing.T) {
208 d, client, transport := testAssembly(t)
209 ctx, cancel := context.WithCancel(t.Context())
212 done := make(chan error, 2)
218 done <- d.publisher.Run(ctx)
223 count := len(transport.all())
227 require.Greater(t, len(transport.all()), count)
228 require.Empty(t, client.writes)
229 require.Empty(t, transport.ofKind(protocol.Event))
231 require.ErrorIs(t, <-done, context.Canceled)
232 require.ErrorIs(t, <-done, context.Canceled)