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 compliance 16 17import ( 18 "fmt" 19 "io" 20 "io/fs" 21 "sort" 22 "strings" 23 "testing" 24) 25 26const ( 27 // AOSP starts a test metadata file for Android Apache-2.0 licensing. 28 AOSP = `` + 29 `package_name: "Android" 30license_kinds: "SPDX-license-identifier-Apache-2.0" 31license_conditions: "notice" 32` 33 34 // GPL starts a test metadata file for GPL 2.0 licensing. 35 GPL = `` + 36 `package_name: "Free Software" 37license_kinds: "SPDX-license-identifier-GPL-2.0" 38license_conditions: "restricted" 39` 40 41 // Classpath starts a test metadata file for GPL 2.0 with classpath exception licensing. 42 Classpath = `` + 43 `package_name: "Free Software" 44license_kinds: "SPDX-license-identifier-GPL-2.0-with-classpath-exception" 45license_conditions: "restricted" 46` 47 48 // DependentModule starts a test metadata file for a module in the same package as `Classpath`. 49 DependentModule = `` + 50 `package_name: "Free Software" 51license_kinds: "SPDX-license-identifier-MIT" 52license_conditions: "notice" 53` 54 55 // LGPL starts a test metadata file for a module with LGPL 2.0 licensing. 56 LGPL = `` + 57 `package_name: "Free Library" 58license_kinds: "SPDX-license-identifier-LGPL-2.0" 59license_conditions: "restricted" 60` 61 62 // MPL starts a test metadata file for a module with MPL 2.0 reciprical licensing. 63 MPL = `` + 64 `package_name: "Reciprocal" 65license_kinds: "SPDX-license-identifier-MPL-2.0" 66license_conditions: "reciprocal" 67` 68 69 // MIT starts a test metadata file for a module with generic notice (MIT) licensing. 70 MIT = `` + 71 `package_name: "Android" 72license_kinds: "SPDX-license-identifier-MIT" 73license_conditions: "notice" 74` 75 76 // Proprietary starts a test metadata file for a module with proprietary licensing. 77 Proprietary = `` + 78 `package_name: "Android" 79license_kinds: "legacy_proprietary" 80license_conditions: "proprietary" 81` 82 83 // ByException starts a test metadata file for a module with by_exception_only licensing. 84 ByException = `` + 85 `package_name: "Special" 86license_kinds: "legacy_by_exception_only" 87license_conditions: "by_exception_only" 88` 89) 90 91var ( 92 // meta maps test file names to metadata file content without dependencies. 93 meta = map[string]string{ 94 "apacheBin.meta_lic": AOSP, 95 "apacheLib.meta_lic": AOSP, 96 "apacheContainer.meta_lic": AOSP + "is_container: true\n", 97 "dependentModule.meta_lic": DependentModule, 98 "gplWithClasspathException.meta_lic": Classpath, 99 "gplBin.meta_lic": GPL, 100 "gplLib.meta_lic": GPL, 101 "gplContainer.meta_lic": GPL + "is_container: true\n", 102 "lgplBin.meta_lic": LGPL, 103 "lgplLib.meta_lic": LGPL, 104 "mitBin.meta_lic": MIT, 105 "mitLib.meta_lic": MIT, 106 "mplBin.meta_lic": MPL, 107 "mplLib.meta_lic": MPL, 108 "proprietary.meta_lic": Proprietary, 109 "by_exception.meta_lic": ByException, 110 } 111) 112 113// newTestNode constructs a test node in the license graph. 114func newTestNode(lg *LicenseGraph, targetName string) *TargetNode { 115 if tn, alreadyExists := lg.targets[targetName]; alreadyExists { 116 return tn 117 } 118 tn := &TargetNode{name: targetName} 119 lg.targets[targetName] = tn 120 return tn 121} 122 123// newTestCondition constructs a test license condition in the license graph. 124func newTestCondition(lg *LicenseGraph, targetName string, conditionName string) LicenseCondition { 125 tn := newTestNode(lg, targetName) 126 cl := LicenseConditionSetFromNames(tn, conditionName).AsList() 127 if len(cl) == 0 { 128 panic(fmt.Errorf("attempt to create unrecognized condition: %q", conditionName)) 129 } else if len(cl) != 1 { 130 panic(fmt.Errorf("unexpected multiple conditions from condition name: %q: got %d, want 1", conditionName, len(cl))) 131 } 132 lc := cl[0] 133 tn.licenseConditions = tn.licenseConditions.Plus(lc) 134 return lc 135} 136 137// newTestConditionSet constructs a test license condition set in the license graph. 138func newTestConditionSet(lg *LicenseGraph, targetName string, conditionName []string) LicenseConditionSet { 139 tn := newTestNode(lg, targetName) 140 cs := LicenseConditionSetFromNames(tn, conditionName...) 141 if cs.IsEmpty() { 142 panic(fmt.Errorf("attempt to create unrecognized condition: %q", conditionName)) 143 } 144 tn.licenseConditions = tn.licenseConditions.Union(cs) 145 return cs 146} 147 148// testFS implements a test file system (fs.FS) simulated by a map from filename to []byte content. 149type testFS map[string][]byte 150 151// Open implements fs.FS.Open() to open a file based on the filename. 152func (fs *testFS) Open(name string) (fs.File, error) { 153 if _, ok := (*fs)[name]; !ok { 154 return nil, fmt.Errorf("unknown file %q", name) 155 } 156 return &testFile{fs, name, 0}, nil 157} 158 159// testFile implements a test file (fs.File) based on testFS above. 160type testFile struct { 161 fs *testFS 162 name string 163 posn int 164} 165 166// Stat not implemented to obviate implementing fs.FileInfo. 167func (f *testFile) Stat() (fs.FileInfo, error) { 168 return nil, fmt.Errorf("unimplemented") 169} 170 171// Read copies bytes from the testFS map. 172func (f *testFile) Read(b []byte) (int, error) { 173 if f.posn < 0 { 174 return 0, fmt.Errorf("file not open: %q", f.name) 175 } 176 if f.posn >= len((*f.fs)[f.name]) { 177 return 0, io.EOF 178 } 179 n := copy(b, (*f.fs)[f.name][f.posn:]) 180 f.posn += n 181 return n, nil 182} 183 184// Close marks the testFile as no longer in use. 185func (f *testFile) Close() error { 186 if f.posn < 0 { 187 return fmt.Errorf("file already closed: %q", f.name) 188 } 189 f.posn = -1 190 return nil 191} 192 193// edge describes test data edges to define test graphs. 194type edge struct { 195 target, dep string 196} 197 198// String returns a string representation of the edge. 199func (e edge) String() string { 200 return e.target + " -> " + e.dep 201} 202 203// byEdge orders edges by target then dep name then annotations. 204type byEdge []edge 205 206// Len returns the count of elements in the slice. 207func (l byEdge) Len() int { return len(l) } 208 209// Swap rearranges 2 elements of the slice so that each occupies the other's 210// former position. 211func (l byEdge) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 212 213// Less returns true when the `i`th element is lexicographically less than 214// the `j`th element. 215func (l byEdge) Less(i, j int) bool { 216 if l[i].target == l[j].target { 217 return l[i].dep < l[j].dep 218 } 219 return l[i].target < l[j].target 220} 221 222// annotated describes annotated test data edges to define test graphs. 223type annotated struct { 224 target, dep string 225 annotations []string 226} 227 228func (e annotated) String() string { 229 if e.annotations != nil { 230 return e.target + " -> " + e.dep + " [" + strings.Join(e.annotations, ", ") + "]" 231 } 232 return e.target + " -> " + e.dep 233} 234 235func (e annotated) IsEqualTo(other annotated) bool { 236 if e.target != other.target { 237 return false 238 } 239 if e.dep != other.dep { 240 return false 241 } 242 if len(e.annotations) != len(other.annotations) { 243 return false 244 } 245 a1 := append([]string{}, e.annotations...) 246 a2 := append([]string{}, other.annotations...) 247 for i := 0; i < len(a1); i++ { 248 if a1[i] != a2[i] { 249 return false 250 } 251 } 252 return true 253} 254 255// toGraph converts a list of roots and a list of annotated edges into a test license graph. 256func toGraph(stderr io.Writer, roots []string, edges []annotated) (*LicenseGraph, error) { 257 deps := make(map[string][]annotated) 258 for _, root := range roots { 259 deps[root] = []annotated{} 260 } 261 for _, edge := range edges { 262 if prev, ok := deps[edge.target]; ok { 263 deps[edge.target] = append(prev, edge) 264 } else { 265 deps[edge.target] = []annotated{edge} 266 } 267 if _, ok := deps[edge.dep]; !ok { 268 deps[edge.dep] = []annotated{} 269 } 270 } 271 fs := make(testFS) 272 for file, edges := range deps { 273 body := meta[file] 274 for _, edge := range edges { 275 body += fmt.Sprintf("deps: {\n file: %q\n", edge.dep) 276 for _, ann := range edge.annotations { 277 body += fmt.Sprintf(" annotations: %q\n", ann) 278 } 279 body += "}\n" 280 } 281 fs[file] = []byte(body) 282 } 283 284 return ReadLicenseGraph(&fs, stderr, roots) 285} 286 287// logGraph outputs a representation of the graph to a test log. 288func logGraph(lg *LicenseGraph, t *testing.T) { 289 t.Logf("license graph:") 290 t.Logf(" targets:") 291 for _, target := range lg.Targets() { 292 t.Logf(" %s%s in package %q", target.Name(), target.LicenseConditions().String(), target.PackageName()) 293 } 294 t.Logf(" /targets") 295 t.Logf(" edges:") 296 for _, edge := range lg.Edges() { 297 t.Logf(" %s", edge.String()) 298 } 299 t.Logf(" /edges") 300 t.Logf("/license graph") 301} 302 303// byAnnotatedEdge orders edges by target then dep name then annotations. 304type byAnnotatedEdge []annotated 305 306func (l byAnnotatedEdge) Len() int { return len(l) } 307func (l byAnnotatedEdge) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 308func (l byAnnotatedEdge) Less(i, j int) bool { 309 if l[i].target == l[j].target { 310 if l[i].dep == l[j].dep { 311 ai := append([]string{}, l[i].annotations...) 312 aj := append([]string{}, l[j].annotations...) 313 sort.Strings(ai) 314 sort.Strings(aj) 315 for k := 0; k < len(ai) && k < len(aj); k++ { 316 if ai[k] == aj[k] { 317 continue 318 } 319 return ai[k] < aj[k] 320 } 321 return len(ai) < len(aj) 322 } 323 return l[i].dep < l[j].dep 324 } 325 return l[i].target < l[j].target 326} 327 328// act describes test data resolution actions to define test action sets. 329type act struct { 330 actsOn, origin, condition string 331} 332 333// String returns a human-readable string representing the test action. 334func (a act) String() string { 335 return fmt.Sprintf("%s{%s:%s}", a.actsOn, a.origin, a.condition) 336} 337 338// toActionSet converts a list of act test data into a test action set. 339func toActionSet(lg *LicenseGraph, data []act) ActionSet { 340 as := make(ActionSet) 341 for _, a := range data { 342 actsOn := newTestNode(lg, a.actsOn) 343 cs := newTestConditionSet(lg, a.origin, strings.Split(a.condition, "|")) 344 as[actsOn] = cs 345 } 346 return as 347} 348 349// res describes test data resolutions to define test resolution sets. 350type res struct { 351 attachesTo, actsOn, origin, condition string 352} 353 354// toResolutionSet converts a list of res test data into a test resolution set. 355func toResolutionSet(lg *LicenseGraph, data []res) ResolutionSet { 356 rmap := make(ResolutionSet) 357 for _, r := range data { 358 attachesTo := newTestNode(lg, r.attachesTo) 359 actsOn := newTestNode(lg, r.actsOn) 360 if _, ok := rmap[attachesTo]; !ok { 361 rmap[attachesTo] = make(ActionSet) 362 } 363 cs := newTestConditionSet(lg, r.origin, strings.Split(r.condition, ":")) 364 rmap[attachesTo][actsOn] |= cs 365 } 366 return rmap 367} 368 369// tcond associates a target name with '|' separated string conditions. 370type tcond struct { 371 target, conditions string 372} 373 374// action represents a single element of an ActionSet for testing. 375type action struct { 376 target *TargetNode 377 cs LicenseConditionSet 378} 379 380// String returns a human-readable string representation of the action. 381func (a action) String() string { 382 return fmt.Sprintf("%s%s", a.target.Name(), a.cs.String()) 383} 384 385// actionList represents an array of actions and a total order defined by 386// target name followed by license condition set. 387type actionList []action 388 389// String returns a human-readable string representation of the list. 390func (l actionList) String() string { 391 var sb strings.Builder 392 fmt.Fprintf(&sb, "[") 393 sep := "" 394 for _, a := range l { 395 fmt.Fprintf(&sb, "%s%s", sep, a.String()) 396 sep = ", " 397 } 398 fmt.Fprintf(&sb, "]") 399 return sb.String() 400} 401 402// Len returns the count of elements in the slice. 403func (l actionList) Len() int { return len(l) } 404 405// Swap rearranges 2 elements of the slice so that each occupies the other's 406// former position. 407func (l actionList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 408 409// Less returns true when the `i`th element is lexicographically less than 410// the `j`th element. 411func (l actionList) Less(i, j int) bool { 412 if l[i].target == l[j].target { 413 return l[i].cs < l[j].cs 414 } 415 return l[i].target.Name() < l[j].target.Name() 416} 417 418// asActionList represents the resolved license conditions in a license graph 419// as an actionList for comparison in a test. 420func asActionList(lg *LicenseGraph) actionList { 421 result := make(actionList, 0, len(lg.targets)) 422 for _, target := range lg.targets { 423 cs := target.resolution 424 if cs.IsEmpty() { 425 continue 426 } 427 result = append(result, action{target, cs}) 428 } 429 return result 430} 431 432// toActionList converts an array of tcond into an actionList for comparison 433// in a test. 434func toActionList(lg *LicenseGraph, actions []tcond) actionList { 435 result := make(actionList, 0, len(actions)) 436 for _, actn := range actions { 437 target := newTestNode(lg, actn.target) 438 cs := NewLicenseConditionSet() 439 for _, name := range strings.Split(actn.conditions, "|") { 440 lc, ok := RecognizedConditionNames[name] 441 if !ok { 442 panic(fmt.Errorf("Unrecognized test condition name: %q", name)) 443 } 444 cs = cs.Plus(lc) 445 } 446 result = append(result, action{target, cs}) 447 } 448 return result 449} 450 451// confl defines test data for a SourceSharePrivacyConflict as a target name, 452// source condition name, privacy condition name triple. 453type confl struct { 454 sourceNode, share, privacy string 455} 456 457// toConflictList converts confl test data into an array of 458// SourceSharePrivacyConflict for comparison in a test. 459func toConflictList(lg *LicenseGraph, data []confl) []SourceSharePrivacyConflict { 460 result := make([]SourceSharePrivacyConflict, 0, len(data)) 461 for _, c := range data { 462 fields := strings.Split(c.share, ":") 463 oshare := fields[0] 464 cshare := fields[1] 465 fields = strings.Split(c.privacy, ":") 466 oprivacy := fields[0] 467 cprivacy := fields[1] 468 result = append(result, SourceSharePrivacyConflict{ 469 newTestNode(lg, c.sourceNode), 470 newTestCondition(lg, oshare, cshare), 471 newTestCondition(lg, oprivacy, cprivacy), 472 }) 473 } 474 return result 475} 476 477// checkSameActions compares an actual action set to an expected action set for a test. 478func checkSameActions(lg *LicenseGraph, asActual, asExpected ActionSet, t *testing.T) { 479 rsActual := make(ResolutionSet) 480 rsExpected := make(ResolutionSet) 481 testNode := newTestNode(lg, "test") 482 rsActual[testNode] = asActual 483 rsExpected[testNode] = asExpected 484 checkSame(rsActual, rsExpected, t) 485} 486 487// checkSame compares an actual resolution set to an expected resolution set for a test. 488func checkSame(rsActual, rsExpected ResolutionSet, t *testing.T) { 489 t.Logf("actual resolution set: %s", rsActual.String()) 490 t.Logf("expected resolution set: %s", rsExpected.String()) 491 492 actualTargets := rsActual.AttachesTo() 493 sort.Sort(actualTargets) 494 495 expectedTargets := rsExpected.AttachesTo() 496 sort.Sort(expectedTargets) 497 498 t.Logf("actual targets: %s", actualTargets.String()) 499 t.Logf("expected targets: %s", expectedTargets.String()) 500 501 for _, target := range expectedTargets { 502 if !rsActual.AttachesToTarget(target) { 503 t.Errorf("unexpected missing target: got AttachesToTarget(%q) is false, want true", target.name) 504 continue 505 } 506 expectedRl := rsExpected.Resolutions(target) 507 sort.Sort(expectedRl) 508 actualRl := rsActual.Resolutions(target) 509 sort.Sort(actualRl) 510 if len(expectedRl) != len(actualRl) { 511 t.Errorf("unexpected number of resolutions attach to %q: %d elements, %d elements", 512 target.name, len(actualRl), len(expectedRl)) 513 continue 514 } 515 for i := 0; i < len(expectedRl); i++ { 516 if expectedRl[i].attachesTo.name != actualRl[i].attachesTo.name || expectedRl[i].actsOn.name != actualRl[i].actsOn.name { 517 t.Errorf("unexpected resolution attaches to %q at index %d: got %s, want %s", 518 target.name, i, actualRl[i].asString(), expectedRl[i].asString()) 519 continue 520 } 521 expectedConditions := expectedRl[i].Resolves() 522 actualConditions := actualRl[i].Resolves() 523 if expectedConditions != actualConditions { 524 t.Errorf("unexpected conditions apply to %q acting on %q: got %04x with names %s, want %04x with names %s", 525 target.name, expectedRl[i].actsOn.name, 526 actualConditions, actualConditions.Names(), 527 expectedConditions, expectedConditions.Names()) 528 continue 529 } 530 } 531 532 } 533 for _, target := range actualTargets { 534 if !rsExpected.AttachesToTarget(target) { 535 t.Errorf("unexpected extra target: got expected.AttachesTo(%q) is false, want true", target.name) 536 } 537 } 538} 539 540// checkResolvesActions compares an actual action set to an expected action set for a test verifying the actual set 541// resolves all of the expected conditions. 542func checkResolvesActions(lg *LicenseGraph, asActual, asExpected ActionSet, t *testing.T) { 543 rsActual := make(ResolutionSet) 544 rsExpected := make(ResolutionSet) 545 testNode := newTestNode(lg, "test") 546 rsActual[testNode] = asActual 547 rsExpected[testNode] = asExpected 548 checkResolves(rsActual, rsExpected, t) 549} 550 551// checkResolves compares an actual resolution set to an expected resolution set for a test verifying the actual set 552// resolves all of the expected conditions. 553func checkResolves(rsActual, rsExpected ResolutionSet, t *testing.T) { 554 t.Logf("actual resolution set: %s", rsActual.String()) 555 t.Logf("expected resolution set: %s", rsExpected.String()) 556 557 actualTargets := rsActual.AttachesTo() 558 sort.Sort(actualTargets) 559 560 expectedTargets := rsExpected.AttachesTo() 561 sort.Sort(expectedTargets) 562 563 t.Logf("actual targets: %s", actualTargets.String()) 564 t.Logf("expected targets: %s", expectedTargets.String()) 565 566 for _, target := range expectedTargets { 567 if !rsActual.AttachesToTarget(target) { 568 t.Errorf("unexpected missing target: got AttachesToTarget(%q) is false, want true", target.name) 569 continue 570 } 571 expectedRl := rsExpected.Resolutions(target) 572 sort.Sort(expectedRl) 573 actualRl := rsActual.Resolutions(target) 574 sort.Sort(actualRl) 575 if len(expectedRl) != len(actualRl) { 576 t.Errorf("unexpected number of resolutions attach to %q: %d elements, %d elements", 577 target.name, len(actualRl), len(expectedRl)) 578 continue 579 } 580 for i := 0; i < len(expectedRl); i++ { 581 if expectedRl[i].attachesTo.name != actualRl[i].attachesTo.name || expectedRl[i].actsOn.name != actualRl[i].actsOn.name { 582 t.Errorf("unexpected resolution attaches to %q at index %d: got %s, want %s", 583 target.name, i, actualRl[i].asString(), expectedRl[i].asString()) 584 continue 585 } 586 expectedConditions := expectedRl[i].Resolves() 587 actualConditions := actualRl[i].Resolves() 588 if expectedConditions != (expectedConditions & actualConditions) { 589 t.Errorf("expected conditions missing from %q acting on %q: got %04x with names %s, want %04x with names %s", 590 target.name, expectedRl[i].actsOn.name, 591 actualConditions, actualConditions.Names(), 592 expectedConditions, expectedConditions.Names()) 593 continue 594 } 595 } 596 597 } 598 for _, target := range actualTargets { 599 if !rsExpected.AttachesToTarget(target) { 600 t.Errorf("unexpected extra target: got expected.AttachesTo(%q) is false, want true", target.name) 601 } 602 } 603} 604