| // Copyright 2019 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 ( |
| "bufio" |
| "bytes" |
| "context" |
| "crypto/rand" |
| "io" |
| "path/filepath" |
| |
| "io/ioutil" |
| "net" |
| "os" |
| "testing" |
| |
| "go.fuchsia.dev/fuchsia/tools/lib/color" |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| ) |
| |
| var warningLogger = logger.NewLogger(logger.WarningLevel, color.NewColor(color.ColorNever), nil, nil, "test-serial") |
| |
| func TestServerDrainsSerial(t *testing.T) { |
| serial, device := serialAndDevice() |
| |
| s := NewServer(serial, ServerOptions{}) |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| |
| var serverErr error |
| serverDone := make(chan struct{}) |
| go func() { |
| l, _ := testListener(t) |
| serverErr = s.Run(ctx, l) |
| close(serverDone) |
| }() |
| |
| // copy 1mb of random output to the server, if there were general IO problems |
| // we'd likely block at around 64kb, this should shake out any common issues |
| // with the normal copy pipeline |
| _, err := io.CopyN(device, rand.Reader, 1024*1024) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| cancel() |
| <-serverDone |
| if serverErr != nil { |
| t.Fatalf("unexpected server error: %s", serverErr) |
| } |
| } |
| |
| func TestServerAuxOutput(t *testing.T) { |
| serial, device := serialAndDevice() |
| aux := mkTempFile(t) |
| |
| s := NewServer(serial, ServerOptions{AuxiliaryOutput: aux}) |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| |
| var serverErr error |
| serverDone := make(chan struct{}) |
| go func() { |
| l, _ := testListener(t) |
| serverErr = s.Run(ctx, l) |
| close(serverDone) |
| }() |
| |
| buf := make([]byte, 1024*1024) |
| if _, err := io.ReadFull(rand.Reader, buf); err != nil { |
| t.Fatal(err) |
| } |
| n, err := device.Write(buf) |
| if err != nil || n != len(buf) { |
| t.Fatalf("device write short or error: %s (%d/%d)", err, n, len(buf)) |
| } |
| |
| cancel() |
| <-serverDone |
| if serverErr != nil { |
| t.Fatalf("unexpected server error: %s", serverErr) |
| } |
| if _, err := aux.Seek(0, io.SeekStart); err != nil { |
| t.Fatal(err) |
| } |
| gotBuf, err := ioutil.ReadAll(aux) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !bytes.Equal(gotBuf, buf) { |
| t.Fatalf("got\n%x\nwant\n%x", gotBuf, buf) |
| } |
| } |
| |
| func TestServerSocketOutput(t *testing.T) { |
| serial, device := serialAndDevice() |
| aux := mkTempFile(t) |
| |
| s := NewServer(serial, ServerOptions{AuxiliaryOutput: aux, Logger: warningLogger}) |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| |
| l, sockPath := testListener(t) |
| |
| var serverErr error |
| serverDone := make(chan struct{}) |
| go func() { |
| serverErr = s.Run(ctx, l) |
| close(serverDone) |
| }() |
| |
| c, err := net.DialUnix("unix", nil, &net.UnixAddr{Net: "unix", Name: sockPath}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // A device writes things to serial, eventually writing a completion match the |
| // reader was looking for |
| go func() { |
| for i := 0; i < 10000; i++ { |
| if _, err := io.WriteString(device, "hello world\n"); err != nil { |
| t.Fatal(err) |
| } |
| } |
| if _, err := io.WriteString(device, "MAGIC!\n"); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| |
| // a client is tailing the log and tells the server it's done when it reads the magic: |
| bio := bufio.NewReader(c) |
| for { |
| l, err := bio.ReadString('\n') |
| if err != nil { |
| t.Fatal(err) |
| } |
| if l == "MAGIC!\n" { |
| break |
| } |
| } |
| |
| cancel() |
| <-serverDone |
| if serverErr != nil { |
| t.Fatalf("unexpected server error: %s", serverErr) |
| } |
| } |
| |
| func TestServerSerialWrites(t *testing.T) { |
| serial, device := serialAndDevice() |
| aux := mkTempFile(t) |
| |
| s := NewServer(serial, ServerOptions{AuxiliaryOutput: aux, Logger: warningLogger}) |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| |
| l, sockPath := testListener(t) |
| |
| var serverErr error |
| serverDone := make(chan struct{}) |
| go func() { |
| serverErr = s.Run(ctx, l) |
| close(serverDone) |
| }() |
| |
| c, err := net.DialUnix("unix", nil, &net.UnixAddr{Net: "unix", Name: sockPath}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| var expectedCount = 10000 |
| // A test driver writes things to conn |
| go func() { |
| for i := 0; i < expectedCount-1; i++ { |
| if _, err := io.WriteString(c, "hello world\n"); err != nil { |
| t.Fatal(err) |
| } |
| } |
| if _, err := io.WriteString(c, "MAGIC!\n"); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| |
| var count int |
| // a device is receiving the input from the conn |
| bio := bufio.NewReader(device) |
| for { |
| count++ |
| l, err := bio.ReadString('\n') |
| if err != nil { |
| t.Fatal(err) |
| } |
| if l == "MAGIC!\n" { |
| break |
| } |
| } |
| |
| cancel() |
| <-serverDone |
| if serverErr != nil { |
| t.Fatalf("unexpected server error: %s", serverErr) |
| } |
| |
| if expectedCount != count { |
| t.Fatalf("got %d, want %d", count, expectedCount) |
| } |
| } |
| |
| func TestServerSerialClosing(t *testing.T) { |
| serial, _ := serialAndDevice() |
| aux := mkTempFile(t) |
| |
| s := NewServer(serial, ServerOptions{AuxiliaryOutput: aux, Logger: warningLogger}) |
| |
| ctx := context.Background() |
| |
| l, sockPath := testListener(t) |
| |
| var serverErr error |
| serverDone := make(chan struct{}) |
| go func() { |
| serverErr = s.Run(ctx, l) |
| close(serverDone) |
| }() |
| |
| c, err := net.DialUnix("unix", nil, &net.UnixAddr{Net: "unix", Name: sockPath}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // serial goes away, say someone pulls the plug |
| serial.Close() |
| |
| // the connection should get closed |
| io.Copy(ioutil.Discard, c) |
| |
| // the server should exit cleanly |
| <-serverDone |
| // and it returns the underlying io error |
| if serverErr == nil { |
| t.Fatalf("server failed to return expected error") |
| } |
| } |
| |
| func TestIsErrNetClosing(t *testing.T) { |
| // see golang issue 4373, this is the sad story from upstream, and the pattern |
| // we follow is similar to that of the net/http and net/http2 package. There's |
| // a variable stashed away in internal/poll, but it's not actually exported to |
| // us for comparison. |
| |
| l, err := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "foo.sock"}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| l.Close() |
| _, err = l.Accept() |
| |
| if !IsErrNetClosing(err) { |
| t.Fatalf("expected a wrapped errnetclosing, got: %#v", err) |
| } |
| } |
| |
| // 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 testListener(t *testing.T) (net.Listener, string) { |
| t.Helper() |
| |
| name := filepath.Join(t.TempDir(), "test-serial-listener") |
| l, err := net.ListenUnix("unix", &net.UnixAddr{Name: name, Net: "unix"}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| l.SetUnlinkOnClose(true) |
| return l, name |
| } |
| |
| 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() |
| } |
| |
| // mkTempFile returns a new temporary file that will be cleaned up |
| // automatically. |
| func mkTempFile(t *testing.T) *os.File { |
| f, err := os.Create(filepath.Join(t.TempDir(), "serial-test")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(func() { |
| if err := f.Close(); err != nil { |
| t.Error(err) |
| } |
| }) |
| return f |
| } |