cros-test-finder: identify all tests matched suite definition

Identify all tests matched suite definition

$ cros-test-finder --input \
  ./src/chromiumos/test/test_finder/data/request.json \
  -metadatadir ./src/chromiumos/test/execution/data/metadata \
  -output finder.json
2021/09/30 23:53:10 cros-test-finder version  cros-test-finder-9999
2021/09/30 23:53:10 Reading metadata from directory:  ./src/chromiumos/test/execution/data/metadata
2021/09/30 23:53:10 Reading input file:  ./src/chromiumos/test/test_finder/data/request.json
2021/09/30 23:53:10 Writing output file:  finder.json

BUG=b:200620228
TEST=./fast_build.sh -T; sudo emerge cros-test-finder; cros-test-finder

Change-Id: I55de6078683edc354c67ffe4457a343032e7a7f2
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/3198804
Reviewed-by: Sean McAllister <smcallis@google.com>
Reviewed-by: Jesse McGuire <jessemcguire@google.com>
Reviewed-by: Tim Bain <tbain@google.com>
Tested-by: Seewai Fu <seewaifu@google.com>
Commit-Queue: Seewai Fu <seewaifu@google.com>
diff --git a/src/chromiumos/test/test_finder/cmd/cros-test-finder/main.go b/src/chromiumos/test/test_finder/cmd/cros-test-finder/main.go
index 5809866..093ca8e 100644
--- a/src/chromiumos/test/test_finder/cmd/cros-test-finder/main.go
+++ b/src/chromiumos/test/test_finder/cmd/cros-test-finder/main.go
@@ -12,9 +12,15 @@
 	"log"
 	"os"
 	"path/filepath"
+	"strings"
 	"time"
 
+	"github.com/golang/protobuf/jsonpb"
+	"go.chromium.org/chromiumos/config/go/test/api"
+
 	"chromiumos/test/execution/errors"
+	"chromiumos/test/util/finder"
+	"chromiumos/test/util/metadata"
 )
 
 const (
@@ -51,6 +57,62 @@
 	return log.New(mw, "", log.LstdFlags|log.LUTC)
 }
 
+// readInput reads a CrosTestFinderRequest jsonproto file and returns a pointer to RunTestsRequest.
+func readInput(fileName string) (*api.CrosTestFinderRequest, error) {
+	f, err := os.Open(fileName)
+	if err != nil {
+		return nil, errors.NewStatusError(errors.IOAccessError,
+			fmt.Errorf("fail to read file %v: %v", fileName, err))
+	}
+	req := api.CrosTestFinderRequest{}
+	if err := jsonpb.Unmarshal(f, &req); err != nil {
+		return nil, errors.NewStatusError(errors.UnmarshalError,
+			fmt.Errorf("fail to unmarshal file %v: %v", fileName, err))
+	}
+	return &req, nil
+}
+
+// writeOutput writes a CrosTestFinderResponse json.
+func writeOutput(output string, resp *api.CrosTestFinderResponse) error {
+	f, err := os.Create(output)
+	if err != nil {
+		return errors.NewStatusError(errors.IOCreateError,
+			fmt.Errorf("fail to create file %v: %v", output, err))
+	}
+	m := jsonpb.Marshaler{}
+	if err := m.Marshal(f, resp); err != nil {
+		return errors.NewStatusError(errors.MarshalError,
+			fmt.Errorf("failed to marshall response to file %v: %v", output, err))
+	}
+	return nil
+}
+
+// combineTestSuiteNames combines a list of test suite names to one single name.
+func combineTestSuiteNames(suites []*api.TestSuite) string {
+	if len(suites) == 0 {
+		return "CombinedSuite"
+	}
+	var names []string
+	for _, s := range suites {
+		names = append(names, s.Name)
+	}
+	return strings.Join(names, ",")
+}
+
+// metadataToTestSuite convert a list of test metadata to a test suite.
+func metadataToTestSuite(name string, mdList []*api.TestCaseMetadata) *api.TestSuite {
+	testIds := []*api.TestCase_Id{}
+	for _, md := range mdList {
+		testIds = append(testIds, md.TestCase.Id)
+	}
+	return &api.TestSuite{
+		Name: name,
+		Spec: &api.TestSuite_TestCaseIds{
+			TestCaseIds: &api.TestCaseIdList{TestCaseIds: testIds},
+		},
+	}
+}
+
 func main() {
 	os.Exit(func() int {
 		t := time.Now()
@@ -79,9 +141,37 @@
 
 		logger := newLogger(logFile)
 		logger.Println("cros-test-finder version ", Version)
-		logger.Println("cros-test-finder input file: ", *input)
-		logger.Println("cros-test-finder output file ", *output)
-		logger.Println("cros-test-finder metadata directory: ", *metadataDir)
+
+		logger.Println("Reading metadata from directory: ", *metadataDir)
+		allTestMetadata, err := metadata.ReadDir(*metadataDir)
+		if err != nil {
+			logger.Println("Error: ", err)
+			return errors.WriteError(os.Stderr, err)
+		}
+
+		logger.Println("Reading input file: ", *input)
+		req, err := readInput(*input)
+		if err != nil {
+			logger.Println("Error: ", err)
+			return errors.WriteError(os.Stderr, err)
+		}
+
+		suiteName := combineTestSuiteNames(req.TestSuites)
+
+		selectedTestMetadata, err := finder.MatchedTestsForSuites(allTestMetadata.Values, req.TestSuites)
+		if err != nil {
+			logger.Println("Error: ", err)
+			return errors.WriteError(os.Stderr, err)
+		}
+
+		resultTestSuite := metadataToTestSuite(suiteName, selectedTestMetadata)
+
+		logger.Println("Writing output file: ", *output)
+		rspn := &api.CrosTestFinderResponse{TestSuites: []*api.TestSuite{resultTestSuite}}
+		if err := writeOutput(*output, rspn); err != nil {
+			logger.Println("Error: ", err)
+			return errors.WriteError(os.Stderr, err)
+		}
 
 		return 0
 	}())
diff --git a/src/chromiumos/test/test_finder/cmd/cros-test-finder/main_test.go b/src/chromiumos/test/test_finder/cmd/cros-test-finder/main_test.go
new file mode 100644
index 0000000..5637b0e
--- /dev/null
+++ b/src/chromiumos/test/test_finder/cmd/cros-test-finder/main_test.go
@@ -0,0 +1,242 @@
+// 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
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/google/go-cmp/cmp"
+	"go.chromium.org/chromiumos/config/go/test/api"
+)
+
+func TestReadInput(t *testing.T) {
+	expReq := &api.CrosTestFinderRequest{
+		TestSuites: []*api.TestSuite{
+			{
+				Name: "suite1",
+				Spec: &api.TestSuite_TestCaseIds{
+					TestCaseIds: &api.TestCaseIdList{
+						TestCaseIds: []*api.TestCase_Id{
+							{
+								Value: "example.Pass",
+							},
+							{
+								Value: "example.Fail",
+							},
+						},
+					},
+				},
+			},
+			{
+				Name: "suite2",
+				Spec: &api.TestSuite_TestCaseTagCriteria_{
+					TestCaseTagCriteria: &api.TestSuite_TestCaseTagCriteria{
+						Tags: []string{"group:meta"},
+					},
+				},
+			},
+		},
+	}
+
+	m := jsonpb.Marshaler{}
+	encodedData, err := m.MarshalToString(expReq)
+	if err != nil {
+		t.Fatal("Failed to marshall request")
+	}
+	td, err := ioutil.TempDir("", "testexecserver_TestReadInput_*")
+	if err != nil {
+		t.Fatal("Failed to create temporary dictectory: ", err)
+	}
+
+	defer os.RemoveAll(td)
+	fn := filepath.Join(td, "t.json")
+	if err := ioutil.WriteFile(fn, []byte(encodedData), 0644); err != nil {
+		t.Fatalf("Failed to write file %v: %v", fn, err)
+	}
+	req, err := readInput(fn)
+	if err != nil {
+		t.Fatalf("Failed to read input file %v: %v", fn, err)
+	}
+	if diff := cmp.Diff(req, expReq, cmp.AllowUnexported(api.RunTestsRequest{})); diff != "" {
+		t.Errorf("Got unexpected request from readInput (-got +want):\n%s", diff)
+	}
+}
+
+func TestWriteOutput(t *testing.T) {
+	expectedRspn := api.CrosTestFinderResponse{
+		TestSuites: []*api.TestSuite{
+			{
+				Name: "suite1",
+				Spec: &api.TestSuite_TestCaseIds{
+					TestCaseIds: &api.TestCaseIdList{
+						TestCaseIds: []*api.TestCase_Id{
+							{
+								Value: "example.Pass",
+							},
+							{
+								Value: "example.Fail",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	td, err := ioutil.TempDir("", "faketestrunner_TestWriteOutput_*")
+	if err != nil {
+		t.Fatal("Failed to create temporary dictectory: ", err)
+	}
+	defer os.RemoveAll(td)
+	fn := filepath.Join(td, "t.json")
+	if err := writeOutput(fn, &expectedRspn); err != nil {
+		t.Fatalf("Failed to write file %v: %v", fn, err)
+	}
+	f, err := os.Open(fn)
+	if err != nil {
+		t.Fatalf("Failed to read response from file %v: %v", fn, err)
+	}
+	rspn := api.CrosTestFinderResponse{}
+	if err := jsonpb.Unmarshal(f, &rspn); err != nil {
+		t.Fatalf("Failed to unmarshall data from file %v: %v", fn, err)
+	}
+	if diff := cmp.Diff(rspn, expectedRspn, cmp.AllowUnexported(api.CrosTestFinderResponse{})); diff != "" {
+		t.Errorf("Got unexpected data from writeOutput (-got +want):\n%s", diff)
+	}
+}
+
+func TestCombineSuiteNames(t *testing.T) {
+	suites := []*api.TestSuite{
+		{
+			Name: "suite1",
+			Spec: &api.TestSuite_TestCaseIds{
+				TestCaseIds: &api.TestCaseIdList{
+					TestCaseIds: []*api.TestCase_Id{
+						{
+							Value: "example.Pass",
+						},
+						{
+							Value: "example.Fail",
+						},
+					},
+				},
+			},
+		},
+		{
+			Name: "suite2",
+			Spec: &api.TestSuite_TestCaseTagCriteria_{
+				TestCaseTagCriteria: &api.TestSuite_TestCaseTagCriteria{
+					Tags: []string{"group:meta"},
+				},
+			},
+		},
+	}
+	name := combineTestSuiteNames(suites)
+	if name != "suite1,suite2" {
+		t.Errorf(`Got %s from combineTestSuiteNames; wanted "suite1,suite2"`, name)
+	}
+}
+
+func TestMetadataToTestSuite(t *testing.T) {
+	mdList := []*api.TestCaseMetadata{
+		{
+			TestCase: &api.TestCase{
+				Id: &api.TestCase_Id{
+					Value: "tast/test001",
+				},
+				Name: "tastTest",
+				Tags: []*api.TestCase_Tag{
+					{Value: "attr1"},
+					{Value: "attr2"},
+				},
+			},
+			TestCaseExec: &api.TestCaseExec{
+				TestHarness: &api.TestHarness{
+					TestHarnessType: &api.TestHarness_Tast_{
+						Tast: &api.TestHarness_Tast{},
+					},
+				},
+			},
+			TestCaseInfo: &api.TestCaseInfo{
+				Owners: []*api.Contact{
+					{Email: "someone1@chromium.org"},
+				},
+			},
+		},
+		{
+			TestCase: &api.TestCase{
+				Id: &api.TestCase_Id{
+					Value: "tauto/test002",
+				},
+				Name: "tautoTest",
+				Tags: []*api.TestCase_Tag{
+					{Value: "attr1"},
+					{Value: "attr2"},
+				},
+			},
+			TestCaseExec: &api.TestCaseExec{
+				TestHarness: &api.TestHarness{
+					TestHarnessType: &api.TestHarness_Tauto_{
+						Tauto: &api.TestHarness_Tauto{},
+					},
+				},
+			},
+			TestCaseInfo: &api.TestCaseInfo{
+				Owners: []*api.Contact{
+					{Email: "someone2@chromium.org"},
+				},
+			},
+		},
+		{
+			TestCase: &api.TestCase{
+				Id: &api.TestCase_Id{
+					Value: "tauto/test003",
+				},
+				Name: "tautoTest",
+				Tags: []*api.TestCase_Tag{
+					{Value: "attr3"},
+				},
+			},
+			TestCaseExec: &api.TestCaseExec{
+				TestHarness: &api.TestHarness{
+					TestHarnessType: &api.TestHarness_Tauto_{
+						Tauto: &api.TestHarness_Tauto{},
+					},
+				},
+			},
+			TestCaseInfo: &api.TestCaseInfo{
+				Owners: []*api.Contact{
+					{Email: "someone3@chromium.org"},
+				},
+			},
+		},
+	}
+	expected := api.TestSuite{
+		Name: "test_suite",
+		Spec: &api.TestSuite_TestCaseIds{
+			TestCaseIds: &api.TestCaseIdList{
+				TestCaseIds: []*api.TestCase_Id{
+					{
+						Value: "tast/test001",
+					},
+					{
+						Value: "tauto/test002",
+					},
+					{
+						Value: "tauto/test003",
+					},
+				},
+			},
+		},
+	}
+	suites := metadataToTestSuite("test_suite", mdList)
+	if diff := cmp.Diff(suites, &expected, cmp.AllowUnexported(api.TestSuite{})); diff != "" {
+		t.Errorf("Got unexpected test suite from metadataToTestSuite (-got +want):\n%s", diff)
+	}
+
+}
diff --git a/src/chromiumos/test/test_finder/data/request.json b/src/chromiumos/test/test_finder/data/request.json
new file mode 100644
index 0000000..95d99dd
--- /dev/null
+++ b/src/chromiumos/test/test_finder/data/request.json
@@ -0,0 +1,17 @@
+{
+    "testSuites":[{
+        "name":"suite1",
+        "testCaseIds":{
+            "testCaseIds":[
+                {"value":"tast.example.Pass"},
+                {"value":"tast.example.Fail"}
+            ]
+        }
+    },
+    {
+        "name":"suite2",
+        "testCaseTagCriteria":{
+            "tags":["group:meta"]
+        }
+    }]
+}