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
192 func (f *fakeTransport) Publish(ctx context.Context, publication protocol.Publication) error {
196 message, err := protocol.Parse(publication.Topic, publication.Data, publication.Retain)
198 return fmt.Errorf("parse fake publication: %w", err)
201 if message.Address.Kind == f.failKind && f.err != nil {
210 if ctx.Err() != nil {
214 f.messages = append(f.messages, message)
219 func (f *fakeTransport) ofKind(kind protocol.Kind) []protocol.Message[any] {
223 var messages []protocol.Message[any]
225 for _, message := range f.messages {
226 if message.Address.Kind == kind {
227 messages = append(messages, message)
234 func (f *fakeTransport) all() []protocol.Message[any] {
238 return slices.Clone(f.messages)
241 func testAssembly(t *testing.T) (*Driver, *fakeClient, *fakeTransport, *driver.Router) {
244 transport := &fakeTransport{}
245 publisher := driver.NewPublisher(transport)
246 router, err := driver.NewRouter([]string{testDeviceID})
247 require.NoError(t, err)
249 client := &fakeClient{
250 inputs: make([]bool, 16),
251 coils: make([]bool, 32),
252 analog: make([]uint16, 8),
253 registers: make(map[uint16]uint16),
256 settings := Settings{
257 DeviceID: testDeviceID,
258 Address: "example.com:502",
260 PollInterval: 50 * time.Millisecond,
262 {Model: HostModel, PulseWidths: map[int]time.Duration{2: 200 * time.Millisecond}},
263 {Model: OutputModel},
264 {Model: AnalogModel},
268 d, err := New(settings, publisher, router, client)
269 require.NoError(t, err)
271 return d, client, transport, router
274 func testCommand(t *testing.T, capability string, value string) driver.Command {
277 id, err := protocol.NewID(time.Now())
278 require.NoError(t, err)
280 expires := time.Now().UTC().Add(time.Minute)
282 return driver.Command{
283 DeviceID: testDeviceID,
284 Capability: capability,
285 Metadata: protocol.Metadata{
289 Payload: protocol.ValuePayload{Value: []byte(value)},
293 func testConnection(t *testing.T) (*connection, *fakeClient, *fakeTransport) {
296 d, client, transport, _ := testAssembly(t)
297 connection := &connection{owner: d}
298 require.NoError(t, connection.start(t.Context()))
300 transport.messages = nil
304 return connection, client, transport
307 func queueCommands(t *testing.T, router *driver.Router, commands []driver.Command) {
311 publication protocol.Publication
315 if len(commands) == 1 {
316 command := commands[0]
317 publication, err = protocol.BuildCommand(command.DeviceID, command.Capability, command.Metadata, command.Payload)
319 members := make([]protocol.BatchCommand, len(commands))
321 for index, command := range commands {
322 members[index] = protocol.BatchCommand{
323 Capability: command.Capability,
324 Metadata: command.Metadata,
325 Payload: command.Payload,
329 publication, err = protocol.BuildCommandBatch(testDeviceID, commands[0].Metadata, protocol.BatchPayload{Commands: members})
332 require.NoError(t, err)
333 require.NoError(t, router.Handle(t.Context(), publication.Topic, publication.Data, false))