From 378b3970d2e169c7dce99c8e039e85597297e27e Mon Sep 17 00:00:00 2001 From: Nikita Stroganov Date: Mon, 5 Dec 2022 20:00:59 +0300 Subject: [PATCH 1/3] Add sarif logical locations --- .../kotlin/org/utbot/sarif/DataClasses.kt | 63 ++++++-- .../kotlin/org/utbot/sarif/SarifReport.kt | 150 +++++++++++++----- .../generator/CodeGenerationController.kt | 5 +- .../inspection/UnitTestBotInspectionTool.kt | 37 +++-- 4 files changed, 193 insertions(+), 62 deletions(-) diff --git a/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt index 1cc571f047..e71294914d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt @@ -1,8 +1,11 @@ package org.utbot.sarif -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.* +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue @@ -17,6 +20,12 @@ data class Sarif( val runs: List ) { companion object { + + private val jsonMapper = jacksonObjectMapper() + .registerModule(SimpleModule() + .addDeserializer(SarifLocationWrapper::class.java, SarifLocationWrapperDeserializer()) + ) + private const val defaultSchema = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" private const val defaultVersion = @@ -29,11 +38,11 @@ data class Sarif( Sarif(defaultSchema, defaultVersion, listOf(run)) fun fromJson(reportInJson: String): Sarif = - jacksonObjectMapper().readValue(reportInJson) + jsonMapper.readValue(reportInJson) } fun toJson(): String = - jacksonObjectMapper() + jsonMapper .setSerializationInclusion(JsonInclude.Include.NON_NULL) .writerWithDefaultPrettyPrinter() .writeValueAsString(this) @@ -111,7 +120,7 @@ data class SarifResult( val ruleId: String, val level: Level, val message: Message, - val locations: List = listOf(), + val locations: List = listOf(), val relatedLocations: List = listOf(), val codeFlows: List = listOf() ) { @@ -145,14 +154,41 @@ data class Message( val markdown: String? = null ) -// physical location +// location /** * [Documentation](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#location-object) */ +sealed class SarifLocationWrapper + data class SarifPhysicalLocationWrapper( - val physicalLocation: SarifPhysicalLocation, -) + val physicalLocation: SarifPhysicalLocation // this name used in the SarifLocationWrapperDeserializer +) : SarifLocationWrapper() + +data class SarifLogicalLocationsWrapper( + val logicalLocations: List // this name used in the SarifLocationWrapperDeserializer +) : SarifLocationWrapper() + +/** + * Custom JSON deserializer for sealed class [SarifLocationWrapper]. + * Returns [SarifPhysicalLocationWrapper] or [SarifLogicalLocationsWrapper]. + */ +class SarifLocationWrapperDeserializer : JsonDeserializer() { + override fun deserialize(jp: JsonParser, context: DeserializationContext?): SarifLocationWrapper { + val node: JsonNode = jp.codec.readTree(jp) + val isPhysicalLocation = node.get("physicalLocation") != null // field name + val isLogicalLocations = node.get("logicalLocations") != null // field name + return when { + isPhysicalLocation -> { + jacksonObjectMapper().readValue(node.toString()) + } + isLogicalLocations -> { + return jacksonObjectMapper().readValue(node.toString()) + } + else -> error("SarifLocationWrapperDeserializer: Cannot parse ${node.toPrettyString()}") + } + } +} /** * [Documentation](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#physicallocation-object) @@ -189,6 +225,15 @@ data class SarifRegion( } } +// logical locations + +/** + * [Documentation](https://github.com/microsoft/sarif-tutorials/blob/main/docs/2-Basics.md#physical-and-logical-locations) + */ +data class SarifLogicalLocation( + val fullyQualifiedName: String +) + // related locations /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt index 8b14243dcb..8f6e4438a5 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt @@ -4,6 +4,7 @@ import org.utbot.common.PathUtil.fileExtension import org.utbot.common.PathUtil.toPath import org.utbot.framework.UtSettings import org.utbot.framework.plugin.api.* +import java.nio.file.Path import kotlin.io.path.nameWithoutExtension /** @@ -27,6 +28,41 @@ class SarifReport( reports.fold(Sarif.empty()) { sarif: Sarif, report: String -> sarif.copy(runs = sarif.runs + Sarif.fromJson(report).runs) }.toJson() + + /** + * Minimizes SARIF results between several reports. + * + * More complex version of the [SarifReport.minimizeResults]. + */ + fun minimizeSarifResults(srcPathToSarif: MutableMap): MutableMap { + val pathToSarifResult = srcPathToSarif.entries.flatMap { (path, sarif) -> + sarif.getAllResults().map { sarifResult -> + path to sarifResult + } + } + val groupedResults = pathToSarifResult.groupBy { (_, sarifResult) -> + sarifResult.ruleId to sarifResult.locations + } + val minimizedResults = groupedResults.map { (_, sarifResultsGroup) -> + sarifResultsGroup.minByOrNull { (_, sarifResult) -> + sarifResult.totalCodeFlowLocations() + }!! + } + val groupedByPath = minimizedResults + .groupBy { (path, _) -> path } + .mapValues { (_, resultsWithPath) -> + resultsWithPath.map { (_, sarifResult) -> sarifResult } // remove redundant path + } + val pathToSarifTool = srcPathToSarif.mapValues { (_, sarif) -> + sarif.runs.first().tool + } + val paths = pathToSarifTool.keys intersect groupedByPath.keys + return paths.associateWith { path -> + val sarifTool = pathToSarifTool[path]!! + val sarifResults = groupedByPath[path]!! + Sarif.fromRun(SarifRun(sarifTool, sarifResults)) + }.toMutableMap() + } } /** @@ -67,6 +103,8 @@ class SarifReport( */ private val relatedLocationId = 1 // for attaching link to generated test in related locations + private val stackTraceLengthForStackOverflow = 50 // stack overflow error may have too many elements + /** * Minimizes detected errors and removes duplicates. * @@ -125,7 +163,7 @@ class SarifReport( [Generated test for this case]($relatedLocationId) """.trimIndent() ), - getLocations(utExecution, classFqn), + getLocations(method, utExecution, classFqn), getRelatedLocations(utExecution), getCodeFlows(method, utExecution, executionFailure) ) @@ -146,16 +184,23 @@ class SarifReport( return Pair(sarifResult, sarifRule) } - private fun getLocations(utExecution: UtExecution, classFqn: String?): List { + private fun getLocations( + method: ExecutableId, + utExecution: UtExecution, + classFqn: String? + ): List { if (classFqn == null) return listOf() - val sourceRelativePath = sourceFinding.getSourceRelativePath(classFqn) - val startLine = getLastLineNumber(utExecution, classFqn) ?: defaultLineNumber - val sourceCode = sourceFinding.getSourceFile(classFqn)?.readText() ?: "" + val (startLine, classWithErrorFqn) = getLastLineNumberWithClassFqn(method, utExecution, classFqn) + val sourceCode = sourceFinding.getSourceFile(classWithErrorFqn)?.readText() ?: "" val sourceRegion = SarifRegion.withStartLine(sourceCode, startLine) + val sourceRelativePath = sourceFinding.getSourceRelativePath(classWithErrorFqn) return listOf( SarifPhysicalLocationWrapper( SarifPhysicalLocation(SarifArtifact(sourceRelativePath), sourceRegion) + ), + SarifLogicalLocationsWrapper( + listOf(SarifLogicalLocation(classWithErrorFqn)) // class name without method name ) ) } @@ -181,31 +226,9 @@ class SarifReport( utExecution: UtExecution, executionFailure: UtExecutionFailure ): List { - /* Example of a typical stack trace: - - java.lang.Math.multiplyExact(Math.java:867) - - com.abc.Util.multiply(Util.java:10) - - com.abc.Util.multiply(Util.java:6) - - com.abc.Main.example(Main.java:11) // <- `lastMethodCallIndex` - - sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - - ... - */ - val stackTrace = executionFailure.exception.stackTrace - - val lastMethodCallIndex = stackTrace.indexOfLast { - it.className == method.classId.name && it.methodName == method.name - } - if (lastMethodCallIndex == -1) - return listOf() - - val stackTraceFiltered = stackTrace - .take(lastMethodCallIndex + 1) // taking all elements before the last `method` call - .filter { - !it.className.startsWith("org.utbot.") // filter all internal calls - } - - val stackTraceResolved = stackTraceFiltered.mapNotNull { - findStackTraceElementLocation(it) - }.toMutableList() + val stackTraceResolved = filterStackTrace(method, utExecution, executionFailure) + .mapNotNull { findStackTraceElementLocation(it) } + .toMutableList() if (stackTraceResolved.isEmpty()) return listOf() // empty stack trace is not shown @@ -235,6 +258,40 @@ class SarifReport( ) } + private fun filterStackTrace( + method: ExecutableId, + utExecution: UtExecution, + executionFailure: UtExecutionFailure + ): List { + /* Example of a typical stack trace: + - java.lang.Math.multiplyExact(Math.java:867) + - com.abc.Util.multiply(Util.java:10) + - com.abc.Util.multiply(Util.java:6) + - com.abc.Main.example(Main.java:11) // <- `lastMethodCallIndex` + - sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + - ... + */ + var stackTrace = executionFailure.exception.stackTrace.toList() + + val lastMethodCallIndex = stackTrace.indexOfLast { + it.className == method.classId.name && it.methodName == method.name + } + if (lastMethodCallIndex != -1) { + // taking all elements before the last `method` call + stackTrace = stackTrace.take(lastMethodCallIndex + 1) + } + + if (executionFailure.exception is StackOverflowError) { + stackTrace = stackTrace.takeLast(stackTraceLengthForStackOverflow) + } + + val stackTraceFiltered = stackTrace.filter { + !it.className.startsWith("org.utbot.") // filter all internal calls + } + + return stackTraceFiltered + } + private fun findStackTraceElementLocation(stackTraceElement: StackTraceElement): SarifFlowLocationWrapper? { val lineNumber = stackTraceElement.lineNumber if (lineNumber < 1) @@ -327,15 +384,21 @@ class SarifReport( } /** - * Returns the number of the last line in the execution path which is located in the [classFqn]. + * Returns the number of the last line in the execution path + * And the name of the class in which it is located. */ - private fun getLastLineNumber(utExecution: UtExecution, classFqn: String): Int? { - val classFqnPath = classFqn.replace(".", "/") + private fun getLastLineNumberWithClassFqn( + method: ExecutableId, + utExecution: UtExecution, + defaultClassFqn: String + ): Pair { val coveredInstructions = utExecution.coverage?.coveredInstructions - val lastCoveredInstruction = coveredInstructions?.lastOrNull { it.className == classFqnPath } - ?: coveredInstructions?.lastOrNull() + val lastCoveredInstruction = coveredInstructions?.lastOrNull() if (lastCoveredInstruction != null) - return lastCoveredInstruction.lineNumber + return Pair( + lastCoveredInstruction.lineNumber, + lastCoveredInstruction.className.replace('/', '.') + ) // if for some reason we can't extract the last line from the coverage val lastPathElementLineNumber = try { @@ -345,7 +408,20 @@ class SarifReport( } catch (t: Throwable) { null } - return lastPathElementLineNumber + if (lastPathElementLineNumber != null) { + return Pair(lastPathElementLineNumber, defaultClassFqn) + } + + val methodDefinitionLine = getMethodDefinitionLineNumber(method) + return Pair(methodDefinitionLine ?: defaultLineNumber, defaultClassFqn) + } + + private fun getMethodDefinitionLineNumber(method: ExecutableId): Int? { + val sourceFile = sourceFinding.getSourceFile(method.classId.canonicalName) + val lineNumber = sourceFile?.readLines()?.indexOfFirst { line -> + line.contains(" ${method.name}(") // method definition + } + return if (lineNumber == null || lineNumber == -1) null else lineNumber + 1 // to one-based } private fun shouldProcessExecutionResult(result: UtExecutionResult): Boolean { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt index 35659403cc..5dbf400933 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt @@ -93,12 +93,11 @@ import org.utbot.intellij.plugin.util.RunConfigurationHelper import org.utbot.intellij.plugin.util.assertIsDispatchThread import org.utbot.intellij.plugin.util.assertIsWriteThread import org.utbot.intellij.plugin.util.extractClassMethodsIncludingNested -import org.utbot.sarif.Sarif -import org.utbot.sarif.SarifReport import java.nio.file.Path import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import org.utbot.sarif.* object CodeGenerationController { private val logger = KotlinLogging.logger {} @@ -219,7 +218,7 @@ object CodeGenerationController { return } UnitTestBotInspectionManager - .getInstance(model.project, srcClassPathToSarifReport) + .getInstance(model.project, SarifReport.minimizeSarifResults(srcClassPathToSarifReport)) .createNewGlobalContext() .doInspections(AnalysisScope(model.project)) } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionTool.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionTool.kt index 469d0527fb..ae7490a7ac 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionTool.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionTool.kt @@ -3,11 +3,11 @@ package org.utbot.intellij.plugin.inspection import com.intellij.codeInspection.* import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile -import org.utbot.sarif.Sarif -import org.utbot.sarif.SarifRegion -import org.utbot.sarif.SarifResult +import org.jetbrains.kotlin.idea.search.allScope +import org.utbot.sarif.* import java.nio.file.Path /** @@ -34,24 +34,35 @@ class UnitTestBotInspectionTool : GlobalSimpleInspectionTool() { override fun getGroupDisplayName() = "Errors detected by UnitTestBot" /** - * Appends all the errors from the SARIF report for [psiFile] to the [problemDescriptionsProcessor]. + * Appends all the errors from the SARIF report for [srcPsiFile] to the [problemDescriptionsProcessor]. */ override fun checkFile( - psiFile: PsiFile, + srcPsiFile: PsiFile, manager: InspectionManager, problemsHolder: ProblemsHolder, globalContext: GlobalInspectionContext, problemDescriptionsProcessor: ProblemDescriptionsProcessor ) { - val sarifReport = srcClassPathToSarifReport[psiFile.virtualFile.toNioPath()] + val sarifReport = srcClassPathToSarifReport[srcPsiFile.virtualFile.toNioPath()] ?: return // no results for this file for (sarifResult in sarifReport.getAllResults()) { - val srcFileLocation = sarifResult.locations.firstOrNull() - ?: continue - - val errorRegion = srcFileLocation.physicalLocation.region - val errorTextRange = getTextRange(problemsHolder.project, psiFile, errorRegion) + val srcFilePhysicalLocation = sarifResult.locations + .filterIsInstance(SarifPhysicalLocationWrapper::class.java) + .firstOrNull()?.physicalLocation ?: continue + val srcFileLogicalLocation = sarifResult.locations + .filterIsInstance(SarifLogicalLocationsWrapper::class.java) + .firstOrNull() + ?.logicalLocations?.firstOrNull() + + // srcPsiFile may != errorPsiFile (if srcFileLogicalLocation != null) + val errorPsiFile = srcFileLogicalLocation?.fullyQualifiedName?.let { errorClassFqn -> + val psiFacade = JavaPsiFacade.getInstance(srcPsiFile.project) + val psiClass = psiFacade.findClass(errorClassFqn, srcPsiFile.project.allScope()) + psiClass?.containingFile + } ?: srcPsiFile + val errorRegion = srcFilePhysicalLocation.region + val errorTextRange = getTextRange(problemsHolder.project, errorPsiFile, errorRegion) // see `org.utbot.sarif.SarifReport.processUncheckedException` for the message template val (exceptionMessage, testCaseMessage) = @@ -71,7 +82,7 @@ class UnitTestBotInspectionTool : GlobalSimpleInspectionTool() { val analyzeStackTraceFix = AnalyzeStackTraceFix(exceptionMessage, stackTraceLines) val problemDescriptor = problemsHolder.manager.createProblemDescriptor( - psiFile, + errorPsiFile, errorTextRange, sarifResultMessage, ProblemHighlightType.ERROR, @@ -80,7 +91,7 @@ class UnitTestBotInspectionTool : GlobalSimpleInspectionTool() { analyzeStackTraceFix ) problemDescriptionsProcessor.addProblemElement( - globalContext.refManager.getReference(psiFile), + globalContext.refManager.getReference(errorPsiFile), problemDescriptor ) } From 5728fb4f9faa536bbf8638d63cdef86911588570 Mon Sep 17 00:00:00 2001 From: Nikita Stroganov Date: Wed, 7 Dec 2022 10:29:54 +0300 Subject: [PATCH 2/3] Fix tests --- .../src/test/kotlin/org/utbot/sarif/SarifReportTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt index 6fa013b66f..29cb6a2b0d 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt @@ -73,7 +73,7 @@ class SarifReportTest { val report = sarifReportMain.createReport() val result = report.runs.first().results.first() - val location = result.locations.first().physicalLocation + val location = result.locations.filterIsInstance().first().physicalLocation val relatedLocation = result.relatedLocations.first().physicalLocation assert(location.artifactLocation.uri.contains("Main.java")) From d4c8cf046f633c42cfb7ead9dbb2800558186a2e Mon Sep 17 00:00:00 2001 From: Nikita Stroganov Date: Wed, 7 Dec 2022 11:00:32 +0300 Subject: [PATCH 3/3] Fix tests again --- .../kotlin/org/utbot/sarif/SarifReportTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt index 29cb6a2b0d..7473cdff94 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt @@ -44,6 +44,9 @@ class SarifReportTest { ) Mockito.`when`(mockUtExecutionAIOBE.stateBefore.parameters).thenReturn(listOf()) + defaultMockCoverage(mockUtExecutionNPE) + defaultMockCoverage(mockUtExecutionAIOBE) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecutionNPE)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecutionAIOBE)) @@ -68,6 +71,7 @@ class SarifReportTest { ) Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) Mockito.`when`(mockUtExecution.coverage?.coveredInstructions?.lastOrNull()?.lineNumber).thenReturn(1337) + Mockito.`when`(mockUtExecution.coverage?.coveredInstructions?.lastOrNull()?.className).thenReturn("Main") Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") val report = sarifReportMain.createReport() @@ -98,6 +102,8 @@ class SarifReportTest { ) ) + defaultMockCoverage(mockUtExecution) + val report = sarifReportMain.createReport() val result = report.runs.first().results.first() @@ -121,6 +127,8 @@ class SarifReportTest { ) Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) + defaultMockCoverage(mockUtExecution) + val report = sarifReportMain.createReport() val result = report.runs.first().results.first().codeFlows.first().threadFlows.first().locations.map { @@ -140,6 +148,8 @@ class SarifReportTest { Mockito.`when`(uncheckedException.stackTrace).thenReturn(arrayOf()) Mockito.`when`(mockUtExecution.result).thenReturn(UtSandboxFailure(uncheckedException)) + defaultMockCoverage(mockUtExecution) + val report = sarifReportMain.createReport() val result = report.runs.first().results.first() assert(result.message.text.contains("AccessControlException")) @@ -159,6 +169,8 @@ class SarifReportTest { Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") + defaultMockCoverage(mockUtExecution) + val report = sarifReportMain.createReport() val codeFlowPhysicalLocations = report.runs[0].results[0].codeFlows[0].threadFlows[0].locations.map { @@ -183,6 +195,8 @@ class SarifReportTest { Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") + defaultMockCoverage(mockUtExecution) + val report = sarifReportPrivateMain.createReport() val codeFlowPhysicalLocations = report.runs[0].results[0].codeFlows[0].threadFlows[0].locations.map { @@ -200,6 +214,8 @@ class SarifReportTest { val mockUtExecution = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS) Mockito.`when`(mockUtExecution.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false)) + defaultMockCoverage(mockUtExecution) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecution)) // duplicate @@ -225,6 +241,9 @@ class SarifReportTest { Mockito.`when`(mockUtExecution1.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false)) Mockito.`when`(mockUtExecution2.result).thenReturn(UtImplicitlyThrownException(ArithmeticException(), false)) + defaultMockCoverage(mockUtExecution1) + defaultMockCoverage(mockUtExecution2) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution1)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecution2)) // not a duplicate @@ -252,7 +271,9 @@ class SarifReportTest { // different locations Mockito.`when`(mockUtExecution1.coverage?.coveredInstructions?.lastOrNull()?.lineNumber).thenReturn(11) + Mockito.`when`(mockUtExecution1.coverage?.coveredInstructions?.lastOrNull()?.className).thenReturn("Main") Mockito.`when`(mockUtExecution2.coverage?.coveredInstructions?.lastOrNull()?.lineNumber).thenReturn(22) + Mockito.`when`(mockUtExecution2.coverage?.coveredInstructions?.lastOrNull()?.className).thenReturn("Main") val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution1)), @@ -288,6 +309,9 @@ class SarifReportTest { Mockito.`when`(mockNPE1.stackTrace).thenReturn(arrayOf(stackTraceElement1)) Mockito.`when`(mockNPE2.stackTrace).thenReturn(arrayOf(stackTraceElement1, stackTraceElement2)) + defaultMockCoverage(mockUtExecution1) + defaultMockCoverage(mockUtExecution2) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution1)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecution2)) // duplicate with a longer stack trace @@ -316,6 +340,11 @@ class SarifReportTest { Mockito.`when`(mockExecutableId.classId.name).thenReturn("Main") } + private fun defaultMockCoverage(mockExecution: UtExecution) { + Mockito.`when`(mockExecution.coverage?.coveredInstructions?.lastOrNull()?.lineNumber).thenReturn(1) + Mockito.`when`(mockExecution.coverage?.coveredInstructions?.lastOrNull()?.className).thenReturn("Main") + } + // constants private val sourceFindingEmpty = SourceFindingStrategyDefault(