blob: 0af0cd73068c9b9f0a3419a5542bb15543ea65f9 [file] [log] [blame]
Bob Badourdc62de42022-10-12 20:10:17 -07001// 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
Ibrahim Kanouche776ad802022-10-21 20:47:42 +000022 "android/soong/compliance/project_metadata_proto"
Bob Badourdc62de42022-10-12 20:10:17 -070023 "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" }`
Ibrahim Kanouche776ad802022-10-21 20:47:42 +000044
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/"
Bob Badourdc62de42022-10-12 20:10:17 -070055)
56
Ibrahim Kanouche776ad802022-10-21 20:47:42 +000057// 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
Bob Badourdc62de42022-10-12 20:10:17 -0700117func 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"},
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000131 expected: []pmeta{{
132 project: "/a",
133 versionedName: "Android",
134 name: "Android",
135 version: "",
136 downloadUrl: "",
137 }},
Bob Badourdc62de42022-10-12 20:10:17 -0700138 },
139 {
140 name: "versioned",
141 fs: &testfs.TestFS{
142 "/a/METADATA": []byte(MY_LIB_1_0),
143 },
144 projects: []string{"/a"},
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000145 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 }},
Bob Badourdc62de42022-10-12 20:10:17 -0700376 },
377 {
378 name: "versioneddesc",
379 fs: &testfs.TestFS{
380 "/a/METADATA": []byte(NO_NAME_0_1),
381 },
382 projects: []string{"/a"},
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000383 expected: []pmeta{{
384 project: "/a",
385 versionedName: "my library",
386 name: "",
387 version: "0.1",
388 downloadUrl: "",
389 }},
Bob Badourdc62de42022-10-12 20:10:17 -0700390 },
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{
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000408 {
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 },
Bob Badourdc62de42022-10-12 20:10:17 -0700429 },
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{
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000439 {
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 },
Bob Badourdc62de42022-10-12 20:10:17 -0700453 },
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{
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000463 {
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 },
Bob Badourdc62de42022-10-12 20:10:17 -0700477 },
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{
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000487 {
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 },
Bob Badourdc62de42022-10-12 20:10:17 -0700501 },
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"},
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000541 expected: []pmeta{{
542 project: "/a",
543 versionedName: "",
544 name: "",
545 version: "",
546 downloadUrl: "",
547 }},
Bob Badourdc62de42022-10-12 20:10:17 -0700548 },
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"},
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000568 expected: []pmeta{{
569 project: "/a",
570 versionedName: "mylib_v_1.0",
571 name: "mylib",
572 version: "1.0",
573 downloadUrl: "",
574 }},
Bob Badourdc62de42022-10-12 20:10:17 -0700575 },
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{
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000586 {
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 },
Bob Badourdc62de42022-10-12 20:10:17 -0700607 },
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
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000656 name string
657 version string
658 downloadUrl string
Bob Badourdc62de42022-10-12 20:10:17 -0700659}
660
661func (pm pmeta) String() string {
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000662 return fmt.Sprintf("project: %q versionedName: %q name: %q version: %q downloadUrl: %q\n", pm.project, pm.versionedName, pm.name, pm.version, pm.downloadUrl)
Bob Badourdc62de42022-10-12 20:10:17 -0700663}
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 }
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000672 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 }
Bob Badourdc62de42022-10-12 20:10:17 -0700681 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 }
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000696 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 }
Bob Badourdc62de42022-10-12 20:10:17 -0700705 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 }
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000712 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 }
Bob Badourdc62de42022-10-12 20:10:17 -0700721 return sb.String()
722}