| // Copyright 2023 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package driver implements drivers to execute tests. |
| package driver |
| |
| import ( |
| "context" |
| "encoding/xml" |
| "fmt" |
| "log" |
| "time" |
| |
| "go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/device" |
| "go.chromium.org/chromiumos/test/execution/errors" |
| |
| "go.chromium.org/chromiumos/config/go/test/api" |
| "google.golang.org/grpc" |
| ) |
| |
| const ( |
| testInitScriptPath = "/usr/local/libexec/crosier/init_env.sh" |
| testRunScriptPath = "/usr/local/libexec/crosier/run_tests.sh" |
| ) |
| |
| // CrosierDriver runs gtest and report its results. |
| type CrosierDriver struct { |
| // logger provides logging service. |
| logger *log.Logger |
| } |
| |
| // NewCrosierDriver creates a new driver to run tests. |
| func NewCrosierDriver(logger *log.Logger) *CrosierDriver { |
| return &CrosierDriver{ |
| logger: logger, |
| } |
| } |
| |
| // Name returns the name of the driver. |
| func (td *CrosierDriver) Name() string { |
| return "crosier" |
| } |
| |
| // runCrosierInitCmd executes a script on DUT that initializes Crosier testing environment. |
| // This script needs to run once before any tests run. |
| func runCrosierInitCmd(ctx context.Context, logger *log.Logger, dut api.DutServiceClient) error { |
| var err error |
| |
| cmdExec := api.ExecCommandRequest{ |
| Command: testInitScriptPath, |
| Args: []string{}, |
| } |
| |
| var client api.DutService_ExecCommandClient |
| if client, err = dut.ExecCommand(ctx, &cmdExec); err != nil { |
| return fmt.Errorf("failed to execute init command on DUT: %v", err) |
| } |
| |
| var resp *api.ExecCommandResponse |
| if resp, err = client.Recv(); err != nil { |
| return fmt.Errorf("failed to get init command results: %v", err) |
| } |
| |
| logCmd(logger, &cmdExec, resp) |
| if resp.ExitInfo.Status != 0 || len(string(resp.Stdout)) > 0 || len(string(resp.Stderr)) > 0 { |
| return fmt.Errorf("non-zero exit code (%d) or non-empty output for init command:\nstderr:%v\nstdout:%v", |
| resp.ExitInfo.Status, |
| string(resp.Stderr), |
| string(resp.Stdout)) |
| } |
| |
| return nil |
| } |
| |
| // parseResultXml wraps parsing of XML test results to make it unit testable. |
| func parseResultXml(res []byte, gtestResults *gtestResult) error { |
| if err := xml.Unmarshal(res, >estResults); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // runCrosierCmd executes a test on the DUT. |
| // reasons for failure, if any. Empty reasons means command passed. |
| func runCrosierCmd(ctx context.Context, logger *log.Logger, dut api.DutServiceClient, test *api.TestCaseMetadata) *executionData { |
| var err error |
| |
| startTime := time.Now() |
| outFileName := fmt.Sprintf("/tmp/%v-%d.xml", test.TestCase.Id.Value, startTime.Unix()) |
| |
| // Execute the gtest on the DUT. |
| cmdArgs := []string{ |
| fmt.Sprintf("--gtest_output=xml:%v", outFileName), |
| fmt.Sprintf("--gtest_filter=%v", test.TestCase.Name), |
| } |
| cmdExec := api.ExecCommandRequest{ |
| Command: testRunScriptPath, |
| Args: cmdArgs, |
| } |
| |
| var client api.DutService_ExecCommandClient |
| if client, err = dut.ExecCommand(ctx, &cmdExec); err != nil { |
| return newExecutionData(startTime, invalidDuration, []string{fmt.Sprintf("failed to exec command on DUT: %v", err)}) |
| } |
| |
| var resp *api.ExecCommandResponse |
| if resp, err = client.Recv(); err != nil { |
| return newExecutionData(startTime, invalidDuration, []string{fmt.Sprintf("failed to get command results: %v", err)}) |
| } |
| |
| logCmd(logger, &cmdExec, resp) |
| |
| if resp.ExitInfo.Status != 0 { |
| return newExecutionData(startTime, invalidDuration, []string{fmt.Sprintf("unexpected failure: stderr: %v, err: %v", string(resp.Stderr), resp.ExitInfo.ErrorMessage)}) |
| } |
| |
| // Test has passed, now get the results. |
| cmdExec = api.ExecCommandRequest{ |
| Command: "cat", |
| Args: []string{outFileName}, |
| } |
| |
| if client, err = dut.ExecCommand(ctx, &cmdExec); err != nil { |
| return newExecutionData(startTime, invalidDuration, []string{fmt.Sprintf("failed to exec command on DUT: %v", err)}) |
| } |
| |
| if resp, err = client.Recv(); err != nil { |
| return newExecutionData(startTime, invalidDuration, []string{fmt.Sprintf("failed to get command results: %v", err)}) |
| } |
| |
| logCmd(logger, &cmdExec, resp) |
| |
| if resp.ExitInfo.Status != 0 { |
| return newExecutionData(startTime, invalidDuration, []string{fmt.Sprintf("non-zero exit code (%d) reading test results:\nstderr:%v\nerr:%v", |
| resp.ExitInfo.Status, |
| string(resp.Stderr), |
| resp.ExitInfo.ErrorMessage)}) |
| } |
| |
| // Build the test results struct. |
| var gtestResults gtestResult |
| if err = parseResultXml(resp.Stdout, >estResults); err != nil { |
| return newExecutionData(startTime, invalidDuration, []string{fmt.Sprintf("failed to parse gtest xml data: %v", err)}) |
| } |
| |
| return testResult(test.TestCase.Name, startTime, >estResults) |
| } |
| |
| // RunTests drives a test framework to execute tests. |
| func (td *CrosierDriver) RunTests(ctx context.Context, resultsDir string, req *api.CrosTestRequest, tlwAddr string, tests []*api.TestCaseMetadata) (*api.CrosTestResponse, error) { |
| var err error |
| var testCaseResults []*api.TestCaseResult |
| |
| // Setup dut connection to be able to run the tests and get results. |
| var dutInfo *device.DutInfo |
| if dutInfo, err = device.FillDUTInfo(req.Primary, ""); err != nil { |
| return nil, errors.NewStatusError(errors.InvalidArgument, |
| fmt.Errorf("cannot get address from primary device: %v", dutInfo)) |
| } |
| |
| var primaryDutConn *grpc.ClientConn |
| if primaryDutConn, err = grpc.Dial(dutInfo.DutServer, grpc.WithInsecure()); err != nil { |
| return nil, errors.NewStatusError(errors.InvalidArgument, |
| fmt.Errorf("cannot create connection with primary device: %v, address: %v", req.Primary, dutInfo.DutServer)) |
| } |
| defer primaryDutConn.Close() |
| |
| dut := api.NewDutServiceClient(primaryDutConn) |
| |
| // Initialize the DUT for Crosier testing. |
| if err = runCrosierInitCmd(ctx, td.logger, dut); err != nil { |
| return nil, errors.NewStatusError(errors.InvalidArgument, fmt.Errorf("failed to initialize DUT: %v", err)) |
| } |
| |
| // Run tests and parse results. |
| harness := &api.TestHarness{TestHarnessType: &api.TestHarness_Gtest_{Gtest: &api.TestHarness_Gtest{}}} |
| for _, test := range tests { |
| results := runCrosierCmd(ctx, td.logger, dut, test) |
| |
| tcResult := buildTestCaseResults(test.TestCase.Id.Value, harness, results) |
| testCaseResults = append(testCaseResults, tcResult) |
| } |
| return &api.CrosTestResponse{TestCaseResults: testCaseResults}, nil |
| } |