Skip to content

Commit f4808f8

Browse files
committed
feat(model): guard duplicate scan results / file lists per provenance
Prep work for upcoming PR #10502. * `ScannerRun.init` - Add a `require` check that no two `ScanResult`s share the same provenance and scanner. - Add a similar check that no two `FileList`s share the same provenance. The offending provenance is rendered via `toYaml()` to aid debugging. * `ScannerRunTest` - Introduce two unit tests that assert an `IllegalArgumentException` is thrown when duplicates are present for scan results or file lists, respectively. These runtime checks make improper scan runs surface early and provide clear error messages, which will simplify the upcoming deduplication work. Signed-off-by: Jonatan Männchen <jonatan@maennchen.ch>
1 parent 4efb591 commit f4808f8

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

model/src/main/kotlin/ScannerRun.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,20 @@ data class ScannerRun(
135135
}
136136
}
137137

138+
scanResults.toList().getDuplicates { it.provenance to it.scanner }.keys.let { duplicates ->
139+
require(duplicates.isEmpty()) {
140+
val (dupProvenance, dupScanner) = duplicates.first()
141+
142+
buildString {
143+
appendLine("Found multiple scan results for the same provenance and scanner.")
144+
appendLine("Scanner:")
145+
appendLine(dupScanner.toYaml())
146+
appendLine("Provenance:")
147+
append(dupProvenance.toYaml())
148+
}
149+
}
150+
}
151+
138152
provenances.getDuplicates { it.id }.keys.let { idsForDuplicateProvenanceResolutionResults ->
139153
require(idsForDuplicateProvenanceResolutionResults.isEmpty()) {
140154
"Found multiple provenance resolution results for the following ids: " +
@@ -173,6 +187,13 @@ data class ScannerRun(
173187
}
174188
}
175189
}
190+
191+
files.toList().getDuplicates { it.provenance }.keys.let { duplicateProvenances ->
192+
require(duplicateProvenances.isEmpty()) {
193+
"Found multiple file lists for the same provenance:\n" +
194+
duplicateProvenances.first().toYaml()
195+
}
196+
}
176197
}
177198

178199
private val provenancesById: Map<Identifier, ProvenanceResolutionResult> by lazy {

model/src/test/kotlin/ScannerRunTest.kt

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,178 @@
1919

2020
package org.ossreviewtoolkit.model
2121

22+
import io.kotest.assertions.throwables.shouldThrow
2223
import io.kotest.core.spec.style.WordSpec
2324
import io.kotest.matchers.collections.containExactlyInAnyOrder
2425
import io.kotest.matchers.maps.containExactly
2526
import io.kotest.matchers.nulls.shouldNotBeNull
2627
import io.kotest.matchers.should
28+
import io.kotest.matchers.shouldBe
2729

2830
import org.ossreviewtoolkit.model.FileList.Entry
2931
import org.ossreviewtoolkit.model.utils.alignRevisions
3032
import org.ossreviewtoolkit.model.utils.clearVcsPath
3133
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
3234

3335
class ScannerRunTest : WordSpec({
36+
"init" should {
37+
"error on duplicate provenance and scanner scan results" {
38+
val provenance = RepositoryProvenance(
39+
VcsInfo(type = VcsType.GIT, url = "https://github.com/example.git", revision = "revision"),
40+
"revision"
41+
)
42+
val otherProvenance = RepositoryProvenance(
43+
VcsInfo(type = VcsType.GIT, url = "https://github.com/example.git", revision = "other_revision"),
44+
"other_revision"
45+
)
46+
val provenances = setOf(
47+
ProvenanceResolutionResult(
48+
id = Identifier("maven::example:1.0"),
49+
packageProvenance = provenance
50+
),
51+
ProvenanceResolutionResult(
52+
id = Identifier("maven::other_example:1.0"),
53+
packageProvenance = otherProvenance
54+
)
55+
)
56+
57+
val scanner = ScannerDetails("scanner", "1.0.0", "configuration")
58+
val otherScanner = ScannerDetails("other-scanner", "1.0.0", "configuration")
59+
60+
// Shared provenance and scanner.
61+
val ex = shouldThrow<IllegalArgumentException> {
62+
ScannerRun.EMPTY.copy(
63+
provenances = provenances,
64+
scanResults = setOf(
65+
ScanResult(
66+
provenance = provenance,
67+
scanner = scanner,
68+
summary = ScanSummary.EMPTY.copy(
69+
licenseFindings = setOf(
70+
LicenseFinding("MIT", TextLocation("file1.txt", 1, 1))
71+
)
72+
)
73+
),
74+
ScanResult(
75+
provenance = provenance,
76+
scanner = scanner,
77+
summary = ScanSummary.EMPTY.copy(
78+
licenseFindings = setOf(
79+
LicenseFinding("MIT", TextLocation("file2.txt", 1, 1))
80+
)
81+
)
82+
)
83+
)
84+
)
85+
}
86+
87+
val expectedMessage = buildString {
88+
appendLine("Found multiple scan results for the same provenance and scanner.")
89+
appendLine("Scanner:")
90+
appendLine(scanner.toYaml())
91+
appendLine("Provenance:")
92+
append(provenance.toYaml())
93+
}.trimEnd()
94+
95+
ex.message!!.trimEnd() shouldBe expectedMessage
96+
97+
// Shared provenance and different scanners.
98+
ScannerRun.EMPTY.copy(
99+
provenances = provenances,
100+
scanResults = setOf(
101+
ScanResult(
102+
provenance = provenance,
103+
scanner = scanner,
104+
summary = ScanSummary.EMPTY.copy(
105+
licenseFindings = setOf(
106+
LicenseFinding("MIT", TextLocation("file1.txt", 1, 1))
107+
)
108+
)
109+
),
110+
ScanResult(
111+
provenance = provenance,
112+
scanner = otherScanner,
113+
summary = ScanSummary.EMPTY.copy(
114+
licenseFindings = setOf(
115+
LicenseFinding("MIT", TextLocation("file2.txt", 1, 1))
116+
)
117+
)
118+
)
119+
)
120+
)
121+
122+
// Different provenance and shared scanner.
123+
ScannerRun.EMPTY.copy(
124+
provenances = provenances,
125+
scanResults = setOf(
126+
ScanResult(
127+
provenance = provenance,
128+
scanner = scanner,
129+
summary = ScanSummary.EMPTY.copy(
130+
licenseFindings = setOf(
131+
LicenseFinding("MIT", TextLocation("file1.txt", 1, 1))
132+
)
133+
)
134+
),
135+
ScanResult(
136+
provenance = otherProvenance,
137+
scanner = scanner,
138+
summary = ScanSummary.EMPTY.copy(
139+
licenseFindings = setOf(
140+
LicenseFinding("MIT", TextLocation("file2.txt", 1, 1))
141+
)
142+
)
143+
)
144+
)
145+
)
146+
}
147+
148+
"error on duplicate provenance file lists" {
149+
val provenance = RepositoryProvenance(
150+
VcsInfo(type = VcsType.GIT, url = "https://github.com/example.git", revision = "revision"),
151+
"revision"
152+
)
153+
154+
val ex = shouldThrow<IllegalArgumentException> {
155+
ScannerRun.EMPTY.copy(
156+
provenances = setOf(
157+
ProvenanceResolutionResult(
158+
id = Identifier("maven::other_example:1.0"),
159+
packageProvenance = provenance
160+
)
161+
),
162+
files = setOf(
163+
FileList(
164+
provenance = provenance,
165+
files = setOf(
166+
Entry(
167+
path = "vcs/path/file1.txt",
168+
sha1 = "1111111111111111111111111111111111111111"
169+
)
170+
)
171+
),
172+
FileList(
173+
provenance = provenance,
174+
files = setOf(
175+
Entry(
176+
path = "some/dir/file2.txt",
177+
sha1 = "2222222222222222222222222222222222222222"
178+
)
179+
)
180+
)
181+
)
182+
)
183+
}
184+
185+
val expectedMessage = buildString {
186+
appendLine("Found multiple file lists for the same provenance:")
187+
append(provenance.toYaml())
188+
}.trimEnd()
189+
190+
ex.message!!.trimEnd() shouldBe expectedMessage
191+
}
192+
}
193+
34194
"getFileList()" should {
35195
"filter by VCS path and merge sub-repository lists as expected" {
36196
val id = Identifier("a:b:c:1.0.0")

0 commit comments

Comments
 (0)