blob: fc935c10daae88243499769ac39a6f55a97f9a82 [file] [log] [blame]
Bob Badour6ea14572022-01-23 17:15:46 -08001// Copyright 2021 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 main
16
17import (
18 "bufio"
19 "bytes"
20 "fmt"
21 "html"
22 "os"
23 "regexp"
24 "strings"
25 "testing"
26)
27
28var (
29 horizontalRule = regexp.MustCompile(`^\s*<hr>\s*$`)
30 bodyTag = regexp.MustCompile(`^\s*<body>\s*$`)
31 boilerPlate = regexp.MustCompile(`^\s*(?:<ul class="file-list">|<ul>|</.*)\s*$`)
32 tocTag = regexp.MustCompile(`^\s*<ul class="toc">\s*$`)
33 libraryName = regexp.MustCompile(`^\s*<strong>(.*)</strong>\s\s*used\s\s*by\s*:\s*$`)
34 licenseText = regexp.MustCompile(`^\s*<a id="[^"]{32}"/><pre class="license-text">(.*)$`)
35 titleTag = regexp.MustCompile(`^\s*<title>(.*)</title>\s*$`)
36 h1Tag = regexp.MustCompile(`^\s*<h1>(.*)</h1>\s*$`)
37 usedByTarget = regexp.MustCompile(`^\s*<li>(?:<a href="#id[0-9]+">)?((?:out/(?:[^/<]*/)+)[^/<]*)(?:</a>)?\s*$`)
38 installTarget = regexp.MustCompile(`^\s*<li id="id[0-9]+"><strong>(.*)</strong>\s*$`)
39 libReference = regexp.MustCompile(`^\s*<li><a href="#[^"]{32}">(.*)</a>\s*$`)
40)
41
Colin Crossd0f05c92022-01-27 15:40:29 -080042func TestMain(m *testing.M) {
43 // Change into the parent directory before running the tests
44 // so they can find the testdata directory.
45 if err := os.Chdir(".."); err != nil {
46 fmt.Printf("failed to change to testdata directory: %s\n", err)
47 os.Exit(1)
48 }
49 os.Exit(m.Run())
50}
51
Bob Badour6ea14572022-01-23 17:15:46 -080052func Test(t *testing.T) {
53 tests := []struct {
54 condition string
55 name string
56 roots []string
57 includeTOC bool
58 stripPrefix string
59 title string
60 expectedOut []matcher
61 }{
62 {
63 condition: "firstparty",
64 name: "apex",
65 roots: []string{"highest.apex.meta_lic"},
66 expectedOut: []matcher{
67 hr{},
68 library{"Android"},
69 usedBy{"highest.apex"},
70 usedBy{"highest.apex/bin/bin1"},
71 usedBy{"highest.apex/bin/bin2"},
72 usedBy{"highest.apex/lib/liba.so"},
73 usedBy{"highest.apex/lib/libb.so"},
74 firstParty{},
75 },
76 },
77 {
78 condition: "firstparty",
79 name: "apex+toc",
80 roots: []string{"highest.apex.meta_lic"},
81 includeTOC: true,
82 expectedOut: []matcher{
83 toc{},
84 target{"highest.apex"},
85 uses{"Android"},
86 target{"highest.apex/bin/bin1"},
87 uses{"Android"},
88 target{"highest.apex/bin/bin2"},
89 uses{"Android"},
90 target{"highest.apex/lib/liba.so"},
91 uses{"Android"},
92 target{"highest.apex/lib/libb.so"},
93 uses{"Android"},
94 hr{},
95 library{"Android"},
96 usedBy{"highest.apex"},
97 usedBy{"highest.apex/bin/bin1"},
98 usedBy{"highest.apex/bin/bin2"},
99 usedBy{"highest.apex/lib/liba.so"},
100 usedBy{"highest.apex/lib/libb.so"},
101 firstParty{},
102 },
103 },
104 {
105 condition: "firstparty",
106 name: "apex-with-title",
107 roots: []string{"highest.apex.meta_lic"},
108 title: "Emperor",
109 expectedOut: []matcher{
110 pageTitle{"Emperor"},
111 hr{},
112 library{"Android"},
113 usedBy{"highest.apex"},
114 usedBy{"highest.apex/bin/bin1"},
115 usedBy{"highest.apex/bin/bin2"},
116 usedBy{"highest.apex/lib/liba.so"},
117 usedBy{"highest.apex/lib/libb.so"},
118 firstParty{},
119 },
120 },
121 {
122 condition: "firstparty",
123 name: "apex-with-title+toc",
124 roots: []string{"highest.apex.meta_lic"},
125 includeTOC: true,
126 title: "Emperor",
127 expectedOut: []matcher{
128 pageTitle{"Emperor"},
129 toc{},
130 target{"highest.apex"},
131 uses{"Android"},
132 target{"highest.apex/bin/bin1"},
133 uses{"Android"},
134 target{"highest.apex/bin/bin2"},
135 uses{"Android"},
136 target{"highest.apex/lib/liba.so"},
137 uses{"Android"},
138 target{"highest.apex/lib/libb.so"},
139 uses{"Android"},
140 hr{},
141 library{"Android"},
142 usedBy{"highest.apex"},
143 usedBy{"highest.apex/bin/bin1"},
144 usedBy{"highest.apex/bin/bin2"},
145 usedBy{"highest.apex/lib/liba.so"},
146 usedBy{"highest.apex/lib/libb.so"},
147 firstParty{},
148 },
149 },
150 {
151 condition: "firstparty",
152 name: "container",
153 roots: []string{"container.zip.meta_lic"},
154 expectedOut: []matcher{
155 hr{},
156 library{"Android"},
157 usedBy{"container.zip"},
158 usedBy{"container.zip/bin1"},
159 usedBy{"container.zip/bin2"},
160 usedBy{"container.zip/liba.so"},
161 usedBy{"container.zip/libb.so"},
162 firstParty{},
163 },
164 },
165 {
166 condition: "firstparty",
167 name: "application",
168 roots: []string{"application.meta_lic"},
169 expectedOut: []matcher{
170 hr{},
171 library{"Android"},
172 usedBy{"application"},
173 firstParty{},
174 },
175 },
176 {
177 condition: "firstparty",
178 name: "binary",
179 roots: []string{"bin/bin1.meta_lic"},
180 expectedOut: []matcher{
181 hr{},
182 library{"Android"},
183 usedBy{"bin/bin1"},
184 firstParty{},
185 },
186 },
187 {
188 condition: "firstparty",
189 name: "library",
190 roots: []string{"lib/libd.so.meta_lic"},
191 expectedOut: []matcher{
192 hr{},
193 library{"Android"},
194 usedBy{"lib/libd.so"},
195 firstParty{},
196 },
197 },
198 {
199 condition: "notice",
200 name: "apex",
201 roots: []string{"highest.apex.meta_lic"},
202 expectedOut: []matcher{
203 hr{},
204 library{"Android"},
205 usedBy{"highest.apex"},
206 usedBy{"highest.apex/bin/bin1"},
207 usedBy{"highest.apex/bin/bin2"},
208 usedBy{"highest.apex/lib/libb.so"},
209 firstParty{},
210 hr{},
211 library{"Device"},
212 usedBy{"highest.apex/bin/bin1"},
213 usedBy{"highest.apex/lib/liba.so"},
214 library{"External"},
215 usedBy{"highest.apex/bin/bin1"},
216 notice{},
217 },
218 },
219 {
220 condition: "notice",
221 name: "container",
222 roots: []string{"container.zip.meta_lic"},
223 expectedOut: []matcher{
224 hr{},
225 library{"Android"},
226 usedBy{"container.zip"},
227 usedBy{"container.zip/bin1"},
228 usedBy{"container.zip/bin2"},
229 usedBy{"container.zip/libb.so"},
230 firstParty{},
231 hr{},
232 library{"Device"},
233 usedBy{"container.zip/bin1"},
234 usedBy{"container.zip/liba.so"},
235 library{"External"},
236 usedBy{"container.zip/bin1"},
237 notice{},
238 },
239 },
240 {
241 condition: "notice",
242 name: "application",
243 roots: []string{"application.meta_lic"},
244 expectedOut: []matcher{
245 hr{},
246 library{"Android"},
247 usedBy{"application"},
248 firstParty{},
249 hr{},
250 library{"Device"},
251 usedBy{"application"},
252 notice{},
253 },
254 },
255 {
256 condition: "notice",
257 name: "binary",
258 roots: []string{"bin/bin1.meta_lic"},
259 expectedOut: []matcher{
260 hr{},
261 library{"Android"},
262 usedBy{"bin/bin1"},
263 firstParty{},
264 hr{},
265 library{"Device"},
266 usedBy{"bin/bin1"},
267 library{"External"},
268 usedBy{"bin/bin1"},
269 notice{},
270 },
271 },
272 {
273 condition: "notice",
274 name: "library",
275 roots: []string{"lib/libd.so.meta_lic"},
276 expectedOut: []matcher{
277 hr{},
278 library{"External"},
279 usedBy{"lib/libd.so"},
280 notice{},
281 },
282 },
283 {
284 condition: "reciprocal",
285 name: "apex",
286 roots: []string{"highest.apex.meta_lic"},
287 expectedOut: []matcher{
288 hr{},
289 library{"Android"},
290 usedBy{"highest.apex"},
291 usedBy{"highest.apex/bin/bin1"},
292 usedBy{"highest.apex/bin/bin2"},
293 usedBy{"highest.apex/lib/libb.so"},
294 firstParty{},
295 hr{},
296 library{"Device"},
297 usedBy{"highest.apex/bin/bin1"},
298 usedBy{"highest.apex/lib/liba.so"},
299 library{"External"},
300 usedBy{"highest.apex/bin/bin1"},
301 reciprocal{},
302 },
303 },
304 {
305 condition: "reciprocal",
306 name: "container",
307 roots: []string{"container.zip.meta_lic"},
308 expectedOut: []matcher{
309 hr{},
310 library{"Android"},
311 usedBy{"container.zip"},
312 usedBy{"container.zip/bin1"},
313 usedBy{"container.zip/bin2"},
314 usedBy{"container.zip/libb.so"},
315 firstParty{},
316 hr{},
317 library{"Device"},
318 usedBy{"container.zip/bin1"},
319 usedBy{"container.zip/liba.so"},
320 library{"External"},
321 usedBy{"container.zip/bin1"},
322 reciprocal{},
323 },
324 },
325 {
326 condition: "reciprocal",
327 name: "application",
328 roots: []string{"application.meta_lic"},
329 expectedOut: []matcher{
330 hr{},
331 library{"Android"},
332 usedBy{"application"},
333 firstParty{},
334 hr{},
335 library{"Device"},
336 usedBy{"application"},
337 reciprocal{},
338 },
339 },
340 {
341 condition: "reciprocal",
342 name: "binary",
343 roots: []string{"bin/bin1.meta_lic"},
344 expectedOut: []matcher{
345 hr{},
346 library{"Android"},
347 usedBy{"bin/bin1"},
348 firstParty{},
349 hr{},
350 library{"Device"},
351 usedBy{"bin/bin1"},
352 library{"External"},
353 usedBy{"bin/bin1"},
354 reciprocal{},
355 },
356 },
357 {
358 condition: "reciprocal",
359 name: "library",
360 roots: []string{"lib/libd.so.meta_lic"},
361 expectedOut: []matcher{
362 hr{},
363 library{"External"},
364 usedBy{"lib/libd.so"},
365 notice{},
366 },
367 },
368 {
369 condition: "restricted",
370 name: "apex",
371 roots: []string{"highest.apex.meta_lic"},
372 expectedOut: []matcher{
373 hr{},
374 library{"Android"},
375 usedBy{"highest.apex"},
376 usedBy{"highest.apex/bin/bin1"},
377 usedBy{"highest.apex/bin/bin2"},
378 firstParty{},
379 hr{},
380 library{"Android"},
381 usedBy{"highest.apex/bin/bin2"},
382 usedBy{"highest.apex/lib/libb.so"},
383 library{"Device"},
384 usedBy{"highest.apex/bin/bin1"},
385 usedBy{"highest.apex/lib/liba.so"},
386 restricted{},
387 hr{},
388 library{"External"},
389 usedBy{"highest.apex/bin/bin1"},
390 reciprocal{},
391 },
392 },
393 {
394 condition: "restricted",
395 name: "container",
396 roots: []string{"container.zip.meta_lic"},
397 expectedOut: []matcher{
398 hr{},
399 library{"Android"},
400 usedBy{"container.zip"},
401 usedBy{"container.zip/bin1"},
402 usedBy{"container.zip/bin2"},
403 firstParty{},
404 hr{},
405 library{"Android"},
406 usedBy{"container.zip/bin2"},
407 usedBy{"container.zip/libb.so"},
408 library{"Device"},
409 usedBy{"container.zip/bin1"},
410 usedBy{"container.zip/liba.so"},
411 restricted{},
412 hr{},
413 library{"External"},
414 usedBy{"container.zip/bin1"},
415 reciprocal{},
416 },
417 },
418 {
419 condition: "restricted",
420 name: "application",
421 roots: []string{"application.meta_lic"},
422 expectedOut: []matcher{
423 hr{},
424 library{"Android"},
425 usedBy{"application"},
426 firstParty{},
427 hr{},
428 library{"Device"},
429 usedBy{"application"},
430 restricted{},
431 },
432 },
433 {
434 condition: "restricted",
435 name: "binary",
436 roots: []string{"bin/bin1.meta_lic"},
437 expectedOut: []matcher{
438 hr{},
439 library{"Android"},
440 usedBy{"bin/bin1"},
441 firstParty{},
442 hr{},
443 library{"Device"},
444 usedBy{"bin/bin1"},
445 restricted{},
446 hr{},
447 library{"External"},
448 usedBy{"bin/bin1"},
449 reciprocal{},
450 },
451 },
452 {
453 condition: "restricted",
454 name: "library",
455 roots: []string{"lib/libd.so.meta_lic"},
456 expectedOut: []matcher{
457 hr{},
458 library{"External"},
459 usedBy{"lib/libd.so"},
460 notice{},
461 },
462 },
463 {
464 condition: "proprietary",
465 name: "apex",
466 roots: []string{"highest.apex.meta_lic"},
467 expectedOut: []matcher{
468 hr{},
469 library{"Android"},
470 usedBy{"highest.apex/bin/bin2"},
471 usedBy{"highest.apex/lib/libb.so"},
472 restricted{},
473 hr{},
474 library{"Android"},
475 usedBy{"highest.apex"},
476 usedBy{"highest.apex/bin/bin1"},
477 firstParty{},
478 hr{},
479 library{"Android"},
480 usedBy{"highest.apex/bin/bin2"},
481 library{"Device"},
482 usedBy{"highest.apex/bin/bin1"},
483 usedBy{"highest.apex/lib/liba.so"},
484 library{"External"},
485 usedBy{"highest.apex/bin/bin1"},
486 proprietary{},
487 },
488 },
489 {
490 condition: "proprietary",
491 name: "container",
492 roots: []string{"container.zip.meta_lic"},
493 expectedOut: []matcher{
494 hr{},
495 library{"Android"},
496 usedBy{"container.zip/bin2"},
497 usedBy{"container.zip/libb.so"},
498 restricted{},
499 hr{},
500 library{"Android"},
501 usedBy{"container.zip"},
502 usedBy{"container.zip/bin1"},
503 firstParty{},
504 hr{},
505 library{"Android"},
506 usedBy{"container.zip/bin2"},
507 library{"Device"},
508 usedBy{"container.zip/bin1"},
509 usedBy{"container.zip/liba.so"},
510 library{"External"},
511 usedBy{"container.zip/bin1"},
512 proprietary{},
513 },
514 },
515 {
516 condition: "proprietary",
517 name: "application",
518 roots: []string{"application.meta_lic"},
519 expectedOut: []matcher{
520 hr{},
521 library{"Android"},
522 usedBy{"application"},
523 firstParty{},
524 hr{},
525 library{"Device"},
526 usedBy{"application"},
527 proprietary{},
528 },
529 },
530 {
531 condition: "proprietary",
532 name: "binary",
533 roots: []string{"bin/bin1.meta_lic"},
534 expectedOut: []matcher{
535 hr{},
536 library{"Android"},
537 usedBy{"bin/bin1"},
538 firstParty{},
539 hr{},
540 library{"Device"},
541 usedBy{"bin/bin1"},
542 library{"External"},
543 usedBy{"bin/bin1"},
544 proprietary{},
545 },
546 },
547 {
548 condition: "proprietary",
549 name: "library",
550 roots: []string{"lib/libd.so.meta_lic"},
551 expectedOut: []matcher{
552 hr{},
553 library{"External"},
554 usedBy{"lib/libd.so"},
555 notice{},
556 },
557 },
558 }
559 for _, tt := range tests {
560 t.Run(tt.condition+" "+tt.name, func(t *testing.T) {
561 stdout := &bytes.Buffer{}
562 stderr := &bytes.Buffer{}
563
564 rootFiles := make([]string, 0, len(tt.roots))
565 for _, r := range tt.roots {
566 rootFiles = append(rootFiles, "testdata/"+tt.condition+"/"+r)
567 }
568
569 ctx := context{stdout, stderr, os.DirFS("."), tt.includeTOC, tt.stripPrefix, tt.title}
570
571 err := htmlNotice(&ctx, rootFiles...)
572 if err != nil {
Colin Cross179ec3e2022-01-27 15:47:09 -0800573 t.Fatalf("htmlnotice: error = %v, stderr = %v", err, stderr)
Bob Badour6ea14572022-01-23 17:15:46 -0800574 return
575 }
576 if stderr.Len() > 0 {
577 t.Errorf("htmlnotice: gotStderr = %v, want none", stderr)
578 }
579
580 t.Logf("got stdout: %s", stdout.String())
581
582 t.Logf("want stdout: %s", matcherList(tt.expectedOut).String())
583
584 out := bufio.NewScanner(stdout)
585 lineno := 0
586 inBody := false
587 hasTitle := false
588 ttle, expectTitle := tt.expectedOut[0].(pageTitle)
589 for out.Scan() {
590 line := out.Text()
591 if strings.TrimLeft(line, " ") == "" {
592 continue
593 }
594 if !inBody {
595 if expectTitle {
596 if tl := checkTitle(line); 0 < len(tl) {
597 if tl != ttle.t {
598 t.Errorf("htmlnotice: unexpected title: got %q, want %q", tl, ttle.t)
599 }
600 hasTitle = true
601 }
602 }
603 if bodyTag.MatchString(line) {
604 inBody = true
605 if expectTitle && !hasTitle {
606 t.Errorf("htmlnotice: missing title: got no <title> tag, want <title>%s</title>", ttle.t)
607 }
608 }
609 continue
610 }
611 if boilerPlate.MatchString(line) {
612 continue
613 }
614 if len(tt.expectedOut) <= lineno {
615 t.Errorf("htmlnotice: unexpected output at line %d: got %q, want nothing (wanted %d lines)", lineno+1, line, len(tt.expectedOut))
616 } else if !tt.expectedOut[lineno].isMatch(line) {
617 t.Errorf("htmlnotice: unexpected output at line %d: got %q, want %q", lineno+1, line, tt.expectedOut[lineno].String())
618 }
619 lineno++
620 }
621 if !inBody {
622 t.Errorf("htmlnotice: missing body: got no <body> tag, want <body> tag followed by %s", matcherList(tt.expectedOut).String())
623 return
624 }
625 for ; lineno < len(tt.expectedOut); lineno++ {
626 t.Errorf("htmlnotice: missing output line %d: ended early, want %q", lineno+1, tt.expectedOut[lineno].String())
627 }
628 })
629 }
630}
631
632func checkTitle(line string) string {
633 groups := titleTag.FindStringSubmatch(line)
634 if len(groups) != 2 {
635 return ""
636 }
637 return groups[1]
638}
639
640type matcher interface {
641 isMatch(line string) bool
642 String() string
643}
644
645type pageTitle struct {
646 t string
647}
648
649func (m pageTitle) isMatch(line string) bool {
650 groups := h1Tag.FindStringSubmatch(line)
651 if len(groups) != 2 {
652 return false
653 }
654 return groups[1] == html.EscapeString(m.t)
655}
656
657func (m pageTitle) String() string {
658 return " <h1>" + html.EscapeString(m.t) + "</h1>"
659}
660
661type toc struct{}
662
663func (m toc) isMatch(line string) bool {
664 return tocTag.MatchString(line)
665}
666
667func (m toc) String() string {
668 return ` <ul class="toc">`
669}
670
671type target struct {
672 name string
673}
674
675func (m target) isMatch(line string) bool {
676 groups := installTarget.FindStringSubmatch(line)
677 if len(groups) != 2 {
678 return false
679 }
680 return strings.HasPrefix(groups[1], "out/") && strings.HasSuffix(groups[1], "/"+html.EscapeString(m.name))
681}
682
683func (m target) String() string {
684 return ` <li id="id#"><strong>` + html.EscapeString(m.name) + `</strong>`
685}
686
687type uses struct {
688 name string
689}
690
691func (m uses) isMatch(line string) bool {
692 groups := libReference.FindStringSubmatch(line)
693 if len(groups) != 2 {
694 return false
695 }
696 return groups[1] == html.EscapeString(m.name)
697}
698
699func (m uses) String() string {
700 return ` <li><a href="#hash">` + html.EscapeString(m.name) + `</a>`
701}
702
703type hr struct{}
704
705func (m hr) isMatch(line string) bool {
706 return horizontalRule.MatchString(line)
707}
708
709func (m hr) String() string {
710 return " <hr>"
711}
712
713type library struct {
714 name string
715}
716
717func (m library) isMatch(line string) bool {
718 groups := libraryName.FindStringSubmatch(line)
719 if len(groups) != 2 {
720 return false
721 }
722 return groups[1] == html.EscapeString(m.name)
723}
724
725func (m library) String() string {
726 return " <strong>" + html.EscapeString(m.name) + "</strong> used by:"
727}
728
729type usedBy struct {
730 name string
731}
732
733func (m usedBy) isMatch(line string) bool {
734 groups := usedByTarget.FindStringSubmatch(line)
735 if len(groups) != 2 {
736 return false
737 }
738 return strings.HasPrefix(groups[1], "out/") && strings.HasSuffix(groups[1], "/"+html.EscapeString(m.name))
739}
740
741func (m usedBy) String() string {
742 return " <li>out/.../" + html.EscapeString(m.name)
743}
744
745func matchesText(line, text string) bool {
746 groups := licenseText.FindStringSubmatch(line)
747 if len(groups) != 2 {
748 return false
749 }
750 return groups[1] == html.EscapeString(text)
751}
752
753func expectedText(text string) string {
754 return ` <a href="#hash"/><pre class="license-text">` + html.EscapeString(text)
755}
756
757type firstParty struct{}
758
759func (m firstParty) isMatch(line string) bool {
760 return matchesText(line, "&&&First Party License&&&")
761}
762
763func (m firstParty) String() string {
764 return expectedText("&&&First Party License&&&")
765}
766
767type notice struct{}
768
769func (m notice) isMatch(line string) bool {
770 return matchesText(line, "%%%Notice License%%%")
771}
772
773func (m notice) String() string {
774 return expectedText("%%%Notice License%%%")
775}
776
777type reciprocal struct{}
778
779func (m reciprocal) isMatch(line string) bool {
780 return matchesText(line, "$$$Reciprocal License$$$")
781}
782
783func (m reciprocal) String() string {
784 return expectedText("$$$Reciprocal License$$$")
785}
786
787type restricted struct{}
788
789func (m restricted) isMatch(line string) bool {
790 return matchesText(line, "###Restricted License###")
791}
792
793func (m restricted) String() string {
794 return expectedText("###Restricted License###")
795}
796
797type proprietary struct{}
798
799func (m proprietary) isMatch(line string) bool {
800 return matchesText(line, "@@@Proprietary License@@@")
801}
802
803func (m proprietary) String() string {
804 return expectedText("@@@Proprietary License@@@")
805}
806
807type matcherList []matcher
808
809func (l matcherList) String() string {
810 var sb strings.Builder
811 for _, m := range l {
812 s := m.String()
813 if s[:3] == s[len(s)-3:] {
814 fmt.Fprintln(&sb)
815 }
816 fmt.Fprintf(&sb, "%s\n", s)
817 if s[:3] == s[len(s)-3:] {
818 fmt.Fprintln(&sb)
819 }
820 }
821 return sb.String()
822}