blob: cf0c87445d28e4ccc144d628b7d602b24fbfa1a0 [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"
"errors"
"fmt"
"io"
"os"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/empty"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/tast/core/internal/logging"
"go.chromium.org/tast/core/internal/planner"
"go.chromium.org/tast/core/internal/protocol"
"go.chromium.org/tast/core/internal/testing"
"go.chromium.org/tast/core/internal/timing"
frameworkprotocol "go.chromium.org/tast/core/framework/protocol"
)
// fixtureService implements FixtureServiceServer gRPC service.
type fixtureService struct {
reg *testing.Registry
}
var _ protocol.FixtureServiceServer = (*fixtureService)(nil)
// registerFixtureService registers fixture service.
func registerFixtureService(srv *grpc.Server, reg *testing.Registry) {
protocol.RegisterFixtureServiceServer(srv, &fixtureService{reg: reg})
}
// RunFixture provides operations to set up and tear down fixtures.
// It accepts multiple pairs of push and pop requests in a loop until client
// closes the connection.
func (s *fixtureService) RunFixture(srv protocol.FixtureService_RunFixtureServer) error {
for {
if err := s.pushAndPop(srv); err == errFixtureServiceNormalEOF {
return nil
} else if err != nil {
return err
}
}
}
var errFixtureServiceNormalEOF = errors.New("normal EOF")
// pushAndPop handles push and pop operations. If the connection is terminated
// normally, it returns errFixtureServiceNormalEOF.
func (s *fixtureService) pushAndPop(srv protocol.FixtureService_RunFixtureServer) (retErr error) {
ctx := srv.Context()
sendDone := func() error {
return srv.Send(&protocol.RunFixtureResponse{
Control: &protocol.RunFixtureResponse_RequestDone{
RequestDone: &empty.Empty{},
},
Timestamp: ptypes.TimestampNow(),
})
}
// sendDone for the pop operation is run after all other deferred
// operations are done. This resolves timing issues in the unit test
// TestFixtureServiceDefaultTempDir.
defer func() {
if retErr != nil {
return
}
retErr = sendDone()
}()
req, err := srv.Recv()
if err == io.EOF {
return errFixtureServiceNormalEOF
} else if err != nil {
return err
}
r := req.GetPush()
if r == nil {
return fmt.Errorf("req = %v, want push request", req)
}
f := s.reg.AllFixtures()[r.Name]
if f == nil {
return fmt.Errorf("push %v: no such fixture", r.Name)
}
if r.Config.TempDir == "" {
r.Config.TempDir, err = defaultTempDir()
if err != nil {
return fmt.Errorf("push %v: %v", r.Name, err)
}
defer func() {
if err := os.RemoveAll(r.Config.TempDir); err != nil && retErr == nil {
retErr = fmt.Errorf("push %v: %v", r.Name, err)
}
}()
}
restoreTempDir, err := prepareTempDir(r.Config.TempDir)
if err != nil {
return fmt.Errorf("push %v: %v", r.Name, err)
}
defer restoreTempDir()
logging.Info(ctx, "Connecting to DUT")
dt, err := connectToTarget(ctx, r.Config.ConnectionSpec, r.Config.KeyFile, r.Config.KeyDir, r.Config.ProxyCommand, nil)
if err != nil {
return fmt.Errorf("push %v: %v", r.Name, err)
}
defer func() {
logging.Info(ctx, "Disconnecting from DUT")
// It is OK to ignore the error since we've finished running fixture at this point.
dt.Close(ctx)
}()
pcfg := &planner.Config{
Dirs: &protocol.RunDirectories{
DataDir: r.Config.DataDir,
OutDir: r.Config.OutDir,
},
Service: &protocol.ServiceConfig{
Devservers: r.Config.Devservers,
DutServer: r.Config.DutServer,
TlwServer: r.Config.TlwServer,
TlwSelfName: r.Config.DutName,
},
DataFile: &protocol.DataFileConfig{
BuildArtifactsUrl: r.Config.BuildArtifactsUrl,
DownloadMode: r.Config.GetDownloadMode(),
},
RemoteData: &testing.RemoteData{
RPCHint: testing.NewRPCHint(r.Config.LocalBundleDir, r.Config.TestVars),
DUT: dt,
// TODO(oka): fill Meta field.
},
Features: &protocol.Features{
Infra: &protocol.InfraFeatures{
Vars: r.Config.GetTestVars(),
DUTLabConfig: r.Config.GetDUTLabConfig(),
},
Dut: &frameworkprotocol.DUTFeatures{
Software: &frameworkprotocol.SoftwareFeatures{
Available: r.Config.GetAvailableSoftwareFeatures(),
Unavailable: r.Config.GetUnavailableSoftwareFeatures(),
},
// TODO(oka): fill HardwareFeatures field.
},
},
}
if r.Config.CustomGracePeriod != nil {
d, err := ptypes.Duration(r.Config.CustomGracePeriod)
if err != nil {
return fmt.Errorf("push %v: invalid CustomGracePeriod: %v", r.Name, err)
}
pcfg.CustomGracePeriod = &d
}
stack := planner.NewFixtureStack(pcfg, &fixtureServiceLogger{srv})
if dt != nil && !dt.Connected(ctx) {
// Try to reconnect to the DUT.
waitConnectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
if err := dt.WaitConnect(waitConnectCtx); err != nil {
return fmt.Errorf("Failed to connect to DUT before running fixture %s: %v", r.Name, err)
}
}
if err := stack.Push(ctx, f); err != nil {
return fmt.Errorf("push %v: %v", r.Name, err)
}
if err := sendDone(); err != nil {
return fmt.Errorf("push %v: %v", r.Name, err)
}
req, err = srv.Recv()
if err != nil {
return fmt.Errorf("pop %v: %v", r.Name, err)
}
if req.GetPop() == nil {
return fmt.Errorf("req = %v, want pop for %v", req, r.Name)
}
if !dt.Connected(ctx) {
logging.Info(ctx, "Reconnecting to DUT")
if err := dt.Connect(ctx); err != nil {
return fmt.Errorf("pop %v: %v", r.Name, err)
}
}
if err := stack.Pop(ctx); err != nil {
return fmt.Errorf("pop %v: %v", r.Name, err)
}
return nil
}
// fixtureServiceLogger implements planner.OutputStream.
type fixtureServiceLogger struct {
stream protocol.FixtureService_RunFixtureServer
}
func (l *fixtureServiceLogger) Log(level logging.Level, ts time.Time, msg string) error {
return l.stream.Send(&protocol.RunFixtureResponse{
Control: &protocol.RunFixtureResponse_Log{Log: msg},
Timestamp: timestamppb.New(ts),
Level: protocol.LevelToProto(level),
})
}
func (l *fixtureServiceLogger) Error(e *protocol.Error) error {
return l.stream.Send(&protocol.RunFixtureResponse{
Control: &protocol.RunFixtureResponse_Error{
Error: &protocol.RunFixtureError{
Reason: e.GetReason(),
File: e.GetLocation().GetFile(),
Line: int32(e.GetLocation().GetLine()),
Stack: e.GetLocation().GetStack(),
},
},
Timestamp: timestamppb.Now(),
})
}
func (l *fixtureServiceLogger) EntityStart(ei *protocol.Entity, outDir string) error {
return nil
}
func (l *fixtureServiceLogger) EntityLog(ei *protocol.Entity, level logging.Level, ts time.Time, msg string) error {
return l.Log(level, ts, msg)
}
func (l *fixtureServiceLogger) EntityError(ei *protocol.Entity, e *protocol.Error) error {
return l.Error(e)
}
func (l *fixtureServiceLogger) EntityEnd(ei *protocol.Entity, skipReasons []string, timingLog *timing.Log) error {
return nil
}
func (l *fixtureServiceLogger) ExternalEvent(req *protocol.RunTestsResponse) error {
return nil
}
func (l *fixtureServiceLogger) StackOperation(ctx context.Context, req *protocol.StackOperationRequest) (*protocol.StackOperationResponse, error) {
return nil, nil
}