]> git.giacomolli.uk - onehouse/blob - driver/m31/fake_helper_test.go
Poll M31 assemblies with verified reconnect setup
[onehouse] / driver / m31 / fake_helper_test.go
1 package m31
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "slices"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/require"
13
14 "onehouse/driver"
15 "onehouse/protocol"
16 )
17
18 const testDeviceID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
19
20 type fakeClient struct {
21 mu sync.Mutex
22 inputs []bool
23 coils []bool
24 analog []uint16
25 registers map[uint16]uint16
26 writes []coilWrite
27 registerWrites []uint16
28 attempts []time.Time
29 closes int
30 coilReads int
31 connectErrors []error
32 inputErr error
33 readErr error
34 writeErr error
35 registerErr error
36 closeErr error
37 wrongConfig bool
38 shortRead bool
39 mismatch int
40 writeDelay time.Duration
41 inputDelay time.Duration
42 }
43
44 func (c *fakeClient) Connect(ctx context.Context, _ string, _ byte) error {
45 c.mu.Lock()
46 defer c.mu.Unlock()
47
48 c.attempts = append(c.attempts, time.Now())
49 if len(c.connectErrors) != 0 {
50 err := c.connectErrors[0]
51 c.connectErrors = c.connectErrors[1:]
52
53 return err
54 }
55
56 return ctx.Err()
57 }
58
59 func (c *fakeClient) Close() error {
60 c.mu.Lock()
61 defer c.mu.Unlock()
62
63 c.closes++
64
65 return c.closeErr
66 }
67
68 func (c *fakeClient) ReadInputs(ctx context.Context, _ uint16) ([]bool, error) {
69 c.mu.Lock()
70 defer c.mu.Unlock()
71
72 time.Sleep(c.inputDelay)
73
74 if c.inputErr != nil || ctx.Err() != nil {
75 return nil, errors.Join(c.inputErr, ctx.Err())
76 }
77
78 return slices.Clone(c.inputs), nil
79 }
80
81 func (c *fakeClient) ReadCoils(ctx context.Context, _ uint16) ([]bool, error) {
82 c.mu.Lock()
83 defer c.mu.Unlock()
84
85 c.coilReads++
86 if c.readErr != nil || ctx.Err() != nil {
87 return nil, errors.Join(c.readErr, ctx.Err())
88 }
89
90 if c.shortRead {
91 return nil, nil
92 }
93
94 return slices.Clone(c.coils), nil
95 }
96
97 func (c *fakeClient) ReadRegisters(ctx context.Context, address uint16, count uint16) ([]uint16, error) {
98 c.mu.Lock()
99 defer c.mu.Unlock()
100
101 if c.registerErr != nil || ctx.Err() != nil {
102 return nil, errors.Join(c.registerErr, ctx.Err())
103 }
104
105 if address >= pulseWidthBase {
106 value := c.registers[address]
107 if c.wrongConfig {
108 value++
109 }
110
111 return []uint16{value}, nil
112 }
113
114 return slices.Clone(c.analog[:min(int(count), len(c.analog))]), nil
115 }
116
117 func (c *fakeClient) WriteCoil(ctx context.Context, address uint16, value bool) error {
118 return c.write(ctx, address, []bool{value}, false)
119 }
120
121 func (c *fakeClient) WriteCoils(ctx context.Context, address uint16, values []bool) error {
122 return c.write(ctx, address, values, true)
123 }
124
125 func (c *fakeClient) write(ctx context.Context, address uint16, values []bool, multiple bool) error {
126 c.mu.Lock()
127 defer c.mu.Unlock()
128
129 time.Sleep(c.writeDelay)
130
131 if c.writeErr != nil || ctx.Err() != nil {
132 return errors.Join(c.writeErr, ctx.Err())
133 }
134
135 c.writes = append(c.writes, coilWrite{
136 address: address,
137 values: slices.Clone(values),
138 multiple: multiple,
139 })
140
141 for offset, value := range values {
142 if int(address)+offset != c.mismatch {
143 c.coils[int(address)+offset] = value
144 }
145 }
146
147 return nil
148 }
149
150 func (c *fakeClient) WriteRegister(ctx context.Context, address uint16, value uint16) error {
151 c.mu.Lock()
152 defer c.mu.Unlock()
153
154 if c.registerErr != nil || ctx.Err() != nil {
155 return errors.Join(c.registerErr, ctx.Err())
156 }
157
158 c.registerWrites = append(c.registerWrites, address)
159 if address >= pulseWidthBase {
160 c.registers[address] = value
161 } else if int(address) != c.mismatch {
162 c.analog[address] = value
163 }
164
165 return nil
166 }
167
168 func (c *fakeClient) withLock(fn func(*fakeClient)) {
169 c.mu.Lock()
170 defer c.mu.Unlock()
171
172 fn(c)
173 }
174
175 type coilWrite struct {
176 address uint16
177 values []bool
178 multiple bool
179 }
180
181 type fakeTransport struct {
182 mu sync.Mutex
183 messages []protocol.Message[any]
184 failKind protocol.Kind
185 err error
186 }
187
188 func (f *fakeTransport) Publish(ctx context.Context, publication protocol.Publication) error {
189 f.mu.Lock()
190 defer f.mu.Unlock()
191
192 message, err := protocol.Parse(publication.Topic, publication.Data, publication.Retain)
193 if err != nil {
194 return fmt.Errorf("parse fake publication: %w", err)
195 }
196
197 if message.Address.Kind == f.failKind && f.err != nil {
198 return f.err
199 }
200
201 if ctx.Err() != nil {
202 return ctx.Err()
203 }
204
205 f.messages = append(f.messages, message)
206
207 return nil
208 }
209
210 func (f *fakeTransport) ofKind(kind protocol.Kind) []protocol.Message[any] {
211 f.mu.Lock()
212 defer f.mu.Unlock()
213
214 var messages []protocol.Message[any]
215
216 for _, message := range f.messages {
217 if message.Address.Kind == kind {
218 messages = append(messages, message)
219 }
220 }
221
222 return messages
223 }
224
225 func (f *fakeTransport) all() []protocol.Message[any] {
226 f.mu.Lock()
227 defer f.mu.Unlock()
228
229 return slices.Clone(f.messages)
230 }
231
232 func testAssembly(t *testing.T) (*Driver, *fakeClient, *fakeTransport) {
233 t.Helper()
234
235 transport := &fakeTransport{}
236 publisher := driver.NewPublisher(transport)
237 client := &fakeClient{
238 inputs: make([]bool, 16),
239 coils: make([]bool, 32),
240 analog: make([]uint16, 8),
241 registers: make(map[uint16]uint16),
242 mismatch: -1,
243 }
244 settings := Settings{
245 DeviceID: testDeviceID,
246 Address: "example.com:502",
247 Unit: 1,
248 PollInterval: 50 * time.Millisecond,
249 Modules: []Module{
250 {Model: HostModel, PulseWidths: map[int]time.Duration{2: 200 * time.Millisecond}},
251 {Model: OutputModel},
252 {Model: AnalogModel},
253 {Model: MixedModel},
254 },
255 }
256 d, err := New(settings, publisher, client)
257 require.NoError(t, err)
258
259 return d, client, transport
260 }
261
262 func testConnection(t *testing.T) (*connection, *fakeClient, *fakeTransport) {
263 t.Helper()
264
265 d, client, transport := testAssembly(t)
266 connection := &connection{owner: d}
267 require.NoError(t, connection.start(t.Context()))
268
269 transport.messages = nil
270 client.writes = nil
271 client.coilReads = 0
272
273 return connection, client, transport
274 }