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