blob: 8b0a8dbf07c86d612ecef1dc6de53b20ab3a9ec1 [file] [log] [blame]
// Copyright 2020 The Chromium OS 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 cmd
import (
"context"
"fmt"
"go.chromium.org/luci/common/cli"
"io"
"io/ioutil"
"os"
"path/filepath"
"tnull/driver"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/maruel/subcommands"
tnProto "go.chromium.org/chromiumos/config/go/api/test/harness/tnull/v1"
metadata "go.chromium.org/chromiumos/config/go/api/test/metadata/v1"
rtd "go.chromium.org/chromiumos/config/go/api/test/rtd/v1"
"go.chromium.org/luci/common/errors"
)
type run struct {
subcommands.CommandRunBase
debugDest io.Writer
invocationPath string
}
func RunSteps() *subcommands.Command {
return &subcommands.Command{
UsageLine: "run-steps -input /path/to/input.binaryproto",
ShortDesc: "run an Invocation object.",
CommandRun: func() subcommands.CommandRun {
r := &run{}
r.Flags.StringVar(&r.invocationPath, "input", "", "Path that contains a binaryproto rtd.Invocation object")
return r
},
}
}
func (r *run) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if err := r.innerRun(cli.GetContext(a, r, env), a); err != nil {
fmt.Fprintf(a.GetErr(), "%s\n", err)
return 1
}
return 0
}
func (r *run) innerRun(ctx context.Context, a subcommands.Application) error {
if err := r.validateArgs(); err != nil {
return errors.Annotate(err, "validation").Err()
}
var inv rtd.Invocation
if err := readBinaryProto(r.invocationPath, &inv); err != nil {
return errors.Annotate(err, "reading in the Invocation").Err()
}
if inv.GetRequests() == nil || len(inv.GetRequests()) == 0 {
return errors.Reason("no requests in invocation").Err()
}
lookup, err := extractTestsFromSpecification()
if err != nil {
return err
}
conf := inv.ProgressSinkClientConfig
reqs := inv.Requests
errs := errors.MultiError{}
r.debugDest = a.GetErr()
for _, req := range reqs {
fmt.Fprintf(a.GetOut(), "req %s: executing test %s\n", req.GetName(), req.GetTest())
if steps, present := lookup.Lookup[req.GetTest()]; !present {
errs = append(errs, errors.Reason("no test with the name %s", req.GetTest()).Err())
} else {
steps.Setup.Config = conf
r.Execute(req.GetTest(), steps)
}
}
if len(errs) == 0 {
return nil
}
return errs
}
func extractTestsFromSpecification() (*tnProto.TestMap, error) {
p, err := specPath()
if err != nil {
return nil, errors.Annotate(err, "extracting tests from spec").Err()
}
var s metadata.Specification
if err := readJSONPb(p, &s); err != nil {
return nil, errors.Annotate(err, "extracting tests from spec").Err()
}
testMap := &tnProto.TestMap{Lookup: map[string]*tnProto.Steps{}}
for _, rtd := range s.RemoteTestDrivers {
if rtd.Name != TNullName {
return nil, errors.Reason(
"bad specification, RTD was %s, should be %s",
rtd.Name, TNullName).Err()
}
marshaler := jsonpb.Marshaler{}
for _, test := range rtd.Tests {
steps, err := detailsToSteps(marshaler, test.Informational)
if err != nil {
return nil, errors.Annotate(
err, "extracting tests from spec").Err()
}
testMap.Lookup[test.Name] = steps
}
}
return testMap, nil
}
func detailsToSteps(m jsonpb.Marshaler, info *metadata.Informational) (*tnProto.Steps, error) {
jsonSteps, err := m.MarshalToString(info.Details.GetFields()["steps"])
if err != nil {
return nil, errors.Annotate(err, "Details>json").Err()
}
var steps tnProto.Steps
if err := jsonpb.UnmarshalString(jsonSteps, &steps); err != nil {
return nil, errors.Annotate(err, "json>Steps").Err()
}
return &steps, nil
}
func (r *run) Execute(name string, s *tnProto.Steps) error {
var d driver.Driver
if r.debugDest != nil {
d.SetDebugErrDestination(r.debugDest)
}
if err := d.Setup(name, *s.Setup); err != nil {
return err
}
for _, st := range s.Steps {
ste := st.GetStep()
switch t := ste.(type) {
case *tnProto.Step_Archive:
req := st.GetArchive().GetCommonArgs().GetRequestName()
return d.Archive(req)
case *tnProto.Step_Log:
req := st.GetLog().GetCommonArgs().GetRequestName()
return d.Log(req)
case *tnProto.Step_Result:
req := st.GetResult().GetCommonArgs().GetRequestName()
return d.Result(req)
case *tnProto.Step_Other:
step := st.GetOther()
return errors.Reason(
"driver has no method %s or support for args %v",
step.GetMethodName(), step.GetArgs(),
).Err()
default:
_ = t
return errors.Reason("no step specified").Err()
}
}
return nil
}
func (r *run) validateArgs() error {
if r.invocationPath == "" {
return fmt.Errorf("input_json must specify an rtd.Invocation")
}
return nil
}
// readJSONPb reads a JSON string from inFile and unpacks it as a proto.
// Unexpected fields are ignored.
func readJSONPb(inFile string, payload proto.Message) error {
r, err := os.Open(inFile)
if err != nil {
return errors.Annotate(err, "read JSON pb").Err()
}
defer r.Close()
unmarshaler := jsonpb.Unmarshaler{AllowUnknownFields: true}
if err := unmarshaler.Unmarshal(r, payload); err != nil {
return errors.Annotate(err, "read JSON pb").Err()
}
return nil
}
func readBinaryProto(inFile string, payload proto.Message) error {
b, err := ioutil.ReadFile(inFile)
if err != nil {
return errors.Annotate(err, "readBinaryProto").Err()
}
if err := proto.Unmarshal(b, payload); err != nil {
return errors.Annotate(err, "readBinaryProto").Err()
}
return nil
}
func specPath() (string, error) {
if home, err := os.UserHomeDir(); err != nil {
return "", err
} else {
return filepath.Join(home, SpecRelPath), nil
}
}