// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package compliance import ( "fmt" "io" "sort" "strings" "testing" "android/soong/tools/compliance/testfs" ) const ( // AOSP starts a test metadata file for Android Apache-2.0 licensing. AOSP = `` + `package_name: "Android" license_kinds: "SPDX-license-identifier-Apache-2.0" license_conditions: "notice" ` // GPL starts a test metadata file for GPL 2.0 licensing. GPL = `` + `package_name: "Free Software" license_kinds: "SPDX-license-identifier-GPL-2.0" license_conditions: "restricted" ` // Classpath starts a test metadata file for GPL 2.0 with classpath exception licensing. Classpath = `` + `package_name: "Free Software" license_kinds: "SPDX-license-identifier-GPL-2.0-with-classpath-exception" license_conditions: "permissive" ` // DependentModule starts a test metadata file for a module in the same package as `Classpath`. DependentModule = `` + `package_name: "Free Software" license_kinds: "SPDX-license-identifier-MIT" license_conditions: "notice" ` // LGPL starts a test metadata file for a module with LGPL 2.0 licensing. LGPL = `` + `package_name: "Free Library" license_kinds: "SPDX-license-identifier-LGPL-2.0" license_conditions: "restricted_if_statically_linked" ` // MPL starts a test metadata file for a module with MPL 2.0 reciprical licensing. MPL = `` + `package_name: "Reciprocal" license_kinds: "SPDX-license-identifier-MPL-2.0" license_conditions: "reciprocal" ` // MIT starts a test metadata file for a module with generic notice (MIT) licensing. MIT = `` + `package_name: "Android" license_kinds: "SPDX-license-identifier-MIT" license_conditions: "notice" ` // Proprietary starts a test metadata file for a module with proprietary licensing. Proprietary = `` + `package_name: "Android" license_kinds: "legacy_proprietary" license_conditions: "proprietary" ` // ByException starts a test metadata file for a module with by_exception_only licensing. ByException = `` + `package_name: "Special" license_kinds: "legacy_by_exception_only" license_conditions: "by_exception_only" ` ) var ( // meta maps test file names to metadata file content without dependencies. meta = map[string]string{ "apacheBin.meta_lic": AOSP, "apacheLib.meta_lic": AOSP, "apacheContainer.meta_lic": AOSP + "is_container: true\n", "dependentModule.meta_lic": DependentModule, "gplWithClasspathException.meta_lic": Classpath, "gplBin.meta_lic": GPL, "gplLib.meta_lic": GPL, "gplContainer.meta_lic": GPL + "is_container: true\n", "lgplBin.meta_lic": LGPL, "lgplLib.meta_lic": LGPL, "mitBin.meta_lic": MIT, "mitLib.meta_lic": MIT, "mplBin.meta_lic": MPL, "mplLib.meta_lic": MPL, "proprietary.meta_lic": Proprietary, "by_exception.meta_lic": ByException, } ) // newTestNode constructs a test node in the license graph. func newTestNode(lg *LicenseGraph, targetName string) *TargetNode { if tn, alreadyExists := lg.targets[targetName]; alreadyExists { return tn } tn := &TargetNode{name: targetName} lg.targets[targetName] = tn return tn } // newTestCondition constructs a test license condition. func newTestCondition(conditionName string) LicenseCondition { cl := LicenseConditionSetFromNames(conditionName).AsList() if len(cl) == 0 { panic(fmt.Errorf("attempt to create unrecognized condition: %q", conditionName)) } else if len(cl) != 1 { panic(fmt.Errorf("unexpected multiple conditions from condition name: %q: got %d, want 1", conditionName, len(cl))) } lc := cl[0] return lc } // newTestConditionSet constructs a test license condition set. func newTestConditionSet(conditionName []string) LicenseConditionSet { cs := LicenseConditionSetFromNames(conditionName...) if cs.IsEmpty() { panic(fmt.Errorf("attempt to create unrecognized condition: %q", conditionName)) } return cs } // edge describes test data edges to define test graphs. type edge struct { target, dep string } // String returns a string representation of the edge. func (e edge) String() string { return e.target + " -> " + e.dep } // byEdge orders edges by target then dep name then annotations. type byEdge []edge // Len returns the count of elements in the slice. func (l byEdge) Len() int { return len(l) } // Swap rearranges 2 elements of the slice so that each occupies the other's // former position. func (l byEdge) Swap(i, j int) { l[i], l[j] = l[j], l[i] } // Less returns true when the `i`th element is lexicographically less than // the `j`th element. func (l byEdge) Less(i, j int) bool { if l[i].target == l[j].target { return l[i].dep < l[j].dep } return l[i].target < l[j].target } // annotated describes annotated test data edges to define test graphs. type annotated struct { target, dep string annotations []string } func (e annotated) String() string { if e.annotations != nil { return e.target + " -> " + e.dep + " [" + strings.Join(e.annotations, ", ") + "]" } return e.target + " -> " + e.dep } func (e annotated) IsEqualTo(other annotated) bool { if e.target != other.target { return false } if e.dep != other.dep { return false } if len(e.annotations) != len(other.annotations) { return false } a1 := append([]string{}, e.annotations...) a2 := append([]string{}, other.annotations...) for i := 0; i < len(a1); i++ { if a1[i] != a2[i] { return false } } return true } // toGraph converts a list of roots and a list of annotated edges into a test license graph. func toGraph(stderr io.Writer, roots []string, edges []annotated) (*LicenseGraph, error) { deps := make(map[string][]annotated) for _, root := range roots { deps[root] = []annotated{} } for _, edge := range edges { if prev, ok := deps[edge.target]; ok { deps[edge.target] = append(prev, edge) } else { deps[edge.target] = []annotated{edge} } if _, ok := deps[edge.dep]; !ok { deps[edge.dep] = []annotated{} } } fs := make(testfs.TestFS) for file, edges := range deps { body := meta[file] for _, edge := range edges { body += fmt.Sprintf("deps: {\n file: %q\n", edge.dep) for _, ann := range edge.annotations { body += fmt.Sprintf(" annotations: %q\n", ann) } body += "}\n" } fs[file] = []byte(body) } return ReadLicenseGraph(&fs, stderr, roots) } // logGraph outputs a representation of the graph to a test log. func logGraph(lg *LicenseGraph, t *testing.T) { t.Logf("license graph:") t.Logf(" targets:") for _, target := range lg.Targets() { t.Logf(" %s%s in package %q", target.Name(), target.LicenseConditions().String(), target.PackageName()) } t.Logf(" /targets") t.Logf(" edges:") for _, edge := range lg.Edges() { t.Logf(" %s", edge.String()) } t.Logf(" /edges") t.Logf("/license graph") } // byAnnotatedEdge orders edges by target then dep name then annotations. type byAnnotatedEdge []annotated func (l byAnnotatedEdge) Len() int { return len(l) } func (l byAnnotatedEdge) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l byAnnotatedEdge) Less(i, j int) bool { if l[i].target == l[j].target { if l[i].dep == l[j].dep { ai := append([]string{}, l[i].annotations...) aj := append([]string{}, l[j].annotations...) sort.Strings(ai) sort.Strings(aj) for k := 0; k < len(ai) && k < len(aj); k++ { if ai[k] == aj[k] { continue } return ai[k] < aj[k] } return len(ai) < len(aj) } return l[i].dep < l[j].dep } return l[i].target < l[j].target } // act describes test data resolution actions to define test action sets. type act struct { actsOn, condition string } // String returns a human-readable string representing the test action. func (a act) String() string { return fmt.Sprintf("%s{%s}", a.actsOn, a.condition) } // toActionSet converts a list of act test data into a test action set. func toActionSet(lg *LicenseGraph, data []act) ActionSet { as := make(ActionSet) for _, a := range data { actsOn := newTestNode(lg, a.actsOn) cs := newTestConditionSet(strings.Split(a.condition, "|")) as[actsOn] = cs } return as } // res describes test data resolutions to define test resolution sets. type res struct { attachesTo, actsOn, condition string } // toResolutionSet converts a list of res test data into a test resolution set. func toResolutionSet(lg *LicenseGraph, data []res) ResolutionSet { rmap := make(ResolutionSet) for _, r := range data { attachesTo := newTestNode(lg, r.attachesTo) actsOn := newTestNode(lg, r.actsOn) if _, ok := rmap[attachesTo]; !ok { rmap[attachesTo] = make(ActionSet) } cs := newTestConditionSet(strings.Split(r.condition, "|")) rmap[attachesTo][actsOn] |= cs } return rmap } // tcond associates a target name with '|' separated string conditions. type tcond struct { target, conditions string } // action represents a single element of an ActionSet for testing. type action struct { target *TargetNode cs LicenseConditionSet } // String returns a human-readable string representation of the action. func (a action) String() string { return fmt.Sprintf("%s%s", a.target.Name(), a.cs.String()) } // actionList represents an array of actions and a total order defined by // target name followed by license condition set. type actionList []action // String returns a human-readable string representation of the list. func (l actionList) String() string { var sb strings.Builder fmt.Fprintf(&sb, "[") sep := "" for _, a := range l { fmt.Fprintf(&sb, "%s%s", sep, a.String()) sep = ", " } fmt.Fprintf(&sb, "]") return sb.String() } // Len returns the count of elements in the slice. func (l actionList) Len() int { return len(l) } // Swap rearranges 2 elements of the slice so that each occupies the other's // former position. func (l actionList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } // Less returns true when the `i`th element is lexicographically less than // the `j`th element. func (l actionList) Less(i, j int) bool { if l[i].target == l[j].target { return l[i].cs < l[j].cs } return l[i].target.Name() < l[j].target.Name() } // asActionList represents the resolved license conditions in a license graph // as an actionList for comparison in a test. func asActionList(lg *LicenseGraph) actionList { result := make(actionList, 0, len(lg.targets)) for _, target := range lg.targets { cs := target.resolution if cs.IsEmpty() { continue } result = append(result, action{target, cs}) } return result } // toActionList converts an array of tcond into an actionList for comparison // in a test. func toActionList(lg *LicenseGraph, actions []tcond) actionList { result := make(actionList, 0, len(actions)) for _, actn := range actions { target := newTestNode(lg, actn.target) cs := NewLicenseConditionSet() for _, name := range strings.Split(actn.conditions, "|") { lc, ok := RecognizedConditionNames[name] if !ok { panic(fmt.Errorf("Unrecognized test condition name: %q", name)) } cs = cs.Plus(lc) } result = append(result, action{target, cs}) } return result } // confl defines test data for a SourceSharePrivacyConflict as a target name, // source condition name, privacy condition name triple. type confl struct { sourceNode, share, privacy string } // toConflictList converts confl test data into an array of // SourceSharePrivacyConflict for comparison in a test. func toConflictList(lg *LicenseGraph, data []confl) []SourceSharePrivacyConflict { result := make([]SourceSharePrivacyConflict, 0, len(data)) for _, c := range data { fields := strings.Split(c.share, ":") cshare := fields[1] fields = strings.Split(c.privacy, ":") cprivacy := fields[1] result = append(result, SourceSharePrivacyConflict{ newTestNode(lg, c.sourceNode), newTestCondition(cshare), newTestCondition(cprivacy), }) } return result } // checkSameActions compares an actual action set to an expected action set for a test. func checkSameActions(lg *LicenseGraph, asActual, asExpected ActionSet, t *testing.T) { rsActual := make(ResolutionSet) rsExpected := make(ResolutionSet) testNode := newTestNode(lg, "test") rsActual[testNode] = asActual rsExpected[testNode] = asExpected checkSame(rsActual, rsExpected, t) } // checkSame compares an actual resolution set to an expected resolution set for a test. func checkSame(rsActual, rsExpected ResolutionSet, t *testing.T) { t.Logf("actual resolution set: %s", rsActual.String()) t.Logf("expected resolution set: %s", rsExpected.String()) actualTargets := rsActual.AttachesTo() sort.Sort(actualTargets) expectedTargets := rsExpected.AttachesTo() sort.Sort(expectedTargets) t.Logf("actual targets: %s", actualTargets.String()) t.Logf("expected targets: %s", expectedTargets.String()) for _, target := range expectedTargets { if !rsActual.AttachesToTarget(target) { t.Errorf("unexpected missing target: got AttachesToTarget(%q) is false, want true", target.name) continue } expectedRl := rsExpected.Resolutions(target) sort.Sort(expectedRl) actualRl := rsActual.Resolutions(target) sort.Sort(actualRl) if len(expectedRl) != len(actualRl) { t.Errorf("unexpected number of resolutions attach to %q: %d elements, %d elements", target.name, len(actualRl), len(expectedRl)) continue } for i := 0; i < len(expectedRl); i++ { if expectedRl[i].attachesTo.name != actualRl[i].attachesTo.name || expectedRl[i].actsOn.name != actualRl[i].actsOn.name { t.Errorf("unexpected resolution attaches to %q at index %d: got %s, want %s", target.name, i, actualRl[i].asString(), expectedRl[i].asString()) continue } expectedConditions := expectedRl[i].Resolves() actualConditions := actualRl[i].Resolves() if expectedConditions != actualConditions { t.Errorf("unexpected conditions apply to %q acting on %q: got %#v with names %s, want %#v with names %s", target.name, expectedRl[i].actsOn.name, actualConditions, actualConditions.Names(), expectedConditions, expectedConditions.Names()) continue } } } for _, target := range actualTargets { if !rsExpected.AttachesToTarget(target) { t.Errorf("unexpected extra target: got expected.AttachesTo(%q) is false, want true", target.name) } } } // checkResolvesActions compares an actual action set to an expected action set for a test verifying the actual set // resolves all of the expected conditions. func checkResolvesActions(lg *LicenseGraph, asActual, asExpected ActionSet, t *testing.T) { rsActual := make(ResolutionSet) rsExpected := make(ResolutionSet) testNode := newTestNode(lg, "test") rsActual[testNode] = asActual rsExpected[testNode] = asExpected checkResolves(rsActual, rsExpected, t) } // checkResolves compares an actual resolution set to an expected resolution set for a test verifying the actual set // resolves all of the expected conditions. func checkResolves(rsActual, rsExpected ResolutionSet, t *testing.T) { t.Logf("actual resolution set: %s", rsActual.String()) t.Logf("expected resolution set: %s", rsExpected.String()) actualTargets := rsActual.AttachesTo() sort.Sort(actualTargets) expectedTargets := rsExpected.AttachesTo() sort.Sort(expectedTargets) t.Logf("actual targets: %s", actualTargets.String()) t.Logf("expected targets: %s", expectedTargets.String()) for _, target := range expectedTargets { if !rsActual.AttachesToTarget(target) { t.Errorf("unexpected missing target: got AttachesToTarget(%q) is false, want true", target.name) continue } expectedRl := rsExpected.Resolutions(target) sort.Sort(expectedRl) actualRl := rsActual.Resolutions(target) sort.Sort(actualRl) if len(expectedRl) != len(actualRl) { t.Errorf("unexpected number of resolutions attach to %q: %d elements, %d elements", target.name, len(actualRl), len(expectedRl)) continue } for i := 0; i < len(expectedRl); i++ { if expectedRl[i].attachesTo.name != actualRl[i].attachesTo.name || expectedRl[i].actsOn.name != actualRl[i].actsOn.name { t.Errorf("unexpected resolution attaches to %q at index %d: got %s, want %s", target.name, i, actualRl[i].asString(), expectedRl[i].asString()) continue } expectedConditions := expectedRl[i].Resolves() actualConditions := actualRl[i].Resolves() if expectedConditions != (expectedConditions & actualConditions) { t.Errorf("expected conditions missing from %q acting on %q: got %#v with names %s, want %#v with names %s", target.name, expectedRl[i].actsOn.name, actualConditions, actualConditions.Names(), expectedConditions, expectedConditions.Names()) continue } } } for _, target := range actualTargets { if !rsExpected.AttachesToTarget(target) { t.Errorf("unexpected extra target: got expected.AttachesTo(%q) is false, want true", target.name) } } }