| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package serial |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "math/rand" |
| "net" |
| "os" |
| "testing" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| "golang.org/x/sync/errgroup" |
| |
| "go.fuchsia.dev/fuchsia/tools/lib/iomisc" |
| ) |
| |
| type fakeSerialServer struct { |
| received []byte |
| shutdownString string |
| socketPath string |
| listeningChan chan bool |
| } |
| |
| func (s *fakeSerialServer) Serve() error { |
| listener, err := net.Listen("unix", s.socketPath) |
| if err != nil { |
| s.listeningChan <- false |
| return fmt.Errorf("Listen(%s) failed: %v", s.socketPath, err) |
| } |
| defer listener.Close() |
| s.listeningChan <- true |
| conn, err := listener.Accept() |
| if err != nil { |
| return fmt.Errorf("Accept() failed: %v", err) |
| } |
| defer conn.Close() |
| // Signal we're ready to accept input. |
| if _, err := conn.Write([]byte(consoleCursor)); err != nil { |
| return fmt.Errorf("conn.Write() failed: %v", err) |
| } |
| reader := iomisc.NewMatchingReader(conn, []byte(s.shutdownString)) |
| for { |
| buf := make([]byte, 1024) |
| bytesRead, err := reader.Read(buf) |
| s.received = append(s.received, buf[:bytesRead]...) |
| if err != nil { |
| if err == io.EOF { |
| return nil |
| } |
| return fmt.Errorf("conn.Read() failed: %v", err) |
| } |
| } |
| } |
| |
| func TestRunCommands(t *testing.T) { |
| socketPath := fmt.Sprintf("%d.sock", rand.Uint32()) |
| defer os.Remove(socketPath) |
| server := fakeSerialServer{ |
| shutdownString: "dm shutdown", |
| socketPath: socketPath, |
| listeningChan: make(chan bool), |
| } |
| eg := errgroup.Group{} |
| eg.Go(server.Serve) |
| |
| if !<-server.listeningChan { |
| t.Fatalf("fakeSerialServer.Serve() returned: %v", eg.Wait()) |
| } |
| |
| clientSocket, err := NewSocket(context.Background(), socketPath) |
| if err != nil { |
| t.Fatalf("NewSocket() failed: %v", err) |
| } |
| |
| // Test that SetIOTimeout causes the socket to return from Read() even |
| // if there is nothing to read. |
| clientSocket.SetIOTimeout(time.Nanosecond) |
| buf := make([]byte, 10) |
| _, err = clientSocket.Read(buf) |
| if err == nil { |
| t.Errorf("expected Read() to get an error, got nil") |
| } |
| clientSocket.SetIOTimeout(defaultSocketIOTimeout) |
| |
| diagnosticCmds = []Command{ |
| { |
| Cmd: []string{"foo"}, |
| // Ensure we don't waste time sleeping in this test. |
| SleepDuration: time.Microsecond, |
| }, |
| { |
| Cmd: []string{"bar"}, |
| // Ensure we don't waste time sleeping in this test. |
| SleepDuration: time.Microsecond, |
| }, |
| { |
| // This is a hack to ensure the shutdown command gets sent to the serial server. |
| // Rather than introduce a new synchronization mechanism, just use the code under test's |
| // existing mechanism for sending commands. |
| Cmd: []string{server.shutdownString}, |
| // Ensure we don't waste time sleeping in this test. |
| SleepDuration: time.Microsecond, |
| }, |
| } |
| // RunDiagnostics calls RunCommands so this covers both functions for testability. |
| err = RunDiagnostics(context.Background(), clientSocket) |
| if err != nil { |
| t.Errorf("RunDiagnostics failed: %v", err) |
| } |
| if err = eg.Wait(); err != nil { |
| t.Errorf("server returned: %v", err) |
| } |
| if err = clientSocket.Close(); err != nil { |
| t.Errorf("clientSocket.Close() returned: %v", err) |
| } |
| // Verify that each command was seen in the received data, and in the |
| // proper order. The first command run was an empty command in NewSocket(). |
| expectedCommands := asSerialCmd([]string{}) |
| for _, cmd := range diagnosticCmds { |
| expectedCommands += asSerialCmd(cmd.Cmd) |
| } |
| if diff := cmp.Diff(expectedCommands, string(server.received)); diff != "" { |
| t.Errorf("Unexpected server.received (-want +got):\n%s", diff) |
| } |
| } |