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