1// Copyright 2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package projectmetadata
16
17import (
18	"fmt"
19	"strings"
20	"testing"
21
22	"android/soong/compliance/project_metadata_proto"
23	"android/soong/tools/compliance/testfs"
24)
25
26const (
27	// EMPTY represents a METADATA file with no recognized fields
28	EMPTY = ``
29
30	// INVALID_NAME represents a METADATA file with the wrong type of name
31	INVALID_NAME = `name: a library\n`
32
33	// INVALID_DESCRIPTION represents a METADATA file with the wrong type of description
34	INVALID_DESCRIPTION = `description: unquoted text\n`
35
36	// INVALID_VERSION represents a METADATA file with the wrong type of version
37	INVALID_VERSION = `third_party { version: 1 }`
38
39	// MY_LIB_1_0 represents a METADATA file for version 1.0 of mylib
40	MY_LIB_1_0 = `name: "mylib" description: "my library" third_party { version: "1.0" }`
41
42	// NO_NAME_0_1 represents a METADATA file with a description but no name
43	NO_NAME_0_1 = `description: "my library" third_party { version: "0.1" }`
44
45	// URL values per type
46	GIT_URL          = "http://example.github.com/my_lib"
47	SVN_URL          = "http://example.svn.com/my_lib"
48	HG_URL           = "http://example.hg.com/my_lib"
49	DARCS_URL        = "http://example.darcs.com/my_lib"
50	PIPER_URL        = "http://google3/third_party/my/package"
51	HOMEPAGE_URL     = "http://example.com/homepage"
52	OTHER_URL        = "http://google.com/"
53	ARCHIVE_URL      = "http://ftp.example.com/"
54	LOCAL_SOURCE_URL = "https://android.googlesource.com/platform/external/apache-http/"
55)
56
57// libWithUrl returns a METADATA file with the right download url
58func libWithUrl(urlTypes ...string) string {
59	var sb strings.Builder
60
61	fmt.Fprintln(&sb, `name: "mylib" description: "my library"
62	 third_party {
63	 	version: "1.0"`)
64
65	for _, urltype := range urlTypes {
66		var urlValue string
67		switch urltype {
68		case "GIT":
69			urlValue = GIT_URL
70		case "SVN":
71			urlValue = SVN_URL
72		case "HG":
73			urlValue = HG_URL
74		case "DARCS":
75			urlValue = DARCS_URL
76		case "PIPER":
77			urlValue = PIPER_URL
78		case "HOMEPAGE":
79			urlValue = HOMEPAGE_URL
80		case "OTHER":
81			urlValue = OTHER_URL
82		case "ARCHIVE":
83			urlValue = ARCHIVE_URL
84		case "LOCAL_SOURCE":
85			urlValue = LOCAL_SOURCE_URL
86		default:
87			panic(fmt.Errorf("unknown url type: %q. Please update libWithUrl() in build/make/tools/compliance/projectmetadata/projectmetadata_test.go", urltype))
88		}
89		fmt.Fprintf(&sb, "  url { type: %s value: %q }\n", urltype, urlValue)
90	}
91	fmt.Fprintln(&sb, `}`)
92
93	return sb.String()
94}
95
96func TestVerifyAllUrlTypes(t *testing.T) {
97	t.Run("verifyAllUrlTypes", func(t *testing.T) {
98		types := make([]string, 0, len(project_metadata_proto.URL_Type_value))
99		for t := range project_metadata_proto.URL_Type_value {
100			types = append(types, t)
101		}
102		libWithUrl(types...)
103	})
104}
105
106func TestUnknownPanics(t *testing.T) {
107	t.Run("Unknown panics", func(t *testing.T) {
108		defer func() {
109			if r := recover(); r == nil {
110				t.Errorf("unexpected success: got no error, want panic")
111			}
112		}()
113		libWithUrl("SOME WILD VALUE THAT DOES NOT EXIST")
114	})
115}
116
117func TestReadMetadataForProjects(t *testing.T) {
118	tests := []struct {
119		name          string
120		fs            *testfs.TestFS
121		projects      []string
122		expectedError string
123		expected      []pmeta
124	}{
125		{
126			name: "trivial",
127			fs: &testfs.TestFS{
128				"/a/METADATA": []byte("name: \"Android\"\n"),
129			},
130			projects: []string{"/a"},
131			expected: []pmeta{{
132				project:       "/a",
133				versionedName: "Android",
134				name:          "Android",
135				version:       "",
136				downloadUrl:   "",
137			}},
138		},
139		{
140			name: "versioned",
141			fs: &testfs.TestFS{
142				"/a/METADATA": []byte(MY_LIB_1_0),
143			},
144			projects: []string{"/a"},
145			expected: []pmeta{{
146				project:       "/a",
147				versionedName: "mylib_v_1.0",
148				name:          "mylib",
149				version:       "1.0",
150				downloadUrl:   "",
151			}},
152		},
153		{
154			name: "lib_with_homepage",
155			fs: &testfs.TestFS{
156				"/a/METADATA": []byte(libWithUrl("HOMEPAGE")),
157			},
158			projects: []string{"/a"},
159			expected: []pmeta{{
160				project:       "/a",
161				versionedName: "mylib_v_1.0",
162				name:          "mylib",
163				version:       "1.0",
164				downloadUrl:   "",
165			}},
166		},
167		{
168			name: "lib_with_git",
169			fs: &testfs.TestFS{
170				"/a/METADATA": []byte(libWithUrl("GIT")),
171			},
172			projects: []string{"/a"},
173			expected: []pmeta{{
174				project:       "/a",
175				versionedName: "mylib_v_1.0",
176				name:          "mylib",
177				version:       "1.0",
178				downloadUrl:   GIT_URL,
179			}},
180		},
181		{
182			name: "lib_with_svn",
183			fs: &testfs.TestFS{
184				"/a/METADATA": []byte(libWithUrl("SVN")),
185			},
186			projects: []string{"/a"},
187			expected: []pmeta{{
188				project:       "/a",
189				versionedName: "mylib_v_1.0",
190				name:          "mylib",
191				version:       "1.0",
192				downloadUrl:   SVN_URL,
193			}},
194		},
195		{
196			name: "lib_with_hg",
197			fs: &testfs.TestFS{
198				"/a/METADATA": []byte(libWithUrl("HG")),
199			},
200			projects: []string{"/a"},
201			expected: []pmeta{{
202				project:       "/a",
203				versionedName: "mylib_v_1.0",
204				name:          "mylib",
205				version:       "1.0",
206				downloadUrl:   HG_URL,
207			}},
208		},
209		{
210			name: "lib_with_darcs",
211			fs: &testfs.TestFS{
212				"/a/METADATA": []byte(libWithUrl("DARCS")),
213			},
214			projects: []string{"/a"},
215			expected: []pmeta{{
216				project:       "/a",
217				versionedName: "mylib_v_1.0",
218				name:          "mylib",
219				version:       "1.0",
220				downloadUrl:   DARCS_URL,
221			}},
222		},
223		{
224			name: "lib_with_piper",
225			fs: &testfs.TestFS{
226				"/a/METADATA": []byte(libWithUrl("PIPER")),
227			},
228			projects: []string{"/a"},
229			expected: []pmeta{{
230				project:       "/a",
231				versionedName: "mylib_v_1.0",
232				name:          "mylib",
233				version:       "1.0",
234				downloadUrl:   "",
235			}},
236		},
237		{
238			name: "lib_with_other",
239			fs: &testfs.TestFS{
240				"/a/METADATA": []byte(libWithUrl("OTHER")),
241			},
242			projects: []string{"/a"},
243			expected: []pmeta{{
244				project:       "/a",
245				versionedName: "mylib_v_1.0",
246				name:          "mylib",
247				version:       "1.0",
248				downloadUrl:   "",
249			}},
250		},
251		{
252			name: "lib_with_local_source",
253			fs: &testfs.TestFS{
254				"/a/METADATA": []byte(libWithUrl("LOCAL_SOURCE")),
255			},
256			projects: []string{"/a"},
257			expected: []pmeta{{
258				project:       "/a",
259				versionedName: "mylib_v_1.0",
260				name:          "mylib",
261				version:       "1.0",
262				downloadUrl:   "",
263			}},
264		},
265		{
266			name: "lib_with_archive",
267			fs: &testfs.TestFS{
268				"/a/METADATA": []byte(libWithUrl("ARCHIVE")),
269			},
270			projects: []string{"/a"},
271			expected: []pmeta{{
272				project:       "/a",
273				versionedName: "mylib_v_1.0",
274				name:          "mylib",
275				version:       "1.0",
276				downloadUrl:   "",
277			}},
278		},
279		{
280			name: "lib_with_all_downloads",
281			fs: &testfs.TestFS{
282				"/a/METADATA": []byte(libWithUrl("DARCS", "HG", "SVN", "GIT")),
283			},
284			projects: []string{"/a"},
285			expected: []pmeta{{
286				project:       "/a",
287				versionedName: "mylib_v_1.0",
288				name:          "mylib",
289				version:       "1.0",
290				downloadUrl:   GIT_URL,
291			}},
292		},
293		{
294			name: "lib_with_all_downloads_in_different_order",
295			fs: &testfs.TestFS{
296				"/a/METADATA": []byte(libWithUrl("DARCS", "GIT", "SVN", "HG")),
297			},
298			projects: []string{"/a"},
299			expected: []pmeta{{
300				project:       "/a",
301				versionedName: "mylib_v_1.0",
302				name:          "mylib",
303				version:       "1.0",
304				downloadUrl:   GIT_URL,
305			}},
306		},
307		{
308			name: "lib_with_all_but_git",
309			fs: &testfs.TestFS{
310				"/a/METADATA": []byte(libWithUrl("DARCS", "HG", "SVN")),
311			},
312			projects: []string{"/a"},
313			expected: []pmeta{{
314				project:       "/a",
315				versionedName: "mylib_v_1.0",
316				name:          "mylib",
317				version:       "1.0",
318				downloadUrl:   SVN_URL,
319			}},
320		},
321		{
322			name: "lib_with_all_but_git_and_svn",
323			fs: &testfs.TestFS{
324				"/a/METADATA": []byte(libWithUrl("DARCS", "HG")),
325			},
326			projects: []string{"/a"},
327			expected: []pmeta{{
328				project:       "/a",
329				versionedName: "mylib_v_1.0",
330				name:          "mylib",
331				version:       "1.0",
332				downloadUrl:   HG_URL,
333			}},
334		},
335		{
336			name: "lib_with_all_nondownloads_and_git",
337			fs: &testfs.TestFS{
338				"/a/METADATA": []byte(libWithUrl("HOMEPAGE", "LOCAL_SOURCE", "PIPER", "ARCHIVE", "GIT")),
339			},
340			projects: []string{"/a"},
341			expected: []pmeta{{
342				project:       "/a",
343				versionedName: "mylib_v_1.0",
344				name:          "mylib",
345				version:       "1.0",
346				downloadUrl:   GIT_URL,
347			}},
348		},
349		{
350			name: "lib_with_all_nondownloads",
351			fs: &testfs.TestFS{
352				"/a/METADATA": []byte(libWithUrl("HOMEPAGE", "LOCAL_SOURCE", "PIPER", "ARCHIVE")),
353			},
354			projects: []string{"/a"},
355			expected: []pmeta{{
356				project:       "/a",
357				versionedName: "mylib_v_1.0",
358				name:          "mylib",
359				version:       "1.0",
360				downloadUrl:   "",
361			}},
362		},
363		{
364			name: "lib_with_all_nondownloads",
365			fs: &testfs.TestFS{
366				"/a/METADATA": []byte(libWithUrl()),
367			},
368			projects: []string{"/a"},
369			expected: []pmeta{{
370				project:       "/a",
371				versionedName: "mylib_v_1.0",
372				name:          "mylib",
373				version:       "1.0",
374				downloadUrl:   "",
375			}},
376		},
377		{
378			name: "versioneddesc",
379			fs: &testfs.TestFS{
380				"/a/METADATA": []byte(NO_NAME_0_1),
381			},
382			projects: []string{"/a"},
383			expected: []pmeta{{
384				project:       "/a",
385				versionedName: "my library",
386				name:          "",
387				version:       "0.1",
388				downloadUrl:   "",
389			}},
390		},
391		{
392			name: "unterminated",
393			fs: &testfs.TestFS{
394				"/a/METADATA": []byte("name: \"Android\n"),
395			},
396			projects:      []string{"/a"},
397			expectedError: `invalid character '\n' in string`,
398		},
399		{
400			name: "abc",
401			fs: &testfs.TestFS{
402				"/a/METADATA": []byte(EMPTY),
403				"/b/METADATA": []byte(MY_LIB_1_0),
404				"/c/METADATA": []byte(NO_NAME_0_1),
405			},
406			projects: []string{"/a", "/b", "/c"},
407			expected: []pmeta{
408				{
409					project:       "/a",
410					versionedName: "",
411					name:          "",
412					version:       "",
413					downloadUrl:   "",
414				},
415				{
416					project:       "/b",
417					versionedName: "mylib_v_1.0",
418					name:          "mylib",
419					version:       "1.0",
420					downloadUrl:   "",
421				},
422				{
423					project:       "/c",
424					versionedName: "my library",
425					name:          "",
426					version:       "0.1",
427					downloadUrl:   "",
428				},
429			},
430		},
431		{
432			name: "ab",
433			fs: &testfs.TestFS{
434				"/a/METADATA": []byte(EMPTY),
435				"/b/METADATA": []byte(MY_LIB_1_0),
436			},
437			projects: []string{"/a", "/b", "/c"},
438			expected: []pmeta{
439				{
440					project:       "/a",
441					versionedName: "",
442					name:          "",
443					version:       "",
444					downloadUrl:   "",
445				},
446				{
447					project:       "/b",
448					versionedName: "mylib_v_1.0",
449					name:          "mylib",
450					version:       "1.0",
451					downloadUrl:   "",
452				},
453			},
454		},
455		{
456			name: "ac",
457			fs: &testfs.TestFS{
458				"/a/METADATA": []byte(EMPTY),
459				"/c/METADATA": []byte(NO_NAME_0_1),
460			},
461			projects: []string{"/a", "/b", "/c"},
462			expected: []pmeta{
463				{
464					project:       "/a",
465					versionedName: "",
466					name:          "",
467					version:       "",
468					downloadUrl:   "",
469				},
470				{
471					project:       "/c",
472					versionedName: "my library",
473					name:          "",
474					version:       "0.1",
475					downloadUrl:   "",
476				},
477			},
478		},
479		{
480			name: "bc",
481			fs: &testfs.TestFS{
482				"/b/METADATA": []byte(MY_LIB_1_0),
483				"/c/METADATA": []byte(NO_NAME_0_1),
484			},
485			projects: []string{"/a", "/b", "/c"},
486			expected: []pmeta{
487				{
488					project:       "/b",
489					versionedName: "mylib_v_1.0",
490					name:          "mylib",
491					version:       "1.0",
492					downloadUrl:   "",
493				},
494				{
495					project:       "/c",
496					versionedName: "my library",
497					name:          "",
498					version:       "0.1",
499					downloadUrl:   "",
500				},
501			},
502		},
503		{
504			name: "wrongnametype",
505			fs: &testfs.TestFS{
506				"/a/METADATA": []byte(INVALID_NAME),
507			},
508			projects:      []string{"/a"},
509			expectedError: `invalid value for string type`,
510		},
511		{
512			name: "wrongdescriptiontype",
513			fs: &testfs.TestFS{
514				"/a/METADATA": []byte(INVALID_DESCRIPTION),
515			},
516			projects:      []string{"/a"},
517			expectedError: `invalid value for string type`,
518		},
519		{
520			name: "wrongversiontype",
521			fs: &testfs.TestFS{
522				"/a/METADATA": []byte(INVALID_VERSION),
523			},
524			projects:      []string{"/a"},
525			expectedError: `invalid value for string type`,
526		},
527		{
528			name: "wrongtype",
529			fs: &testfs.TestFS{
530				"/a/METADATA": []byte(INVALID_NAME + INVALID_DESCRIPTION + INVALID_VERSION),
531			},
532			projects:      []string{"/a"},
533			expectedError: `invalid value for string type`,
534		},
535		{
536			name: "empty",
537			fs: &testfs.TestFS{
538				"/a/METADATA": []byte(EMPTY),
539			},
540			projects: []string{"/a"},
541			expected: []pmeta{{
542				project:       "/a",
543				versionedName: "",
544				name:          "",
545				version:       "",
546				downloadUrl:   "",
547			}},
548		},
549		{
550			name: "emptyother",
551			fs: &testfs.TestFS{
552				"/a/METADATA.bp": []byte(EMPTY),
553			},
554			projects: []string{"/a"},
555		},
556		{
557			name:     "emptyfs",
558			fs:       &testfs.TestFS{},
559			projects: []string{"/a"},
560		},
561		{
562			name: "override",
563			fs: &testfs.TestFS{
564				"/a/METADATA":         []byte(INVALID_NAME + INVALID_DESCRIPTION + INVALID_VERSION),
565				"/a/METADATA.android": []byte(MY_LIB_1_0),
566			},
567			projects: []string{"/a"},
568			expected: []pmeta{{
569				project:       "/a",
570				versionedName: "mylib_v_1.0",
571				name:          "mylib",
572				version:       "1.0",
573				downloadUrl:   "",
574			}},
575		},
576		{
577			name: "enchilada",
578			fs: &testfs.TestFS{
579				"/a/METADATA":         []byte(INVALID_NAME + INVALID_DESCRIPTION + INVALID_VERSION),
580				"/a/METADATA.android": []byte(EMPTY),
581				"/b/METADATA":         []byte(MY_LIB_1_0),
582				"/c/METADATA":         []byte(NO_NAME_0_1),
583			},
584			projects: []string{"/a", "/b", "/c"},
585			expected: []pmeta{
586				{
587					project:       "/a",
588					versionedName: "",
589					name:          "",
590					version:       "",
591					downloadUrl:   "",
592				},
593				{
594					project:       "/b",
595					versionedName: "mylib_v_1.0",
596					name:          "mylib",
597					version:       "1.0",
598					downloadUrl:   "",
599				},
600				{
601					project:       "/c",
602					versionedName: "my library",
603					name:          "",
604					version:       "0.1",
605					downloadUrl:   "",
606				},
607			},
608		},
609	}
610	for _, tt := range tests {
611		t.Run(tt.name, func(t *testing.T) {
612			ix := NewIndex(tt.fs)
613			pms, err := ix.MetadataForProjects(tt.projects...)
614			if err != nil {
615				if len(tt.expectedError) == 0 {
616					t.Errorf("unexpected error: got %s, want no error", err)
617				} else if !strings.Contains(err.Error(), tt.expectedError) {
618					t.Errorf("unexpected error: got %s, want %q", err, tt.expectedError)
619				}
620				return
621			}
622			t.Logf("actual %d project metadata", len(pms))
623			for _, pm := range pms {
624				t.Logf("  %v", pm.String())
625			}
626			t.Logf("expected %d project metadata", len(tt.expected))
627			for _, pm := range tt.expected {
628				t.Logf("  %s", pm.String())
629			}
630			if len(tt.expectedError) > 0 {
631				t.Errorf("unexpected success: got no error, want %q err", tt.expectedError)
632				return
633			}
634			if len(pms) != len(tt.expected) {
635				t.Errorf("missing project metadata: got %d project metadata, want %d", len(pms), len(tt.expected))
636			}
637			for i := 0; i < len(pms) && i < len(tt.expected); i++ {
638				if msg := tt.expected[i].difference(pms[i]); msg != "" {
639					t.Errorf("unexpected metadata starting at index %d: %s", i, msg)
640					return
641				}
642			}
643			if len(pms) < len(tt.expected) {
644				t.Errorf("missing metadata starting at index %d: got nothing, want %s", len(pms), tt.expected[len(pms)].String())
645			}
646			if len(tt.expected) < len(pms) {
647				t.Errorf("unexpected metadata starting at index %d: got %s, want nothing", len(tt.expected), pms[len(tt.expected)].String())
648			}
649		})
650	}
651}
652
653type pmeta struct {
654	project       string
655	versionedName string
656	name          string
657	version       string
658	downloadUrl   string
659}
660
661func (pm pmeta) String() string {
662	return fmt.Sprintf("project: %q versionedName: %q name: %q version: %q downloadUrl: %q\n", pm.project, pm.versionedName, pm.name, pm.version, pm.downloadUrl)
663}
664
665func (pm pmeta) equals(other *ProjectMetadata) bool {
666	if pm.project != other.project {
667		return false
668	}
669	if pm.versionedName != other.VersionedName() {
670		return false
671	}
672	if pm.name != other.Name() {
673		return false
674	}
675	if pm.version != other.Version() {
676		return false
677	}
678	if pm.downloadUrl != other.UrlsByTypeName().DownloadUrl() {
679		return false
680	}
681	return true
682}
683
684func (pm pmeta) difference(other *ProjectMetadata) string {
685	if pm.equals(other) {
686		return ""
687	}
688	var sb strings.Builder
689	fmt.Fprintf(&sb, "got")
690	if pm.project != other.project {
691		fmt.Fprintf(&sb, " project: %q", other.project)
692	}
693	if pm.versionedName != other.VersionedName() {
694		fmt.Fprintf(&sb, " versionedName: %q", other.VersionedName())
695	}
696	if pm.name != other.Name() {
697		fmt.Fprintf(&sb, " name: %q", other.Name())
698	}
699	if pm.version != other.Version() {
700		fmt.Fprintf(&sb, " version: %q", other.Version())
701	}
702	if pm.downloadUrl != other.UrlsByTypeName().DownloadUrl() {
703		fmt.Fprintf(&sb, " downloadUrl: %q", other.UrlsByTypeName().DownloadUrl())
704	}
705	fmt.Fprintf(&sb, ", want")
706	if pm.project != other.project {
707		fmt.Fprintf(&sb, " project: %q", pm.project)
708	}
709	if pm.versionedName != other.VersionedName() {
710		fmt.Fprintf(&sb, " versionedName: %q", pm.versionedName)
711	}
712	if pm.name != other.Name() {
713		fmt.Fprintf(&sb, " name: %q", pm.name)
714	}
715	if pm.version != other.Version() {
716		fmt.Fprintf(&sb, " version: %q", pm.version)
717	}
718	if pm.downloadUrl != other.UrlsByTypeName().DownloadUrl() {
719		fmt.Fprintf(&sb, " downloadUrl: %q", pm.downloadUrl)
720	}
721	return sb.String()
722}
723