Skip to content

Commit c3dcf29

Browse files
authored
Add sarif results minimization #490 (#498)
1 parent 731a2c6 commit c3dcf29

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,17 @@ data class SarifResult(
9999
val locations: List<SarifPhysicalLocationWrapper> = listOf(),
100100
val relatedLocations: List<SarifRelatedPhysicalLocationWrapper> = listOf(),
101101
val codeFlows: List<SarifCodeFlow> = listOf()
102-
)
102+
) {
103+
/**
104+
* Returns the total number of locations in all [codeFlows].
105+
*/
106+
fun totalCodeFlowLocations() =
107+
codeFlows.sumBy { codeFlow ->
108+
codeFlow.threadFlows.sumBy { threadFlow ->
109+
threadFlow.locations.size
110+
}
111+
}
112+
}
103113

104114
/**
105115
* The severity of the result. "Error" for detected unchecked exceptions.

utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,45 @@ class SarifReport(
8787
return Sarif.fromRun(
8888
SarifRun(
8989
SarifTool.fromRules(sarifRules.toList()),
90-
sarifResults
90+
minimizeResults(sarifResults)
9191
)
9292
)
9393
}
9494

95+
/**
96+
* Minimizes detected errors and removes duplicates.
97+
*
98+
* Between two [SarifResult]s with the same `ruleId` and `locations`
99+
* it chooses the one with the shorter length of the execution trace.
100+
*
101+
* __Example:__
102+
*
103+
* The SARIF report for the code below contains only one unchecked exception in `methodB`.
104+
* But without minimization, the report will contain two results: for `methodA` and for `methodB`.
105+
*
106+
* ```
107+
* class Example {
108+
* int methodA(int a) {
109+
* return methodB(a);
110+
* }
111+
* int methodB(int b) {
112+
* return 1 / b;
113+
* }
114+
* }
115+
* ```
116+
*/
117+
private fun minimizeResults(sarifResults: List<SarifResult>): List<SarifResult> {
118+
val groupedResults = sarifResults.groupBy { sarifResult ->
119+
Pair(sarifResult.ruleId, sarifResult.locations)
120+
}
121+
val minimizedResults = groupedResults.map { (_, sarifResultsGroup) ->
122+
sarifResultsGroup.minByOrNull { sarifResult ->
123+
sarifResult.totalCodeFlowLocations()
124+
}!!
125+
}
126+
return minimizedResults
127+
}
128+
95129
private fun processUncheckedException(
96130
method: UtMethod<*>,
97131
utExecution: UtExecution,

utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,116 @@ class SarifReportTest {
186186
assert(codeFlowPhysicalLocations[0].region.startColumn == 5)
187187
}
188188

189+
@Test
190+
fun testMinimizationRemovesDuplicates() {
191+
mockUtMethodNames()
192+
193+
val mockUtExecution = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS)
194+
Mockito.`when`(mockUtExecution.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false))
195+
196+
val testCases = listOf(
197+
UtTestCase(mockUtMethod, listOf(mockUtExecution)),
198+
UtTestCase(mockUtMethod, listOf(mockUtExecution)) // duplicate
199+
)
200+
201+
val report = SarifReport(
202+
testCases = testCases,
203+
generatedTestsCode = "",
204+
sourceFindingMain
205+
).createReport().toSarif()
206+
207+
assert(report.runs.first().results.size == 1) // no duplicates
208+
}
209+
210+
@Test
211+
fun testMinimizationDoesNotRemoveResultsWithDifferentRuleId() {
212+
mockUtMethodNames()
213+
214+
val mockUtExecution1 = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS)
215+
val mockUtExecution2 = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS)
216+
217+
// different ruleId's
218+
Mockito.`when`(mockUtExecution1.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false))
219+
Mockito.`when`(mockUtExecution2.result).thenReturn(UtImplicitlyThrownException(ArithmeticException(), false))
220+
221+
val testCases = listOf(
222+
UtTestCase(mockUtMethod, listOf(mockUtExecution1)),
223+
UtTestCase(mockUtMethod, listOf(mockUtExecution2)) // not a duplicate
224+
)
225+
226+
val report = SarifReport(
227+
testCases = testCases,
228+
generatedTestsCode = "",
229+
sourceFindingMain
230+
).createReport().toSarif()
231+
232+
assert(report.runs.first().results.size == 2) // no results have been removed
233+
}
234+
235+
@Test
236+
fun testMinimizationDoesNotRemoveResultsWithDifferentLocations() {
237+
mockUtMethodNames()
238+
239+
val mockUtExecution1 = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS)
240+
val mockUtExecution2 = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS)
241+
242+
// the same ruleId's
243+
Mockito.`when`(mockUtExecution1.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false))
244+
Mockito.`when`(mockUtExecution2.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false))
245+
246+
// different locations
247+
Mockito.`when`(mockUtExecution1.path.lastOrNull()?.stmt?.javaSourceStartLineNumber).thenReturn(11)
248+
Mockito.`when`(mockUtExecution2.path.lastOrNull()?.stmt?.javaSourceStartLineNumber).thenReturn(22)
249+
250+
val testCases = listOf(
251+
UtTestCase(mockUtMethod, listOf(mockUtExecution1)),
252+
UtTestCase(mockUtMethod, listOf(mockUtExecution2)) // not a duplicate
253+
)
254+
255+
val report = SarifReport(
256+
testCases = testCases,
257+
generatedTestsCode = "",
258+
sourceFindingMain
259+
).createReport().toSarif()
260+
261+
assert(report.runs.first().results.size == 2) // no results have been removed
262+
}
263+
264+
@Test
265+
fun testMinimizationChoosesShortestCodeFlow() {
266+
mockUtMethodNames()
267+
268+
val mockNPE1 = Mockito.mock(NullPointerException::class.java)
269+
val mockNPE2 = Mockito.mock(NullPointerException::class.java)
270+
271+
val mockUtExecution1 = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS)
272+
val mockUtExecution2 = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS)
273+
274+
// the same ruleId's
275+
Mockito.`when`(mockUtExecution1.result).thenReturn(UtImplicitlyThrownException(mockNPE1, false))
276+
Mockito.`when`(mockUtExecution2.result).thenReturn(UtImplicitlyThrownException(mockNPE2, false))
277+
278+
// but different stack traces
279+
val stackTraceElement1 = StackTraceElement("Main", "main", "Main.java", 3)
280+
val stackTraceElement2 = StackTraceElement("Main", "main", "Main.java", 7)
281+
Mockito.`when`(mockNPE1.stackTrace).thenReturn(arrayOf(stackTraceElement1))
282+
Mockito.`when`(mockNPE2.stackTrace).thenReturn(arrayOf(stackTraceElement1, stackTraceElement2))
283+
284+
val testCases = listOf(
285+
UtTestCase(mockUtMethod, listOf(mockUtExecution1)),
286+
UtTestCase(mockUtMethod, listOf(mockUtExecution2)) // duplicate with a longer stack trace
287+
)
288+
289+
val report = SarifReport(
290+
testCases = testCases,
291+
generatedTestsCode = "",
292+
sourceFindingMain
293+
).createReport().toSarif()
294+
295+
assert(report.runs.first().results.size == 1) // no duplicates
296+
assert(report.runs.first().results.first().totalCodeFlowLocations() == 1) // with a shorter stack trace
297+
}
298+
189299
// internal
190300

191301
private val mockUtMethod = Mockito.mock(UtMethod::class.java, Mockito.RETURNS_DEEP_STUBS)

0 commit comments

Comments
 (0)