package integration import ( "encoding/json" "fmt" "io" "strings" "time" "edge-infra.dev/test/framework" ) type TestEvent struct { Time *time.Time // encodes as an RFC3339-format string Action string Package string Test string Elapsed *float64 // seconds Output string } type TestCase struct { State string Output strings.Builder Duration *time.Duration } type TestPackage struct { Duration *time.Duration State string TestCases map[string]*TestCase PackageName string } // Interface to parse test results from various data formats into a Go struct type TestResultsParser interface { ParseTestResults(logs io.Reader, packageName string) (*TestPackage, error) } // Process the result of each test case in a TestPackage as though it were a locally-run test func ReportTestCases(f *framework.Framework, testPackage *TestPackage) { f.Run(testPackage.PackageName, func() { for testname, tc := range testPackage.TestCases { // Ideally each level of test nesting would have their own s.Run call, because this would then // mirror the actual pod test. Alternatively could just filter them out when parseJsonTestResults f.Run(testname, func() { f.Log(tc.Output.String()) switch tc.State { case "skip": f.Skip(testname, "skipped") case "fail": f.Fail("Failed") case "pass": // do nothing default: // Should we error if there was no pass/skip/fail event for test? } }) } }) } // Parse JSON-formatted test results type JSONTestResultsParser struct{} // Convert JSON-formatted test results to TestPackage Go struct func (p JSONTestResultsParser) ParseTestResults(logs io.Reader, packageName string) (*TestPackage, error) { testcases := make(map[string]*TestCase) // Returns the testcase with the given test name, creating a new empty test // case if one does not exist testCaseByName := func(name string) *TestCase { if name == "" { return nil } if _, ok := testcases[name]; !ok { testcases[name] = &TestCase{} } return testcases[name] } dec := json.NewDecoder(logs) var pkgDuration *time.Duration var state string for { var e TestEvent if err := dec.Decode(&e); err == io.EOF { break } else if err != nil { return nil, fmt.Errorf("error decoding test output: %w", err) } switch s := e.Action; s { case "run": if c := testCaseByName(e.Test); c != nil { c.State = s } case "output": if c := testCaseByName(e.Test); c != nil { c.Output.WriteString(e.Output) } case "skip": if c := testCaseByName(e.Test); c != nil { c.Output.WriteString(e.Output) c.State, state = s, s elapsed := time.Duration(*e.Elapsed * float64(time.Second)) c.Duration = &elapsed } case "fail", "pass": elapsed := time.Duration(*e.Elapsed * float64(time.Second)) if c := testCaseByName(e.Test); c != nil { c.State = s c.Duration = &elapsed } else { state = s pkgDuration = &elapsed } } } testpkg := TestPackage{Duration: pkgDuration, State: state, TestCases: testcases, PackageName: packageName} return &testpkg, nil }