12 "github.com/stretchr/testify/require"
18 const testDeviceID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
20 type fakeClient struct {
25 registers map[uint16]uint16
27 registerWrites []uint16
40 writeDelay time.Duration
41 inputDelay time.Duration
44 func (c *fakeClient) Connect(ctx context.Context, _ string, _ byte) error {
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:]
59 func (c *fakeClient) Close() error {
68 func (c *fakeClient) ReadInputs(ctx context.Context, _ uint16) ([]bool, error) {
72 time.Sleep(c.inputDelay)
74 if c.inputErr != nil || ctx.Err() != nil {
75 return nil, errors.Join(c.inputErr, ctx.Err())
78 return slices.Clone(c.inputs), nil
81 func (c *fakeClient) ReadCoils(ctx context.Context, _ uint16) ([]bool, error) {
86 if c.readErr != nil || ctx.Err() != nil {
87 return nil, errors.Join(c.readErr, ctx.Err())
94 return slices.Clone(c.coils), nil
97 func (c *fakeClient) ReadRegisters(ctx context.Context, address uint16, count uint16) ([]uint16, error) {
101 if c.registerErr != nil || ctx.Err() != nil {
102 return nil, errors.Join(c.registerErr, ctx.Err())
105 if address >= pulseWidthBase {
106 value := c.registers[address]
111 return []uint16{value}, nil
114 return slices.Clone(c.analog[:min(int(count), len(c.analog))]), nil
117 func (c *fakeClient) WriteCoil(ctx context.Context, address uint16, value bool) error {
118 return c.write(ctx, address, []bool{value}, false)
121 func (c *fakeClient) WriteCoils(ctx context.Context, address uint16, values []bool) error {
122 return c.write(ctx, address, values, true)
125 func (c *fakeClient) write(ctx context.Context, address uint16, values []bool, multiple bool) error {
129 time.Sleep(c.writeDelay)
131 if c.writeErr != nil || ctx.Err() != nil {
132 return errors.Join(c.writeErr, ctx.Err())
135 c.writes = append(c.writes, coilWrite{
137 values: slices.Clone(values),
141 for offset, value := range values {
142 if int(address)+offset != c.mismatch {
143 c.coils[int(address)+offset] = value
150 func (c *fakeClient) WriteRegister(ctx context.Context, address uint16, value uint16) error {
154 if c.registerErr != nil || ctx.Err() != nil {
155 return errors.Join(c.registerErr, ctx.Err())
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
168 func (c *fakeClient) withLock(fn func(*fakeClient)) {
175 type coilWrite struct {
181 type fakeTransport struct {
183 messages []protocol.Message[any]
184 failKind protocol.Kind
188 func (f *fakeTransport) Publish(ctx context.Context, publication protocol.Publication) error {
192 message, err := protocol.Parse(publication.Topic, publication.Data, publication.Retain)
194 return fmt.Errorf("parse fake publication: %w", err)
197 if message.Address.Kind == f.failKind && f.err != nil {
201 if ctx.Err() != nil {
205 f.messages = append(f.messages, message)
210 func (f *fakeTransport) ofKind(kind protocol.Kind) []protocol.Message[any] {
214 var messages []protocol.Message[any]
216 for _, message := range f.messages {
217 if message.Address.Kind == kind {
218 messages = append(messages, message)
225 func (f *fakeTransport) all() []protocol.Message[any] {
229 return slices.Clone(f.messages)
232 func testAssembly(t *testing.T) (*Driver, *fakeClient, *fakeTransport) {
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),
244 settings := Settings{
245 DeviceID: testDeviceID,
246 Address: "example.com:502",
248 PollInterval: 50 * time.Millisecond,
250 {Model: HostModel, PulseWidths: map[int]time.Duration{2: 200 * time.Millisecond}},
251 {Model: OutputModel},
252 {Model: AnalogModel},
256 d, err := New(settings, publisher, client)
257 require.NoError(t, err)
259 return d, client, transport
262 func testConnection(t *testing.T) (*connection, *fakeClient, *fakeTransport) {
265 d, client, transport := testAssembly(t)
266 connection := &connection{owner: d}
267 require.NoError(t, connection.start(t.Context()))
269 transport.messages = nil
273 return connection, client, transport