Skip to content

Refactor TestCaseGenerator and CodeGenerator and their usages #508 #540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,15 @@ class BunchTestGeneratorCommand : GenerateTestsAbstractCommand(
val targetMethods = classUnderTest.targetMethods()
if (targetMethods.isEmpty()) return

initializeEngine(workingDirectory)
val testCaseGenerator = initializeGenerator(workingDirectory)

// utContext is used in `generate`, `generateTest`, `generateReport`
withUtContext(UtContext(classLoader)) {

val testClassName = "${classUnderTest.simpleName}Test"

val testSets = generateTestSets(
testCaseGenerator,
targetMethods,
searchDirectory = workingDirectory,
chosenClassesToMockAlways = (Mocker.defaultSuperClassesToMockAlwaysNames + classesToMockAlways)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,13 @@ abstract class GenerateTestsAbstractCommand(name: String, help: String) :
classLoader.loadClass(classFqn).kotlin

protected fun generateTestSets(
testCaseGenerator: TestCaseGenerator,
targetMethods: List<UtMethod<*>>,
sourceCodeFile: Path? = null,
searchDirectory: Path,
chosenClassesToMockAlways: Set<ClassId>
): List<UtMethodTestSet> =
TestCaseGenerator.generate(
testCaseGenerator.generate(
targetMethods,
mockStrategy,
chosenClassesToMockAlways,
Expand Down Expand Up @@ -190,30 +191,26 @@ abstract class GenerateTestsAbstractCommand(name: String, help: String) :
classUnderTest
).generateAsString(testSets, testClassname)

protected fun initializeEngine(workingDirectory: Path) {
protected fun initializeGenerator(workingDirectory: Path): TestCaseGenerator {
val classPathNormalized =
classLoader.urLs.joinToString(separator = File.pathSeparator) { it.toPath().absolutePath }

// TODO: SAT-1566
// Set UtSettings parameters.
// TODO: SAT-1566 Set UtSettings parameters.
UtSettings.treatOverflowAsError = treatOverflowAsError == TreatOverflowAsError.AS_ERROR

TestCaseGenerator.init(workingDirectory, classPathNormalized, System.getProperty("java.class.path"))
return TestCaseGenerator(workingDirectory, classPathNormalized, System.getProperty("java.class.path"))
}

private fun initializeCodeGenerator(testFramework: String, classUnderTest: KClass<*>): CodeGenerator {
val generateWarningsForStaticMocking =
forceStaticMocking == ForceStaticMocking.FORCE && staticsMocking is NoStaticMocking
return CodeGenerator().apply {
init(
testFramework = testFrameworkByName(testFramework),
classUnderTest = classUnderTest.java,
codegenLanguage = codegenLanguage,
staticsMocking = staticsMocking,
forceStaticMocking = forceStaticMocking,
generateWarningsForStaticMocking = generateWarningsForStaticMocking,
)
}
return CodeGenerator(
testFramework = testFrameworkByName(testFramework),
classUnderTest = classUnderTest.java,
codegenLanguage = codegenLanguage,
staticsMocking = staticsMocking,
forceStaticMocking = forceStaticMocking,
generateWarningsForStaticMocking = generateWarningsForStaticMocking,
)
}

protected fun KClass<*>.targetMethods() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class GenerateTestsCommand :
val classUnderTest: KClass<*> = loadClassBySpecifiedFqn(targetClassFqn)
val targetMethods = classUnderTest.targetMethods()
.filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { !isKnownSyntheticMethod(it) }
initializeEngine(workingDirectory)
val testCaseGenerator = initializeGenerator(workingDirectory)

if (targetMethods.isEmpty()) {
throw Exception("Nothing to process. No methods were provided")
Expand All @@ -107,6 +107,7 @@ class GenerateTestsCommand :
val testClassName = output?.toPath()?.toFile()?.nameWithoutExtension
?: "${classUnderTest.simpleName}Test"
val testSets = generateTestSets(
testCaseGenerator,
targetMethods,
Paths.get(sourceCodeFile),
searchDirectory = workingDirectory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ object UtBotJavaApi {
}

return withUtContext(utContext) {
val testGenerator = CodeGenerator().apply {
init(
val codeGenerator = CodeGenerator(
classUnderTest = classUnderTest,
testFramework = testFramework,
mockFramework = mockFramework,
Expand All @@ -93,12 +92,8 @@ object UtBotJavaApi {
generateWarningsForStaticMocking = generateWarningsForStaticMocking,
testClassPackageName = testClassPackageName
)
}

testGenerator.generateAsString(
testSets,
destinationClassName
)
codeGenerator.generateAsString(testSets, destinationClassName)
}
}

Expand All @@ -122,12 +117,8 @@ object UtBotJavaApi {
val testSets: MutableList<UtMethodTestSet> = mutableListOf()

testSets.addAll(withUtContext(utContext) {
TestCaseGenerator
.apply {
init(
FileUtil.isolateClassFiles(classUnderTest.kotlin).toPath(), classpath, dependencyClassPath
)
}
val buildPath = FileUtil.isolateClassFiles(classUnderTest.kotlin).toPath()
TestCaseGenerator(buildPath, classpath, dependencyClassPath)
.generate(
methodsForAutomaticGeneration.map {
toUtMethod(
Expand Down Expand Up @@ -189,12 +180,9 @@ object UtBotJavaApi {
}

return withUtContext(UtContext(classUnderTest.classLoader)) {
TestCaseGenerator
.apply {
init(
FileUtil.isolateClassFiles(classUnderTest.kotlin).toPath(), classpath, dependencyClassPath
)
}.generate(
val buildPath = FileUtil.isolateClassFiles(classUnderTest.kotlin).toPath()
TestCaseGenerator(buildPath, classpath, dependencyClassPath)
.generate(
methodsForAutomaticGeneration.map {
toUtMethod(
it.methodToBeTestedFromUserInput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,36 @@ import org.utbot.framework.plugin.api.UtMethod
import org.utbot.framework.plugin.api.UtMethodTestSet
import org.utbot.framework.plugin.api.util.id

class CodeGenerator {
private lateinit var context: CgContext

fun init(
classUnderTest: Class<*>,
params: MutableMap<UtMethod<*>, List<String>> = mutableMapOf(),
testFramework: TestFramework = TestFramework.defaultItem,
mockFramework: MockFramework? = MockFramework.defaultItem,
staticsMocking: StaticsMocking = StaticsMocking.defaultItem,
forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem,
generateWarningsForStaticMocking: Boolean = true,
codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem,
parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem,
runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem,
hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(),
enableTestsTimeout: Boolean = true,
testClassPackageName: String = classUnderTest.packageName,
) {
context = CgContext(
classUnderTest = classUnderTest.id,
// TODO: remove existingNames parameter completely
existingMethodNames = mutableSetOf(),
paramNames = params,
testFramework = testFramework,
mockFramework = mockFramework ?: MockFramework.MOCKITO,
codegenLanguage = codegenLanguage,
parameterizedTestSource = parameterizedTestSource,
staticsMocking = staticsMocking,
forceStaticMocking = forceStaticMocking,
generateWarningsForStaticMocking = generateWarningsForStaticMocking,
runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour,
hangingTestsTimeout = hangingTestsTimeout,
enableTestsTimeout = enableTestsTimeout,
testClassPackageName = testClassPackageName
)
}
class CodeGenerator(
private val classUnderTest: Class<*>,
params: MutableMap<UtMethod<*>, List<String>> = mutableMapOf(),
testFramework: TestFramework = TestFramework.defaultItem,
mockFramework: MockFramework? = MockFramework.defaultItem,
staticsMocking: StaticsMocking = StaticsMocking.defaultItem,
forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem,
generateWarningsForStaticMocking: Boolean = true,
codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem,
parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem,
runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem,
hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(),
enableTestsTimeout: Boolean = true,
testClassPackageName: String = classUnderTest.packageName,
) {
private var context: CgContext = CgContext(
classUnderTest = classUnderTest.id,
paramNames = params,
testFramework = testFramework,
mockFramework = mockFramework ?: MockFramework.MOCKITO,
codegenLanguage = codegenLanguage,
parameterizedTestSource = parameterizedTestSource,
staticsMocking = staticsMocking,
forceStaticMocking = forceStaticMocking,
generateWarningsForStaticMocking = generateWarningsForStaticMocking,
runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour,
hangingTestsTimeout = hangingTestsTimeout,
enableTestsTimeout = enableTestsTimeout,
testClassPackageName = testClassPackageName
)

//TODO: we support custom test class name only in utbot-online, probably support them in plugin as well
fun generateAsString(testSets: Collection<UtMethodTestSet>, testClassCustomName: String? = null): String =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package org.utbot.framework.plugin.api

import org.utbot.common.FileUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flattenConcat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import mu.KLogger
import mu.KotlinLogging
import org.utbot.common.bracket
import org.utbot.common.runBlockingWithCancellationPredicate
import org.utbot.common.runIgnoringCancellationException
import org.utbot.common.trace
import org.utbot.engine.EngineController
import org.utbot.engine.Mocker
import org.utbot.engine.UtBotSymbolicEngine
import org.utbot.framework.TestSelectionStrategyType
import org.utbot.framework.UtSettings
import org.utbot.framework.UtSettings.checkSolverTimeoutMillis
Expand Down Expand Up @@ -34,85 +47,58 @@ import java.util.*
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.min
import kotlin.reflect.KCallable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flattenConcat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import mu.KotlinLogging
import org.utbot.engine.UtBotSymbolicEngine

object TestCaseGenerator {

private val logger = KotlinLogging.logger {}
private val timeoutLogger = KotlinLogging.logger(logger.name + ".timeout")
/**
* Generates test cases: one by one or a whole set for the method under test.
*
* Note: the instantiating of [TestCaseGenerator] may take some time,
* because it requires initializing Soot for the current [buildDir] and [classpath].
*/
open class TestCaseGenerator(
private val buildDir: Path,
private val classpath: String?,
private val dependencyPaths: String,
val engineActions: MutableList<(UtBotSymbolicEngine) -> Unit> = mutableListOf(),
val isCanceled: () -> Boolean = { false },
) {
private val logger: KLogger = KotlinLogging.logger {}
private val timeoutLogger: KLogger = KotlinLogging.logger(logger.name + ".timeout")

lateinit var engineActions: MutableList<(UtBotSymbolicEngine) -> Unit>
lateinit var isCanceled: () -> Boolean

//properties to save time on soot initialization
private var previousBuildDir: Path? = null
private var previousClasspath: String? = null
private var previousTimestamp: Long? = null
private var dependencyPaths: String = ""

fun init(
buildDir: Path,
classpath: String?,
dependencyPaths: String,
engineActions: MutableList<(UtBotSymbolicEngine) -> Unit> = mutableListOf(),
isCanceled: () -> Boolean = { false },
) {
this.isCanceled = isCanceled
this.engineActions = engineActions
if (isCanceled()) return
private val classpathForEngine: String
get() = buildDir.toString() + (classpath?.let { File.pathSeparator + it } ?: "")

checkFrameworkDependencies(dependencyPaths)
init {
if (!isCanceled()) {
checkFrameworkDependencies(dependencyPaths)

logger.trace("Initializing ${this.javaClass.name} with buildDir = $buildDir, classpath = $classpath")
logger.trace("Initializing ${this.javaClass.name} with buildDir = $buildDir, classpath = $classpath")

//optimization: maxLastModifiedRecursivelyMillis can take time
val timestamp = if (UtSettings.classfilesCanChange) maxLastModifiedRecursivelyMillis(buildDir, classpath) else 0
if (buildDir == previousBuildDir && classpath == previousClasspath && timestamp == previousTimestamp) {
logger.info { "Ignoring soot initialization because parameters are the same as on previous initialization" }
return
}

if (disableCoroutinesDebug) {
System.setProperty(kotlinx.coroutines.DEBUG_PROPERTY_NAME, kotlinx.coroutines.DEBUG_PROPERTY_VALUE_OFF)
}
if (disableCoroutinesDebug) {
System.setProperty(kotlinx.coroutines.DEBUG_PROPERTY_NAME, kotlinx.coroutines.DEBUG_PROPERTY_VALUE_OFF)
}

timeoutLogger.trace().bracket("Soot initialization") {
runSoot(buildDir, classpath)
}
timeoutLogger.trace().bracket("Soot initialization") {
runSoot(buildDir, classpath)
}

previousBuildDir = buildDir
previousClasspath = classpath
previousTimestamp = timestamp
this.dependencyPaths = dependencyPaths

//warmup
if (warmupConcreteExecution) {
ConcreteExecutor(
UtExecutionInstrumentation,
classpathForEngine,
dependencyPaths
).apply {
classLoader = utContext.classLoader
withUtContext(UtContext(Warmup::class.java.classLoader)) {
runBlocking {
constructExecutionsForWarmup().forEach { (method, data) ->
executeAsync(method, emptyArray(), data)
//warmup
if (warmupConcreteExecution) {
ConcreteExecutor(
UtExecutionInstrumentation,
classpathForEngine,
dependencyPaths
).apply {
classLoader = utContext.classLoader
withUtContext(UtContext(Warmup::class.java.classLoader)) {
runBlocking {
constructExecutionsForWarmup().forEach { (method, data) ->
executeAsync(method, emptyArray(), data)
}
}
}
warmup()
}
warmup()
}
}
}
Expand Down Expand Up @@ -254,18 +240,6 @@ object TestCaseGenerator {
)
}

private val classpathForEngine: String
get() = previousBuildDir!!.toString() + (previousClasspath?.let { File.pathSeparator + it } ?: "")

private fun maxLastModifiedRecursivelyMillis(buildDir: Path, classpath: String?): Long {
val paths = mutableListOf<File>()
paths += buildDir.toFile()
if (classpath != null) {
paths += classpath.split(File.pathSeparatorChar).map { File(it) }
}
return FileUtil.maxLastModifiedRecursivelyMillis(paths)
}

private fun createSymbolicEngine(
controller: EngineController,
method: UtMethod<*>,
Expand Down
Loading