12 "github.com/stretchr/testify/require"
18 const testDeviceID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
20 type fakeClient struct {
28 registers map[uint16]uint16
30 registerWrites []uint16
43 writeDelay time.Duration
44 inputDelay time.Duration
47 func (c *fakeClient) Connect(ctx context.Context, _ string, _ byte) error {
51 c.attempts = append(c.attempts, time.Now())
52 if len(c.connectErrors) != 0 {
53 err := c.connectErrors[0]
54 c.connectErrors = c.connectErrors[1:]
62 func (c *fakeClient) Close() error {
71 func (c *fakeClient) ReadInputs(ctx context.Context, _ uint16) ([]bool, error) {
75 time.Sleep(c.inputDelay)
77 if c.inputErr != nil || ctx.Err() != nil {
78 return nil, errors.Join(c.inputErr, ctx.Err())
81 return slices.Clone(c.inputs), nil
84 func (c *fakeClient) ReadCoils(ctx context.Context, _ uint16) ([]bool, error) {
89 if c.readErr != nil || ctx.Err() != nil {
90 return nil, errors.Join(c.readErr, ctx.Err())
97 return slices.Clone(c.coils), nil
100 func (c *fakeClient) ReadRegisters(ctx context.Context, address uint16, count uint16) ([]uint16, error) {
104 if c.registerErr != nil || c.registerReadErr != nil || ctx.Err() != nil {
105 return nil, errors.Join(c.registerErr, c.registerReadErr, ctx.Err())
108 if address >= pulseWidthBase {
109 value := c.registers[address]
110 if c.wrongConfig || address == c.wrongRegister {
114 return []uint16{value}, nil
117 return slices.Clone(c.analog[:min(int(count), len(c.analog))]), nil
120 func (c *fakeClient) WriteCoil(ctx context.Context, address uint16, value bool) error {
121 return c.write(ctx, address, []bool{value}, false)
124 func (c *fakeClient) WriteCoils(ctx context.Context, address uint16, values []bool) error {
125 return c.write(ctx, address, values, true)
128 func (c *fakeClient) write(ctx context.Context, address uint16, values []bool, multiple bool) error {
132 time.Sleep(c.writeDelay)
134 if c.writeErr != nil && len(c.writes) >= c.failAfterWrites || ctx.Err() != nil {
135 return errors.Join(c.writeErr, ctx.Err())
138 c.writes = append(c.writes, coilWrite{
140 values: slices.Clone(values),
144 for offset, value := range values {
145 if int(address)+offset != c.mismatch {
146 c.coils[int(address)+offset] = value
153 func (c *fakeClient) WriteRegister(ctx context.Context, address uint16, value uint16) error {
157 if c.registerErr != nil || ctx.Err() != nil {
158 return errors.Join(c.registerErr, ctx.Err())
161 c.registerWrites = append(c.registerWrites, address)
162 if address >= pulseWidthBase {
163 c.registers[address] = value
164 } else if int(address) != c.mismatch {
165 c.analog[address] = value
171 func (c *fakeClient) withLock(fn func(*fakeClient)) {
178 type coilWrite struct {
184 type fakeTransport struct {
186 messages []protocol.Message[any]
187 failKind protocol.Kind
191 func (f *fakeTransport) Publish(ctx context.Context, publication protocol.Publication) error {
195 message, err := protocol.Parse(publication.Topic, publication.Data, publication.Retain)
197 return fmt.Errorf("parse fake publication: %w", err)
200 if message.Address.Kind == f.failKind && f.err != nil {
204 if ctx.Err() != nil {
208 f.messages = append(f.messages, message)
213 func (f *fakeTransport) ofKind(kind protocol.Kind) []protocol.Message[any] {
217 var messages []protocol.Message[any]
219 for _, message := range f.messages {
220 if message.Address.Kind == kind {
221 messages = append(messages, message)
228 func (f *fakeTransport) all() []protocol.Message[any] {
232 return slices.Clone(f.messages)
235 func testAssembly(t *testing.T) (*Driver, *fakeClient, *fakeTransport, *driver.Router) {
238 transport := &fakeTransport{}
239 publisher := driver.NewPublisher(transport)
240 router, err := driver.NewRouter([]string{testDeviceID})
241 require.NoError(t, err)
243 client := &fakeClient{
244 inputs: make([]bool, 16),
245 coils: make([]bool, 32),
246 analog: make([]uint16, 8),
247 registers: make(map[uint16]uint16),
250 settings := Settings{
251 DeviceID: testDeviceID,
252 Address: "example.com:502",
254 PollInterval: 50 * time.Millisecond,
256 {Model: HostModel, PulseWidths: map[int]time.Duration{2: 200 * time.Millisecond}},
257 {Model: OutputModel},
258 {Model: AnalogModel},
262 d, err := New(settings, publisher, router, client)
263 require.NoError(t, err)
265 return d, client, transport, router
268 func testCommand(t *testing.T, capability string, value string) driver.Command {
271 id, err := protocol.NewID(time.Now())
272 require.NoError(t, err)
274 expires := time.Now().UTC().Add(time.Minute)
276 return driver.Command{
277 DeviceID: testDeviceID,
278 Capability: capability,
279 Metadata: protocol.Metadata{
283 Payload: protocol.ValuePayload{Value: []byte(value)},
287 func testConnection(t *testing.T) (*connection, *fakeClient, *fakeTransport) {
290 d, client, transport, _ := testAssembly(t)
291 connection := &connection{owner: d}
292 require.NoError(t, connection.start(t.Context()))
294 transport.messages = nil
298 return connection, client, transport
301 func queueCommands(t *testing.T, router *driver.Router, commands []driver.Command) {
305 publication protocol.Publication
309 if len(commands) == 1 {
310 command := commands[0]
311 publication, err = protocol.BuildCommand(command.DeviceID, command.Capability, command.Metadata, command.Payload)
313 members := make([]protocol.BatchCommand, len(commands))
315 for index, command := range commands {
316 members[index] = protocol.BatchCommand{
317 Capability: command.Capability,
318 Metadata: command.Metadata,
319 Payload: command.Payload,
323 publication, err = protocol.BuildCommandBatch(testDeviceID, commands[0].Metadata, protocol.BatchPayload{Commands: members})
326 require.NoError(t, err)
327 require.NoError(t, router.Handle(t.Context(), publication.Topic, publication.Data, false))