Skip to content

Commit b7305ed

Browse files
authored
The first version of the utbot-maven plugin (#150)
* #149 The first version of the utbot-maven plugin * #149 Add doLast call * #149 Add dependencies to fix CI * #149 Make install task depends on the plugin descriptor generation * #63 Introduce a facade for createSarifReport task * #63 Move files from utbot-gradle to utbot-framework module * #63 Add comments * #63 Add utbot-maven.md * #63 Add comments to build.gradle * #149 Use file separator
1 parent 5f56eb1 commit b7305ed

File tree

18 files changed

+1064
-265
lines changed

18 files changed

+1064
-265
lines changed

gradle.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ testng_version=7.4.0
3030
mockito_inline_version=4.0.0
3131
jackson_version = 2.12.3
3232
javasmt_solver_z3_version=4.8.9-sosy1
33+
slf4j_version=1.7.36
34+
eclipse_aether_version=1.1.0
35+
maven_wagon_version=3.5.1
36+
maven_plugin_api_version=3.8.5
37+
maven_plugin_tools_version=3.6.4
3338
# soot also depends on asm, so there could be two different versions

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ include 'utbot-instrumentation-tests'
1515

1616
include 'utbot-summary'
1717
include 'utbot-gradle'
18+
include 'utbot-maven'
1819

utbot-framework/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies {
1515
fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration:'instrumentationArchive')
1616

1717
api project(':utbot-core')
18+
api project(':utbot-summary')
1819
implementation 'junit:junit:4.13.1'
1920
api project(':utbot-framework-api')
2021

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package org.utbot.framework.plugin.sarif
2+
3+
import org.utbot.framework.codegen.ForceStaticMocking
4+
import org.utbot.framework.codegen.NoStaticMocking
5+
import org.utbot.framework.codegen.model.ModelBasedTestCodeGenerator
6+
import org.utbot.framework.plugin.api.UtBotTestCaseGenerator
7+
import org.utbot.framework.plugin.api.UtTestCase
8+
import org.utbot.sarif.SarifReport
9+
import org.utbot.sarif.SourceFindingStrategy
10+
import org.utbot.summary.summarize
11+
import java.io.File
12+
import java.net.URLClassLoader
13+
import java.nio.file.Path
14+
15+
/**
16+
* Facade for `createSarifReport` task/mojo.
17+
* Stores common logic between gradle and maven plugins.
18+
*/
19+
class CreateSarifReportFacade(
20+
val sarifProperties: SarifExtensionProvider,
21+
val sourceFindingStrategy: SourceFindingStrategy
22+
) {
23+
24+
/**
25+
* Generates tests and a SARIF report for the class [targetClass].
26+
* Requires withUtContext() { ... }.
27+
*/
28+
fun generateForClass(
29+
targetClass: TargetClassWrapper,
30+
workingDirectory: Path,
31+
runtimeClasspath: String
32+
) {
33+
initializeEngine(runtimeClasspath, workingDirectory)
34+
35+
val testCases = generateTestCases(targetClass, workingDirectory)
36+
val testClassBody = generateTestCode(targetClass, testCases)
37+
targetClass.testsCodeFile.writeText(testClassBody)
38+
39+
generateReport(targetClass, testCases, testClassBody, sourceFindingStrategy)
40+
}
41+
42+
companion object {
43+
/**
44+
* Merges all [sarifReports] into one large [mergedSarifReportFile] containing all the information.
45+
* Prints a message about where the SARIF file is saved if [verbose] is true.
46+
*/
47+
fun mergeReports(
48+
sarifReports: List<String>,
49+
mergedSarifReportFile: File,
50+
verbose: Boolean = true
51+
) {
52+
val mergedReport = SarifReport.mergeReports(sarifReports)
53+
mergedSarifReportFile.writeText(mergedReport)
54+
if (verbose) {
55+
println("SARIF report was saved to \"${mergedSarifReportFile.path}\"")
56+
println("You can open it using the VS Code extension \"Sarif Viewer\"")
57+
}
58+
}
59+
}
60+
61+
// internal
62+
63+
private val dependencyPaths by lazy {
64+
val thisClassLoader = this::class.java.classLoader as URLClassLoader
65+
thisClassLoader.urLs.joinToString(File.pathSeparator) { it.path }
66+
}
67+
68+
private fun initializeEngine(classPath: String, workingDirectory: Path) {
69+
UtBotTestCaseGenerator.init(workingDirectory, classPath, dependencyPaths) { false }
70+
}
71+
72+
private fun generateTestCases(targetClass: TargetClassWrapper, workingDirectory: Path): List<UtTestCase> =
73+
UtBotTestCaseGenerator.generateForSeveralMethods(
74+
targetClass.targetMethods(),
75+
sarifProperties.mockStrategy,
76+
sarifProperties.classesToMockAlways,
77+
sarifProperties.generationTimeout
78+
).map {
79+
it.summarize(targetClass.sourceCodeFile, workingDirectory)
80+
}
81+
82+
private fun generateTestCode(targetClass: TargetClassWrapper, testCases: List<UtTestCase>): String =
83+
initializeCodeGenerator(targetClass)
84+
.generateAsString(testCases, targetClass.testsCodeFile.nameWithoutExtension)
85+
86+
private fun initializeCodeGenerator(targetClass: TargetClassWrapper) =
87+
ModelBasedTestCodeGenerator().apply {
88+
val isNoStaticMocking = sarifProperties.staticsMocking is NoStaticMocking
89+
val isForceStaticMocking = sarifProperties.forceStaticMocking == ForceStaticMocking.FORCE
90+
init(
91+
classUnderTest = targetClass.classUnderTest.java,
92+
testFramework = sarifProperties.testFramework,
93+
mockFramework = sarifProperties.mockFramework,
94+
staticsMocking = sarifProperties.staticsMocking,
95+
forceStaticMocking = sarifProperties.forceStaticMocking,
96+
generateWarningsForStaticMocking = isNoStaticMocking && isForceStaticMocking,
97+
codegenLanguage = sarifProperties.codegenLanguage
98+
)
99+
}
100+
101+
/**
102+
* Creates a SARIF report for the class [targetClass].
103+
* Saves the report to the file specified in [targetClass].
104+
*/
105+
private fun generateReport(
106+
targetClass: TargetClassWrapper,
107+
testCases: List<UtTestCase>,
108+
testClassBody: String,
109+
sourceFinding: SourceFindingStrategy
110+
) {
111+
val sarifReport = SarifReport(testCases, testClassBody, sourceFinding).createReport()
112+
targetClass.sarifReportFile.writeText(sarifReport)
113+
}
114+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.utbot.framework.plugin.sarif
2+
3+
import org.utbot.engine.Mocker
4+
import org.utbot.framework.codegen.*
5+
import org.utbot.framework.plugin.api.ClassId
6+
import org.utbot.framework.plugin.api.CodegenLanguage
7+
import org.utbot.framework.plugin.api.MockFramework
8+
import org.utbot.framework.plugin.api.MockStrategyApi
9+
import java.io.File
10+
11+
/**
12+
* Provides fields needed to create a SARIF report.
13+
* Defines transform function for these fields.
14+
*/
15+
interface SarifExtensionProvider {
16+
17+
/**
18+
* Classes for which the SARIF report will be created.
19+
*/
20+
val targetClasses: List<String>
21+
22+
/**
23+
* Absolute path to the root of the relative paths in the SARIF report.
24+
*/
25+
val projectRoot: File
26+
27+
/**
28+
* Relative path to the root of the generated tests.
29+
*/
30+
val generatedTestsRelativeRoot: String
31+
32+
/**
33+
* Relative path to the root of the SARIF reports.
34+
*/
35+
val sarifReportsRelativeRoot: String
36+
37+
/**
38+
* Mark the directory with generated tests as `test sources root` or not.
39+
*/
40+
val markGeneratedTestsDirectoryAsTestSourcesRoot: Boolean
41+
42+
val testFramework: TestFramework
43+
44+
val mockFramework: MockFramework
45+
46+
/**
47+
* Maximum tests generation time for one class (in milliseconds).
48+
*/
49+
val generationTimeout: Long
50+
51+
val codegenLanguage: CodegenLanguage
52+
53+
val mockStrategy: MockStrategyApi
54+
55+
val staticsMocking: StaticsMocking
56+
57+
val forceStaticMocking: ForceStaticMocking
58+
59+
/**
60+
* Classes to force mocking theirs static methods and constructors.
61+
* Contains user-specified classes and `Mocker.defaultSuperClassesToMockAlwaysNames`.
62+
*/
63+
val classesToMockAlways: Set<ClassId>
64+
65+
// transform functions
66+
67+
fun testFrameworkParse(testFramework: String): TestFramework =
68+
when (testFramework.toLowerCase()) {
69+
"junit4" -> Junit4
70+
"junit5" -> Junit5
71+
"testng" -> TestNg
72+
else -> error("Parameter testFramework == '$testFramework', but it can take only 'junit4', 'junit5' or 'testng'")
73+
}
74+
75+
fun mockFrameworkParse(mockFramework: String): MockFramework =
76+
when (mockFramework.toLowerCase()) {
77+
"mockito" -> MockFramework.MOCKITO
78+
else -> error("Parameter mockFramework == '$mockFramework', but it can take only 'mockito'")
79+
}
80+
81+
fun generationTimeoutParse(generationTimeout: Long): Long {
82+
if (generationTimeout < 0)
83+
error("Parameter generationTimeout == $generationTimeout, but it should be non-negative")
84+
return generationTimeout
85+
}
86+
87+
fun codegenLanguageParse(codegenLanguage: String): CodegenLanguage =
88+
when (codegenLanguage.toLowerCase()) {
89+
"java" -> CodegenLanguage.JAVA
90+
"kotlin" -> CodegenLanguage.KOTLIN
91+
else -> error("Parameter codegenLanguage == '$codegenLanguage', but it can take only 'java' or 'kotlin'")
92+
}
93+
94+
fun mockStrategyParse(mockStrategy: String): MockStrategyApi =
95+
when (mockStrategy.toLowerCase()) {
96+
"do-not-mock" -> MockStrategyApi.NO_MOCKS
97+
"package-based" -> MockStrategyApi.OTHER_PACKAGES
98+
"all-except-cut" -> MockStrategyApi.OTHER_CLASSES
99+
else -> error("Parameter mockStrategy == '$mockStrategy', but it can take only 'do-not-mock', 'package-based' or 'all-except-cut'")
100+
}
101+
102+
fun staticsMockingParse(staticsMocking: String): StaticsMocking =
103+
when (staticsMocking.toLowerCase()) {
104+
"do-not-mock-statics" -> NoStaticMocking
105+
"mock-statics" -> MockitoStaticMocking
106+
else -> error("Parameter staticsMocking == '$staticsMocking', but it can take only 'do-not-mock-statics' or 'mock-statics'")
107+
}
108+
109+
fun forceStaticMockingParse(forceStaticMocking: String): ForceStaticMocking =
110+
when (forceStaticMocking.toLowerCase()) {
111+
"force" -> ForceStaticMocking.FORCE
112+
"do-not-force" -> ForceStaticMocking.DO_NOT_FORCE
113+
else -> error("Parameter forceStaticMocking == '$forceStaticMocking', but it can take only 'force' or 'do-not-force'")
114+
}
115+
116+
fun classesToMockAlwaysParse(specifiedClasses: List<String>): Set<ClassId> =
117+
(Mocker.defaultSuperClassesToMockAlwaysNames + specifiedClasses).map { className ->
118+
ClassId(className)
119+
}.toSet()
120+
}

utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/TargetClassWrapper.kt renamed to utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/TargetClassWrapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.utbot.gradle.plugin.wrappers
1+
package org.utbot.framework.plugin.sarif
22

33
import org.utbot.framework.plugin.api.UtMethod
44
import java.io.File
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.utbot.framework.plugin.sarif.util
2+
3+
import org.utbot.common.PathUtil
4+
import org.utbot.common.loadClassesFromDirectory
5+
import org.utbot.common.tryLoadClass
6+
import org.utbot.framework.plugin.api.util.UtContext
7+
import org.utbot.framework.plugin.api.util.withUtContext
8+
import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter
9+
import java.io.File
10+
11+
object ClassUtil {
12+
13+
/**
14+
* Finds all classes loaded by the [classLoader] and whose class files
15+
* are located in the [classesDirectory]. Returns the names of the found classes.
16+
* Does not return classes without declared methods or without a canonicalName.
17+
*/
18+
fun findAllDeclaredClasses(
19+
classLoader: ClassLoader,
20+
classesDirectory: File
21+
): List<String> =
22+
classLoader
23+
.loadClassesFromDirectory(classesDirectory)
24+
.filter { clazz ->
25+
clazz.canonicalName != null && clazz.declaredMethods.isNotEmpty()
26+
}
27+
.map { clazz ->
28+
clazz.canonicalName
29+
}
30+
31+
/**
32+
* Finds the source code file in the [sourceCodeFiles] by the given [classFqn].
33+
* Tries to find the file by the information available to [classLoader]
34+
* if the [classFqn] is not found in the [sourceCodeFiles].
35+
*/
36+
fun findSourceCodeFile(
37+
classFqn: String,
38+
sourceCodeFiles: List<File>,
39+
classLoader: ClassLoader
40+
): File? =
41+
sourceCodeFiles.firstOrNull { sourceCodeFile ->
42+
val relativePath = "${PathUtil.classFqnToPath(classFqn)}.${sourceCodeFile.extension}"
43+
sourceCodeFile.endsWith(File(relativePath))
44+
} ?: findSourceCodeFileByClass(classFqn, sourceCodeFiles, classLoader)
45+
46+
// internal
47+
48+
/**
49+
* Fallback logic: called after a failure of [findSourceCodeFile].
50+
*/
51+
private fun findSourceCodeFileByClass(
52+
classFqn: String,
53+
sourceCodeFiles: List<File>,
54+
classLoader: ClassLoader
55+
): File? {
56+
val clazz = classLoader.tryLoadClass(classFqn)
57+
?: return null
58+
val sourceFileName = withUtContext(UtContext(classLoader)) {
59+
Instrumenter.computeSourceFileName(clazz) // finds the file name in bytecode
60+
} ?: return null
61+
val candidates = sourceCodeFiles.filter { sourceCodeFile ->
62+
sourceCodeFile.endsWith(File(sourceFileName))
63+
}
64+
return if (candidates.size == 1)
65+
candidates.first()
66+
else // we can't decide which file is needed
67+
null
68+
}
69+
}

utbot-gradle/build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle"
66

77
dependencies {
88
api project(":utbot-framework")
9-
api project(':utbot-summary')
109

1110
implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version
1211
}

0 commit comments

Comments
 (0)