15 "github.com/stretchr/testify/require"
20 func TestTCPClientEncodesAndChecksModbusOperations(t *testing.T) {
21 testCases := []struct {
23 call func(*tcpClient) error
28 name: "reads low bit first across byte boundaries",
29 call: func(c *tcpClient) error {
30 value, err := c.ReadInputs(t.Context(), 9)
31 require.Equal(t, []bool{true, false, true, false, false, false, false, false, true}, value)
35 request: []byte{2, 0, 0, 0, 9},
36 reply: []byte{2, 2, 5, 1},
39 name: "reads coil state",
40 call: func(c *tcpClient) error {
41 value, err := c.ReadCoils(t.Context(), 1)
42 require.Equal(t, []bool{true}, value)
46 request: []byte{1, 0, 0, 0, 1},
47 reply: []byte{1, 1, 1},
50 name: "reads full sixteen bit registers",
51 call: func(c *tcpClient) error {
52 value, err := c.ReadRegisters(t.Context(), 8, 2)
53 require.Equal(t, []uint16{10000, 65535}, value)
57 request: []byte{3, 0, 8, 0, 2},
58 reply: []byte{3, 4, 0x27, 0x10, 0xff, 0xff},
61 name: "writes true with function five",
62 call: func(c *tcpClient) error {
63 return c.WriteCoil(t.Context(), 7, true)
65 request: []byte{5, 0, 7, 0xff, 0},
66 reply: []byte{5, 0, 7, 0xff, 0},
69 name: "writes false with function five",
70 call: func(c *tcpClient) error {
71 return c.WriteCoil(t.Context(), 7, false)
73 request: []byte{5, 0, 7, 0, 0},
74 reply: []byte{5, 0, 7, 0, 0},
77 name: "writes a packed contiguous coil range",
78 call: func(c *tcpClient) error {
79 return c.WriteCoils(t.Context(), 6, []bool{true, false, true, false, false, false, false, false, true})
81 request: []byte{15, 0, 6, 0, 9, 2, 5, 1},
82 reply: []byte{15, 0, 6, 0, 9},
85 name: "writes a holding register with function sixteen",
86 call: func(c *tcpClient) error {
87 return c.WriteRegister(t.Context(), 0x30d4, 200)
89 request: []byte{16, 0x30, 0xd4, 0, 1, 2, 0, 200},
90 reply: []byte{16, 0x30, 0xd4, 0, 1},
94 for _, tc := range testCases {
95 t.Run(tc.name, func(t *testing.T) {
96 request, err := wireCall(t, tc.call, func(header []byte) []byte {
97 return append(append(header[:4:4], words(uint16(len(tc.reply)+1))...), append([]byte{7}, tc.reply...)...)
99 require.NoError(t, err)
100 require.Equal(t, append([]byte{0, 1, 0, 0, 0, byte(len(tc.request) + 1), 7}, tc.request...), request)
105 func TestTCPClientRejectsInvalidAndTruncatedResponses(t *testing.T) {
106 testCases := []struct {
110 {name: "rejects another transaction", reply: []byte{0, 2, 0, 0, 0, 4, 7, 1, 1, 0}},
111 {name: "rejects another protocol", reply: []byte{0, 1, 0, 1, 0, 4, 7, 1, 1, 0}},
112 {name: "rejects another unit", reply: []byte{0, 1, 0, 0, 0, 4, 8, 1, 1, 0}},
113 {name: "rejects empty PDU", reply: []byte{0, 1, 0, 0, 0, 1, 7}},
114 {name: "rejects excessive PDU", reply: []byte{0, 1, 0, 0, 0, 255, 7}},
115 {name: "rejects truncated header", reply: []byte{0, 1}},
116 {name: "rejects truncated body", reply: []byte{0, 1, 0, 0, 0, 4, 7, 1}},
117 {name: "rejects wrong function", reply: []byte{0, 1, 0, 0, 0, 4, 7, 2, 1, 0}},
118 {name: "rejects exception", reply: []byte{0, 1, 0, 0, 0, 3, 7, 0x81, 2}},
119 {name: "rejects wrong byte count", reply: []byte{0, 1, 0, 0, 0, 4, 7, 1, 2, 0}},
122 for _, tc := range testCases {
123 t.Run(tc.name, func(t *testing.T) {
124 _, err := wireCall(t, func(c *tcpClient) error {
125 _, err := c.ReadCoils(t.Context(), 1)
127 }, func([]byte) []byte {
130 require.Error(t, err)
135 func TestTCPClientRejectsInvalidRangesAndAcknowledgements(t *testing.T) {
137 _, err := c.ReadInputs(t.Context(), 0)
138 require.Error(t, err)
140 _, err = c.ReadCoils(t.Context(), 2001)
141 require.Error(t, err)
143 _, err = c.ReadRegisters(t.Context(), 0, 0)
144 require.Error(t, err)
146 _, err = c.ReadRegisters(t.Context(), 0, 126)
147 require.Error(t, err)
149 _, err = c.ReadRegisters(t.Context(), 65535, 2)
150 require.Error(t, err)
151 require.Error(t, c.WriteCoils(t.Context(), 0, nil))
152 require.Error(t, c.WriteCoils(t.Context(), 65535, []bool{true, true}))
153 require.Error(t, c.WriteCoils(t.Context(), 0, make([]bool, 1969)))
154 require.Error(t, c.WriteCoil(t.Context(), 0, true))
155 require.NoError(t, c.Close())
156 require.Error(t, c.Connect(t.Context(), "", 0))
157 require.Error(t, c.Connect(t.Context(), "", 248))
158 require.Error(t, c.Connect(t.Context(), "", 1))
160 _, err = wireCall(t, func(c *tcpClient) error {
161 return c.WriteRegister(t.Context(), 0, 20)
162 }, func(header []byte) []byte {
163 return append(header[:7:7], 16, 0, 0, 0, 2)
165 require.Error(t, err)
167 _, err = wireCall(t, func(c *tcpClient) error {
168 _, readErr := c.ReadRegisters(t.Context(), 0, 1)
170 }, func(header []byte) []byte {
171 return append(header[:7:7], 3, 3, 0, 0, 0)
173 require.Error(t, err)
176 func TestTCPClientCancellationAndTimeouts(t *testing.T) {
177 testCases := []struct {
182 {name: "cancels a blocked write", cancel: true, write: true},
183 {name: "cancels a blocked read", cancel: true},
184 {name: "limits a blocked write", write: true},
185 {name: "limits a blocked read"},
188 for _, tc := range testCases {
189 t.Run(tc.name, func(t *testing.T) {
190 synctest.Test(t, func(t *testing.T) {
191 conn, peer := net.Pipe()
192 c := &tcpClient{conn: conn, unit: 1}
194 require.NoError(t, c.Close())
195 require.NoError(t, peer.Close())
198 ctx, cancel := context.WithCancel(t.Context())
201 result := make(chan error, 1)
207 _, err := io.ReadFull(peer, request[:])
212 done := make(chan error, 1)
214 _, err := c.ReadCoils(ctx, 1)
221 require.NoError(t, <-result)
227 time.Sleep(2 * time.Second)
231 require.Error(t, err)
234 require.ErrorIs(t, err, context.Canceled)
241 func TestTCPClientCancellationIsAnExpectedRunnerStop(t *testing.T) {
242 testCases := []struct {
246 {name: "stops a blocked write"},
247 {name: "stops a blocked read", reading: true},
250 for _, tc := range testCases {
251 t.Run(tc.name, func(t *testing.T) {
252 synctest.Test(t, func(t *testing.T) {
253 conn, peer := net.Pipe()
254 c := &tcpClient{conn: conn, unit: 1}
257 require.NoError(t, peer.Close())
260 ctx, cancel := context.WithCancel(t.Context())
263 ready := make(chan error, 1)
268 _, err := io.ReadFull(peer, request[:])
273 done := make(chan error, 1)
275 done <- driver.Run(ctx, []driver.Driver{&requestDriver{client: c}}, nil)
281 require.NoError(t, <-ready)
285 require.NoError(t, <-done)
291 func TestTCPClientUsesCallerDeadlineAndStartsClosed(t *testing.T) {
292 client := NewTCPClient()
293 _, err := client.ReadCoils(t.Context(), 1)
294 require.Error(t, err)
295 require.NoError(t, client.Close())
296 synctest.Test(t, func(t *testing.T) {
297 conn, peer := net.Pipe()
298 c := &tcpClient{conn: conn, unit: 1}
300 require.NoError(t, c.Close())
301 require.NoError(t, peer.Close())
304 ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
307 started := time.Now()
308 _, err := c.ReadCoils(ctx, 1)
309 require.Error(t, err)
310 require.Equal(t, time.Millisecond, time.Since(started))
314 func TestTCPClientCancellationPreservesIndependentFailure(t *testing.T) {
315 synctest.Test(t, func(t *testing.T) {
316 conn, peer := net.Pipe()
318 require.NoError(t, peer.Close())
321 ctx, cancel := context.WithCancel(t.Context())
324 failure := errors.New("independent write failure")
325 transport := &interruptConn{
328 failure: errors.Join(os.ErrDeadlineExceeded, failure),
329 interrupted: make(chan struct{}),
331 c := &tcpClient{conn: transport, unit: 1}
333 require.NoError(t, c.Close())
336 _, err := c.ReadCoils(ctx, 1)
337 require.ErrorIs(t, err, failure)
338 require.ErrorIs(t, err, context.Canceled)
342 type requestDriver struct {
346 func (d *requestDriver) Run(ctx context.Context) (err error) {
348 err = errors.Join(err, d.client.Close())
351 _, err = d.client.ReadCoils(ctx, 1)
356 func (d *requestDriver) Refresh() {
359 type interruptConn struct {
361 cancel context.CancelFunc
363 interrupted chan struct{}
366 func (c *interruptConn) Write([]byte) (int, error) {
373 func (c *interruptConn) SetDeadline(deadline time.Time) error {
374 defer close(c.interrupted)
376 err := c.Conn.SetDeadline(deadline)
378 return fmt.Errorf("interrupt fake connection: %w", err)
384 func wireCall(t *testing.T, call func(*tcpClient) error, response func([]byte) []byte) ([]byte, error) {
387 conn, peer := net.Pipe()
388 c := &tcpClient{conn: conn, unit: 7}
390 require.NoError(t, c.Close())
391 require.NoError(t, peer.Close())
394 requests := make(chan []byte, 1)
395 done := make(chan error, 1)
400 _, err := io.ReadFull(peer, header[:])
406 body := make([]byte, int(binary.BigEndian.Uint16(header[4:6]))-1)
407 _, err = io.ReadFull(peer, body)
408 requests <- append(header[:7:7], body...)
410 _, err = peer.Write(response(header[:]))
413 if errors.Is(err, io.ErrClosedPipe) {
417 err = errors.Join(err, peer.Close())
422 require.NoError(t, c.Close())
423 require.NoError(t, <-done)
425 return <-requests, err