13 "github.com/stretchr/testify/require"
16 func TestTCPClientEncodesAndChecksModbusOperations(t *testing.T) {
17 testCases := []struct {
19 call func(*tcpClient) error
24 name: "reads low bit first across byte boundaries",
25 call: func(c *tcpClient) error {
26 value, err := c.ReadInputs(t.Context(), 9)
27 require.Equal(t, []bool{true, false, true, false, false, false, false, false, true}, value)
31 request: []byte{2, 0, 0, 0, 9},
32 reply: []byte{2, 2, 5, 1},
35 name: "reads coil state",
36 call: func(c *tcpClient) error {
37 value, err := c.ReadCoils(t.Context(), 1)
38 require.Equal(t, []bool{true}, value)
42 request: []byte{1, 0, 0, 0, 1},
43 reply: []byte{1, 1, 1},
46 name: "reads full sixteen bit registers",
47 call: func(c *tcpClient) error {
48 value, err := c.ReadRegisters(t.Context(), 8, 2)
49 require.Equal(t, []uint16{10000, 65535}, value)
53 request: []byte{3, 0, 8, 0, 2},
54 reply: []byte{3, 4, 0x27, 0x10, 0xff, 0xff},
57 name: "writes true with function five",
58 call: func(c *tcpClient) error {
59 return c.WriteCoil(t.Context(), 7, true)
61 request: []byte{5, 0, 7, 0xff, 0},
62 reply: []byte{5, 0, 7, 0xff, 0},
65 name: "writes false with function five",
66 call: func(c *tcpClient) error {
67 return c.WriteCoil(t.Context(), 7, false)
69 request: []byte{5, 0, 7, 0, 0},
70 reply: []byte{5, 0, 7, 0, 0},
73 name: "writes a packed contiguous coil range",
74 call: func(c *tcpClient) error {
75 return c.WriteCoils(t.Context(), 6, []bool{true, false, true, false, false, false, false, false, true})
77 request: []byte{15, 0, 6, 0, 9, 2, 5, 1},
78 reply: []byte{15, 0, 6, 0, 9},
81 name: "writes a holding register with function sixteen",
82 call: func(c *tcpClient) error {
83 return c.WriteRegister(t.Context(), 0x30d4, 200)
85 request: []byte{16, 0x30, 0xd4, 0, 1, 2, 0, 200},
86 reply: []byte{16, 0x30, 0xd4, 0, 1},
90 for _, tc := range testCases {
91 t.Run(tc.name, func(t *testing.T) {
92 request, err := wireCall(t, tc.call, func(header []byte) []byte {
93 return append(append(header[:4:4], words(uint16(len(tc.reply)+1))...), append([]byte{7}, tc.reply...)...)
95 require.NoError(t, err)
96 require.Equal(t, append([]byte{0, 1, 0, 0, 0, byte(len(tc.request) + 1), 7}, tc.request...), request)
101 func TestTCPClientRejectsInvalidAndTruncatedResponses(t *testing.T) {
102 testCases := []struct {
106 {name: "rejects another transaction", reply: []byte{0, 2, 0, 0, 0, 4, 7, 1, 1, 0}},
107 {name: "rejects another protocol", reply: []byte{0, 1, 0, 1, 0, 4, 7, 1, 1, 0}},
108 {name: "rejects another unit", reply: []byte{0, 1, 0, 0, 0, 4, 8, 1, 1, 0}},
109 {name: "rejects empty PDU", reply: []byte{0, 1, 0, 0, 0, 1, 7}},
110 {name: "rejects excessive PDU", reply: []byte{0, 1, 0, 0, 0, 255, 7}},
111 {name: "rejects truncated header", reply: []byte{0, 1}},
112 {name: "rejects truncated body", reply: []byte{0, 1, 0, 0, 0, 4, 7, 1}},
113 {name: "rejects wrong function", reply: []byte{0, 1, 0, 0, 0, 4, 7, 2, 1, 0}},
114 {name: "rejects exception", reply: []byte{0, 1, 0, 0, 0, 3, 7, 0x81, 2}},
115 {name: "rejects wrong byte count", reply: []byte{0, 1, 0, 0, 0, 4, 7, 1, 2, 0}},
118 for _, tc := range testCases {
119 t.Run(tc.name, func(t *testing.T) {
120 _, err := wireCall(t, func(c *tcpClient) error {
121 _, err := c.ReadCoils(t.Context(), 1)
123 }, func([]byte) []byte {
126 require.Error(t, err)
131 func TestTCPClientRejectsInvalidRangesAndAcknowledgements(t *testing.T) {
133 _, err := c.ReadInputs(t.Context(), 0)
134 require.Error(t, err)
136 _, err = c.ReadCoils(t.Context(), 2001)
137 require.Error(t, err)
139 _, err = c.ReadRegisters(t.Context(), 0, 0)
140 require.Error(t, err)
142 _, err = c.ReadRegisters(t.Context(), 0, 126)
143 require.Error(t, err)
145 _, err = c.ReadRegisters(t.Context(), 65535, 2)
146 require.Error(t, err)
147 require.Error(t, c.WriteCoils(t.Context(), 0, nil))
148 require.Error(t, c.WriteCoils(t.Context(), 65535, []bool{true, true}))
149 require.Error(t, c.WriteCoils(t.Context(), 0, make([]bool, 1969)))
150 require.Error(t, c.WriteCoil(t.Context(), 0, true))
151 require.NoError(t, c.Close())
152 require.Error(t, c.Connect(t.Context(), "", 0))
153 require.Error(t, c.Connect(t.Context(), "", 248))
154 require.Error(t, c.Connect(t.Context(), "", 1))
156 _, err = wireCall(t, func(c *tcpClient) error {
157 return c.WriteRegister(t.Context(), 0, 20)
158 }, func(header []byte) []byte {
159 return append(header[:7:7], 16, 0, 0, 0, 2)
161 require.Error(t, err)
163 _, err = wireCall(t, func(c *tcpClient) error {
164 _, readErr := c.ReadRegisters(t.Context(), 0, 1)
166 }, func(header []byte) []byte {
167 return append(header[:7:7], 3, 3, 0, 0, 0)
169 require.Error(t, err)
172 func TestTCPClientCancellationAndTimeouts(t *testing.T) {
173 testCases := []struct {
178 {name: "cancels a blocked write", cancel: true, write: true},
179 {name: "cancels a blocked read", cancel: true},
180 {name: "limits a blocked write", write: true},
181 {name: "limits a blocked read"},
184 for _, tc := range testCases {
185 t.Run(tc.name, func(t *testing.T) {
186 synctest.Test(t, func(t *testing.T) {
187 conn, peer := net.Pipe()
188 c := &tcpClient{conn: conn, unit: 1}
190 require.NoError(t, c.Close())
191 require.NoError(t, peer.Close())
194 ctx, cancel := context.WithCancel(t.Context())
197 result := make(chan error, 1)
203 _, err := io.ReadFull(peer, request[:])
208 done := make(chan error, 1)
210 _, err := c.ReadCoils(ctx, 1)
217 require.NoError(t, <-result)
223 time.Sleep(2 * time.Second)
227 require.Error(t, err)
230 require.ErrorIs(t, err, context.Canceled)
237 func wireCall(t *testing.T, call func(*tcpClient) error, response func([]byte) []byte) ([]byte, error) {
240 conn, peer := net.Pipe()
241 c := &tcpClient{conn: conn, unit: 7}
243 require.NoError(t, c.Close())
244 require.NoError(t, peer.Close())
247 requests := make(chan []byte, 1)
248 done := make(chan error, 1)
253 _, err := io.ReadFull(peer, header[:])
259 body := make([]byte, int(binary.BigEndian.Uint16(header[4:6]))-1)
260 _, err = io.ReadFull(peer, body)
261 requests <- append(header[:7:7], body...)
263 _, err = peer.Write(response(header[:]))
266 if errors.Is(err, io.ErrClosedPipe) {
270 err = errors.Join(err, peer.Close())
275 require.NoError(t, c.Close())
276 require.NoError(t, <-done)
278 return <-requests, err