blob: f79c1f38903c00badf07d7d864abeab749564568 [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 be
// found in the LICENSE file.
package main
import (
"context"
"fmt"
"io"
"path"
"go.fuchsia.dev/tools/integration/testsharder"
"go.fuchsia.dev/tools/lib/logger"
"go.fuchsia.dev/tools/lib/runner"
"go.fuchsia.dev/tools/net/sshutil"
"golang.org/x/crypto/ssh"
)
const (
// The test output directory to create on the Fuchsia device.
fuchsiaOutputDir = "/data/infra/testrunner"
// A conventionally used global request name for checking the status of a client
// connection to an OpenSSH server.
keepAliveOpenSSH = "keepalive@openssh.com"
)
// Tester is executes a Test.
type Tester func(context.Context, testsharder.Test, io.Writer, io.Writer) error
// SubprocessTester executes tests in local subprocesses.
type SubprocessTester struct {
dir string
env []string
}
func (t *SubprocessTester) Test(ctx context.Context, test testsharder.Test, stdout io.Writer, stderr io.Writer) error {
command := test.Command
if len(test.Command) == 0 {
if test.Path == "" {
return fmt.Errorf("test %q has no `command` or `path` set", test.Name)
}
command = []string{test.Path}
}
runner := &runner.SubprocessRunner{
Dir: t.dir,
Env: t.env,
}
return runner.Run(ctx, command, stdout, stderr)
}
// SSHTester executes tests over an SSH connection. It assumes the test.Command
// 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
newClient func(ctx context.Context) (*ssh.Client, error)
}
func NewSSHTester(newClient func(context.Context) (*ssh.Client, error)) (*SSHTester, error) {
client, err := newClient(context.Background())
if err != nil {
return nil, err
}
return &SSHTester{client: client, newClient: newClient}, nil
}
func (t *SSHTester) Test(ctx context.Context, test testsharder.Test, stdout io.Writer, stderr io.Writer) error {
if _, _, err := t.client.Conn.SendRequest(keepAliveOpenSSH, true, nil); err != nil {
logger.Errorf(ctx, "SSH client not responsive: %v", err)
client, err := t.newClient(ctx)
if err != nil {
return fmt.Errorf("failed to create new SSH client: %v", err)
}
t.client.Close()
t.client = client
}
session, err := t.client.NewSession()
if err != nil {
return err
}
defer session.Close()
runner := &runner.SSHRunner{Session: session}
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
// behavior. Later we'll delete this and use SSHTester directly.
type FuchsiaTester struct {
remoteOutputDir string
delegate *SSHTester
}
// 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(nodename string, sshKey []byte) (*FuchsiaTester, error) {
newClient := func(ctx context.Context) (*ssh.Client, error) {
config, err := sshutil.DefaultSSHConfig(sshKey)
if err != nil {
return nil, fmt.Errorf("failed to create an SSH client config: %v", err)
}
client, err := sshutil.ConnectToNode(ctx, nodename, config)
if err != nil {
return nil, fmt.Errorf("failed to connect to node %q: %v", nodename, err)
}
return client, nil
}
delegate, err := NewSSHTester(newClient)
if err != nil {
return nil, err
}
tester := &FuchsiaTester{
remoteOutputDir: fuchsiaOutputDir,
delegate: delegate,
}
return tester, nil
}
func (t *FuchsiaTester) Test(ctx context.Context, test testsharder.Test, stdout io.Writer, stderr io.Writer) error {
if len(test.Command) == 0 {
name := path.Base(test.Path)
test.Command = []string{"runtests", "-t", name, "-o", t.remoteOutputDir + "runtests"}
}
return t.delegate.Test(ctx, test, stdout, 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.delegate.Close(); err != nil {
return fmt.Errorf("failed to close delegate ssh runner: %v", err)
}
return nil
}