blob: a183b9014f802e06ee7669e3ad3b375469a1ca14 [file] [log] [blame]
Bob Badoura99ac622021-10-25 16:21:00 -07001// 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`
Bob Badour9ee7d032021-10-25 16:51:48 -070033
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)
91
92var (
93 // meta maps test file names to metadata file content without dependencies.
94 meta = map[string]string{
95 "apacheBin.meta_lic": AOSP,
96 "apacheLib.meta_lic": AOSP,
97 "apacheContainer.meta_lic": AOSP + "is_container: true\n",
98 "dependentModule.meta_lic": DependentModule,
99 "gplWithClasspathException.meta_lic": Classpath,
100 "gplBin.meta_lic": GPL,
101 "gplLib.meta_lic": GPL,
102 "gplContainer.meta_lic": GPL + "is_container: true\n",
103 "lgplBin.meta_lic": LGPL,
104 "lgplLib.meta_lic": LGPL,
105 "mitBin.meta_lic": MIT,
106 "mitLib.meta_lic": MIT,
107 "mplBin.meta_lic": MPL,
108 "mplLib.meta_lic": MPL,
109 "proprietary.meta_lic": Proprietary,
110 "by_exception.meta_lic": ByException,
111 }
Bob Badoura99ac622021-10-25 16:21:00 -0700112)
113
114// toConditionList converts a test data map of condition name to origin names into a ConditionList.
115func toConditionList(lg *LicenseGraph, conditions map[string][]string) ConditionList {
116 cl := make(ConditionList, 0)
117 for name, origins := range conditions {
118 for _, origin := range origins {
119 cl = append(cl, LicenseCondition{name, newTestNode(lg, origin)})
120 }
121 }
122 return cl
123}
124
125// newTestNode constructs a test node in the license graph.
126func newTestNode(lg *LicenseGraph, targetName string) *TargetNode {
127 if _, ok := lg.targets[targetName]; !ok {
128 lg.targets[targetName] = &TargetNode{name: targetName}
129 }
130 return lg.targets[targetName]
131}
132
133// testFS implements a test file system (fs.FS) simulated by a map from filename to []byte content.
134type testFS map[string][]byte
135
136// Open implements fs.FS.Open() to open a file based on the filename.
137func (fs *testFS) Open(name string) (fs.File, error) {
138 if _, ok := (*fs)[name]; !ok {
139 return nil, fmt.Errorf("unknown file %q", name)
140 }
141 return &testFile{fs, name, 0}, nil
142}
143
144// testFile implements a test file (fs.File) based on testFS above.
145type testFile struct {
146 fs *testFS
147 name string
148 posn int
149}
150
151// Stat not implemented to obviate implementing fs.FileInfo.
152func (f *testFile) Stat() (fs.FileInfo, error) {
153 return nil, fmt.Errorf("unimplemented")
154}
155
156// Read copies bytes from the testFS map.
157func (f *testFile) Read(b []byte) (int, error) {
158 if f.posn < 0 {
159 return 0, fmt.Errorf("file not open: %q", f.name)
160 }
161 if f.posn >= len((*f.fs)[f.name]) {
162 return 0, io.EOF
163 }
164 n := copy(b, (*f.fs)[f.name][f.posn:])
165 f.posn += n
166 return n, nil
167}
168
169// Close marks the testFile as no longer in use.
170func (f *testFile) Close() error {
171 if f.posn < 0 {
172 return fmt.Errorf("file already closed: %q", f.name)
173 }
174 f.posn = -1
175 return nil
176}
177
178// edge describes test data edges to define test graphs.
179type edge struct {
180 target, dep string
181}
182
183// String returns a string representation of the edge.
184func (e edge) String() string {
185 return e.target + " -> " + e.dep
186}
187
188// byEdge orders edges by target then dep name then annotations.
189type byEdge []edge
190
191// Len returns the count of elements in the slice.
192func (l byEdge) Len() int { return len(l) }
193
194// Swap rearranges 2 elements of the slice so that each occupies the other's
195// former position.
196func (l byEdge) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
197
198// Less returns true when the `i`th element is lexicographically less than
199// the `j`th element.
200func (l byEdge) Less(i, j int) bool {
201 if l[i].target == l[j].target {
202 return l[i].dep < l[j].dep
203 }
204 return l[i].target < l[j].target
205}
206
Bob Badour9ee7d032021-10-25 16:51:48 -0700207
208// annotated describes annotated test data edges to define test graphs.
209type annotated struct {
210 target, dep string
211 annotations []string
212}
213
214func (e annotated) String() string {
215 if e.annotations != nil {
216 return e.target + " -> " + e.dep + " [" + strings.Join(e.annotations, ", ") + "]"
217 }
218 return e.target + " -> " + e.dep
219}
220
221func (e annotated) IsEqualTo(other annotated) bool {
222 if e.target != other.target {
223 return false
224 }
225 if e.dep != other.dep {
226 return false
227 }
228 if len(e.annotations) != len(other.annotations) {
229 return false
230 }
231 a1 := append([]string{}, e.annotations...)
232 a2 := append([]string{}, other.annotations...)
233 for i := 0; i < len(a1); i++ {
234 if a1[i] != a2[i] {
235 return false
236 }
237 }
238 return true
239}
240
241// toGraph converts a list of roots and a list of annotated edges into a test license graph.
242func toGraph(stderr io.Writer, roots []string, edges []annotated) (*LicenseGraph, error) {
243 deps := make(map[string][]annotated)
244 for _, root := range roots {
245 deps[root] = []annotated{}
246 }
247 for _, edge := range edges {
248 if prev, ok := deps[edge.target]; ok {
249 deps[edge.target] = append(prev, edge)
250 } else {
251 deps[edge.target] = []annotated{edge}
252 }
253 if _, ok := deps[edge.dep]; !ok {
254 deps[edge.dep] = []annotated{}
255 }
256 }
257 fs := make(testFS)
258 for file, edges := range deps {
259 body := meta[file]
260 for _, edge := range edges {
261 body += fmt.Sprintf("deps: {\n file: %q\n", edge.dep)
262 for _, ann := range edge.annotations {
263 body += fmt.Sprintf(" annotations: %q\n", ann)
264 }
265 body += "}\n"
266 }
267 fs[file] = []byte(body)
268 }
269
270 return ReadLicenseGraph(&fs, stderr, roots)
271}
272
273
274// byAnnotatedEdge orders edges by target then dep name then annotations.
275type byAnnotatedEdge []annotated
276
277func (l byAnnotatedEdge) Len() int { return len(l) }
278func (l byAnnotatedEdge) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
279func (l byAnnotatedEdge) Less(i, j int) bool {
280 if l[i].target == l[j].target {
281 if l[i].dep == l[j].dep {
282 ai := append([]string{}, l[i].annotations...)
283 aj := append([]string{}, l[j].annotations...)
284 sort.Strings(ai)
285 sort.Strings(aj)
286 for k := 0; k < len(ai) && k < len(aj); k++ {
287 if ai[k] == aj[k] {
288 continue
289 }
290 return ai[k] < aj[k]
291 }
292 return len(ai) < len(aj)
293 }
294 return l[i].dep < l[j].dep
295 }
296 return l[i].target < l[j].target
297}
298
Bob Badoura99ac622021-10-25 16:21:00 -0700299// res describes test data resolutions to define test resolution sets.
300type res struct {
301 attachesTo, actsOn, origin, condition string
302}
303
304// toResolutionSet converts a list of res test data into a test resolution set.
305func toResolutionSet(lg *LicenseGraph, data []res) *ResolutionSet {
306 rmap := make(map[*TargetNode]actionSet)
307 for _, r := range data {
308 attachesTo := newTestNode(lg, r.attachesTo)
309 actsOn := newTestNode(lg, r.actsOn)
310 origin := newTestNode(lg, r.origin)
311 if _, ok := rmap[attachesTo]; !ok {
312 rmap[attachesTo] = make(actionSet)
313 }
314 if _, ok := rmap[attachesTo][actsOn]; !ok {
315 rmap[attachesTo][actsOn] = newLicenseConditionSet()
316 }
317 rmap[attachesTo][actsOn].add(origin, r.condition)
318 }
319 return &ResolutionSet{rmap}
320}
321
Bob Badour9ee7d032021-10-25 16:51:48 -0700322type confl struct {
323 sourceNode, share, privacy string
324}
325
326func toConflictList(lg *LicenseGraph, data []confl) []SourceSharePrivacyConflict {
327 result := make([]SourceSharePrivacyConflict, 0, len(data))
328 for _, c := range data {
329 fields := strings.Split(c.share, ":")
330 oshare := fields[0]
331 cshare := fields[1]
332 fields = strings.Split(c.privacy, ":")
333 oprivacy := fields[0]
334 cprivacy := fields[1]
335 result = append(result, SourceSharePrivacyConflict{
336 newTestNode(lg, c.sourceNode),
337 LicenseCondition{cshare, newTestNode(lg, oshare)},
338 LicenseCondition{cprivacy, newTestNode(lg, oprivacy)},
339 })
340 }
341 return result
342}
343
Bob Badoura99ac622021-10-25 16:21:00 -0700344// checkSameActions compares an actual action set to an expected action set for a test.
345func checkSameActions(lg *LicenseGraph, asActual, asExpected actionSet, t *testing.T) {
346 rsActual := ResolutionSet{make(map[*TargetNode]actionSet)}
347 rsExpected := ResolutionSet{make(map[*TargetNode]actionSet)}
348 testNode := newTestNode(lg, "test")
349 rsActual.resolutions[testNode] = asActual
350 rsExpected.resolutions[testNode] = asExpected
351 checkSame(&rsActual, &rsExpected, t)
352}
353
354// checkSame compares an actual resolution set to an expected resolution set for a test.
355func checkSame(rsActual, rsExpected *ResolutionSet, t *testing.T) {
356 expectedTargets := rsExpected.AttachesTo()
357 sort.Sort(expectedTargets)
358 for _, target := range expectedTargets {
359 if !rsActual.AttachesToTarget(target) {
360 t.Errorf("unexpected missing target: got AttachesToTarget(%q) is false in %s, want true in %s", target.name, rsActual, rsExpected)
361 continue
362 }
363 expectedRl := rsExpected.Resolutions(target)
364 sort.Sort(expectedRl)
365 actualRl := rsActual.Resolutions(target)
366 sort.Sort(actualRl)
367 if len(expectedRl) != len(actualRl) {
368 t.Errorf("unexpected number of resolutions attach to %q: got %s with %d elements, want %s with %d elements",
369 target.name, actualRl, len(actualRl), expectedRl, len(expectedRl))
370 continue
371 }
372 for i := 0; i < len(expectedRl); i++ {
373 if expectedRl[i].attachesTo.name != actualRl[i].attachesTo.name || expectedRl[i].actsOn.name != actualRl[i].actsOn.name {
374 t.Errorf("unexpected resolution attaches to %q at index %d: got %s, want %s",
375 target.name, i, actualRl[i].asString(), expectedRl[i].asString())
376 continue
377 }
378 expectedConditions := expectedRl[i].Resolves().AsList()
379 actualConditions := actualRl[i].Resolves().AsList()
380 sort.Sort(expectedConditions)
381 sort.Sort(actualConditions)
382 if len(expectedConditions) != len(actualConditions) {
383 t.Errorf("unexpected number of conditions apply to %q acting on %q: got %s with %d elements, want %s with %d elements",
384 target.name, expectedRl[i].actsOn.name,
385 actualConditions, len(actualConditions),
386 expectedConditions, len(expectedConditions))
387 continue
388 }
389 for j := 0; j < len(expectedConditions); j++ {
390 if expectedConditions[j] != actualConditions[j] {
391 t.Errorf("unexpected condition attached to %q acting on %q at index %d: got %s at index %d in %s, want %s in %s",
392 target.name, expectedRl[i].actsOn.name, i,
393 actualConditions[j].asString(":"), j, actualConditions,
394 expectedConditions[j].asString(":"), expectedConditions)
395 }
396 }
397 }
398
399 }
400 actualTargets := rsActual.AttachesTo()
401 sort.Sort(actualTargets)
402 for i, target := range actualTargets {
403 if !rsExpected.AttachesToTarget(target) {
404 t.Errorf("unexpected target: got %q element %d in AttachesTo() %s with %d elements in %s, want %s with %d elements in %s",
405 target.name, i, actualTargets, len(actualTargets), rsActual, expectedTargets, len(expectedTargets), rsExpected)
406 }
407 }
408}