Skip to content

Commit 7b6eda5

Browse files
committed
feat(model): guard against duplicate scan results / file lists per provenance
Prep work for upcoming PR oss-review-toolkit#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 a15d3fb commit 7b6eda5

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,17 +19,177 @@
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

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

0 commit comments

Comments
 (0)