• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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