Files
build/tools/compliance/test_util.go
Bob Badour 103eb0f9bc Performance and scale.
Defer edge creation.

Don't create edges until the count is known to avoid repeated allocate+
copy operatios.

Limit resolutions.

Allow only a single resolution condition set per target, and overwrite
intermediate results. Reduces memory and obviates allocations.

Propagate fewer conditions.

Instead of propagating notice conditions to parents in graph during
initial resolve, leave them on leaf node, and attach to ancestors in
the final walk. Reduces copies.

Parallelize resolutions.

Use goroutines, mutexes, and waitgroups to resolve branches of the
graph in parallel. Makes better use of available cores.

Don't accumulate resolutions inside non-containers.

During the final resolution walk, only attach actions to ancestors from
the root down until the 1st non-aggregate. Prevents an explosion of
copies in the lower levels of the graph.

Drop origin for scale.

Tracking the origin of every potential origin for every restricted
condition does not scale. By dropping origin, propagating from top
to bottom can prune many redundant paths avoiding an exponential
explosion.

Conditions as bitmask.

Use bit masks for license conditions and condition sets. Reduces maps
and allocations.

Bug: 68860345
Bug: 151177513
Bug: 151953481

Test: m all
Test: m systemlicense
Test: m listshare; out/soong/host/linux-x86/bin/listshare ...
Test: m checkshare; out/soong/host/linux-x86/bin/checkshare ...
Test: m dumpgraph; out/soong/host/linux-x86/dumpgraph ...
Test: m dumpresolutions; out/soong/host/linux-x86/dumpresolutions ...

where ... is the path to the .meta_lic file for the system image. In my
case if

$ export PRODUCT=$(realpath $ANDROID_PRODUCT_OUT --relative-to=$PWD)

... can be expressed as:

${PRODUCT}/gen/META/lic_intermediates/${PRODUCT}/system.img.meta_lic

Change-Id: Ia2ec1b818de6122c239fbd0824754f1d65daffd3
2022-01-11 10:40:50 -08:00

605 lines
18 KiB
Go

// 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"
"io/fs"
"sort"
"strings"
"testing"
)
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: "restricted"
`
// 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"
`
// 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 in the license graph.
func newTestCondition(lg *LicenseGraph, targetName string, conditionName string) LicenseCondition {
tn := newTestNode(lg, targetName)
cl := LicenseConditionSetFromNames(tn, 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]
tn.licenseConditions = tn.licenseConditions.Plus(lc)
return lc
}
// newTestConditionSet constructs a test license condition set in the license graph.
func newTestConditionSet(lg *LicenseGraph, targetName string, conditionName []string) LicenseConditionSet {
tn := newTestNode(lg, targetName)
cs := LicenseConditionSetFromNames(tn, conditionName...)
if cs.IsEmpty() {
panic(fmt.Errorf("attempt to create unrecognized condition: %q", conditionName))
}
tn.licenseConditions = tn.licenseConditions.Union(cs)
return cs
}
// testFS implements a test file system (fs.FS) simulated by a map from filename to []byte content.
type testFS map[string][]byte
// Open implements fs.FS.Open() to open a file based on the filename.
func (fs *testFS) Open(name string) (fs.File, error) {
if _, ok := (*fs)[name]; !ok {
return nil, fmt.Errorf("unknown file %q", name)
}
return &testFile{fs, name, 0}, nil
}
// testFile implements a test file (fs.File) based on testFS above.
type testFile struct {
fs *testFS
name string
posn int
}
// Stat not implemented to obviate implementing fs.FileInfo.
func (f *testFile) Stat() (fs.FileInfo, error) {
return nil, fmt.Errorf("unimplemented")
}
// Read copies bytes from the testFS map.
func (f *testFile) Read(b []byte) (int, error) {
if f.posn < 0 {
return 0, fmt.Errorf("file not open: %q", f.name)
}
if f.posn >= len((*f.fs)[f.name]) {
return 0, io.EOF
}
n := copy(b, (*f.fs)[f.name][f.posn:])
f.posn += n
return n, nil
}
// Close marks the testFile as no longer in use.
func (f *testFile) Close() error {
if f.posn < 0 {
return fmt.Errorf("file already closed: %q", f.name)
}
f.posn = -1
return nil
}
// 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)
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, origin, condition string
}
// String returns a human-readable string representing the test action.
func (a act) String() string {
return fmt.Sprintf("%s{%s:%s}", a.actsOn, a.origin, 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(lg, a.origin, strings.Split(a.condition, "|"))
as[actsOn] = cs
}
return as
}
// res describes test data resolutions to define test resolution sets.
type res struct {
attachesTo, actsOn, origin, 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(lg, r.origin, 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, ":")
oshare := fields[0]
cshare := fields[1]
fields = strings.Split(c.privacy, ":")
oprivacy := fields[0]
cprivacy := fields[1]
result = append(result, SourceSharePrivacyConflict{
newTestNode(lg, c.sourceNode),
newTestCondition(lg, oshare, cshare),
newTestCondition(lg, oprivacy, 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 %04x with names %s, want %04x 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 %04x with names %s, want %04x 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)
}
}
}