blob: 0e93e60ff2b2b1c854e6e663223434071c637dfd [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package tautoresults provides test results for Tauto.
package tautoresults
import (
"go.chromium.org/chromiumos/test/execution/errors"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/golang/protobuf/ptypes"
_go "go.chromium.org/chromiumos/config/go"
"go.chromium.org/chromiumos/config/go/test/api"
)
// Report TODO: probably need to eventually move tauto over from results parsing.
type Report struct {
tests []string // Tests to be run.
testCaseResults []*api.TestCaseResult // Reported test results.
testResultsDir string // Parent directory for all test results.N
reportedTests map[string]struct{} // Tests that have received results.
testNamesToIds map[string]string // Mapping between test names and test ids.
RawResults results
}
// results TODO: Temp structs to support testing autotest result parsing/reporting.
type results struct {
Tests []test `json:"tests"`
}
// Test mirrors of the test_results.json example.
type test struct {
Verdict string `json:"verdict"`
Testname string `json:"testname"`
Errmsg string `json:"errmsg"`
Resultspath string `json:"resultspath"`
StartTime string `json:"starttime"`
EndTime string `json:"endtime"`
}
// loadJSON unmarshals the json into the Report.RawResults struct.
func (r *Report) loadJSON(resultsDir string) error {
plan, _ := ioutil.ReadFile(filepath.Join(resultsDir, "results.json"))
err := json.Unmarshal(plan, &r.RawResults)
if err != nil {
return errors.NewStatusError(errors.UnmarshalError,
fmt.Errorf("failed to unmarshal results: %v From: %v", err, resultsDir))
}
return nil
}
func getTime(givenTime string) time.Time {
start, err := strconv.ParseInt(givenTime, 10, 64)
if err != nil {
// If we can't convert return nil I guess.
return time.Unix(0, 0)
}
unixTime := time.Unix(start, 0)
return unixTime
}
// GetDuration gets the duration from the start/end time of a test.
func GetDuration(test test) time.Duration {
start, err := strconv.Atoi(test.StartTime)
if err != nil {
return time.Duration(0)
}
end, err := strconv.Atoi(test.EndTime)
if err != nil {
return time.Duration(0)
}
diff := end - start
durationDiff := time.Second * time.Duration(diff)
return durationDiff
}
// GenerateReport gets a report request from tast and passes on to progress sink.
func GenerateReport(test test, testID string, resultsDir string, testCaseMetadata *api.TestCaseMetadata) *api.TestCaseResult {
// For now, assume results will be in $results_dir/"test_results.json"
// Mark the result as found.
// r.reportedTests[test] = struct{}{}
// If we cannot validate the status name, we will default to Crash.
testResult := api.TestCaseResult{
TestCaseId: &api.TestCase_Id{Value: testID},
ResultDirPath: &_go.StoragePath{
HostType: _go.StoragePath_LOCAL,
Path: filepath.Join(test.Resultspath),
},
Verdict: &api.TestCaseResult_Crash_{Crash: &api.TestCaseResult_Crash{}},
TestHarness: &api.TestHarness{
TestHarnessType: &api.TestHarness_Tauto_{
Tauto: &api.TestHarness_Tauto{},
},
},
Reason: "Result status indicator unknown, defaulting to CRASH",
TestCaseMetadata: testCaseMetadata,
}
// If there is an errmsg, append it for clarity.
if test.Errmsg != "" {
testResult.Reason = fmt.Sprintf("%s: %s", testResult.Reason, test.Errmsg)
}
// Overwrite the tauto harness in the test was a tast subtest.
if strings.HasPrefix(testID, "tast.") {
testResult.TestHarness = &api.TestHarness{TestHarnessType: &api.TestHarness_Tast_{Tast: &api.TestHarness_Tast{}}}
}
startTime := getTime(test.StartTime)
if startTime != time.Unix(0, 0) {
startProtoTime, err := ptypes.TimestampProto(startTime)
// Only add times if there was no err in conversion.
if err == nil {
durationProtoTime := ptypes.DurationProto(GetDuration(test))
testResult.StartTime = startProtoTime
testResult.Duration = durationProtoTime
}
}
// Change result to fail/err as needed.
if test.Verdict == "Fail" {
testResult.Verdict = &api.TestCaseResult_Fail_{Fail: &api.TestCaseResult_Fail{}}
testResult.Reason = test.Errmsg
if test.Errmsg == "" {
testResult.Reason = "Test failed"
}
} else if test.Verdict == "Error" {
// ToDo: b/199940635 -- Update RawResult not to use "Error"
testResult.Verdict = &api.TestCaseResult_Crash_{Crash: &api.TestCaseResult_Crash{}}
testResult.Reason = test.Errmsg
if test.Errmsg == "" {
testResult.Reason = "Test did not finish"
}
} else if test.Verdict == "Abort" {
testResult.Verdict = &api.TestCaseResult_Abort_{Abort: &api.TestCaseResult_Abort{}}
testResult.Reason = test.Errmsg
if test.Errmsg == "" {
testResult.Reason = "Test Aborted."
}
} else if test.Verdict == "Pass" {
testResult.Verdict = &api.TestCaseResult_Pass_{Pass: &api.TestCaseResult_Pass{}}
testResult.Reason = ""
} else if test.Verdict == "Warn" {
testResult.Verdict = &api.TestCaseResult_Pass_{Pass: &api.TestCaseResult_Pass{}}
testResult.Reason = test.Errmsg
if test.Errmsg == "" {
testResult.Reason = "Test had warnings."
}
} else if test.Verdict == "Not Run" {
testResult.Verdict = &api.TestCaseResult_Skip_{Skip: &api.TestCaseResult_Skip{}}
testResult.Reason = test.Errmsg
if test.Errmsg == "" {
testResult.Reason = "Test ended with Not Run state."
}
}
// r.testCaseResults = append(r.testCaseResults, &testResult)
return &testResult
}
// MissingTestsReports returns tests not found in the resultsdir, marked as err.
func (r *Report) MissingTestsReports(reason string, testNamesToMetadata map[string]*api.TestCaseMetadata) []*api.TestCaseResult {
var missingTestResults []*api.TestCaseResult
for _, t := range r.tests {
if _, ok := r.reportedTests[t]; ok {
continue
}
testID, ok := r.testNamesToIds[t]
if !ok {
continue
}
testMetadata, ok := testNamesToMetadata[t]
if !ok {
testMetadata = nil
log.Printf("failed to find test case metadata for missing test: %v.", t)
}
missingTestResults = append(missingTestResults, &api.TestCaseResult{
TestCaseId: &api.TestCase_Id{Value: testID},
Verdict: &api.TestCaseResult_NotRun_{NotRun: &api.TestCaseResult_NotRun{}},
Reason: reason,
TestHarness: &api.TestHarness{
TestHarnessType: &api.TestHarness_Tauto_{
Tauto: &api.TestHarness_Tauto{},
},
},
TestCaseMetadata: testMetadata,
})
}
return missingTestResults
}
// TestsReports returns results to all tests.
func TestsReports(resultsDir string, tests []string, testNamesToIds map[string]string, testNamesToMetadata map[string]*api.TestCaseMetadata, missingReason string) ([]*api.TestCaseResult, error) {
report := Report{
reportedTests: make(map[string]struct{}),
tests: tests,
testResultsDir: resultsDir,
testNamesToIds: testNamesToIds,
}
err := report.loadJSON(resultsDir)
if err != nil {
return append(report.testCaseResults, report.MissingTestsReports(missingReason, testNamesToMetadata)...), err
}
for _, test := range report.RawResults.Tests {
var testID string
testID, ok := testNamesToIds[test.Testname]
if !ok {
testID = test.Testname
log.Printf("failed to find test id for test: %v, will default to this name.", test.Testname)
}
testMetadata, ok := testNamesToMetadata[test.Testname]
if !ok {
testMetadata = nil
log.Printf("failed to find test case metadata for test: %v.", test.Testname)
}
report.reportedTests[test.Testname] = struct{}{}
report.testCaseResults = append(report.testCaseResults, GenerateReport(test, testID, resultsDir, testMetadata))
}
return append(report.testCaseResults, report.MissingTestsReports(missingReason, testNamesToMetadata)...), nil
}