Refactor projectmetadata into separate package.

Replace regular expressions to extract fields from a text proto with
and actual parsed protobuf.

Refactor TestFS into its own package, and implement StatFS.

Test: m droid dist cts alllicensemetadata

Test: repo forall -c 'echo -n "$REPO_PATH  " && $ANDROID_BUILD_TOP/out/host/linux-x86/bin/compliance_checkmetadata . 2>&1' | fgrep -v PASS

Change-Id: Icd17a6a2b6a4e2b6ffded48e964b9c9d6e4d64d6
diff --git a/tools/compliance/projectmetadata/projectmetadata_test.go b/tools/compliance/projectmetadata/projectmetadata_test.go
new file mode 100644
index 0000000..1e4256f
--- /dev/null
+++ b/tools/compliance/projectmetadata/projectmetadata_test.go
@@ -0,0 +1,294 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package projectmetadata
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"android/soong/tools/compliance/testfs"
+)
+
+const (
+	// EMPTY represents a METADATA file with no recognized fields
+	EMPTY = ``
+
+	// INVALID_NAME represents a METADATA file with the wrong type of name
+	INVALID_NAME = `name: a library\n`
+
+	// INVALID_DESCRIPTION represents a METADATA file with the wrong type of description
+	INVALID_DESCRIPTION = `description: unquoted text\n`
+
+	// INVALID_VERSION represents a METADATA file with the wrong type of version
+	INVALID_VERSION = `third_party { version: 1 }`
+
+	// MY_LIB_1_0 represents a METADATA file for version 1.0 of mylib
+	MY_LIB_1_0 = `name: "mylib" description: "my library" third_party { version: "1.0" }`
+
+	// NO_NAME_0_1 represents a METADATA file with a description but no name
+	NO_NAME_0_1 = `description: "my library" third_party { version: "0.1" }`
+)
+
+func TestReadMetadataForProjects(t *testing.T) {
+	tests := []struct {
+		name          string
+		fs            *testfs.TestFS
+		projects      []string
+		expectedError string
+		expected      []pmeta
+	}{
+		{
+			name: "trivial",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte("name: \"Android\"\n"),
+			},
+			projects: []string{"/a"},
+			expected: []pmeta{{project: "/a", versionedName: "Android"}},
+		},
+		{
+			name: "versioned",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(MY_LIB_1_0),
+			},
+			projects: []string{"/a"},
+			expected: []pmeta{{project: "/a", versionedName: "mylib_v_1.0"}},
+		},
+		{
+			name: "versioneddesc",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(NO_NAME_0_1),
+			},
+			projects: []string{"/a"},
+			expected: []pmeta{{project: "/a", versionedName: "my library"}},
+		},
+		{
+			name: "unterminated",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte("name: \"Android\n"),
+			},
+			projects:      []string{"/a"},
+			expectedError: `invalid character '\n' in string`,
+		},
+		{
+			name: "abc",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(EMPTY),
+				"/b/METADATA": []byte(MY_LIB_1_0),
+				"/c/METADATA": []byte(NO_NAME_0_1),
+			},
+			projects: []string{"/a", "/b", "/c"},
+			expected: []pmeta{
+				{project: "/a", versionedName: ""},
+				{project: "/b", versionedName: "mylib_v_1.0"},
+				{project: "/c", versionedName: "my library"},
+			},
+		},
+		{
+			name: "ab",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(EMPTY),
+				"/b/METADATA": []byte(MY_LIB_1_0),
+			},
+			projects: []string{"/a", "/b", "/c"},
+			expected: []pmeta{
+				{project: "/a", versionedName: ""},
+				{project: "/b", versionedName: "mylib_v_1.0"},
+			},
+		},
+		{
+			name: "ac",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(EMPTY),
+				"/c/METADATA": []byte(NO_NAME_0_1),
+			},
+			projects: []string{"/a", "/b", "/c"},
+			expected: []pmeta{
+				{project: "/a", versionedName: ""},
+				{project: "/c", versionedName: "my library"},
+			},
+		},
+		{
+			name: "bc",
+			fs: &testfs.TestFS{
+				"/b/METADATA": []byte(MY_LIB_1_0),
+				"/c/METADATA": []byte(NO_NAME_0_1),
+			},
+			projects: []string{"/a", "/b", "/c"},
+			expected: []pmeta{
+				{project: "/b", versionedName: "mylib_v_1.0"},
+				{project: "/c", versionedName: "my library"},
+			},
+		},
+		{
+			name: "wrongnametype",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(INVALID_NAME),
+			},
+			projects:      []string{"/a"},
+			expectedError: `invalid value for string type`,
+		},
+		{
+			name: "wrongdescriptiontype",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(INVALID_DESCRIPTION),
+			},
+			projects:      []string{"/a"},
+			expectedError: `invalid value for string type`,
+		},
+		{
+			name: "wrongversiontype",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(INVALID_VERSION),
+			},
+			projects:      []string{"/a"},
+			expectedError: `invalid value for string type`,
+		},
+		{
+			name: "wrongtype",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(INVALID_NAME + INVALID_DESCRIPTION + INVALID_VERSION),
+			},
+			projects:      []string{"/a"},
+			expectedError: `invalid value for string type`,
+		},
+		{
+			name: "empty",
+			fs: &testfs.TestFS{
+				"/a/METADATA": []byte(EMPTY),
+			},
+			projects: []string{"/a"},
+			expected: []pmeta{{project: "/a", versionedName: ""}},
+		},
+		{
+			name: "emptyother",
+			fs: &testfs.TestFS{
+				"/a/METADATA.bp": []byte(EMPTY),
+			},
+			projects: []string{"/a"},
+		},
+		{
+			name:     "emptyfs",
+			fs:       &testfs.TestFS{},
+			projects: []string{"/a"},
+		},
+		{
+			name: "override",
+			fs: &testfs.TestFS{
+				"/a/METADATA":         []byte(INVALID_NAME + INVALID_DESCRIPTION + INVALID_VERSION),
+				"/a/METADATA.android": []byte(MY_LIB_1_0),
+			},
+			projects: []string{"/a"},
+			expected: []pmeta{{project: "/a", versionedName: "mylib_v_1.0"}},
+		},
+		{
+			name: "enchilada",
+			fs: &testfs.TestFS{
+				"/a/METADATA":         []byte(INVALID_NAME + INVALID_DESCRIPTION + INVALID_VERSION),
+				"/a/METADATA.android": []byte(EMPTY),
+				"/b/METADATA":         []byte(MY_LIB_1_0),
+				"/c/METADATA":         []byte(NO_NAME_0_1),
+			},
+			projects: []string{"/a", "/b", "/c"},
+			expected: []pmeta{
+				{project: "/a", versionedName: ""},
+				{project: "/b", versionedName: "mylib_v_1.0"},
+				{project: "/c", versionedName: "my library"},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ix := NewIndex(tt.fs)
+			pms, err := ix.MetadataForProjects(tt.projects...)
+			if err != nil {
+				if len(tt.expectedError) == 0 {
+					t.Errorf("unexpected error: got %s, want no error", err)
+				} else if !strings.Contains(err.Error(), tt.expectedError) {
+					t.Errorf("unexpected error: got %s, want %q", err, tt.expectedError)
+				}
+				return
+			}
+			t.Logf("actual %d project metadata", len(pms))
+			for _, pm := range pms {
+				t.Logf("  %v", pm.String())
+			}
+			t.Logf("expected %d project metadata", len(tt.expected))
+			for _, pm := range tt.expected {
+				t.Logf("  %s", pm.String())
+			}
+			if len(tt.expectedError) > 0 {
+				t.Errorf("unexpected success: got no error, want %q err", tt.expectedError)
+				return
+			}
+			if len(pms) != len(tt.expected) {
+				t.Errorf("missing project metadata: got %d project metadata, want %d", len(pms), len(tt.expected))
+			}
+			for i := 0; i < len(pms) && i < len(tt.expected); i++ {
+				if msg := tt.expected[i].difference(pms[i]); msg != "" {
+					t.Errorf("unexpected metadata starting at index %d: %s", i, msg)
+					return
+				}
+			}
+			if len(pms) < len(tt.expected) {
+				t.Errorf("missing metadata starting at index %d: got nothing, want %s", len(pms), tt.expected[len(pms)].String())
+			}
+			if len(tt.expected) < len(pms) {
+				t.Errorf("unexpected metadata starting at index %d: got %s, want nothing", len(tt.expected), pms[len(tt.expected)].String())
+			}
+		})
+	}
+}
+
+type pmeta struct {
+	project       string
+	versionedName string
+}
+
+func (pm pmeta) String() string {
+	return fmt.Sprintf("project: %q versionedName: %q\n", pm.project, pm.versionedName)
+}
+
+func (pm pmeta) equals(other *ProjectMetadata) bool {
+	if pm.project != other.project {
+		return false
+	}
+	if pm.versionedName != other.VersionedName() {
+		return false
+	}
+	return true
+}
+
+func (pm pmeta) difference(other *ProjectMetadata) string {
+	if pm.equals(other) {
+		return ""
+	}
+	var sb strings.Builder
+	fmt.Fprintf(&sb, "got")
+	if pm.project != other.project {
+		fmt.Fprintf(&sb, " project: %q", other.project)
+	}
+	if pm.versionedName != other.VersionedName() {
+		fmt.Fprintf(&sb, " versionedName: %q", other.VersionedName())
+	}
+	fmt.Fprintf(&sb, ", want")
+	if pm.project != other.project {
+		fmt.Fprintf(&sb, " project: %q", pm.project)
+	}
+	if pm.versionedName != other.VersionedName() {
+		fmt.Fprintf(&sb, " versionedName: %q", pm.versionedName)
+	}
+	return sb.String()
+}