blob: a6f12e94f62b3c1b57b2ca7906bfe1dd2659c71f [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 (
"fmt"
"io"
"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_json /path/to/input.json",
ShortDesc: "run an Invocation object.",
CommandRun: func() subcommands.CommandRun {
r := &run{}
r.Flags.StringVar(&r.invocationPath, "input_json", "", "Path that contains a JSON-encoded rtd.Invocation object")
return r
},
}
}
func (r *run) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if err := r.innerRun(a, args, env); err != nil {
fmt.Fprintf(a.GetErr(), "%s\n", err)
return 1
}
return 0
}
func (r *run) innerRun(a subcommands.Application, args []string, env subcommands.Env) error {
if err := r.validateArgs(); err != nil {
return errors.Annotate(err, "validation").Err()
}
var inv rtd.Invocation
if err := readJSONPb(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.Name, req.Test)
if steps, present := lookup.Lookup[req.Test]; !present {
errs = append(errs, errors.Reason("no test with the name %s", req.Test).Err())
} else {
steps.Setup.Config = conf
r.Execute(req.Test, 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 specPath() (string, error) {
if home, err := os.UserHomeDir(); err != nil {
return "", err
} else {
return filepath.Join(home, SpecRelPath), nil
}
}