blob: f175689b147a4484960c43ecfcb5e0000d211fbc [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package bundle
import (
"context"
"io"
"os"
"path/filepath"
"reflect"
gotesting "testing"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/empty"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc"
"google.golang.org/protobuf/testing/protocmp"
"go.chromium.org/tast/core/internal/logging"
"go.chromium.org/tast/core/internal/protocol"
"go.chromium.org/tast/core/internal/rpc"
"go.chromium.org/tast/core/internal/sshtest"
"go.chromium.org/tast/core/internal/testing"
"go.chromium.org/tast/core/testutil"
)
type fakeFixture struct {
setUp func(ctx context.Context, s *testing.FixtState) interface{}
tearDown func(ctx context.Context, s *testing.FixtState)
}
var _ testing.FixtureImpl = (*fakeFixture)(nil)
func (ff *fakeFixture) SetUp(ctx context.Context, s *testing.FixtState) interface{} {
if ff.setUp != nil {
return ff.setUp(ctx, s)
}
return nil
}
func (ff *fakeFixture) PreTest(ctx context.Context, s *testing.FixtTestState) {}
func (ff *fakeFixture) PostTest(ctx context.Context, s *testing.FixtTestState) {}
func (ff *fakeFixture) TearDown(ctx context.Context, s *testing.FixtState) {
if ff.tearDown != nil {
ff.tearDown(ctx, s)
}
}
func (ff *fakeFixture) Reset(ctx context.Context) error {
panic("Reset is not supported for remote fixtures yet.")
}
// startFakeFixtureService starts a fixture service server and returns a client connected to it.
// stop must be called by the caller.
func startFakeFixtureService(ctx context.Context, t *gotesting.T, reg *testing.Registry) (rfcl protocol.FixtureService_RunFixtureClient, stop func()) {
t.Helper()
sr, cw := io.Pipe()
cr, sw := io.Pipe()
stopped := make(chan error, 1)
go func() {
stopped <- rpc.RunServer(sr, sw, nil, func(srv *grpc.Server, req *protocol.HandshakeRequest) error {
registerFixtureService(srv, reg)
return nil
})
}()
var stopFunc []func()
stop = func() {
for i := len(stopFunc) - 1; i >= 0; i-- {
stopFunc[i]()
}
}
success := false
defer func() {
if !success {
stop()
}
}()
stopFunc = append(stopFunc, func() { // stop the server
cw.Close()
cr.Close()
if err := <-stopped; err != nil {
t.Errorf("Server error: %v", err)
}
})
rpcCL, err := rpc.NewClient(ctx, cr, cw, &protocol.HandshakeRequest{
NeedUserServices: false,
})
if err != nil {
t.Fatalf("rpc.NewClient: %v", err)
}
stopFunc = append(stopFunc, func() {
if err := rpcCL.Close(); err != nil {
t.Errorf("rpcCL.Close(): %v", err)
}
})
cl := protocol.NewFixtureServiceClient(rpcCL.Conn())
rfcl, err = cl.RunFixture(ctx)
if err != nil {
t.Fatal(err)
}
stopFunc = append(stopFunc, func() {
// Make sure the server code finishes. No error check; tests may
// already have called it.
rfcl.CloseSend()
for {
if _, err := rfcl.Recv(); err != nil {
return
}
}
})
success = true
return rfcl, stop
}
func TestFixtureServiceResponses(t *gotesting.T) {
td := sshtest.NewTestData(nil)
defer td.Close()
requests := []*protocol.RunFixtureRequest{
{
Control: &protocol.RunFixtureRequest_Push{
Push: &protocol.RunFixturePushRequest{
Name: "fake",
Config: &protocol.RunFixtureConfig{
ConnectionSpec: td.Srvs[0].Addr().String(),
KeyFile: td.UserKeyFile,
},
},
},
},
{
Control: &protocol.RunFixtureRequest_Pop{
Pop: &protocol.RunFixturePopRequest{},
},
},
}
for _, tc := range []struct {
name string
fixt *fakeFixture
wantResults [][]*protocol.RunFixtureResponse
}{
{
name: "success",
fixt: &fakeFixture{
setUp: func(ctx context.Context, s *testing.FixtState) interface{} {
s.Log("SetUp")
logging.Info(ctx, "SetUp context log")
return nil
},
tearDown: func(ctx context.Context, s *testing.FixtState) {
s.Log("TearDown")
},
},
wantResults: [][]*protocol.RunFixtureResponse{
{
{Control: &protocol.RunFixtureResponse_Log{Log: "SetUp"}},
{Control: &protocol.RunFixtureResponse_Log{Log: "SetUp context log"}},
{Control: &protocol.RunFixtureResponse_RequestDone{RequestDone: &empty.Empty{}}},
}, {
{Control: &protocol.RunFixtureResponse_Log{Log: "TearDown"}},
{Control: &protocol.RunFixtureResponse_RequestDone{RequestDone: &empty.Empty{}}},
},
},
},
{
name: "panic",
fixt: &fakeFixture{
setUp: func(ctx context.Context, s *testing.FixtState) interface{} {
panic("SetUp panic")
},
tearDown: func(ctx context.Context, s *testing.FixtState) {
t.Error("TearDown called unexpectedly")
},
},
wantResults: [][]*protocol.RunFixtureResponse{
{
{Control: &protocol.RunFixtureResponse_Error{Error: &protocol.RunFixtureError{Reason: "Panic: SetUp panic"}}},
{Control: &protocol.RunFixtureResponse_RequestDone{RequestDone: &empty.Empty{}}},
},
{
{Control: &protocol.RunFixtureResponse_RequestDone{RequestDone: &empty.Empty{}}},
},
},
},
} {
t.Run(tc.name, func(t *gotesting.T) {
reg := testing.NewRegistry("bundle")
reg.AddFixtureInstance(&testing.FixtureInstance{Name: "fake", Impl: tc.fixt})
ctx := context.Background()
rfcl, stop := startFakeFixtureService(ctx, t, reg)
defer stop()
// responses reads responses from rfcl. It checks fields not suitable for exact
// comparison (e.g. timestamp) are non-zero and fills in zero values.
responses := func() []*protocol.RunFixtureResponse {
var res []*protocol.RunFixtureResponse
for {
r, err := rfcl.Recv()
if err == io.EOF {
return res
} else if err != nil {
t.Fatalf("rfcl.Recv(): %v", err)
return nil
}
if ts := r.Timestamp; ts.GetNanos() == 0 && ts.GetSeconds() == 0 {
t.Fatalf("r.Timestamp = 0, want non-zero")
}
r.Timestamp = nil
res = append(res, r)
switch x := r.Control.(type) {
case *protocol.RunFixtureResponse_RequestDone:
return res
case *protocol.RunFixtureResponse_Error:
if x.Error.File == "" {
t.Fatalf(`Error.File = "", want file path`)
}
x.Error.File = ""
if x.Error.Line == 0 {
t.Fatalf(`Error.Line = 0, want line number`)
}
x.Error.Line = 0
if x.Error.Stack == "" {
t.Fatalf(`Error.Stack = "", want stack trace`)
}
x.Error.Stack = ""
}
}
}
var got [][]*protocol.RunFixtureResponse
for _, req := range requests {
rfcl.Send(req)
got = append(got, responses())
}
if diff := cmp.Diff(got, tc.wantResults, protocmp.Transform()); diff != "" {
t.Errorf("Results mismatch (-got +want):\n%v", diff)
}
if err := rfcl.CloseSend(); err != nil {
t.Errorf("rfcl.CloseSend() = %v, want nil", err)
}
if r, err := rfcl.Recv(); err != io.EOF {
t.Errorf("rfcl.Recv() = %v, %v, want EOF", r, err)
}
})
}
}
func TestFixtureServiceParameters(t *gotesting.T) {
td := sshtest.NewTestData(nil)
defer td.Close()
tmpDir := testutil.TempDir(t)
defer os.RemoveAll(tmpDir)
cfg := &protocol.RunFixtureConfig{
TempDir: filepath.Join(tmpDir, "tmp"),
ConnectionSpec: td.Srvs[0].Addr().String(),
KeyFile: td.UserKeyFile,
OutDir: filepath.Join(tmpDir, "out"),
TestVars: map[string]string{"var": "value"},
LocalBundleDir: "/bogus/bundle",
CheckSoftwareDeps: true,
AvailableSoftwareFeatures: []string{"valid"},
UnavailableSoftwareFeatures: []string{"missing"},
// TODO(oka): Test Devservers and DataDir after Fixture.Data is implemented.
// TODO(oka): Test features after Fixture.*Deps are implemented.
// TODO(oka): Consider testing TlwServer, DutName, BuildArtifactsUrl and DownloadMode.
}
reg := testing.NewRegistry("bundle")
reg.AddFixtureInstance(&testing.FixtureInstance{
Name: "fake",
Vars: []string{"var"},
SetUpTimeout: time.Second,
Impl: &fakeFixture{
setUp: func(ctx context.Context, s *testing.FixtState) interface{} {
if ctx.Err() != nil {
t.Errorf("ctx.Err() = %v", ctx.Err())
}
if got, want := testing.ExtractLocalBundleDir(s.RPCHint()), cfg.LocalBundleDir; got != want {
t.Errorf("LocalBundleDir = %v, want %v", got, want)
}
if got, want := testing.ExtractTestVars(s.RPCHint()), cfg.TestVars; !reflect.DeepEqual(got, want) {
t.Errorf("TestVars = %v, want %v", got, want)
}
if !s.DUT().Connected(ctx) {
t.Error("s.DUT().Connected() = false, want true")
}
if got, want := s.OutDir(), filepath.Join(cfg.OutDir, "fake"); got != want {
t.Errorf("s.OutDir() = %v, want %v", got, want)
}
if got, want := os.TempDir(), cfg.TempDir; got != want {
t.Errorf("os.TempDir() = %s, want %s", got, want)
}
if got, want := s.RequiredVar("var"), "value"; got != want {
t.Errorf(`s.RequiredVar("var") = %s, want %s`, got, want)
}
return nil
},
},
})
ctx := context.Background()
rfcl, stop := startFakeFixtureService(ctx, t, reg)
defer stop()
if err := rfcl.Send(&protocol.RunFixtureRequest{
Control: &protocol.RunFixtureRequest_Push{
Push: &protocol.RunFixturePushRequest{
Name: "fake",
Config: cfg,
},
},
}); err != nil {
t.Fatal("rfcl.Send():", err)
}
got, err := rfcl.Recv()
if err != nil {
t.Fatal("rfcl.Recv():", err)
}
if got.GetRequestDone() == nil {
t.Errorf("Got response %v, want RequestDone", got)
}
if _, err := os.Stat(cfg.TempDir); err != nil {
t.Errorf("Non-empty cfg.TempDir should not be removed; os.Stat(%v): %v", cfg.TempDir, err)
}
}
func TestFixtureServiceDefaultTempDir(t *gotesting.T) {
td := sshtest.NewTestData(nil)
defer td.Close()
reg := testing.NewRegistry("bundle")
// If TempDir is not set, fixture service should create a temporary
// directory for fixtures to use, and remove it after the pop operation.
cfg := &protocol.RunFixtureConfig{
TempDir: "",
ConnectionSpec: td.Srvs[0].Addr().String(),
KeyFile: td.UserKeyFile,
}
origTempDir := os.TempDir()
var setUpTempDir string
var tearDownTempDir string
reg.AddFixtureInstance(&testing.FixtureInstance{
Name: "fake",
Impl: &fakeFixture{
setUp: func(ctx context.Context, s *testing.FixtState) interface{} {
setUpTempDir = os.TempDir()
return nil
},
tearDown: func(ctx context.Context, s *testing.FixtState) {
tearDownTempDir = os.TempDir()
},
},
})
rfcl, stop := startFakeFixtureService(context.Background(), t, reg)
defer stop()
if err := rfcl.Send(&protocol.RunFixtureRequest{
Control: &protocol.RunFixtureRequest_Push{
Push: &protocol.RunFixturePushRequest{
Name: "fake",
Config: cfg,
},
},
}); err != nil {
t.Fatal("rfcl.Send(push):", err)
}
if res, err := rfcl.Recv(); err != nil {
t.Fatal("push; rfcl.Recv():", err)
} else if res.GetRequestDone() == nil {
t.Fatalf("push; rfcl.Recv() = %v, want RequestDone", res)
}
if err := rfcl.Send(&protocol.RunFixtureRequest{
Control: &protocol.RunFixtureRequest_Pop{
Pop: &protocol.RunFixturePopRequest{},
},
}); err != nil {
t.Fatal("rfcl.Send(pop):", err)
}
if res, err := rfcl.Recv(); err != nil {
t.Fatal("pop; rfcl.Recv():", err)
} else if res.GetRequestDone() == nil {
t.Fatalf("pop; rfcl.Recv() = %v, want RequestDone", res)
}
if d := os.TempDir(); d != origTempDir {
t.Errorf("os.TempDir() after pop = %v, want %v", d, origTempDir)
}
if setUpTempDir == "" || setUpTempDir == origTempDir {
t.Errorf("os.TempDir() in SetUp = %v; originally %v", setUpTempDir, origTempDir)
}
if tearDownTempDir == "" || tearDownTempDir == origTempDir {
t.Errorf("os.TempDir() in TearDown = %v; originally %v", tearDownTempDir, origTempDir)
}
if _, err := os.Stat(setUpTempDir); !os.IsNotExist(err) {
t.Errorf("setUpTempDir not removed; os.Stat(%q) = %v, want not exist", setUpTempDir, err)
}
if err := rfcl.CloseSend(); err != nil {
t.Fatal(err)
}
if r, err := rfcl.Recv(); err != io.EOF {
t.Fatalf("last rfcl.Recv() = %v, %v, want EOF", r, err)
}
}
func TestFixtureServiceNoSuchFixture(t *gotesting.T) {
reg := testing.NewRegistry("bundle")
rfcl, stop := startFakeFixtureService(context.Background(), t, reg)
defer stop()
tmpDir := testutil.TempDir(t)
defer os.RemoveAll(tmpDir)
td := sshtest.NewTestData(nil)
defer td.Close()
if err := rfcl.Send(&protocol.RunFixtureRequest{
Control: &protocol.RunFixtureRequest_Push{
Push: &protocol.RunFixturePushRequest{
Name: "noSuchFixture",
Config: &protocol.RunFixtureConfig{
OutDir: tmpDir,
ConnectionSpec: td.Srvs[0].Addr().String(),
KeyFile: td.UserKeyFile,
},
},
},
}); err != nil {
t.Fatal(err)
}
if _, err := rfcl.Recv(); err == nil || err == io.EOF {
t.Errorf("rfcl.Recv() = %v, want no such fixture error", err)
}
}
func TestFixtureServiceTimeout(t *gotesting.T) {
reg := testing.NewRegistry("bundle")
c := make(chan struct{})
defer close(c)
reg.AddFixtureInstance(&testing.FixtureInstance{Name: "fake", Impl: &fakeFixture{
setUp: func(context.Context, *testing.FixtState) interface{} {
<-c
return nil
},
}})
ctx := context.Background()
rfcl, stop := startFakeFixtureService(ctx, t, reg)
defer stop()
tmpDir := testutil.TempDir(t)
defer os.RemoveAll(tmpDir)
td := sshtest.NewTestData(nil)
defer td.Close()
if err := rfcl.Send(&protocol.RunFixtureRequest{
Control: &protocol.RunFixtureRequest_Push{
Push: &protocol.RunFixturePushRequest{
Name: "fake",
Config: &protocol.RunFixtureConfig{
TempDir: tmpDir,
OutDir: tmpDir,
ConnectionSpec: td.Srvs[0].Addr().String(),
KeyFile: td.UserKeyFile,
CustomGracePeriod: ptypes.DurationProto(time.Millisecond),
},
},
},
}); err != nil {
t.Fatal(err)
}
if _, err := rfcl.Recv(); err == nil || err == io.EOF {
t.Errorf("rfcl.Recv() = %v, want fixture timeout error", err)
}
}
func TestFixtureServiceWrongRequestOrder(t *gotesting.T) {
reg := testing.NewRegistry("bundle")
reg.AddFixtureInstance(&testing.FixtureInstance{Name: "fake", Impl: &fakeFixture{}})
td := sshtest.NewTestData(nil)
defer td.Close()
push := &protocol.RunFixtureRequest{
Control: &protocol.RunFixtureRequest_Push{
Push: &protocol.RunFixturePushRequest{
Name: "fake",
Config: &protocol.RunFixtureConfig{
ConnectionSpec: td.Srvs[0].Addr().String(),
KeyFile: td.UserKeyFile,
},
},
},
}
pop := &protocol.RunFixtureRequest{
Control: &protocol.RunFixtureRequest_Pop{
Pop: &protocol.RunFixturePopRequest{},
},
}
for _, tc := range []struct {
name string
ops []*protocol.RunFixtureRequest
wantErr bool
}{
{
name: "pop without push",
ops: []*protocol.RunFixtureRequest{pop},
wantErr: true,
},
{
name: "push without pop",
ops: []*protocol.RunFixtureRequest{push},
wantErr: true,
},
{
name: "push pop pop",
ops: []*protocol.RunFixtureRequest{push, pop, pop},
wantErr: true,
},
{
name: "push pop push pop",
ops: []*protocol.RunFixtureRequest{push, pop, push, pop},
},
} {
t.Run(tc.name, func(t *gotesting.T) {
ctx := context.Background()
rfcl, stop := startFakeFixtureService(ctx, t, reg)
defer stop()
consume := func() error {
for {
res, err := rfcl.Recv()
if err != nil {
return err
}
if x := res.GetRequestDone(); x != nil {
return nil
}
}
}
var gotErr error
for i, op := range tc.ops {
if gotErr != nil {
t.Fatalf("ops[%d] failed: %v", i-1, gotErr)
}
if err := rfcl.Send(op); err != nil {
t.Fatalf("i = %d, rfcl.Send(): %v", i, err)
}
gotErr = consume()
}
if err := rfcl.CloseSend(); err != nil {
t.Fatalf("rfcl.CloseSend(): %v", err)
}
if r, err := rfcl.Recv(); err == nil {
t.Fatalf("rfcl.Recv() = %v, want EOF", r)
} else if err != io.EOF && gotErr == nil {
gotErr = err
}
if tc.wantErr && gotErr == nil {
t.Errorf("Last err = %v, want real error", gotErr)
}
if !tc.wantErr && gotErr != nil {
t.Errorf("Got error, want nil: %v", gotErr)
}
})
}
}