blob: 5fa506867c46ed6634c32f7e266c163024f0697f [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can
// found in the LICENSE file.
package serial
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net"
"os"
"path/filepath"
"syscall"
"testing"
)
const (
writeBufferSize = 1024
)
// Creates a "serial connection" in terms of its host- and device-side
// descriptors. They are implemented with synchronous in-memory pipes, so
// associated writes and reads will be one-to-one (with the usual caveats of
// io.Pipe).
func serialAndDevice() (io.ReadWriteCloser, io.ReadWriteCloser) {
rs, wd := io.Pipe()
rd, ws := io.Pipe()
serial := &joinedPipeEnds{rs, ws}
device := &joinedPipeEnds{rd, wd}
return serial, device
}
func socketPath() string {
// We randomly construct a socket path that is highly improbable to collide with anything.
randBytes := make([]byte, 16)
rand.Read(randBytes)
return filepath.Join(os.TempDir(), "serial"+hex.EncodeToString(randBytes)+".sock")
}
func TestServer(t *testing.T) {
serial, device := serialAndDevice()
defer func() {
serial.Close()
device.Close()
}()
s := NewServer(serial, ServerOptions{
WriteBufferSize: writeBufferSize,
})
path := socketPath()
addr := &net.UnixAddr{Name: path, Net: "unix"}
l, err := net.ListenUnix("unix", addr)
if err != nil {
t.Fatal(err)
}
defer l.Close()
ctx, cancel := context.WithCancel(context.Background())
// Run the server in the main routine and the tests in a separate routine so
// that we can actually test the shutdown functionality of the server (by the
// termination of this program).
go testServer(t, s, device, path, nil, cancel)
if err := s.Run(ctx, l); err != nil {
t.Fatalf("server encountered shutdown error: %v", err)
}
}
func TestServerWithAuxOutput(t *testing.T) {
serial, device := serialAndDevice()
auxOutputReader, auxOutput := io.Pipe()
defer func() {
serial.Close()
device.Close()
auxOutput.Close()
auxOutputReader.Close()
}()
s := NewServer(serial, ServerOptions{
WriteBufferSize: writeBufferSize,
AuxiliaryOutput: auxOutput,
})
path := socketPath()
addr := &net.UnixAddr{Name: path, Net: "unix"}
l, err := net.ListenUnix("unix", addr)
if err != nil {
t.Fatal(err)
}
defer l.Close()
ctx, cancel := context.WithCancel(context.Background())
// Run the server in the main routine and the tests in a separate routine so
// that we can actually test the shutdown functionality of the server (by the
// termination of this program).
go testServer(t, s, device, path, auxOutputReader, cancel)
if err := s.Run(ctx, l); err != nil {
t.Fatalf("server encountered shutdown error: %v", err)
}
}
func testServer(t *testing.T, s *Server, device io.ReadWriter, socketPath string, auxOutputReader io.Reader, cancel context.CancelFunc) {
t.Helper()
var socket net.Conn
for {
var err error
socket, err = net.Dial("unix", socketPath)
if err != nil {
t.Logf("failed to open client socket connection: %v", err)
continue
}
break
}
defer socket.Close()
//
// X -> device -> serial -> socket -> Y
//
// Make the device "write to serial" on its side. This should unblock the
// reads being made from serial on the host side and forwarded to the
// socket. Reads from the socket are in turn blocking, so we should be able
// to reliably verify that X == Y.
//
t.Run("writes to serial can be read from socket", func(t *testing.T) {
input := []byte("written to serial!")
if _, err := device.Write(input); err != nil {
t.Fatalf("failed to write to device: %v", err)
}
if auxOutputReader != nil {
// Read first from the auxiliary output, as the teeing logic from serial should
// block until we read from the other end of the pipe.
readAndCheckBytes(t, auxOutputReader, "teed output", input)
}
readAndCheckBytes(t, socket, "socket", input)
})
//
// X -> socket -> serial -> device -> Y
//
// Have the device "read from serial" on its side after bytes have been
// written to the socket. This read will block until the bytes have been
// forwarded from the server socket to serial on the host side, which will
// in turn unblock the initial read. This is all to say that this case too
// should be a reliable verification that X == Y.
//
t.Run("writes to socket can be read from serial", func(t *testing.T) {
input := []byte("written to the socket!")
if _, err := socket.Write(input); err != nil {
t.Fatalf("failed to write to socket: %v", err)
}
readAndCheckBytes(t, device, "serial", input)
})
t.Run("server shuts down", func(t *testing.T) {
cancel()
})
}
func readAndCheckBytes(t *testing.T, r io.Reader, readerName string, expected []byte) {
t.Helper()
p := make([]byte, writeBufferSize)
n, err := r.Read(p)
if (err != nil && err != io.EOF) || n == 0 {
t.Fatalf("failed to read from %s: %v", readerName, err)
}
actual := p[0:n]
if bytes.Compare(expected, actual) != 0 {
t.Errorf("unexpected bytes read from %s:\nexpected: %q\nactual: %q", readerName, expected, actual)
}
}
type joinedPipeEnds struct {
r *io.PipeReader
w *io.PipeWriter
}
func (pe *joinedPipeEnds) Read(p []byte) (int, error) {
return pe.r.Read(p)
}
func (pe *joinedPipeEnds) Write(p []byte) (int, error) {
return pe.w.Write(p)
}
func (pe *joinedPipeEnds) Close() error {
if err := pe.r.Close(); err != nil {
pe.w.Close()
return err
}
return pe.w.Close()
}
func TestErrorSanitization(t *testing.T) {
compare := func(actual, expected error) {
if actual != expected {
t.Errorf("expected: %v\nactual: %v", expected, actual)
}
}
t.Run("context canceled -> nil", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
compare(sanitizeError(ctx, fmt.Errorf("real error")), nil)
})
testCases := []struct {
name string
input error
expected error
}{
{
name: "nil -> nil",
input: nil,
expected: nil,
},
{
name: "EPIPE read -> nil",
input: &net.OpError{
Err: &os.SyscallError{
Syscall: "read",
Err: syscall.EPIPE,
},
},
expected: nil,
},
{
name: "EPIPE write -> nil",
input: &net.OpError{
Err: &os.SyscallError{
Syscall: "write",
Err: syscall.EPIPE,
},
},
expected: nil,
},
{
name: "ECONNRESET read -> nil",
input: &net.OpError{
Err: &os.SyscallError{
Syscall: "read",
Err: syscall.ECONNRESET,
},
},
expected: nil,
},
{
name: "ECONNRESET write -> nil",
input: &net.OpError{
Err: &os.SyscallError{
Syscall: "write",
Err: syscall.ECONNRESET,
},
},
expected: nil,
},
{
name: "other error -> itself",
input: os.ErrNotExist,
expected: os.ErrNotExist,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := sanitizeError(context.Background(), tc.input)
compare(actual, tc.expected)
})
}
}