blob: 11b5d1eb0dc37f1992b69ddaae4089eb970c8dfb [file] [log] [blame]
// Copyright 2021 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 main implements the tast_rtd executable, used to invoke tast in RTD.
package main
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"os"
"path/filepath"
structpb "github.com/golang/protobuf/ptypes/struct"
v2 "go.chromium.org/chromiumos/config/go/api/test/results/v2"
rtd "go.chromium.org/chromiumos/config/go/api/test/rtd/v1"
"chromiumos/tast/errors"
)
const (
resultsFilename = "results.json" // File containing stream of newline-separated JSON EntityResult objects
skipReasonKey = "skipReason" // Keyword for skip reason in results.json.
errorsKey = "errors" // Keyword for errors in results.json.
)
// testErrors stores information for each error.
type testError struct {
Time string `json:"time"`
Reason string `json:"reason"`
File string `json:"file"`
Line int `json:"line"`
Stack string `json:"stack"`
}
// testResult stores result for one test.
type testResult struct {
// name contains the test name.
Name string `json:"name"`
// Errors contains errors encountered while running the entity.
// If it is empty, the entity passed.
Errors []testError `json:"errors"`
// It is empty if the test actually ran.
SkipReason string `json:"skipReason"`
}
// sendTestResults reads resultDir/results.json and reports result through progress API.
func sendTestResults(ctx context.Context, logger *log.Logger, psClient rtd.ProgressSinkClient, inv *rtd.Invocation, resultDir string) error {
fullPathName := filepath.Join(resultDir, resultsFilename)
resultFile, err := os.Open(fullPathName)
if err != nil {
return errors.Wrapf(err, "failed to open file %v", fullPathName)
}
defer resultFile.Close()
data, err := ioutil.ReadFile(fullPathName)
if err != nil {
return errors.Wrapf(err, "fail to read file %v", fullPathName)
}
resultRequests, err := makeResultRequests(logger, inv, data)
if err != nil {
return errors.Wrapf(err, "fail to report test result from file %v", fullPathName)
}
for _, rr := range resultRequests {
rspn, err := psClient.ReportResult(ctx, rr)
if err != nil {
return errors.Wrap(err, "failed to report result")
}
if rspn.Terminate {
return nil
}
}
return nil
}
// makeResultRequests parses the json text from testData and prepares test results for progress API to use.
func makeResultRequests(logger *log.Logger, inv *rtd.Invocation, testData []byte) (resultRequests []*rtd.ReportResultRequest, err error) {
var results []*testResult
if err := json.Unmarshal(testData, &results); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal test results")
}
// A map to maintain test names to result structures.
testResults := make(map[string]*testResult)
for i, r := range results {
if r.Name == "" {
return nil, errors.Errorf("Entry %v has empty test name: %+v", i, r)
}
testResults[r.Name] = results[i]
}
for _, request := range inv.Requests {
result, ok := testResults[request.Test]
if !ok {
// TODO: Discuss what is the best way to handle this case.
resultRequests = append(resultRequests, missingTestResult(request.Name))
logger.Println("Did not receive test result for request ", request.Name)
continue
}
if rr := skippedTestResult(request.Name, result); rr != nil {
resultRequests = append(resultRequests, rr)
continue
}
if rr := failedTestResult(request.Name, result); rr != nil {
resultRequests = append(resultRequests, rr)
continue
}
resultRequests = append(resultRequests, &rtd.ReportResultRequest{
Request: request.Name,
Result: &v2.Result{State: v2.Result_SUCCEEDED},
})
}
return resultRequests, nil
}
func missingTestResult(requestName string) *rtd.ReportResultRequest {
// TODO: Discuss what is the best way to handle this case.
return &rtd.ReportResultRequest{
Request: requestName,
Result: &v2.Result{
State: v2.Result_SKIPPED,
Errors: []*v2.Result_Error{
{
Source: v2.Result_Error_TEST,
Severity: v2.Result_Error_WARNING,
Details: &structpb.Struct{
Fields: map[string]*structpb.Value{
skipReasonKey: {
Kind: &structpb.Value_StringValue{
StringValue: "Test was not found",
},
},
},
},
},
},
},
}
}
func skippedTestResult(requestName string, result *testResult) *rtd.ReportResultRequest {
if result.SkipReason != "" {
return &rtd.ReportResultRequest{
Request: requestName,
Result: &v2.Result{
State: v2.Result_SKIPPED,
Errors: []*v2.Result_Error{
{
Source: v2.Result_Error_TEST,
Severity: v2.Result_Error_WARNING,
Details: &structpb.Struct{
Fields: map[string]*structpb.Value{
skipReasonKey: {
Kind: &structpb.Value_StringValue{
StringValue: result.SkipReason,
},
},
},
},
},
},
},
}
}
return nil
}
func failedTestResult(requestName string, result *testResult) *rtd.ReportResultRequest {
if len(result.Errors) > 0 {
rr := rtd.ReportResultRequest{
Request: requestName,
Result: &v2.Result{
State: v2.Result_FAILED,
},
}
for _, e := range result.Errors {
rr.Result.Errors = append(rr.Result.Errors, &v2.Result_Error{
Source: v2.Result_Error_TEST,
Severity: v2.Result_Error_CRITICAL,
Details: &structpb.Struct{
Fields: map[string]*structpb.Value{
"time": {
Kind: &structpb.Value_StringValue{
StringValue: e.Time,
},
},
"reason": {
Kind: &structpb.Value_StringValue{
StringValue: e.Reason,
},
},
"file": {
Kind: &structpb.Value_StringValue{
StringValue: e.File,
},
},
"line": {
Kind: &structpb.Value_NumberValue{
NumberValue: float64(e.Line),
},
},
"stack": {
Kind: &structpb.Value_StringValue{
StringValue: e.Stack,
},
},
},
},
})
}
return &rr
}
return nil
}