TF result given test cases cros-test reponse from XML

Adding given test cases to cros test response from XML parsed data. This is not replacing existing test case result slice.

BUG=None
TEST=go test
TEST=LED http://shortn/_LgQHR5kInT

Change-Id: I8afc664b261696bc620d2d04856609d62d916bae
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/5912801
Auto-Submit: Varun Srivastav <varunsrivastav@google.com>
Commit-Queue: Varun Srivastav <varunsrivastav@google.com>
Commit-Queue: Alex Bergman <abergman@google.com>
Reviewed-by: Alex Bergman <abergman@google.com>
Tested-by: Varun Srivastav <varunsrivastav@google.com>
diff --git a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_driver.go b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_driver.go
index a3b0eeb..007b610 100644
--- a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_driver.go
+++ b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_driver.go
@@ -160,9 +160,10 @@
 	// One difference between Tast/Autotest and this is that the test case given will likely result in many
 	// testcases; where T/AT were normally 1:1
 
-	results, artifacts := buildTradefedResult(td.logger, testType)
+	results, artifacts := buildTradefedResult(td.logger, testType, req)
 	if results.GetTestCaseResults() != nil {
 		allRspn.TestCaseResults = append(allRspn.TestCaseResults, results.TestCaseResults...)
+		allRspn.GivenTestResults = append(allRspn.GivenTestResults, results.GivenTestResults...)
 	} else {
 		td.logger.Println("No results to report: ", results)
 	}
diff --git a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results.go b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results.go
index 88226b8..241a2a9 100644
--- a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results.go
+++ b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results.go
@@ -244,30 +244,77 @@
 			status = testInfo.Status
 		}
 		// TODO; if no testInfo matched, calculate status.
-		errorText := ""
-		switch status {
-		case "ASSUMPTION_FAILURE":
-			errorText = test.AsssumptionFailure
-		case "FAILED":
-			errorText = test.Failure
-		case "INCOMPLETE":
-			errorText = test.Incomplete
-		case "SKIPPED":
-			errorText = test.Skipped
-		case "IGNORED":
-			errorText = test.Ignored
-		default:
-			errorText = "Unknown test status"
-		}
-
+		errorText := getErrorText(status, test)
 		allTestCases = append(allTestCases,
 			buildTcResult(fmt.Sprintf("tradefed.%s.%s#%s", testType, testInfo.ModuleName, fullTestName),
 				status, startTime, test.Time/1000, errorText))
 	}
+
 	return allTestCases
 }
 
-func parseCompatibilityXmlResults(logger *log.Logger, testType string) ([]*api.TestCaseResult, []string, error) {
+func getErrorText(status string, test TestCase) string {
+	errorText := ""
+	switch status {
+	case "ASSUMPTION_FAILURE":
+		errorText = test.AsssumptionFailure
+	case "FAILED":
+		errorText = test.Failure
+	case "INCOMPLETE":
+		errorText = test.Incomplete
+	case "SKIPPED":
+		errorText = test.Skipped
+	case "IGNORED":
+		errorText = test.Ignored
+	default:
+		errorText = "Unknown test status"
+	}
+	return errorText
+}
+
+func processGivenTestResults(logger *log.Logger, TS TestSuite, testInfoMap map[string]EventLogTestInfo) []*api.CrosTestResponse_GivenTestResult {
+	givenTestCases := []*api.CrosTestResponse_GivenTestResult{}
+	// map format for holding parent child like relationship. This will be used later to populate givenTestCases
+	testMap := make(map[string][]*api.TestCaseResult)
+
+	startTime, err := time.Parse(time.RFC3339, normalizeTime(TS.Timestamp))
+	if err != nil {
+		logger.Println("Error parsing Tradefed test start time: ", TS.Timestamp)
+		startTime = time.Now()
+	}
+
+	logger.Println("Creating test map with parent/module as key and test case result as child list")
+	// for all test cases parsed from metrics-*.xml
+	for _, test := range TS.TestCases {
+		fullTestName := fmt.Sprintf("%s#%s", test.ClassName, test.Name)
+		testInfo, ok := testInfoMap[fullTestName]
+		// check if exists in testInfoMap parsed from aggregated-events*.txt, testInfoMap has module name, status info as value for each test in map
+		if ok {
+			// form child test case result
+			testCaseResult := buildTcResult(fullTestName, testInfo.Status, startTime, test.Time/1000, getErrorText(testInfo.Status, test))
+			// add to child case list by module
+			if val, ok := testMap[testInfo.ModuleName]; ok {
+				testMap[testInfo.ModuleName] = append(val, testCaseResult)
+			} else {
+				testMap[testInfo.ModuleName] = []*api.TestCaseResult{testCaseResult}
+			}
+		}
+	}
+
+	// create given test cases object from the testMap
+	logger.Println("Creating given test case results object from test map")
+	for parent, childCaseList := range testMap {
+		givenTestCases = append(givenTestCases, &api.CrosTestResponse_GivenTestResult{
+			ParentTest:           parent,
+			ChildTestCaseResults: childCaseList,
+		})
+		logger.Printf("Given test case with parent: %s added with %d child test cases\n", parent, len(childCaseList))
+	}
+
+	return givenTestCases
+}
+
+func parseCompatibilityXmlResults(logger *log.Logger, testType string, req *api.CrosTestRequest) ([]*api.TestCaseResult, []*api.CrosTestResponse_GivenTestResult, []string, error) {
 	var logsDir []string
 	allTestCases := []*api.TestCaseResult{}
 
@@ -276,7 +323,7 @@
 		// Fallback to stub location for XML result file.
 		xmlResultFile, err = selectFileByPattern(filepath.Join(tfStageLogsPath, tfStageResultPattern))
 		if err != nil {
-			return allTestCases, logsDir, fmt.Errorf("Failed to locate result file: %w", err)
+			return allTestCases, nil, logsDir, fmt.Errorf("Failed to locate result file: %w", err)
 		}
 	}
 
@@ -285,18 +332,49 @@
 	logger.Println("Parsing results from: ", xmlResultFile)
 	R := Result{}
 	if err = parseResultFile(logger, xmlResultFile, &R); err != nil {
-		return allTestCases, logsDir, err
+		return allTestCases, nil, logsDir, err
 	}
+	allTestCases = generateTestCaseResult(logger, testType, R, req)
+	givenTestCases := generateGivenTestCases(logger, testType, R, req)
 
+	return allTestCases, givenTestCases, logsDir, nil
+}
+
+func testResponseNoXMLFound(req *api.CrosTestRequest) *api.CrosTestResponse {
+	r := &api.CrosTestResponse{}
+	if req == nil || req.GetTestSuites() == nil {
+		return r
+	}
+	for _, suites := range req.GetTestSuites() {
+		for _, testCaseIds := range suites.GetTestCaseIds().GetTestCaseIds() {
+			testCaseResult := buildTcResult(testCaseIds.GetValue(), "INCOMPLETE", time.Time{}, 1, "")
+			r.TestCaseResults = append(r.TestCaseResults, testCaseResult)
+			r.GivenTestResults = append(r.GivenTestResults, &api.CrosTestResponse_GivenTestResult{
+				ParentTest:           testCaseIds.GetValue(),
+				ChildTestCaseResults: []*api.TestCaseResult{testCaseResult},
+			})
+		}
+	}
+	return r
+}
+
+func generateTestCaseResult(logger *log.Logger, testType string, R Result, req *api.CrosTestRequest) []*api.TestCaseResult {
+	allTestCases := []*api.TestCaseResult{}
 	startTimeMs, _ := strconv.ParseInt(R.Start, 10, 64)
 	startTime := time.UnixMilli(startTimeMs)
 	logger.Println("Found n module results:", len(R.Modules))
+	modulesFromReq := getAllModulesFromReq(req)
 	for _, module := range R.Modules {
+		delete(modulesFromReq, fmt.Sprintf("tradefed.%s.%s", testType, module.Name))
 		nTests, _ := strconv.Atoi(module.Total_Tests)
 		runTime, _ := strconv.Atoi(module.Runtime)
 		duration := runTime
 		if nTests != 0 {
 			duration = runTime / nTests
+		} else {
+			// mark module as Crash if no tests were executed for the module
+			allTestCases = append(allTestCases,
+				buildTcResult(fmt.Sprintf("tradefed.%s.%s", testType, module.Name), "INCOMPLETE", time.Time{}, 1, ""))
 		}
 
 		logger.Println("Parsing Module: ", module.Name)
@@ -314,11 +392,76 @@
 			}
 		}
 	}
-
-	return allTestCases, logsDir, nil
+	for moduleName := range modulesFromReq {
+		allTestCases = append(allTestCases,
+			buildTcResult(moduleName, "INCOMPLETE", time.Time{}, 1, ""))
+	}
+	return allTestCases
 }
 
-func buildTradefedResult(logger *log.Logger, testType string) (*api.CrosTestResponse, []string) {
+func getAllModulesFromReq(req *api.CrosTestRequest) map[string]struct{} {
+	modules := make(map[string]struct{})
+	if req == nil || req.GetTestSuites() == nil {
+		return modules
+	}
+	for _, suites := range req.GetTestSuites() {
+		for _, testCaseIds := range suites.GetTestCaseIds().GetTestCaseIds() {
+			modules[testCaseIds.GetValue()] = struct{}{}
+		}
+	}
+	return modules
+}
+
+func generateGivenTestCases(logger *log.Logger, testType string, R Result, req *api.CrosTestRequest) []*api.CrosTestResponse_GivenTestResult {
+	givenTestCases := []*api.CrosTestResponse_GivenTestResult{}
+	startTimeMs, _ := strconv.ParseInt(R.Start, 10, 64)
+	startTime := time.UnixMilli(startTimeMs)
+	logger.Println("Found n module results:", len(R.Modules))
+	modulesFromReq := getAllModulesFromReq(req)
+	for _, module := range R.Modules {
+		delete(modulesFromReq, fmt.Sprintf("tradefed.%s.%s", testType, module.Name))
+		nTests, _ := strconv.Atoi(module.Total_Tests)
+		runTime, _ := strconv.Atoi(module.Runtime)
+		duration := runTime
+		if nTests != 0 {
+			duration = runTime / nTests
+		} else {
+			// mark module as Crash if no tests were executed for the module
+			givenTestCases = append(givenTestCases, &api.CrosTestResponse_GivenTestResult{
+				ParentTest:           fmt.Sprintf("tradefed.%s.%s", testType, module.Name),
+				ChildTestCaseResults: []*api.TestCaseResult{buildTcResult(fmt.Sprintf("tradefed.%s.%s", testType, module.Name), "INCOMPLETE", time.Time{}, 1, "")},
+			})
+
+		}
+
+		logger.Println("Parsing Module: ", module.Name)
+		// Make a duration of atleast 1 second so the proto isnt empty.
+		testDur := int64(duration / 1000)
+		if testDur == 0 {
+			testDur = 1
+		}
+		givenTestCase := &api.CrosTestResponse_GivenTestResult{}
+		for _, testcase := range module.TestCases {
+
+			childTestCases := []*api.TestCaseResult{}
+			for _, test := range testcase.Tests {
+				testName := fmt.Sprintf("%s#%s", testcase.Name, test.Name)
+				childTestCases = append(childTestCases,
+					buildTcResult(testName, test.Result, startTime, testDur, test.Failure.Message))
+			}
+			givenTestCase.ParentTest = module.Name
+			givenTestCase.ChildTestCaseResults = childTestCases
+		}
+		givenTestCases = append(givenTestCases, givenTestCase)
+	}
+	for moduleName := range modulesFromReq {
+		givenTestCases = append(givenTestCases,
+			&api.CrosTestResponse_GivenTestResult{ParentTest: moduleName, ChildTestCaseResults: []*api.TestCaseResult{buildTcResult(moduleName, "INCOMPLETE", time.Time{}, 1, "")}})
+	}
+	return givenTestCases
+}
+
+func buildTradefedResult(logger *log.Logger, testType string, req *api.CrosTestRequest) (*api.CrosTestResponse, []string) {
 	// List of glob patterns of artifacts that should be saved in test results.
 	var logsToSave = []string{}
 
@@ -329,9 +472,10 @@
 	if err != nil {
 		logger.Println("Error locating Tradefed metric result file: ", err)
 		logger.Println("Trying to locate and parse compatibility result file...")
-		f.TestCaseResults, logsToSave, err = parseCompatibilityXmlResults(logger, testType)
+		f.TestCaseResults, f.GivenTestResults, logsToSave, err = parseCompatibilityXmlResults(logger, testType, req)
 		if err != nil {
 			logger.Println("Error locating or parsing Tradefed compatibility result file: ", err)
+			return testResponseNoXMLFound(req), logsToSave
 		}
 		return f, logsToSave
 	} else {
@@ -339,7 +483,7 @@
 
 		TS := TestSuite{}
 		if err = parseResultFile(logger, resultPath, &TS); err != nil {
-			return f, logsToSave
+			return testResponseNoXMLFound(req), logsToSave
 		}
 
 		testInfoMap, eventLogs := loadEventLogReport(logger)
@@ -351,12 +495,42 @@
 			TS.Name, TS.Tests, TS.Failures)
 
 		f.TestCaseResults = processResults(logger, TS, testInfoMap, testType)
+		f.GivenTestResults = processGivenTestResults(logger, TS, testInfoMap)
+
+		// report modules as Crash for which no tests were reported.
+		modulesWithNoResult := noResultModules(logger, TS, testInfoMap, req, testType)
+		for module := range modulesWithNoResult {
+			crashStatusTestResult := buildTcResult(fmt.Sprintf("tradefed.%s.%s", testType, module),
+				"INCOMPLETE", time.Time{}, 1, "")
+			f.TestCaseResults = append(f.TestCaseResults, crashStatusTestResult)
+			f.GivenTestResults = append(f.GivenTestResults, &api.CrosTestResponse_GivenTestResult{
+				ParentTest:           fmt.Sprintf("tradefed.%s.%s", testType, module),
+				ChildTestCaseResults: []*api.TestCaseResult{crashStatusTestResult},
+			})
+		}
 	}
 	logger.Println("Found n case results:", len(f.TestCaseResults))
 
 	return f, logsToSave
 }
 
+func noResultModules(logger *log.Logger, TS TestSuite, testInfoMap map[string]EventLogTestInfo, req *api.CrosTestRequest, testType string) map[string]struct{} {
+	modulesFromRequest := getAllModulesFromReq(req)
+	for _, test := range TS.TestCases {
+		fullTestName := fmt.Sprintf("%s#%s", test.ClassName, test.Name)
+		testInfo, ok := testInfoMap[fullTestName]
+		if ok {
+			// remove module if a test has been reported for it.
+			delete(modulesFromRequest, fmt.Sprintf("tradefed.%s.%s", testType, testInfo.ModuleName))
+		}
+	}
+	// logging modules which were not found in results from xml
+	for module := range modulesFromRequest {
+		logger.Printf(module)
+	}
+	return modulesFromRequest
+}
+
 func buildTcResult(testName string, testStatus string, startTime time.Time, duration int64,
 	errorMessage string) *api.TestCaseResult {
 
diff --git a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results_test.go b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results_test.go
index 3961f25..3d82f44 100644
--- a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results_test.go
+++ b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_results_test.go
@@ -19,8 +19,10 @@
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
-const TradefedTestResultFile = "tradefed_test_data/test_result.xml"
-
+const (
+	TradefedTestResultFile              = "tradefed_test_data/test_result.xml"
+	TradefedTestResultFileModulesNoTest = "tradefed_test_data/test_result_modules_no_test.xml"
+)
 const (
 	FileSelectFileByPattern1 = "/tmp/test-SelectFileByPattern-file1.txt"
 	FileSelectFileByPattern2 = "/tmp/test-SelectFileByPattern-file2.txt"
@@ -282,6 +284,34 @@
 // 	})
 // }
 
+func Test_BuildTFResult(t *testing.T) {
+	R := Result{}
+	testType := "CTS"
+	moduleName := "tradefed" + "." + testType + "." + "CtsJvmtiRunTest1976HostTestCases"
+	logger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
+	err := parseResultFile(logger, TradefedTestResultFileModulesNoTest, &R)
+	if err != nil {
+		t.Logf("No error expected")
+	}
+	allTestCases := generateTestCaseResult(logger, testType, R, nil)
+	givenTestCases := generateGivenTestCases(logger, testType, R, nil)
+	if allTestCases[0].TestCaseId.Value != moduleName {
+		t.Errorf("Expected %s, found %s", moduleName, allTestCases[0].TestCaseId.Value)
+	}
+	if givenTestCases[0].GetParentTest() != moduleName {
+		t.Errorf("Expected %s, found %s", moduleName, givenTestCases[0].GetParentTest())
+	}
+	if _, ok := allTestCases[0].Verdict.(*api.TestCaseResult_Crash_); !ok {
+		t.Errorf("Expected verdict CRASH")
+	}
+	if givenTestCases[0].GetParentTest() != moduleName {
+		t.Errorf("Expected %s, found %s", moduleName, givenTestCases[0].GetParentTest())
+	}
+	if _, ok := givenTestCases[0].ChildTestCaseResults[0].Verdict.(*api.TestCaseResult_Crash_); !ok {
+		t.Errorf("Expected verdict CRASH")
+	}
+
+}
 func TestBuildTcResult(t *testing.T) {
 	t.Parallel()
 
diff --git a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_test_data/test_result_modules_no_test.xml b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_test_data/test_result_modules_no_test.xml
new file mode 100644
index 0000000..2ccac82
--- /dev/null
+++ b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/driver/tradefed_test_data/test_result_modules_no_test.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no' ?><?xml-stylesheet type="text/xsl" href="compatibility_result.xsl"?>
+<Result start="1724450705582" end="1724450783748" start_display="Fri Aug 23 22:05:05 UTC 2024" end_display="Fri Aug 23 22:06:23 UTC 2024" command_line_args="cts --include-filter CtsJvmtiRunTest1976HostTestCases --include-filter CtsJvmtiRunTest1977HostTestCases --include-filter CtsJvmtiRunTest1978HostTestCases --include-filter CtsJvmtiRunTest1979HostTestCases --include-filter CtsJvmtiRunTest1981HostTestCases --logcat-on-failure -s satlab-0wgatfqi22088039-host3:5555" suite_name="CTS" suite_variant="CTS" suite_version="14_r4" suite_plan="cts" suite_build_number="11801623" report_version="5.0" devices="satlab-0wgatfqi22088039-host3:5555" host_name="0ed35a7e333f" os_name="Linux" os_version="5.15.103-17409-g07029265d738" os_arch="amd64" java_vendor="N/A" java_version="17.0.4.1">
+  <Build invocation-id="1" command_line_args="cts --include-filter CtsJvmtiRunTest1976HostTestCases --include-filter CtsJdwpTestCases.jar"  />
+  <Summary pass="3" failed="2" modules_done="1" modules_total="1" />
+  <Module name="CtsJvmtiRunTest1976HostTestCases" abi="x86_64" runtime="6254" done="true" pass="3" total_tests="0">
+  </Module>
+</Result>
diff --git a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/testexecserver.go b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/testexecserver.go
index 70376da..643a4e2 100644
--- a/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/testexecserver.go
+++ b/src/go.chromium.org/chromiumos/test/execution/cmd/cros-test/testexecserver.go
@@ -107,6 +107,7 @@
 			return nil, err
 		}
 		allRspn.TestCaseResults = append(allRspn.TestCaseResults, rspn.TestCaseResults...)
+		allRspn.GivenTestResults = append(allRspn.GivenTestResults, rspn.GivenTestResults...)
 	}
 	return &allRspn, nil
 }