[testrunner] Move FuchsiaTester initialization to a function
* Update SSHTester tests as well, since we added a close method.
Change-Id: I931e41838329ac688add2cbc81a6b84a18d674c8
diff --git a/cmd/testrunner/main.go b/cmd/testrunner/main.go
index 3910b9d..49ee0bc 100644
--- a/cmd/testrunner/main.go
+++ b/cmd/testrunner/main.go
@@ -12,6 +12,7 @@
"flag"
"fmt"
"io"
+ "io/ioutil"
"log"
"os"
"time"
@@ -30,9 +31,6 @@
// The username used to authenticate with the Fuchsia device.
sshUser = "fuchsia"
- // The test output directory to create on the Fuchsia device.
- fuchsiaOutputDir = "/data/infra/testrunner"
-
// The filename to use for log_listener's stdout captured during Fuchsia tests.
syslogStdoutFilename = "syslog-stdout.txt"
@@ -215,40 +213,15 @@
}
func runFuchsiaTests(tests []testsharder.Test, output *TestRunnerOutput, devCtx *botanist.DeviceContext) error {
- if len(tests) == 0 {
- return nil
- } else if devCtx == nil {
+ if devCtx == nil {
return errors.New("no DeviceContext set; cannot execute fuchsia tests")
}
- // Initialize the connection to the Fuchsia device.
- sshClient, err := sshIntoNode(devCtx.Nodename, devCtx.SSHKey)
+ tester, err := NewFuchsiaTester(*devCtx)
if err != nil {
- return fmt.Errorf("failed to connect to node %q: %v", devCtx.Nodename, err)
+ return fmt.Errorf("failed to initialize fuchsia tester: %v", err)
}
- defer sshClient.Close()
-
- fuchsiaTester := &FuchsiaTester{
- remoteOutputDir: fuchsiaOutputDir,
- delegate: &SSHTester{
- client: sshClient,
- },
- }
-
- // Record log_listener output. This goes into the output archive as "syslog.txt"
- session, err := sshClient.NewSession()
- if err != nil {
- return err
- }
- defer session.Close()
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- syslogStdout := new(bytes.Buffer)
- syslogStderr := new(bytes.Buffer)
- runner := &testrunner.SSHRunner{Session: session}
- go runner.Run(ctx, []string{"bin/log_listener"}, syslogStdout, syslogStderr)
+ defer tester.Close()
// Ensure the syslog is always included in the output, even if tests fail.
// TODO(IN-824): Run fuchsia/linux/mac tests in go-routines and emit errors on channels.
@@ -257,16 +230,28 @@
return
}
- if err := output.Tar.TarFile(syslogStdout.Bytes(), syslogStdoutFilename); err != nil {
+ stdout, stderr := tester.SyslogOutput()
+ stdoutBytes, err := ioutil.ReadAll(stdout)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ stderrBytes, err := ioutil.ReadAll(stderr)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ if err := output.Tar.TarFile(stdoutBytes, syslogStdoutFilename); err != nil {
log.Println(err)
}
- if err := output.Tar.TarFile(syslogStderr.Bytes(), syslogStderrFilename); err != nil {
+ if err := output.Tar.TarFile(stderrBytes, syslogStderrFilename); err != nil {
log.Println(err)
}
}()
- return runTests(tests, fuchsiaTester.Test, output)
+ return runTests(tests, tester.Test, output)
}
func runTests(tests []testsharder.Test, tester Tester, output *TestRunnerOutput) error {
diff --git a/cmd/testrunner/tester.go b/cmd/testrunner/tester.go
index f16720b..bc6b77f 100644
--- a/cmd/testrunner/tester.go
+++ b/cmd/testrunner/tester.go
@@ -5,15 +5,23 @@
package main
import (
+ "bytes"
"context"
+ "fmt"
"io"
"path"
+ "fuchsia.googlesource.com/tools/botanist"
"fuchsia.googlesource.com/tools/testrunner"
"fuchsia.googlesource.com/tools/testsharder"
"golang.org/x/crypto/ssh"
)
+const (
+ // The test output directory to create on the Fuchsia device.
+ fuchsiaOutputDir = "/data/infra/testrunner"
+)
+
// Tester is executes a Test.
type Tester func(context.Context, testsharder.Test, io.Writer, io.Writer) error
@@ -40,11 +48,20 @@
}
// SSHTester executes tests over an SSH connection. It assumes the test.Command
-// contains the command line to execute on the remote machine.
+// contains the command line to execute on the remote machine. The caller should Close() the
+// tester when finished. Once closed, this object can no longer be used.
type SSHTester struct {
client *ssh.Client
}
+func NewSSHTester(devCtx botanist.DeviceContext) (*SSHTester, error) {
+ client, err := sshIntoNode(devCtx.Nodename, devCtx.SSHKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to node %q: %v", devCtx.Nodename, err)
+ }
+ return &SSHTester{client: client}, nil
+}
+
func (t *SSHTester) Test(ctx context.Context, test testsharder.Test, stdout io.Writer, stderr io.Writer) error {
session, err := t.client.NewSession()
if err != nil {
@@ -56,6 +73,15 @@
return runner.Run(ctx, test.Command, stdout, stderr)
}
+// Close stops this SSHTester. The underlying SSH connection is terminated. The object
+// is no longer usable after calling this method.
+func (t *SSHTester) Close() error {
+ return t.client.Close()
+}
+
+// FuchsiaTester executes tests on remote Fuchsia devices. The caller should Close() the
+// tester when finished. Once closed, this object can no longer be used.
+//
// This is a hack. We have to run Fuchsia tests using `runtests` on the remote device
// because there are many ways to execute Fuchsia tests and runtests already does this
// correctly. This wrapper around SSHTester is meant to keep SSHTester free of OS-specific
@@ -63,6 +89,28 @@
type FuchsiaTester struct {
remoteOutputDir string
delegate *SSHTester
+ remoteSyslog *remoteSyslog
+}
+
+// NewFuchsiaTester creates a FuchsiaTester object and starts a log_listener process on
+// the remote device. The log_listener output can be read from SysLogOutput().
+func NewFuchsiaTester(devCtx botanist.DeviceContext) (*FuchsiaTester, error) {
+ delegate, err := NewSSHTester(devCtx)
+ if err != nil {
+ return nil, err
+ }
+
+ tester := &FuchsiaTester{
+ remoteOutputDir: fuchsiaOutputDir,
+ delegate: delegate,
+ }
+
+ tester.remoteSyslog, err = newRemoteSyslog(context.Background(), delegate.client)
+ if err != nil {
+ return nil, fmt.Errorf("failed to start remote log_listener process: %v", err)
+ }
+
+ return tester, nil
}
func (t *FuchsiaTester) Test(ctx context.Context, test testsharder.Test, stdout io.Writer, stderr io.Writer) error {
@@ -70,3 +118,60 @@
test.Command = []string{"runtests", "-t", name, "-o", t.remoteOutputDir + "runtests"}
return t.delegate.Test(ctx, test, stdout, stderr)
}
+
+// SysLogOutput returns the stdout and stderr streams of the remote log_listener process.
+func (t *FuchsiaTester) SyslogOutput() (stdout, stderr *bytes.Reader) {
+ return t.remoteSyslog.stdout(), t.remoteSyslog.stderr()
+}
+
+// Close stops this FuchsiaTester. The remote log_listener process is terminated along
+// with the underlying SSH connection. The object is no longer usable after calling this
+// method.
+func (t *FuchsiaTester) Close() error {
+ if err := t.remoteSyslog.stop(); err != nil {
+ return fmt.Errorf("failed to terminate remote log_listener: %v", err)
+ }
+ if err := t.delegate.Close(); err != nil {
+ return fmt.Errorf("failed to close delegate ssh runner: %v", err)
+ }
+ return nil
+}
+
+// A handle to a remote log_listener process on some Fuchsia device.
+type remoteSyslog struct {
+ stdoutBuf *bytes.Buffer
+ stderrBuf *bytes.Buffer
+ session *ssh.Session
+}
+
+// Starts a remote process on the Fuchsia device to run log_listener. The output of the
+// process is written to the given stdout and stderr streams. The process will terminate
+// when the FuchsiaTester is Close().
+func newRemoteSyslog(ctx context.Context, client *ssh.Client) (*remoteSyslog, error) {
+ session, err := client.NewSession()
+ if err != nil {
+ return nil, err
+ }
+
+ sl := &remoteSyslog{
+ session: session,
+ stdoutBuf: new(bytes.Buffer),
+ stderrBuf: new(bytes.Buffer),
+ }
+
+ runner := &testrunner.SSHRunner{Session: session}
+ go runner.Run(ctx, []string{"bin/log_listener"}, sl.stdoutBuf, sl.stderrBuf)
+ return sl, nil
+}
+
+func (sl *remoteSyslog) stdout() *bytes.Reader {
+ return bytes.NewReader(sl.stdoutBuf.Bytes())
+}
+
+func (sl *remoteSyslog) stderr() *bytes.Reader {
+ return bytes.NewReader(sl.stderrBuf.Bytes())
+}
+
+func (sl *remoteSyslog) stop() error {
+ return sl.session.Close()
+}
diff --git a/cmd/testrunner/tester_test.go b/cmd/testrunner/tester_test.go
index b40de24..b152c4e 100644
--- a/cmd/testrunner/tester_test.go
+++ b/cmd/testrunner/tester_test.go
@@ -15,21 +15,50 @@
)
func TestTester(t *testing.T) {
- tester := &SubprocessTester{}
- cases := []testCase{
+ cases := []struct {
+ name string
+ test testsharder.Test
+ stdout string
+ stderr string
+ }{
{
- name: "should run a command a local subprocess",
- tester: tester.Test,
+ name: "should run a command a local subprocess",
test: testsharder.Test{
Name: "hello_world_test",
// Assumes that we're running on a Unix system.
Command: []string{"/bin/echo", "Hello world!"},
},
- expectedOutput: "Hello world!",
+ stdout: "Hello world!",
},
}
- runTestCases(t, cases)
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ tester := &SubprocessTester{}
+
+ stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
+
+ if err := tester.Test(context.Background(), tt.test, stdout, stderr); err != nil {
+ t.Errorf("failed to exected test %q: %v", tt.test.Name, err)
+ return
+ }
+
+ // Compare stdout
+ actual := strings.TrimSpace(stdout.String())
+ expected := tt.stdout
+ if actual != expected {
+ t.Errorf("got stdout %q but wanted %q", actual, expected)
+ }
+
+ // Compare stderr
+ actual = strings.TrimSpace(stderr.String())
+ expected = tt.stderr
+ if actual != expected {
+ t.Errorf("got stderr %q but wanted %q", actual, expected)
+ }
+ })
+ }
}
// Verifies that SSHTester can execute tests on a remote device. These tests are
@@ -44,57 +73,73 @@
t.Fatal(err)
}
- client, err := sshIntoNode(devCtx.Nodename, devCtx.SSHKey)
- if err != nil {
- t.Fatalf("failed to connect to node '%s': %v", devCtx.Nodename, err)
- }
-
- tester := &SSHTester{client: client}
- cases := []testCase{
+ cases := []struct {
+ name string
+ tests []testsharder.Test
+ stdout string
+ stderr string
+ }{
{
- name: "should run a command over SSH",
- tester: tester.Test,
- test: testsharder.Test{
- Name: "hello_world_test",
- // Just 'echo' and not '/bin/echo' because this assumes we're running on
- // Fuchsia.
- Command: []string{"echo", "Hello world!"},
+ name: "should run a command over SSH",
+ tests: []testsharder.Test{
+ {
+ Name: "hello_world_test",
+ // Just 'echo' and not '/bin/echo' because this assumes we're running on
+ // Fuchsia.
+ Command: []string{"echo", "Hello world!"},
+ },
},
- expectedOutput: "Hello world!",
+ stdout: "Hello world!",
},
{
- name: "should run successive commands over SSH",
- tester: tester.Test,
- test: testsharder.Test{
- Name: "hello_again_test",
- Command: []string{"echo", "Hello again!"},
+ name: "should run successive commands over SSH",
+ tests: []testsharder.Test{
+ {
+ Name: "test_1",
+ Command: []string{"echo", "this is test 1"},
+ },
+ {
+ Name: "test_2",
+ Command: []string{"echo", "this is test 2"},
+ },
},
- expectedOutput: "Hello again!",
+ stdout: "this is test 1\nthis is test 2",
},
}
- runTestCases(t, cases)
-}
-
-type testCase struct {
- name string
- test testsharder.Test
- tester Tester
- expectedOutput string
- expectError bool
-}
-
-func runTestCases(t *testing.T, cases []testCase) {
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
- output := new(bytes.Buffer)
- err := tt.tester(context.Background(), tt.test, output, output)
- if tt.expectError && err == nil {
- t.Fatalf("%s: got nil, wanted error", tt.name)
- } else if !tt.expectError && err != nil {
- t.Fatalf("%s: got err '%v', wanted nil", tt.name, err)
- } else if tt.expectedOutput != strings.TrimSpace(output.String()) {
- t.Fatalf("%s: got output: '%s', want: '%s'", tt.name, output.String(), tt.expectedOutput)
+ tester, err := NewSSHTester(*devCtx)
+ if err != nil {
+ t.Errorf("failed to intialize tester: %v", err)
+ return
+ }
+
+ stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
+ for _, test := range tt.tests {
+ if err := tester.Test(context.Background(), test, stdout, stderr); err != nil {
+ t.Error(err)
+ return
+ }
+ }
+
+ if err := tester.Close(); err != nil {
+ t.Fatalf("failed to close tester: %v", err)
+ }
+
+ // Compare stdout
+ actual := strings.TrimSpace(stdout.String())
+ expected := tt.stdout
+ if actual != expected {
+ t.Errorf("got stdout %q but wanted %q", actual, expected)
+ }
+
+ // Compare stderr
+ actual = strings.TrimSpace(stderr.String())
+ expected = tt.stderr
+ if actual != expected {
+ t.Errorf("got stderr %q but wanted %q", actual, expected)
}
})
}