| // 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 |
| } |
| } |