+func TestTCPClientCancellationIsAnExpectedRunnerStop(t *testing.T) {
+ testCases := []struct {
+ name string
+ reading bool
+ }{
+ {name: "stops a blocked write"},
+ {name: "stops a blocked read", reading: true},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ conn, peer := net.Pipe()
+ c := &tcpClient{conn: conn, unit: 1}
+
+ t.Cleanup(func() {
+ require.NoError(t, peer.Close())
+ })
+
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ ready := make(chan error, 1)
+ if tc.reading {
+ go func() {
+ var request [12]byte
+
+ _, err := io.ReadFull(peer, request[:])
+ ready <- err
+ }()
+ }
+
+ done := make(chan error, 1)
+ go func() {
+ done <- driver.Run(ctx, []driver.Driver{&requestDriver{client: c}}, nil)
+ }()
+
+ synctest.Wait()
+
+ if tc.reading {
+ require.NoError(t, <-ready)
+ }
+
+ cancel()
+ require.NoError(t, <-done)
+ })
+ })
+ }
+}
+
+func TestTCPClientUsesCallerDeadlineAndStartsClosed(t *testing.T) {
+ client := NewTCPClient()
+ _, err := client.ReadCoils(t.Context(), 1)
+ require.Error(t, err)
+ require.NoError(t, client.Close())
+ synctest.Test(t, func(t *testing.T) {
+ conn, peer := net.Pipe()
+ c := &tcpClient{conn: conn, unit: 1}
+ t.Cleanup(func() {
+ require.NoError(t, c.Close())
+ require.NoError(t, peer.Close())
+ })
+
+ ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
+ defer cancel()
+
+ started := time.Now()
+ _, err := c.ReadCoils(ctx, 1)
+ require.Error(t, err)
+ require.Equal(t, time.Millisecond, time.Since(started))
+ })
+}
+
+func TestTCPClientCancellationPreservesIndependentFailure(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ conn, peer := net.Pipe()
+ t.Cleanup(func() {
+ require.NoError(t, peer.Close())
+ })
+
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ failure := errors.New("independent write failure")
+ transport := &interruptConn{
+ Conn: conn,
+ cancel: cancel,
+ failure: errors.Join(os.ErrDeadlineExceeded, failure),
+ interrupted: make(chan struct{}),
+ }
+ c := &tcpClient{conn: transport, unit: 1}
+ t.Cleanup(func() {
+ require.NoError(t, c.Close())
+ })
+
+ _, err := c.ReadCoils(ctx, 1)
+ require.ErrorIs(t, err, failure)
+ require.ErrorIs(t, err, context.Canceled)
+ })
+}
+
+type requestDriver struct {
+ client *tcpClient
+}
+
+func (d *requestDriver) Run(ctx context.Context) (err error) {
+ defer func() {
+ err = errors.Join(err, d.client.Close())
+ }()
+
+ _, err = d.client.ReadCoils(ctx, 1)
+
+ return err
+}
+
+func (d *requestDriver) Refresh() {
+}
+
+type interruptConn struct {
+ net.Conn
+ cancel context.CancelFunc
+ failure error
+ interrupted chan struct{}
+}
+
+func (c *interruptConn) Write([]byte) (int, error) {
+ c.cancel()
+ <-c.interrupted
+
+ return 0, c.failure
+}
+
+func (c *interruptConn) SetDeadline(deadline time.Time) error {
+ defer close(c.interrupted)
+
+ err := c.Conn.SetDeadline(deadline)
+ if err != nil {
+ return fmt.Errorf("interrupt fake connection: %w", err)
+ }
+
+ return nil
+}
+