Add test purpose to fw_e2e_coverage_summarizer.go

This CL also does a bit of renaming to be more consistent with the names
used in Autotest control files:
* "Owners" becomes "Authors"
* "Description" becomes "Purpose"

BUG=None
TEST=Run script, inspect output

Change-Id: I1543fb0e99a893294c5b77af6d52cbbb1593afa5
Signed-off-by: Greg Edelston <gredelston@google.com>
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crostestutils/+/1880069
Reviewed-by: Kevin Shelton <kmshelton@chromium.org>
Commit-Queue: Kevin Shelton <kmshelton@chromium.org>
diff --git a/provingground/firmware/fw_e2e_coverage_summarizer.go b/provingground/firmware/fw_e2e_coverage_summarizer.go
index 75eaf07..1efffa4 100644
--- a/provingground/firmware/fw_e2e_coverage_summarizer.go
+++ b/provingground/firmware/fw_e2e_coverage_summarizer.go
@@ -25,15 +25,41 @@
 // gitLogAuthorEmailRegexp captures an author's email address from git log.
 var gitLogAuthorEmailRegexp = regexp.MustCompile(`Author:[^<]+<([^>]+)>`)
 
-// controlFileOwnersRegexp captures the AUTHOR line from a test control file.
-var controlFileOwnersRegexp = regexp.MustCompile(`AUTHOR = "([^"]+)"`)
+// controlFileAuthorRegexp captures the AUTHOR line from a test control file.
+var controlFileAuthorRegexp = regexp.MustCompile(`AUTHOR = "+([^"]+)"`)
+
+// controlFilePurposeRegexp captures the PURPOSE line from a test control file.
+var controlFilePurposeRegexp = regexp.MustCompile(`PURPOSE = "+([^"]+)"`)
+
+// e2eTestClassFirmware and e2eTestClassCr50 contain the names of Autotest classes
+// which indicate that a site_test is a firmware test.
+const (
+	e2eTestClassFirmware = "FirmwareTest"
+	e2eTestClassCr50     = "Cr50Test"
+)
+
+// autotestDir is a filepath to the root directory of autotest.
+var autotestDir string
+
+// siteTestsDir is a filepath to autotestDir/server/site_tests/
+var siteTestsDir string
+
+// contains determines whether an array of strings contains a given string.
+func contains(arr []string, elem string) bool {
+	for _, x := range arr {
+		if x == elem {
+			return true
+		}
+	}
+	return false
+}
 
 // e2eTest contains metadata for a single end-to-end tests.
 // End-to-end tests are typically found as directories within
 // autotest/server/site_tests
 type e2eTest struct {
-	Name, TestClass, Description string
-	Owners, RecentEditors        []string
+	Name, TestClass, Purpose string
+	Authors, RecentEditors   []string
 }
 
 // e2eTest.println displays the test struct's field names and values.
@@ -46,50 +72,6 @@
 	return filepath.Join(siteTestsDir, t.Name)
 }
 
-const (
-	e2eTestClassFirmware = "FirmwareTest"
-	e2eTestClassCr50     = "Cr50Test"
-)
-
-var autotestDir string
-var siteTestsDir string
-
-// getDefaultAutotestDir attempts to find Autotest if the -autotest_dir flag is
-// not provided.
-func getDefaultAutotestDir() string {
-	defaultAutotestDir := "/"
-	thisDir, err := os.Getwd()
-	if err != nil {
-		log.Fatal(err, " when getting current working directory")
-	}
-	thisDir, err = filepath.Abs(thisDir)
-	if err != nil {
-		log.Fatal(err, " when parsing cwd as an absolute path")
-	}
-	thisDir = filepath.ToSlash(thisDir)
-	for _, dir := range strings.Split(thisDir, "/") {
-		defaultAutotestDir = filepath.Join(defaultAutotestDir, dir)
-		if dir == "src" {
-			break
-		}
-	}
-	defaultAutotestDir = filepath.Join(defaultAutotestDir, "third_party", "autotest", "files")
-	return defaultAutotestDir
-}
-
-// Init parses command-line args to find autotest and site_tests
-func init() {
-	flag.StringVar(&autotestDir, "autotest_dir", getDefaultAutotestDir(), "The path to your autotest files")
-	flag.Parse()
-	if _, err := os.Stat(autotestDir); os.IsNotExist(err) {
-		log.Fatalf("Could not find autotest directory: <%s>. Try supplying -autotest_dir", autotestDir)
-	}
-	siteTestsDir = filepath.Join(autotestDir, "server", "site_tests")
-	if _, err := os.Stat(siteTestsDir); os.IsNotExist(err) {
-		log.Fatalf("Could not find site_tests directory: <%s>", siteTestsDir)
-	}
-}
-
 // e2eTest.containsStr determines whether server/site_tests/$(t.Name)/*.py contains searchString.
 func (t e2eTest) containsStr(searchString string) (bool, error) {
 	testDir := filepath.Join(siteTestsDir, t.Name)
@@ -113,10 +95,20 @@
 	return false, nil
 }
 
-// contains determines whether an array of strings contains a given string.
-func contains(arr []string, elem string) bool {
-	for _, x := range arr {
-		if x == elem {
+// e2eTest.setClass searches the test's .py files for any strings that indicate
+// that the test is a firmware class. If any matches are found, then t.TestClass
+// is set to the matching string, and the function returns true (indicating that
+// the test is a firmware test).
+// Otherwise, the function returns false (indicating that the test is not a
+// firmware test).
+func (t *e2eTest) setClass() bool {
+	for _, testClass := range [2]string{e2eTestClassFirmware, e2eTestClassCr50} {
+		hasClass, err := t.containsStr(testClass)
+		if err != nil {
+			log.Fatalf("%v for test %s", err, t.Name)
+		}
+		if hasClass {
+			t.TestClass = testClass
 			return true
 		}
 	}
@@ -150,17 +142,51 @@
 	return nil
 }
 
-// e2eTest.setOwners populates e2eTest.Owners based on the AUTHORS line of the control file
-func (t *e2eTest) setOwners() error {
-	controlFile := t.findOneControlFile()
-	controlFileBytes, err := ioutil.ReadFile(controlFile)
+// captureGroupFromFileContents runs a Regexp on the contents of a file,
+// and returns the first capture group (or "" if no match is found).
+func captureGroupFromFileContents(fp string, re *regexp.Regexp) (string, error) {
+	fileBytes, err := ioutil.ReadFile(fp)
 	if err != nil {
-		return fmt.Errorf("%v when reading control file %s", err, controlFile)
+		return "", fmt.Errorf("%v when reading file %s", err, fp)
 	}
-	ownerMatch := controlFileOwnersRegexp.FindStringSubmatch(string(controlFileBytes))
-	for _, owner := range strings.Split(ownerMatch[1], ",") {
-		t.Owners = append(t.Owners, strings.TrimSpace(owner))
+	match := re.FindStringSubmatch(string(fileBytes))
+	if match != nil {
+		return strings.TrimSpace(match[1]), nil
 	}
+	return "", nil
+}
+
+// e2eTest.setAuthors populates e2eTest.Authors based on the AUTHORS line of the control file
+func (t *e2eTest) setAuthors() error {
+	controlFile := t.findOneControlFile()
+	authors, err := captureGroupFromFileContents(controlFile, controlFileAuthorRegexp)
+	if err != nil {
+		return fmt.Errorf("%v when setting Authors", err)
+	}
+	for _, author := range strings.Split(authors, ",") {
+		t.Authors = append(t.Authors, strings.TrimSpace(author))
+	}
+	return nil
+}
+
+// e2eTest.setPurpose populates e2eTest.Purpose based on the PURPOSE line of the control file.
+// If there is a file named "control", then its PURPOSE is assumed to be the source-of-truth.
+// Otherwise, all unique PURPOSEs from control-files are concatenated and semicolon-deliminated.
+func (t *e2eTest) setPurpose() error {
+	allPurposes := []string{}
+	for _, controlFile := range t.findAllControlFiles() {
+		purpose, err := captureGroupFromFileContents(controlFile, controlFilePurposeRegexp)
+		if err != nil {
+			return fmt.Errorf("%v when setting Purpose", err)
+		}
+		if !contains(allPurposes, purpose) {
+			allPurposes = append(allPurposes, purpose)
+		}
+		if filepath.Base(controlFile) == "control" {
+			break
+		}
+	}
+	t.Purpose = strings.Join(allPurposes, "; ")
 	return nil
 }
 
@@ -183,6 +209,42 @@
 	return controlFiles
 }
 
+// getDefaultAutotestDir attempts to find Autotest if the -autotest_dir flag is
+// not provided.
+func getDefaultAutotestDir() string {
+	defaultAutotestDir := "/"
+	thisDir, err := os.Getwd()
+	if err != nil {
+		log.Fatal(err, " when getting current working directory")
+	}
+	thisDir, err = filepath.Abs(thisDir)
+	if err != nil {
+		log.Fatal(err, " when parsing cwd as an absolute path")
+	}
+	thisDir = filepath.ToSlash(thisDir)
+	for _, dir := range strings.Split(thisDir, "/") {
+		defaultAutotestDir = filepath.Join(defaultAutotestDir, dir)
+		if dir == "src" {
+			break
+		}
+	}
+	defaultAutotestDir = filepath.Join(defaultAutotestDir, "third_party", "autotest", "files")
+	return defaultAutotestDir
+}
+
+// Init parses command-line args to find autotest and site_tests.
+func init() {
+	flag.StringVar(&autotestDir, "autotest_dir", getDefaultAutotestDir(), "The path to your autotest files")
+	flag.Parse()
+	if _, err := os.Stat(autotestDir); os.IsNotExist(err) {
+		log.Fatalf("Could not find autotest directory: <%s>. Try supplying -autotest_dir", autotestDir)
+	}
+	siteTestsDir = filepath.Join(autotestDir, "server", "site_tests")
+	if _, err := os.Stat(siteTestsDir); os.IsNotExist(err) {
+		log.Fatalf("Could not find site_tests directory: <%s>", siteTestsDir)
+	}
+}
+
 // Collect and report info on all firmware end-to-end tests.
 func main() {
 	siteTestFileObjs, err := ioutil.ReadDir(siteTestsDir)
@@ -197,21 +259,10 @@
 			continue
 		}
 		t = e2eTest{Name: fo.Name()}
-		for _, testClass := range []string{e2eTestClassFirmware, e2eTestClassCr50} {
-			hasClass, err := t.containsStr(testClass)
-			if err != nil {
-				log.Fatalf("%v for test %s", err, t.Name)
-			}
-			if hasClass {
-				t.TestClass = testClass
-				break
-			}
-		}
-		if t.TestClass == "" {
+		if !t.setClass() {
 			continue
 		}
-		// TODO: Add description
-		for _, f := range []func() error{t.setRecentEditors, t.setOwners} {
+		for _, f := range []func() error{t.setRecentEditors, t.setAuthors, t.setPurpose} {
 			err := f()
 			if err != nil {
 				log.Fatalf("%v for test %s", err, t.Name)