diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt index bf6fd56194..5947ec8674 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt @@ -6,11 +6,16 @@ import org.utbot.framework.codegen.ParametrizedTestSource import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour import org.utbot.framework.codegen.StaticsMocking import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.model.constructor.builtin.UtilClassFileMethodProvider import org.utbot.framework.codegen.model.constructor.CgMethodTestSet import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.constructor.tree.CgUtilClassConstructor import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport -import org.utbot.framework.codegen.model.tree.CgTestClassFile +import org.utbot.framework.codegen.model.tree.AbstractCgClassFile +import org.utbot.framework.codegen.model.tree.CgRegularClassFile import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage @@ -18,12 +23,15 @@ import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.MockFramework import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.codegen.model.tree.CgComment +import org.utbot.framework.codegen.model.tree.CgSingleLineComment class CodeGenerator( private val classUnderTest: ClassId, paramNames: MutableMap> = mutableMapOf(), + generateUtilClassFile: Boolean = false, testFramework: TestFramework = TestFramework.defaultItem, - mockFramework: MockFramework? = MockFramework.defaultItem, + mockFramework: MockFramework = MockFramework.defaultItem, staticsMocking: StaticsMocking = StaticsMocking.defaultItem, forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, generateWarningsForStaticMocking: Boolean = true, @@ -36,9 +44,10 @@ class CodeGenerator( ) { private var context: CgContext = CgContext( classUnderTest = classUnderTest, + generateUtilClassFile = generateUtilClassFile, paramNames = paramNames, testFramework = testFramework, - mockFramework = mockFramework ?: MockFramework.MOCKITO, + mockFramework = mockFramework, codegenLanguage = codegenLanguage, parametrizedTestSource = parameterizedTestSource, staticsMocking = staticsMocking, @@ -58,7 +67,7 @@ class CodeGenerator( fun generateAsStringWithTestReport( testSets: Collection, testClassCustomName: String? = null, - ): TestsCodeWithTestReport { + ): CodeGeneratorResult { val cgTestSets = testSets.map { CgMethodTestSet(it) }.toList() return generateAsStringWithTestReport(cgTestSets, testClassCustomName) } @@ -66,11 +75,15 @@ class CodeGenerator( private fun generateAsStringWithTestReport( cgTestSets: List, testClassCustomName: String? = null, - ): TestsCodeWithTestReport = withCustomContext(testClassCustomName) { + ): CodeGeneratorResult = withCustomContext(testClassCustomName) { context.withTestClassFileScope { val testClassModel = TestClassModel.fromTestSets(classUnderTest, cgTestSets) val testClassFile = CgTestClassConstructor(context).construct(testClassModel) - TestsCodeWithTestReport(renderClassFile(testClassFile), testClassFile.testsGenerationReport) + CodeGeneratorResult( + generatedCode = renderClassFile(testClassFile), + utilClassKind = UtilClassKind.fromCgContextOrNull(context), + testsGenerationReport = testClassFile.testsGenerationReport + ) } } @@ -92,12 +105,153 @@ class CodeGenerator( } } - private fun renderClassFile(file: CgTestClassFile): String { + private fun renderClassFile(file: AbstractCgClassFile<*>): String { val renderer = CgAbstractRenderer.makeRenderer(context) file.accept(renderer) return renderer.toString() } } -data class TestsCodeWithTestReport(val generatedCode: String, val testsGenerationReport: TestsGenerationReport) +/** + * @property generatedCode the source code of the test class + * @property utilClassKind the kind of util class if it is required, otherwise - null + * @property testsGenerationReport some info about test generation process + */ +data class CodeGeneratorResult( + val generatedCode: String, + // null if no util class needed, e.g. when we are generating utils directly into test class + val utilClassKind: UtilClassKind?, + val testsGenerationReport: TestsGenerationReport, +) + +/** + * A kind of util class. See the description of each kind at their respective classes. + * @property utilMethodProvider a [UtilClassFileMethodProvider] containing information about + * utilities that come from a separately generated UtUtils class + * (as opposed to utils that are declared directly in the test class, for example). + * @property mockFrameworkUsed a flag indicating if a mock framework was used. + * For detailed description see [CgContextOwner.mockFrameworkUsed]. + * @property mockFramework a framework used to create mocks + * @property priority when we generate multiple test classes, they can require different [UtilClassKind]. + * We will generate an util class corresponding to the kind with the greatest priority. + * For example, one test class may not use mocks, but the other one does. + * Then we will generate an util class with mocks, because it has a greater priority (see [UtUtilsWithMockito]). + */ +sealed class UtilClassKind( + internal val utilMethodProvider: UtilClassFileMethodProvider, + internal val mockFrameworkUsed: Boolean, + internal val mockFramework: MockFramework = MockFramework.MOCKITO, + private val priority: Int +) : Comparable { + + /** + * The version of util class being generated. + * For more details see [UtilClassFileMethodProvider.UTIL_CLASS_VERSION]. + */ + val utilClassVersion: String + get() = UtilClassFileMethodProvider.UTIL_CLASS_VERSION + + /** + * The comment specifying the version of util class being generated. + * + * @see UtilClassFileMethodProvider.UTIL_CLASS_VERSION + */ + val utilClassVersionComment: CgComment + get() = CgSingleLineComment("$UTIL_CLASS_VERSION_COMMENT_PREFIX${utilClassVersion}") + + + /** + * The comment specifying the kind of util class being generated. + * + * @see utilClassKindCommentText + */ + val utilClassKindComment: CgComment + get() = CgSingleLineComment(utilClassKindCommentText) + + /** + * The text of comment specifying the kind of util class. + * At the moment, there are two kinds: [RegularUtUtils] (without Mockito) and [UtUtilsWithMockito]. + * + * This comment is needed when the plugin decides whether to overwrite an existing util class or not. + * When making that decision, it is important to determine if the existing class uses mocks or not, + * and this comment will help do that. + */ + abstract val utilClassKindCommentText: String + /** + * A kind of regular UtUtils class. "Regular" here means that this class does not use a mock framework. + */ + object RegularUtUtils : UtilClassKind(UtilClassFileMethodProvider, mockFrameworkUsed = false, priority = 0) { + override val utilClassKindCommentText: String + get() = "This is a regular UtUtils class (without mock framework usage)" + } + + /** + * A kind of UtUtils class that uses a mock framework. At the moment the framework is Mockito. + */ + object UtUtilsWithMockito : UtilClassKind(UtilClassFileMethodProvider, mockFrameworkUsed = true, priority = 1) { + override val utilClassKindCommentText: String + get() = "This is UtUtils class with Mockito support" + } + + override fun compareTo(other: UtilClassKind): Int { + return priority.compareTo(other.priority) + } + + /** + * Construct an util class file as a [CgRegularClassFile] and render it. + * @return the text of the generated util class file. + */ + fun getUtilClassText(codegenLanguage: CodegenLanguage): String { + val utilClassFile = CgUtilClassConstructor.constructUtilsClassFile(this) + val renderer = CgAbstractRenderer.makeRenderer(this, codegenLanguage) + utilClassFile.accept(renderer) + return renderer.toString() + } + + companion object { + + /** + * Class UtUtils will contain a comment specifying the version of this util class + * (if we ever change util methods, then util class will be different, hence the update of its version). + * This is a prefix that will go before the version in the comment. + */ + const val UTIL_CLASS_VERSION_COMMENT_PREFIX = "UtUtils class version: " + + fun utilClassKindByCommentOrNull(comment: String): UtilClassKind? { + return when (comment) { + RegularUtUtils.utilClassKindCommentText -> RegularUtUtils + UtUtilsWithMockito.utilClassKindCommentText -> UtUtilsWithMockito + else -> null + } + } + + /** + * Check if an util class is required, and if so, what kind. + * @return `null` if [CgContext.utilMethodProvider] is not [UtilClassFileMethodProvider], + * because it means that util methods will be taken from some other provider (e.g. [TestClassUtilMethodProvider]). + */ + internal fun fromCgContextOrNull(context: CgContext): UtilClassKind? { + if (context.requiredUtilMethods.isEmpty()) return null + if (!context.mockFrameworkUsed) { + return RegularUtUtils + } + return when (context.mockFramework) { + MockFramework.MOCKITO -> UtUtilsWithMockito + // in case we will add any other mock frameworks, newer Kotlin compiler versions + // will report a non-exhaustive 'when', so we will not forget to support them here as well + } + } + + const val UT_UTILS_PACKAGE_NAME = "org.utbot.runtime.utils" + const val UT_UTILS_CLASS_NAME = "UtUtils" + const val PACKAGE_DELIMITER = "." + + /** + * List of package names of UtUtils class. + * See whole package name at [UT_UTILS_PACKAGE_NAME]. + */ + val utilsPackages: List + get() = UT_UTILS_PACKAGE_NAME.split(PACKAGE_DELIMITER) + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt index 9169e42c44..4afe1d8cc8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt @@ -3,6 +3,7 @@ package org.utbot.framework.codegen.model.constructor.builtin import org.utbot.framework.codegen.MockitoStaticMocking import org.utbot.framework.codegen.model.constructor.util.utilMethodId import org.utbot.framework.codegen.model.tree.CgClassId +import org.utbot.framework.codegen.model.visitor.utilMethodTextById import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.MethodId @@ -16,134 +17,169 @@ import org.utbot.framework.plugin.api.util.voidClassId import sun.misc.Unsafe /** - * Set of ids of all possible util methods for a given class + * Set of ids of all possible util methods for a given class. + * * The class may actually not have some of these methods if they - * are not required in the process of code generation + * are not required in the process of code generation (this is the case for [TestClassUtilMethodProvider]). */ -internal val ClassId.possibleUtilMethodIds: Set - get() = setOf( - getUnsafeInstanceMethodId, - createInstanceMethodId, - createArrayMethodId, - setFieldMethodId, - setStaticFieldMethodId, - getFieldValueMethodId, - getStaticFieldValueMethodId, - getEnumConstantByNameMethodId, - deepEqualsMethodId, - arraysDeepEqualsMethodId, - iterablesDeepEqualsMethodId, - streamsDeepEqualsMethodId, - mapsDeepEqualsMethodId, - hasCustomEqualsMethodId, - getArrayLengthMethodId - ) - -internal val ClassId.getUnsafeInstanceMethodId: MethodId - get() = utilMethodId( +internal abstract class UtilMethodProvider(val utilClassId: ClassId) { + val utilMethodIds: Set + get() = setOf( + getUnsafeInstanceMethodId, + createInstanceMethodId, + createArrayMethodId, + setFieldMethodId, + setStaticFieldMethodId, + getFieldValueMethodId, + getStaticFieldValueMethodId, + getEnumConstantByNameMethodId, + deepEqualsMethodId, + arraysDeepEqualsMethodId, + iterablesDeepEqualsMethodId, + streamsDeepEqualsMethodId, + mapsDeepEqualsMethodId, + hasCustomEqualsMethodId, + getArrayLengthMethodId + ) + + val getUnsafeInstanceMethodId: MethodId + get() = utilClassId.utilMethodId( name = "getUnsafeInstance", returnType = Unsafe::class.id, - ) + ) -/** - * Method that creates instance using Unsafe - */ -internal val ClassId.createInstanceMethodId: MethodId - get() = utilMethodId( + /** + * Method that creates instance using Unsafe + */ + val createInstanceMethodId: MethodId + get() = utilClassId.utilMethodId( name = "createInstance", returnType = CgClassId(objectClassId, isNullable = true), arguments = arrayOf(stringClassId) - ) + ) -internal val ClassId.createArrayMethodId: MethodId - get() = utilMethodId( + val createArrayMethodId: MethodId + get() = utilClassId.utilMethodId( name = "createArray", returnType = Array::class.id, arguments = arrayOf(stringClassId, intClassId, Array::class.id) - ) + ) -internal val ClassId.setFieldMethodId: MethodId - get() = utilMethodId( + val setFieldMethodId: MethodId + get() = utilClassId.utilMethodId( name = "setField", returnType = voidClassId, arguments = arrayOf(objectClassId, stringClassId, objectClassId) - ) + ) -internal val ClassId.setStaticFieldMethodId: MethodId - get() = utilMethodId( + val setStaticFieldMethodId: MethodId + get() = utilClassId.utilMethodId( name = "setStaticField", returnType = voidClassId, arguments = arrayOf(Class::class.id, stringClassId, objectClassId) - ) + ) -internal val ClassId.getFieldValueMethodId: MethodId - get() = utilMethodId( + val getFieldValueMethodId: MethodId + get() = utilClassId.utilMethodId( name = "getFieldValue", returnType = objectClassId, arguments = arrayOf(objectClassId, stringClassId) - ) + ) -internal val ClassId.getStaticFieldValueMethodId: MethodId - get() = utilMethodId( + val getStaticFieldValueMethodId: MethodId + get() = utilClassId.utilMethodId( name = "getStaticFieldValue", returnType = objectClassId, arguments = arrayOf(Class::class.id, stringClassId) - ) + ) -internal val ClassId.getEnumConstantByNameMethodId: MethodId - get() = utilMethodId( + val getEnumConstantByNameMethodId: MethodId + get() = utilClassId.utilMethodId( name = "getEnumConstantByName", returnType = objectClassId, arguments = arrayOf(Class::class.id, stringClassId) - ) - -internal val ClassId.deepEqualsMethodId: MethodId - get() = utilMethodId( - name = "deepEquals", - returnType = booleanClassId, - arguments = arrayOf(objectClassId, objectClassId) - ) - -internal val ClassId.arraysDeepEqualsMethodId: MethodId - get() = utilMethodId( - name = "arraysDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(objectClassId, objectClassId) - ) - -internal val ClassId.iterablesDeepEqualsMethodId: MethodId - get() = utilMethodId( - name = "iterablesDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(java.lang.Iterable::class.id, java.lang.Iterable::class.id) - ) - -internal val ClassId.streamsDeepEqualsMethodId: MethodId - get() = utilMethodId( - name = "streamsDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(java.util.stream.Stream::class.id, java.util.stream.Stream::class.id) - ) + ) + + val deepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "deepEquals", + returnType = booleanClassId, + arguments = arrayOf(objectClassId, objectClassId) + ) + + val arraysDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "arraysDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(objectClassId, objectClassId) + ) + + val iterablesDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "iterablesDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(java.lang.Iterable::class.id, java.lang.Iterable::class.id) + ) + + val streamsDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "streamsDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(java.util.stream.Stream::class.id, java.util.stream.Stream::class.id) + ) + + val mapsDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "mapsDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(java.util.Map::class.id, java.util.Map::class.id) + ) + + val hasCustomEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "hasCustomEquals", + returnType = booleanClassId, + arguments = arrayOf(Class::class.id) + ) + + val getArrayLengthMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getArrayLength", + returnType = intClassId, + arguments = arrayOf(objectClassId) + ) +} -internal val ClassId.mapsDeepEqualsMethodId: MethodId - get() = utilMethodId( - name = "mapsDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(java.util.Map::class.id, java.util.Map::class.id) - ) - -internal val ClassId.hasCustomEqualsMethodId: MethodId - get() = utilMethodId( - name = "hasCustomEquals", - returnType = booleanClassId, - arguments = arrayOf(Class::class.id) - ) - -internal val ClassId.getArrayLengthMethodId: MethodId - get() = utilMethodId( - name = "getArrayLength", - returnType = intClassId, - arguments = arrayOf(objectClassId) +/** + * This provider represents an util class file that is generated and put into the user's test module. + * The generated class is UtUtils (its id is defined at [utUtilsClassId]). + * + * Content of this util class may be different (due to mocks in deepEquals), but the methods (and their ids) are the same. + */ +internal object UtilClassFileMethodProvider : UtilMethodProvider(utUtilsClassId) { + /** + * This property contains the current version of util class. + * This version will be written to the util class file inside a comment. + * + * Whenever we want to create an util class, we first check if there is an already existing one. + * If there is, then we decide whether we need to overwrite it or not. One of the factors here + * is the version of this existing class. If the version of existing class is older than the one + * that is currently stored in [UtilClassFileMethodProvider.UTIL_CLASS_VERSION], then we need to + * overwrite an util class, because it might have been changed in the new version. + * + * **IMPORTANT** if you make any changes to util methods (see [utilMethodTextById]), do not forget to update this version. + */ + const val UTIL_CLASS_VERSION = "1.0" +} + +internal class TestClassUtilMethodProvider(testClassId: ClassId) : UtilMethodProvider(testClassId) + +internal val utUtilsClassId: ClassId + get() = BuiltinClassId( + name = "org.utbot.runtime.utils.UtUtils", + canonicalName = "org.utbot.runtime.utils.UtUtils", + simpleName = "UtUtils", + isFinal = true ) /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt index 17512357d6..c9b0dbd76e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt @@ -7,21 +7,6 @@ import org.utbot.framework.codegen.ParametrizedTestSource import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour import org.utbot.framework.codegen.StaticsMocking import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.constructor.builtin.arraysDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.createArrayMethodId -import org.utbot.framework.codegen.model.constructor.builtin.createInstanceMethodId -import org.utbot.framework.codegen.model.constructor.builtin.deepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getArrayLengthMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getEnumConstantByNameMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getFieldValueMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getStaticFieldValueMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getUnsafeInstanceMethodId -import org.utbot.framework.codegen.model.constructor.builtin.hasCustomEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.iterablesDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.mapsDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.possibleUtilMethodIds -import org.utbot.framework.codegen.model.constructor.builtin.setFieldMethodId -import org.utbot.framework.codegen.model.constructor.builtin.setStaticFieldMethodId import org.utbot.framework.codegen.model.constructor.tree.Block import org.utbot.framework.codegen.model.constructor.util.EnvironmentFieldStateCache import org.utbot.framework.codegen.model.constructor.util.importIfNeeded @@ -41,9 +26,11 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentSetOf import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.model.constructor.builtin.UtilClassFileMethodProvider +import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider import org.utbot.framework.codegen.model.constructor.TestClassContext import org.utbot.framework.codegen.model.constructor.TestClassModel -import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId import org.utbot.framework.codegen.model.tree.CgParameterKind import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.ClassId @@ -85,6 +72,9 @@ internal interface CgContextOwner { // test class currently being generated (if series of nested classes is generated, it is the innermost one) var currentTestClass: ClassId + // provider of util methods used for the test class currently being generated + val utilMethodProvider: UtilMethodProvider + // current executable under test var currentExecutable: ExecutableId? @@ -110,6 +100,8 @@ internal interface CgContextOwner { // util methods required by the test class being built val requiredUtilMethods: MutableSet + val utilMethodsUsed: Boolean + // test methods being generated val testMethods: MutableList @@ -141,7 +133,13 @@ internal interface CgContextOwner { val parametrizedTestSource: ParametrizedTestSource - // flag indicating whether a mock framework is used in the generated code + /** + * Flag indicating whether a mock framework is used in the generated code + * NOTE! This flag is not about whether a mock framework is present + * in the user's project dependencies or not. + * This flag is true if the generated test class contains at least one mock object, + * and false otherwise. See method [withMockFramework]. + */ var mockFrameworkUsed: Boolean // object that represents a set of information about JUnit of selected version @@ -324,66 +322,68 @@ internal interface CgContextOwner { } /** - * Check whether a method is an util method of the current class + * [ClassId] of a class that contains util methods. + * For example, it can be the current test class, or it can be a generated separate `UtUtils` class. */ - val MethodId.isUtil: Boolean - get() = this in outerMostTestClass.possibleUtilMethodIds + val utilsClassId: ClassId + get() = utilMethodProvider.utilClassId /** * Checks is it our util reflection field getter method. * When this method is used with type cast in Kotlin, this type cast have to be safety */ val MethodId.isGetFieldUtilMethod: Boolean - get() = isUtil && (name == getFieldValue.name || name == getStaticFieldValue.name) + get() = this == utilMethodProvider.getFieldValueMethodId + || this == utilMethodProvider.getStaticFieldValueMethodId val testClassThisInstance: CgThisInstance // util methods of current test class val getUnsafeInstance: MethodId - get() = outerMostTestClass.getUnsafeInstanceMethodId + get() = utilMethodProvider.getUnsafeInstanceMethodId val createInstance: MethodId - get() = outerMostTestClass.createInstanceMethodId + get() = utilMethodProvider.createInstanceMethodId val createArray: MethodId - get() = outerMostTestClass.createArrayMethodId + get() = utilMethodProvider.createArrayMethodId val setField: MethodId - get() = outerMostTestClass.setFieldMethodId + get() = utilMethodProvider.setFieldMethodId val setStaticField: MethodId - get() = outerMostTestClass.setStaticFieldMethodId + get() = utilMethodProvider.setStaticFieldMethodId val getFieldValue: MethodId - get() = outerMostTestClass.getFieldValueMethodId + get() = utilMethodProvider.getFieldValueMethodId val getStaticFieldValue: MethodId - get() = outerMostTestClass.getStaticFieldValueMethodId + get() = utilMethodProvider.getStaticFieldValueMethodId val getEnumConstantByName: MethodId - get() = outerMostTestClass.getEnumConstantByNameMethodId + get() = utilMethodProvider.getEnumConstantByNameMethodId val deepEquals: MethodId - get() = outerMostTestClass.deepEqualsMethodId + get() = utilMethodProvider.deepEqualsMethodId val arraysDeepEquals: MethodId - get() = outerMostTestClass.arraysDeepEqualsMethodId + get() = utilMethodProvider.arraysDeepEqualsMethodId val iterablesDeepEquals: MethodId - get() = outerMostTestClass.iterablesDeepEqualsMethodId + get() = utilMethodProvider.iterablesDeepEqualsMethodId val streamsDeepEquals: MethodId - get() = outerMostTestClass.streamsDeepEqualsMethodId + get() = utilMethodProvider.streamsDeepEqualsMethodId val mapsDeepEquals: MethodId - get() = outerMostTestClass.mapsDeepEqualsMethodId + get() = utilMethodProvider.mapsDeepEqualsMethodId val hasCustomEquals: MethodId - get() = outerMostTestClass.hasCustomEqualsMethodId + get() = utilMethodProvider.hasCustomEqualsMethodId val getArrayLength: MethodId - get() = outerMostTestClass.getArrayLengthMethodId + get() = utilMethodProvider.getArrayLengthMethodId } /** @@ -391,6 +391,7 @@ internal interface CgContextOwner { */ internal data class CgContext( override val classUnderTest: ClassId, + val generateUtilClassFile: Boolean, override var currentExecutable: ExecutableId? = null, override val collectedExceptions: MutableSet = mutableSetOf(), override val collectedMethodAnnotations: MutableSet = mutableSetOf(), @@ -460,6 +461,19 @@ internal data class CgContext( ) } + /** + * Determine where the util methods will come from. + * If we don't want to use a separately generated util class, + * util methods will be generated directly in the test class (see [TestClassUtilMethodProvider]). + * Otherwise, an util class will be generated separately and we will use util methods from it (see [UtilClassFileMethodProvider]). + */ + override val utilMethodProvider: UtilMethodProvider + get() = if (generateUtilClassFile) { + UtilClassFileMethodProvider + } else { + TestClassUtilMethodProvider(outerMostTestClass) + } + override lateinit var currentTestClass: ClassId override fun withTestClassFileScope(block: () -> R): R { @@ -524,4 +538,7 @@ internal data class CgContext( override val currentMethodParameters: MutableMap = mutableMapOf() override val testClassThisInstance: CgThisInstance = CgThisInstance(outerMostTestClass) + + override val utilMethodsUsed: Boolean + get() = requiredUtilMethods.isNotEmpty() } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt index 83b506f889..b70fd7af1d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt @@ -5,34 +5,20 @@ import org.utbot.framework.codegen.Junit5 import org.utbot.framework.codegen.TestNg import org.utbot.framework.codegen.model.constructor.builtin.any import org.utbot.framework.codegen.model.constructor.builtin.anyOfClass -import org.utbot.framework.codegen.model.constructor.builtin.arraysDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.createArrayMethodId -import org.utbot.framework.codegen.model.constructor.builtin.createInstanceMethodId -import org.utbot.framework.codegen.model.constructor.builtin.deepEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.forName -import org.utbot.framework.codegen.model.constructor.builtin.getArrayLengthMethodId import org.utbot.framework.codegen.model.constructor.builtin.getDeclaredConstructor import org.utbot.framework.codegen.model.constructor.builtin.getDeclaredMethod -import org.utbot.framework.codegen.model.constructor.builtin.getEnumConstantByNameMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getFieldValueMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getStaticFieldValueMethodId import org.utbot.framework.codegen.model.constructor.builtin.getTargetException -import org.utbot.framework.codegen.model.constructor.builtin.getUnsafeInstanceMethodId -import org.utbot.framework.codegen.model.constructor.builtin.hasCustomEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.invoke -import org.utbot.framework.codegen.model.constructor.builtin.iterablesDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.mapsDeepEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.newInstance import org.utbot.framework.codegen.model.constructor.builtin.setAccessible -import org.utbot.framework.codegen.model.constructor.builtin.setFieldMethodId -import org.utbot.framework.codegen.model.constructor.builtin.setStaticFieldMethodId -import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.util.CgComponents import org.utbot.framework.codegen.model.constructor.util.classCgClassId import org.utbot.framework.codegen.model.constructor.util.getAmbiguousOverloadsOf import org.utbot.framework.codegen.model.constructor.util.importIfNeeded +import org.utbot.framework.codegen.model.constructor.util.isUtil import org.utbot.framework.codegen.model.constructor.util.typeCast import org.utbot.framework.codegen.model.tree.CgAllocateArray import org.utbot.framework.codegen.model.tree.CgAssignment @@ -121,13 +107,14 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA } private fun newMethodCall(methodId: MethodId) { - if (methodId.isUtil) requiredUtilMethods += methodId + if (isUtil(methodId)) requiredUtilMethods += methodId importIfNeeded(methodId) //Builtin methods does not have jClass, so [methodId.method] will crash on it, //so we need to collect required exceptions manually from source codes if (methodId is BuiltinMethodId) { - methodId.findExceptionTypes().forEach { addExceptionIfNeeded(it) } + findExceptionTypesOf(methodId) + .forEach { addExceptionIfNeeded(it) } return } @@ -155,33 +142,6 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA } } - //WARN: if you make changes in the following sets of exceptions, - //don't forget to change them in hardcoded [UtilMethods] as well - private fun BuiltinMethodId.findExceptionTypes(): Set { - if (!this.isUtil) return emptySet() - - with(outerMostTestClass) { - return when (this@findExceptionTypes) { - getEnumConstantByNameMethodId -> setOf(IllegalAccessException::class.id) - getStaticFieldValueMethodId, - getFieldValueMethodId, - setStaticFieldMethodId, - setFieldMethodId -> setOf(IllegalAccessException::class.id, NoSuchFieldException::class.id) - createInstanceMethodId -> setOf(Exception::class.id) - getUnsafeInstanceMethodId -> setOf(ClassNotFoundException::class.id, NoSuchFieldException::class.id, IllegalAccessException::class.id) - createArrayMethodId -> setOf(ClassNotFoundException::class.id) - deepEqualsMethodId, - arraysDeepEqualsMethodId, - iterablesDeepEqualsMethodId, - streamsDeepEqualsMethodId, - mapsDeepEqualsMethodId, - hasCustomEqualsMethodId, - getArrayLengthMethodId -> emptySet() - else -> error("Unknown util method $this") - } - } - } - private infix fun CgExpression?.canBeReceiverOf(executable: MethodId): Boolean = when { // TODO: rewrite by using CgMethodId, etc. @@ -265,7 +225,7 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA * @return true if a method can be called with the given arguments without reflection */ private fun MethodId.canBeCalledWith(caller: CgExpression?, args: List): Boolean = - (isUtil || isAccessibleFrom(testClassPackageName)) + (isUtil(this) || isAccessibleFrom(testClassPackageName)) && caller canBeReceiverOf this && args canBeArgsOf this @@ -404,4 +364,35 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA return argumentsArrayVariable } + + //WARN: if you make changes in the following sets of exceptions, + //don't forget to change them in hardcoded [UtilMethods] as well + private fun findExceptionTypesOf(methodId: MethodId): Set { + // TODO: at the moment we treat BuiltinMethodIds that are not util method ids + // as if they have no exceptions. This should be fixed by storing exception types in BuiltinMethodId + // or allowing us to access actual java.lang.Class for classes from mockito and other libraries + // (this could be possibly solved by using user project's class loaders in UtContext) + if (methodId !in utilMethodProvider.utilMethodIds) return emptySet() + + with(utilMethodProvider) { + return when (methodId) { + getEnumConstantByNameMethodId -> setOf(java.lang.IllegalAccessException::class.id) + getStaticFieldValueMethodId, + getFieldValueMethodId, + setStaticFieldMethodId, + setFieldMethodId -> setOf(java.lang.IllegalAccessException::class.id, java.lang.NoSuchFieldException::class.id) + createInstanceMethodId -> setOf(Exception::class.id) + getUnsafeInstanceMethodId -> setOf(java.lang.ClassNotFoundException::class.id, java.lang.NoSuchFieldException::class.id, java.lang.IllegalAccessException::class.id) + createArrayMethodId -> setOf(java.lang.ClassNotFoundException::class.id) + deepEqualsMethodId, + arraysDeepEqualsMethodId, + iterablesDeepEqualsMethodId, + streamsDeepEqualsMethodId, + mapsDeepEqualsMethodId, + hasCustomEqualsMethodId, + getArrayLengthMethodId -> emptySet() + else -> error("Unknown util method $this") + } + } + } } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt index ea2a141db4..82b3fbb70c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt @@ -231,7 +231,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext) val expression = when (val newElement = path[index++]) { is FieldAccess -> { val field = newElement.field - testClassThisInstance[getFieldValue](prev, stringLiteral(field.name)) + utilsClassId[getFieldValue](prev, stringLiteral(field.name)) } is ArrayElementAccess -> { Array::class.id[getArrayElement](prev, newElement.index) @@ -256,7 +256,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext) } else { newVar(classCgClassId) { Class::class.id[forName](owner.name) } } - newVar(objectClassId) { testClassThisInstance[getStaticFieldValue](ownerClass, stringLiteral(firstField.name)) } + newVar(objectClassId) { utilsClassId[getStaticFieldValue](ownerClass, stringLiteral(firstField.name)) } } val path = fieldPath.elements val remainingPath = fieldPath.copy(elements = path.drop(1)) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt index 26262b988f..fee6dc125d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt @@ -231,7 +231,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c val declaringClassVar = newVar(classCgClassId) { Class::class.id[forName](declaringClass.name) } - testClassThisInstance[getStaticFieldValue](declaringClassVar, field.name) + utilsClassId[getStaticFieldValue](declaringClassVar, field.name) } } // remember the previous value of a static field to recover it at the end of the test @@ -257,7 +257,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c val declaringClassVar = newVar(classCgClassId) { Class::class.id[forName](declaringClass.name) } - +testClassThisInstance[setStaticField](declaringClassVar, field.name, fieldValue) + +utilsClassId[setStaticField](declaringClassVar, field.name, fieldValue) } } } @@ -268,7 +268,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c field.declaringClass[field] `=` prevValue } else { val declaringClass = getClassOf(field.declaringClass) - +testClassThisInstance[setStaticField](declaringClass, field.name, prevValue) + +utilsClassId[setStaticField](declaringClass, field.name, prevValue) } } } @@ -721,12 +721,12 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c val cgGetLengthDeclaration = CgDeclaration( intClassId, variableConstructor.constructVarName("${expected.name}Size"), - expected.length(this, testClassThisInstance, getArrayLength) + expected.length(this@CgMethodConstructor) ) currentBlock += cgGetLengthDeclaration currentBlock += assertions[assertEquals]( cgGetLengthDeclaration.variable, - actual.length(this, testClassThisInstance, getArrayLength) + actual.length(this@CgMethodConstructor) ).toStatement() return cgGetLengthDeclaration @@ -1048,7 +1048,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c if (variable.type.hasField(this) && isAccessibleFrom(testClassPackageName)) { if (jField.isStatic) CgStaticFieldAccess(this) else CgFieldAccess(variable, this) } else { - testClassThisInstance[getFieldValue](variable, stringLiteral(name)) + utilsClassId[getFieldValue](variable, stringLiteral(name)) } /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt index 815110caa4..c83942a177 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt @@ -4,6 +4,7 @@ import org.utbot.common.appendHtmlLine import org.utbot.engine.displayName import org.utbot.framework.codegen.ParametrizedTestSource import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.util.CgComponents @@ -48,7 +49,7 @@ internal class CgTestClassConstructor(val context: CgContext) : */ fun construct(testClassModel: TestClassModel): CgTestClassFile { return buildTestClassFile { - this.testClass = withTestClassScope { constructTestClass(testClassModel) } + this.declaredClass = withTestClassScope { constructTestClass(testClassModel) } imports += context.collectedImports testsGenerationReport = this@CgTestClassConstructor.testsGenerationReport } @@ -91,17 +92,19 @@ internal class CgTestClassConstructor(val context: CgContext) : testMethodRegions += executableUnderTestCluster } - val utilMethods = if (currentTestClass == outerMostTestClass) - createUtilMethods() - else - emptyList() - - val additionalMethods = currentTestClassContext.cgDataProviderMethods + utilMethods + val currentTestClassDataProviderMethods = currentTestClassContext.cgDataProviderMethods + if (currentTestClassDataProviderMethods.isNotEmpty()) { + staticDeclarationRegions += CgStaticsRegion("Data providers", currentTestClassDataProviderMethods) + } - dataProvidersAndUtilMethodsRegion += CgStaticsRegion( - "Data providers and utils methods", - additionalMethods - ) + if (currentTestClass == outerMostTestClass) { + val utilMethods = collectUtilMethods() + // If utilMethodProvider is TestClassUtilMethodProvider, then util methods should be declared + // in the test class. Otherwise, util methods will be located elsewhere (e.g. another class or library). + if (utilMethodProvider is TestClassUtilMethodProvider && utilMethods.isNotEmpty()) { + staticDeclarationRegions += CgStaticsRegion("Util methods", utilMethods) + } + } } // It is important that annotations, superclass and interfaces assignment is run after // all methods are generated so that all necessary info is already present in the context @@ -197,8 +200,17 @@ internal class CgTestClassConstructor(val context: CgContext) : }.onFailure { error -> processFailure(testSet, error) } } - // TODO: collect imports of util methods - private fun createUtilMethods(): List { + /** + * This method collects a list of util methods needed by the class. + * By the end of the test method generation [requiredUtilMethods] may not contain all the methods needed. + * That's because some util methods may not be directly used in tests, but they may be used from other util methods. + * We define such method dependencies in [MethodId.dependencies]. + * + * Once all dependencies are collected, they are also added to [requiredUtilMethods]. + * + * @return a list of [CgUtilMethod] representing required util methods (including dependencies). + */ + private fun collectUtilMethods(): List { val utilMethods = mutableListOf() // some util methods depend on the others // using this loop we make sure that all the @@ -208,11 +220,18 @@ internal class CgTestClassConstructor(val context: CgContext) : requiredUtilMethods.remove(method) if (method.name !in existingMethodNames) { utilMethods += CgUtilMethod(method) - importUtilMethodDependencies(method) + // we only need imports from util methods if these util methods are declared in the test class + if (utilMethodProvider is TestClassUtilMethodProvider) { + importUtilMethodDependencies(method) + } existingMethodNames += method.name requiredUtilMethods += method.dependencies() } } + // Collect all util methods back into requiredUtilMethods. + // Now there will also be util methods that weren't present in requiredUtilMethods at first, + // but were needed for the present util methods to work. + requiredUtilMethods += utilMethods.map { method -> method.id } return utilMethods } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgUtilClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgUtilClassConstructor.kt new file mode 100644 index 0000000000..e81af665a4 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgUtilClassConstructor.kt @@ -0,0 +1,32 @@ +package org.utbot.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.CodeGenerator +import org.utbot.framework.codegen.model.UtilClassKind +import org.utbot.framework.codegen.model.constructor.builtin.utUtilsClassId +import org.utbot.framework.codegen.model.tree.CgRegularClassFile +import org.utbot.framework.codegen.model.tree.CgUtilMethod +import org.utbot.framework.codegen.model.tree.buildRegularClass +import org.utbot.framework.codegen.model.tree.buildRegularClassBody +import org.utbot.framework.codegen.model.tree.buildRegularClassFile + +/** + * This class is used to construct a file containing an util class UtUtils. + * The util class is constructed when the argument `generateUtilClassFile` in the [CodeGenerator] is true. + */ +internal object CgUtilClassConstructor { + fun constructUtilsClassFile(utilClassKind: UtilClassKind): CgRegularClassFile { + val utilMethodProvider = utilClassKind.utilMethodProvider + return buildRegularClassFile { + // imports are empty, because we use fully qualified classes and static methods, + // so they will be imported once IDEA reformatting action has worked + declaredClass = buildRegularClass { + id = utUtilsClassId + body = buildRegularClassBody { + content += utilClassKind.utilClassVersionComment + content += utilClassKind.utilClassKindComment + content += utilMethodProvider.utilMethodIds.map { CgUtilMethod(it) } + } + } + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt index baa62c3890..6154bee0fa 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt @@ -118,7 +118,7 @@ internal class CgVariableConstructor(val context: CgContext) : val obj = if (model.isMock) { mockFrameworkManager.createMockFor(model, baseName) } else { - newVar(model.classId, baseName) { testClassThisInstance[createInstance](model.classId.name) } + newVar(model.classId, baseName) { utilsClassId[createInstance](model.classId.name) } } valueByModelId[model.id] = obj @@ -146,7 +146,7 @@ internal class CgVariableConstructor(val context: CgContext) : fieldAccess `=` variableForField } else { // composite models must not have info about static fields, hence only non-static fields are set here - +testClassThisInstance[setField](obj, fieldId.name, variableForField) + +utilsClassId[setField](obj, fieldId.name, variableForField) } } return obj diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt index 157328aef6..459734f0a9 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt @@ -4,13 +4,7 @@ import org.utbot.framework.codegen.Junit4 import org.utbot.framework.codegen.Junit5 import org.utbot.framework.codegen.TestNg import org.utbot.framework.codegen.model.constructor.TestClassContext -import org.utbot.framework.codegen.model.constructor.builtin.arraysDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.deepEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.forName -import org.utbot.framework.codegen.model.constructor.builtin.hasCustomEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.iterablesDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.mapsDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.util.CgComponents @@ -122,19 +116,20 @@ internal abstract class TestFrameworkManager(val context: CgContext) } open fun getDeepEqualsAssertion(expected: CgExpression, actual: CgExpression): CgMethodCall { - requiredUtilMethods += outerMostTestClass.deepEqualsMethodId - requiredUtilMethods += outerMostTestClass.arraysDeepEqualsMethodId - requiredUtilMethods += outerMostTestClass.iterablesDeepEqualsMethodId - requiredUtilMethods += outerMostTestClass.streamsDeepEqualsMethodId - requiredUtilMethods += outerMostTestClass.mapsDeepEqualsMethodId - requiredUtilMethods += outerMostTestClass.hasCustomEqualsMethodId - + requiredUtilMethods += setOf( + utilMethodProvider.deepEqualsMethodId, + utilMethodProvider.arraysDeepEqualsMethodId, + utilMethodProvider.iterablesDeepEqualsMethodId, + utilMethodProvider.streamsDeepEqualsMethodId, + utilMethodProvider.mapsDeepEqualsMethodId, + utilMethodProvider.hasCustomEqualsMethodId + ) // TODO we cannot use common assertEquals because of using custom deepEquals // For this reason we have to use assertTrue here // Unfortunately, if test with assertTrue fails, it gives non informative message false != true // Thus, we need to provide custom message to assertTrue showing compared objects correctly // SAT-1345 - return assertions[assertTrue](testClassThisInstance[deepEquals](expected, actual)) + return assertions[assertTrue](utilsClassId[deepEquals](expected, actual)) } @Suppress("unused") diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt index 0ccc973ce3..f3e1ae412a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt @@ -429,7 +429,7 @@ internal class CgStatementConstructorImpl(context: CgContext) : Class::class.id[forName](enumClass.name) } - ExpressionWithType(objectClassId, testClassThisInstance[getEnumConstantByName](enumClassVariable, constant)) + ExpressionWithType(objectClassId, utilsClassId[getEnumConstantByName](enumClassVariable, constant)) } } @@ -448,7 +448,7 @@ internal class CgStatementConstructorImpl(context: CgContext) : } else { ExpressionWithType( objectArrayClassId, - testClassThisInstance[createArray](arrayType.elementClassId!!.name, arraySize) + utilsClassId[createArray](arrayType.elementClassId!!.name, arraySize) ) } } @@ -500,7 +500,7 @@ internal class CgStatementConstructorImpl(context: CgContext) : } expression.type isNotSubtypeOf baseType && !typeAccessible -> { type = if (expression.type.isArray) objectArrayClassId else objectClassId - expr = if (expression is CgMethodCall && expression.executableId.isUtil) { + expr = if (expression is CgMethodCall && isUtil(expression.executableId)) { CgErrorWrapper("${expression.executableId.name} failed", expression) } else { expression diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt index 179e9272e0..d4059833b1 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -129,6 +129,13 @@ internal data class CgFieldState(val variable: CgVariable, val model: UtModel) data class ExpressionWithType(val type: ClassId, val expression: CgExpression) +/** + * Check if a method is an util method of the current class + */ +internal fun CgContextOwner.isUtil(method: MethodId): Boolean { + return method in utilMethodProvider.utilMethodIds +} + val classCgClassId = CgClassId(Class::class.id, typeParameters = WildcardTypeParameter(), isNullable = false) /** @@ -391,8 +398,14 @@ internal infix fun UtModel.isNotDefaultValueOf(type: ClassId): Boolean = !this.i internal operator fun UtArrayModel.get(index: Int): UtModel = stores[index] ?: constModel -internal fun ClassId.utilMethodId(name: String, returnType: ClassId, vararg arguments: ClassId): MethodId = - BuiltinMethodId(this, name, returnType, arguments.toList()) +internal fun ClassId.utilMethodId( + name: String, + returnType: ClassId, + vararg arguments: ClassId, + // usually util methods are static, so this argument is true by default + isStatic: Boolean = true +): MethodId = + BuiltinMethodId(this, name, returnType, arguments.toList(), isStatic = isStatic) fun ClassId.toImport(): RegularImport = RegularImport(packageName, simpleNameWithEnclosings) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/Builders.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/Builders.kt index 57a91672a3..55be22a41d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/Builders.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/Builders.kt @@ -12,16 +12,39 @@ interface CgBuilder { // Code entities +class CgRegularClassFileBuilder : CgBuilder { + val imports: MutableList = mutableListOf() + lateinit var declaredClass: CgRegularClass + + override fun build() = CgRegularClassFile(imports, declaredClass) +} + +fun buildRegularClassFile(init: CgRegularClassFileBuilder.() -> Unit) = CgRegularClassFileBuilder().apply(init).build() + class CgTestClassFileBuilder : CgBuilder { val imports: MutableList = mutableListOf() - lateinit var testClass: CgTestClass + lateinit var declaredClass: CgTestClass lateinit var testsGenerationReport: TestsGenerationReport - override fun build() = CgTestClassFile(imports, testClass, testsGenerationReport) + override fun build() = CgTestClassFile(imports, declaredClass, testsGenerationReport) } fun buildTestClassFile(init: CgTestClassFileBuilder.() -> Unit) = CgTestClassFileBuilder().apply(init).build() +class CgRegularClassBuilder : CgBuilder { + lateinit var id: ClassId + val annotations: MutableList = mutableListOf() + var superclass: ClassId? = null + val interfaces: MutableList = mutableListOf() + lateinit var body: CgRegularClassBody + var isStatic: Boolean = false + var isNested: Boolean = false + + override fun build() = CgRegularClass(id, annotations, superclass, interfaces, body, isStatic, isNested) +} + +fun buildRegularClass(init: CgRegularClassBuilder.() -> Unit) = CgRegularClassBuilder().apply(init).build() + class CgTestClassBuilder : CgBuilder { lateinit var id: ClassId val annotations: MutableList = mutableListOf() @@ -38,16 +61,22 @@ fun buildTestClass(init: CgTestClassBuilder.() -> Unit) = CgTestClassBuilder().a class CgTestClassBodyBuilder : CgBuilder { val testMethodRegions: MutableList = mutableListOf() - - val dataProvidersAndUtilMethodsRegion: MutableList> = mutableListOf() - + val staticDeclarationRegions: MutableList = mutableListOf() val nestedClassRegions: MutableList> = mutableListOf() - override fun build() = CgTestClassBody(testMethodRegions, dataProvidersAndUtilMethodsRegion, nestedClassRegions) + override fun build() = CgTestClassBody(testMethodRegions, staticDeclarationRegions, nestedClassRegions) } fun buildTestClassBody(init: CgTestClassBodyBuilder.() -> Unit) = CgTestClassBodyBuilder().apply(init).build() +class CgRegularClassBodyBuilder : CgBuilder { + val content: MutableList = mutableListOf() + + override fun build() = CgRegularClassBody(content) +} + +fun buildRegularClassBody(init: CgRegularClassBodyBuilder.() -> Unit) = CgRegularClassBodyBuilder().apply(init).build() + // Methods interface CgMethodBuilder : CgBuilder { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt index 4c84ce2a8c..ec8abe977a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt @@ -3,9 +3,12 @@ package org.utbot.framework.codegen.model.tree import org.utbot.common.WorkaroundReason import org.utbot.common.workaround import org.utbot.framework.codegen.Import +import org.utbot.framework.codegen.model.constructor.tree.CgUtilClassConstructor import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport import org.utbot.framework.codegen.model.util.CgExceptionHandler +import org.utbot.framework.codegen.model.visitor.CgRendererContext import org.utbot.framework.codegen.model.visitor.CgVisitor +import org.utbot.framework.codegen.model.visitor.utilMethodTextById import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId @@ -32,8 +35,11 @@ interface CgElement { // TODO: order of cases is important here due to inheritance between some of the element types fun accept(visitor: CgVisitor): R = visitor.run { when (val element = this@CgElement) { + is CgRegularClassFile -> visit(element) is CgTestClassFile -> visit(element) + is CgRegularClass -> visit(element) is CgTestClass -> visit(element) + is CgRegularClassBody -> visit(element) is CgTestClassBody -> visit(element) is CgStaticsRegion -> visit(element) is CgSimpleRegion<*> -> visit(element) @@ -112,33 +118,89 @@ interface CgElement { // Code entities +sealed class AbstractCgClassFile> : CgElement { + abstract val imports: List + abstract val declaredClass: T +} + +data class CgRegularClassFile( + override val imports: List, + override val declaredClass: CgRegularClass +) : AbstractCgClassFile() + data class CgTestClassFile( - val imports: List, - val testClass: CgTestClass, + override val imports: List, + override val declaredClass: CgTestClass, val testsGenerationReport: TestsGenerationReport -) : CgElement +) : AbstractCgClassFile() -data class CgTestClass( - val id: ClassId, - val annotations: List, - val superclass: ClassId?, - val interfaces: List, - val body: CgTestClassBody, - val isStatic: Boolean, - val isNested: Boolean -) : CgElement { - val packageName = id.packageName - val simpleName = id.simpleName +sealed class AbstractCgClass : CgElement { + abstract val id: ClassId + abstract val annotations: List + abstract val superclass: ClassId? + abstract val interfaces: List + abstract val body: T + abstract val isStatic: Boolean + abstract val isNested: Boolean + + val packageName + get() = id.packageName + + val simpleName + get() = id.simpleName } +/** + * This class represents any class that we may want to generate other than the test class. + * At the moment the only such case is the generation of util class UtUtils. + * + * The difference with [CgTestClass] is in the body. + * The structure of a test class body is fixed (we know what it should contain), + * whereas an arbitrary class could contain anything. + * For example, the body of UtUtils class contains a comment with information + * about the version of UTBot it was generated with, and all the util methods. + * + * @see CgUtilClassConstructor + */ +class CgRegularClass( + override val id: ClassId, + override val annotations: List, + override val superclass: ClassId?, + override val interfaces: List, + override val body: CgRegularClassBody, + override val isStatic: Boolean, + override val isNested: Boolean +) : AbstractCgClass() + +data class CgTestClass( + override val id: ClassId, + override val annotations: List, + override val superclass: ClassId?, + override val interfaces: List, + override val body: CgTestClassBody, + override val isStatic: Boolean, + override val isNested: Boolean +) : AbstractCgClass() + + +sealed class AbstractCgClassBody : CgElement + +data class CgRegularClassBody(val content: List) : AbstractCgClassBody() + +/** + * Body of the test class. + * @property testMethodRegions regions containing the test methods + * @property staticDeclarationRegions regions containing static declarations. + * This is usually util methods and data providers. + * In Kotlin all static declarations must be grouped together in a companion object. + * In Java there is no such restriction, but for uniformity we are grouping + * Java static declarations together as well. It can also improve code readability. + */ data class CgTestClassBody( val testMethodRegions: List, - val utilsRegion: List>, + val staticDeclarationRegions: List, val nestedClassRegions: List> -) : CgElement { - val regions: List> - get() = testMethodRegions -} +) : AbstractCgClassBody() /** * A class representing the IntelliJ IDEA's regions. @@ -157,7 +219,10 @@ open class CgSimpleRegion( ) : CgRegion() /** - * Stores data providers for parametrized tests and util methods + * A region that stores some static declarations, e.g. data providers or util methods. + * There may be more than one static region in a class and they all are stored + * in a [CgTestClassBody.staticDeclarationRegions]. + * In case of Kotlin, they all will be rendered inside of a companion object. */ class CgStaticsRegion( override val header: String?, @@ -184,9 +249,20 @@ data class CgExecutableUnderTestCluster( * This is because util methods are hardcoded. On the rendering stage their text * is retrieved by their [MethodId]. * - * [id] identifier of the util method. + * @property id identifier of the util method. */ -data class CgUtilMethod(val id: MethodId) : CgElement +data class CgUtilMethod(val id: MethodId) : CgElement { + internal fun getText(rendererContext: CgRendererContext): String { + // we should not throw an exception on failure here, + // because this function is used during rendering and + // exceptions can crash rendering, so we use an empty string if the text is not found + return with(rendererContext) { + rendererContext.utilMethodProvider + .utilMethodTextById(id, mockFrameworkUsed, mockFramework, codegenLanguage) + .getOrDefault("") + } + } +} // Methods diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyUtils.kt index 4d0e05b39a..cf09cbc321 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyUtils.kt @@ -1,6 +1,5 @@ package org.utbot.framework.codegen.model.util -import org.utbot.framework.codegen.TestFramework import org.utbot.framework.concrete.UtExecutionInstrumentation import org.utbot.framework.plugin.api.MockFramework import java.io.File diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt index 5edbf60ec6..b351855f7a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt @@ -1,6 +1,7 @@ package org.utbot.framework.codegen.model.util -import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider +import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor import org.utbot.framework.codegen.model.tree.CgArrayElementAccess import org.utbot.framework.codegen.model.tree.CgDecrement import org.utbot.framework.codegen.model.tree.CgEqualTo @@ -15,12 +16,10 @@ import org.utbot.framework.codegen.model.tree.CgIncrement import org.utbot.framework.codegen.model.tree.CgLessThan import org.utbot.framework.codegen.model.tree.CgLiteral import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess -import org.utbot.framework.codegen.model.tree.CgThisInstance import org.utbot.framework.codegen.model.tree.CgVariable import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.byteClassId import org.utbot.framework.plugin.api.util.charClassId @@ -82,19 +81,15 @@ operator fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess = // Get array length /** - * Returns length field access for array type variable and [getArrayLengthMethodId] call otherwise. + * Returns length field access for array type variable and [UtilMethodProvider.getArrayLengthMethodId] call otherwise. */ -fun CgVariable.length( - cgCallableAccessManager: CgCallableAccessManager, - thisInstance: CgThisInstance, - getArrayLengthMethodId: MethodId -): CgExpression { +internal fun CgVariable.length(methodConstructor: CgMethodConstructor): CgExpression { val thisVariable = this return if (type.isArray) { CgGetLength(thisVariable) } else { - with(cgCallableAccessManager) { thisInstance[getArrayLengthMethodId](thisVariable) } + with(methodConstructor) { utilsClassId[getArrayLength](thisVariable) } } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt index 77b739d174..38d5633020 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt @@ -6,7 +6,11 @@ import org.utbot.common.workaround import org.utbot.framework.codegen.Import import org.utbot.framework.codegen.RegularImport import org.utbot.framework.codegen.StaticImport +import org.utbot.framework.codegen.model.UtilClassKind import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.tree.AbstractCgClass +import org.utbot.framework.codegen.model.tree.AbstractCgClassBody +import org.utbot.framework.codegen.model.tree.AbstractCgClassFile import org.utbot.framework.codegen.model.tree.CgAbstractFieldAccess import org.utbot.framework.codegen.model.tree.CgAbstractMultilineComment import org.utbot.framework.codegen.model.tree.CgArrayElementAccess @@ -55,6 +59,9 @@ import org.utbot.framework.codegen.model.tree.CgNonStaticRunnable import org.utbot.framework.codegen.model.tree.CgParameterDeclaration import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod import org.utbot.framework.codegen.model.tree.CgRegion +import org.utbot.framework.codegen.model.tree.CgRegularClass +import org.utbot.framework.codegen.model.tree.CgRegularClassBody +import org.utbot.framework.codegen.model.tree.CgRegularClassFile import org.utbot.framework.codegen.model.tree.CgReturnStatement import org.utbot.framework.codegen.model.tree.CgSimpleRegion import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation @@ -66,7 +73,6 @@ import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess import org.utbot.framework.codegen.model.tree.CgStaticRunnable import org.utbot.framework.codegen.model.tree.CgStaticsRegion import org.utbot.framework.codegen.model.tree.CgTestClass -import org.utbot.framework.codegen.model.tree.CgTestClassBody import org.utbot.framework.codegen.model.tree.CgTestClassFile import org.utbot.framework.codegen.model.tree.CgTestMethod import org.utbot.framework.codegen.model.tree.CgTestMethodCluster @@ -94,7 +100,10 @@ import org.utbot.framework.plugin.api.util.isRefType import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.shortClassId -internal abstract class CgAbstractRenderer(val context: CgContext, val printer: CgPrinter = CgPrinterImpl()) : CgVisitor, +internal abstract class CgAbstractRenderer( + val context: CgRendererContext, + val printer: CgPrinter = CgPrinterImpl() +) : CgVisitor, CgPrinter by printer { protected abstract val statementEnding: String @@ -122,7 +131,7 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: } private val MethodId.accessibleByName: Boolean - get() = (context.shouldOptimizeImports && this in context.importedStaticMethods) || classId == context.outerMostTestClass + get() = (context.shouldOptimizeImports && this in context.importedStaticMethods) || classId == context.generatedClass override fun visit(element: CgElement) { val error = @@ -130,18 +139,40 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: throw IllegalArgumentException(error) } - override fun visit(element: CgTestClassFile) { - renderClassPackage(element.testClass) + override fun visit(element: AbstractCgClassFile<*>) { + renderClassPackage(element.declaredClass) renderClassFileImports(element) - element.testClass.accept(this) + element.declaredClass.accept(this) + } + + override fun visit(element: CgRegularClassFile) { + visit(element as AbstractCgClassFile<*>) + } + + override fun visit(element: CgTestClassFile) { + visit(element as AbstractCgClassFile<*>) } - override fun visit(element: CgTestClassBody) { - // render regions for test methods and utils - for ((i, region) in (element.regions + element.nestedClassRegions + element.utilsRegion).withIndex()) { - if (i != 0) println() + override fun visit(element: CgRegularClass) { + visit(element as AbstractCgClass<*>) + } + + override fun visit(element: CgTestClass) { + visit(element as AbstractCgClass<*>) + } + + override fun visit(element: AbstractCgClassBody) { + visit(element as CgElement) + } - region.accept(this) + override fun visit(element: CgRegularClassBody) { + val content = element.content + for ((index, item) in content.withIndex()) { + item.accept(this) + println() + if (index < content.lastIndex) { + println() + } } } @@ -197,10 +228,9 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: } override fun visit(element: CgUtilMethod) { - context.outerMostTestClass - .utilMethodById(element.id, context) - .split("\n") - .forEach { line -> println(line) } + val utilMethodText = element.getText(context) + utilMethodText.split("\n") + .forEach { line -> println(line) } } // Methods @@ -797,7 +827,7 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: } protected open fun isAccessibleBySimpleNameImpl(classId: ClassId): Boolean = - classId in context.importedClasses || classId.packageName == context.testClassPackageName + classId in context.importedClasses || classId.packageName == context.classPackageName protected abstract fun escapeNamePossibleKeywordImpl(s: String): String @@ -810,14 +840,14 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: return if (this.isAccessibleBySimpleName()) simpleNameWithEnclosings else canonicalName } - private fun renderClassPackage(element: CgTestClass) { + private fun renderClassPackage(element: AbstractCgClass<*>) { if (element.packageName.isNotEmpty()) { println("package ${element.packageName}${statementEnding}") println() } } - private fun renderClassFileImports(element: CgTestClassFile) { + private fun renderClassFileImports(element: AbstractCgClassFile<*>) { val regularImports = element.imports.filterIsInstance() val staticImports = element.imports.filterIsInstance() @@ -836,6 +866,10 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: } } + protected abstract fun renderClassVisibility(classId: ClassId) + + protected abstract fun renderClassModality(aClass: AbstractCgClass<*>) + private fun renderMethodDocumentation(element: CgMethod) { element.documentation.accept(this) } @@ -892,11 +926,26 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: fun makeRenderer( context: CgContext, printer: CgPrinter = CgPrinterImpl() - ): CgAbstractRenderer = - when (context.codegenLanguage) { + ): CgAbstractRenderer { + val rendererContext = CgRendererContext.fromCgContext(context) + return makeRenderer(rendererContext, printer) + } + + fun makeRenderer( + utilClassKind: UtilClassKind, + codegenLanguage: CodegenLanguage, + printer: CgPrinter = CgPrinterImpl() + ): CgAbstractRenderer { + val rendererContext = CgRendererContext.fromUtilClassKind(utilClassKind, codegenLanguage) + return makeRenderer(rendererContext, printer) + } + + private fun makeRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer { + return when (context.codegenLanguage) { CodegenLanguage.JAVA -> CgJavaRenderer(context, printer) CodegenLanguage.KOTLIN -> CgKotlinRenderer(context, printer) } + } /** * @see [LONG_CODE_FRAGMENTS] diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt index b1f00fa728..88bbba9ffa 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt @@ -3,7 +3,7 @@ package org.utbot.framework.codegen.model.visitor import org.apache.commons.text.StringEscapeUtils import org.utbot.framework.codegen.RegularImport import org.utbot.framework.codegen.StaticImport -import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.tree.AbstractCgClass import org.utbot.framework.codegen.model.tree.CgAllocateArray import org.utbot.framework.codegen.model.tree.CgAllocateInitializedArray import org.utbot.framework.codegen.model.tree.CgAnonymousFunction @@ -26,12 +26,14 @@ import org.utbot.framework.codegen.model.tree.CgMethod import org.utbot.framework.codegen.model.tree.CgNotNullAssertion import org.utbot.framework.codegen.model.tree.CgParameterDeclaration import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.model.tree.CgRegularClass import org.utbot.framework.codegen.model.tree.CgReturnStatement import org.utbot.framework.codegen.model.tree.CgStatement import org.utbot.framework.codegen.model.tree.CgStatementExecutableCall import org.utbot.framework.codegen.model.tree.CgSwitchCase import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel import org.utbot.framework.codegen.model.tree.CgTestClass +import org.utbot.framework.codegen.model.tree.CgTestClassBody import org.utbot.framework.codegen.model.tree.CgTestMethod import org.utbot.framework.codegen.model.tree.CgTypeCast import org.utbot.framework.codegen.model.tree.CgVariable @@ -43,7 +45,7 @@ import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.TypeParameters import org.utbot.framework.plugin.api.util.wrapperByPrimitive -internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinterImpl()) : +internal class CgJavaRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : CgAbstractRenderer(context, printer) { override val statementEnding: String = ";" @@ -58,18 +60,22 @@ internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinter override val langPackage: String = "java.lang" - override fun visit(element: CgTestClass) { + override fun visit(element: AbstractCgClass<*>) { for (annotation in element.annotations) { annotation.accept(this) } - print("public ") + + renderClassVisibility(element.id) + renderClassModality(element) if (element.isStatic) { print("static ") } print("class ") print(element.simpleName) - if (element.superclass != null) { - print(" extends ${element.superclass.asString()}") + + val superclass = element.superclass + if (superclass != null) { + print(" extends ${superclass.asString()}") } if (element.interfaces.isNotEmpty()) { print(" implements ") @@ -80,6 +86,16 @@ internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinter println("}") } + override fun visit(element: CgTestClassBody) { + // render regions for test methods and utils + val allRegions = element.testMethodRegions + element.nestedClassRegions + element.staticDeclarationRegions + for ((i, region) in allRegions.withIndex()) { + if (i != 0) println() + + region.accept(this) + } + } + override fun visit(element: CgArrayAnnotationArgument) { print("{") element.values.renderSeparated() @@ -335,6 +351,21 @@ internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinter override fun escapeNamePossibleKeywordImpl(s: String): String = s + override fun renderClassVisibility(classId: ClassId) { + when { + classId.isPublic -> print("public ") + classId.isProtected -> print("protected ") + classId.isPrivate -> print("private ") + } + } + + override fun renderClassModality(aClass: AbstractCgClass<*>) { + when (aClass) { + is CgTestClass -> Unit + is CgRegularClass -> if (aClass.id.isFinal) print("final ") + } + } + private fun renderExceptions(method: CgMethod) { method.exceptions.takeIf { it.isNotEmpty() }?.let { exceptions -> print(" throws ") diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt index 8a25ad5a7f..1e5a107218 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt @@ -6,7 +6,7 @@ import org.utbot.common.workaround import org.utbot.framework.codegen.RegularImport import org.utbot.framework.codegen.StaticImport import org.utbot.framework.codegen.isLanguageKeyword -import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.tree.AbstractCgClass import org.utbot.framework.codegen.model.tree.CgAllocateArray import org.utbot.framework.codegen.model.tree.CgAllocateInitializedArray import org.utbot.framework.codegen.model.tree.CgAnonymousFunction @@ -31,11 +31,13 @@ import org.utbot.framework.codegen.model.tree.CgMethod import org.utbot.framework.codegen.model.tree.CgNotNullAssertion import org.utbot.framework.codegen.model.tree.CgParameterDeclaration import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.model.tree.CgRegularClass import org.utbot.framework.codegen.model.tree.CgSpread import org.utbot.framework.codegen.model.tree.CgStaticsRegion import org.utbot.framework.codegen.model.tree.CgSwitchCase import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel import org.utbot.framework.codegen.model.tree.CgTestClass +import org.utbot.framework.codegen.model.tree.CgTestClassBody import org.utbot.framework.codegen.model.tree.CgTestMethod import org.utbot.framework.codegen.model.tree.CgTypeCast import org.utbot.framework.codegen.model.tree.CgVariable @@ -55,7 +57,7 @@ import org.utbot.framework.plugin.api.util.kClass import org.utbot.framework.plugin.api.util.voidClassId //TODO rewrite using KtPsiFactory? -internal class CgKotlinRenderer(context: CgContext, printer: CgPrinter = CgPrinterImpl()) : CgAbstractRenderer(context, printer) { +internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : CgAbstractRenderer(context, printer) { override val statementEnding: String = "" override val logicalAnd: String @@ -68,30 +70,35 @@ internal class CgKotlinRenderer(context: CgContext, printer: CgPrinter = CgPrint override val langPackage: String = "kotlin" - override fun visit(element: CgTestClass) { + override fun visit(element: AbstractCgClass<*>) { for (annotation in element.annotations) { annotation.accept(this) } + + renderClassVisibility(element.id) + renderClassModality(element) if (!element.isStatic && element.isNested) { print("inner ") } print("class ") print(element.simpleName) + if (element.superclass != null || element.interfaces.isNotEmpty()) { print(" :") } val supertypes = mutableListOf() - .apply { - // Here we do not consider constructors with arguments, but for now they are not needed. - // Also, we do not yet support type parameters in code generation, so generic - // superclasses or interfaces are not supported. Although, they are not needed for now. - if (element.superclass != null) { - add("${element.superclass.asString()}()") - } - element.interfaces.forEach { - add(it.asString()) - } - }.joinToString() + .apply { + // Here we do not consider constructors with arguments, but for now they are not needed. + // Also, we do not yet support type parameters in code generation, so generic + // superclasses or interfaces are not supported. Although, they are not needed for now. + val superclass = element.superclass + if (superclass != null) { + add("${superclass.asString()}()") + } + element.interfaces.forEach { + add(it.asString()) + } + }.joinToString() if (supertypes.isNotEmpty()) { print(" $supertypes") } @@ -100,6 +107,38 @@ internal class CgKotlinRenderer(context: CgContext, printer: CgPrinter = CgPrint println("}") } + override fun visit(element: CgTestClassBody) { + // render regions for test methods + for ((i, region) in (element.testMethodRegions + element.nestedClassRegions).withIndex()) { + if (i != 0) println() + + region.accept(this) + } + + if (element.staticDeclarationRegions.isEmpty()) { + return + } + // render static declaration regions inside a companion object + println() + renderCompanionObject { + for ((i, staticsRegion) in element.staticDeclarationRegions.withIndex()) { + if (i != 0) println() + + staticsRegion.accept(this) + } + } + } + + /** + * Build a companion object. + * @param body a lambda that contains the logic of construction of a companion object's body + */ + private fun renderCompanionObject(body: () -> Unit) { + println("companion object {") + withIndent(body) + println("}") + } + override fun visit(element: CgStaticsRegion) { if (element.content.isEmpty()) return @@ -107,15 +146,11 @@ internal class CgKotlinRenderer(context: CgContext, printer: CgPrinter = CgPrint element.header?.let { print(" $it") } println() - println("companion object {") - withIndent { - for (item in element.content) { - println() - println("@JvmStatic") - item.accept(this) - } + for (item in element.content) { + println() + println("@JvmStatic") + item.accept(this) } - println("}") println(regionEnd) } @@ -450,6 +485,23 @@ internal class CgKotlinRenderer(context: CgContext, printer: CgPrinter = CgPrint override fun escapeNamePossibleKeywordImpl(s: String): String = if (isLanguageKeyword(s, context.codegenLanguage)) "`$s`" else s + override fun renderClassVisibility(classId: ClassId) { + when { + // Kotlin classes are public by default + classId.isPublic -> Unit + classId.isProtected -> print("protected ") + classId.isPrivate -> print("private ") + } + } + + override fun renderClassModality(aClass: AbstractCgClass<*>) { + when (aClass) { + is CgTestClass -> Unit + // Kotlin classes are final by default + is CgRegularClass -> if (!aClass.id.isFinal) print("open ") + } + } + private fun getKotlinClassString(id: ClassId): String = if (id.isArray) { getKotlinArrayClassOfString(id) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt new file mode 100644 index 0000000000..92963984cb --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt @@ -0,0 +1,58 @@ +package org.utbot.framework.codegen.model.visitor + +import org.utbot.framework.codegen.model.UtilClassKind +import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_PACKAGE_NAME +import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider +import org.utbot.framework.codegen.model.constructor.builtin.utUtilsClassId +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.MockFramework + +/** + * Information from [CgContext] that is relevant for the renderer. + * Not all the information from [CgContext] is required to render a class, + * so this more lightweight context is created for this purpose. + */ +internal class CgRendererContext( + val shouldOptimizeImports: Boolean, + val importedClasses: Set, + val importedStaticMethods: Set, + val classPackageName: String, + val generatedClass: ClassId, + val utilMethodProvider: UtilMethodProvider, + val codegenLanguage: CodegenLanguage, + val mockFrameworkUsed: Boolean, + val mockFramework: MockFramework, +) { + companion object { + fun fromCgContext(context: CgContext): CgRendererContext { + return CgRendererContext( + shouldOptimizeImports = context.shouldOptimizeImports, + importedClasses = context.importedClasses, + importedStaticMethods = context.importedStaticMethods, + classPackageName = context.testClassPackageName, + generatedClass = context.outerMostTestClass, + utilMethodProvider = context.utilMethodProvider, + codegenLanguage = context.codegenLanguage, + mockFrameworkUsed = context.mockFrameworkUsed, + mockFramework = context.mockFramework + ) + } + + fun fromUtilClassKind(utilClassKind: UtilClassKind, language: CodegenLanguage): CgRendererContext { + return CgRendererContext( + shouldOptimizeImports = false, + importedClasses = emptySet(), + importedStaticMethods = emptySet(), + classPackageName = UT_UTILS_PACKAGE_NAME, + generatedClass = utUtilsClassId, + utilMethodProvider = utilClassKind.utilMethodProvider, + codegenLanguage = language, + mockFrameworkUsed = utilClassKind.mockFrameworkUsed, + mockFramework = utilClassKind.mockFramework + ) + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt index 30c269e88c..839c611dc4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt @@ -1,5 +1,8 @@ package org.utbot.framework.codegen.model.visitor +import org.utbot.framework.codegen.model.tree.AbstractCgClass +import org.utbot.framework.codegen.model.tree.AbstractCgClassBody +import org.utbot.framework.codegen.model.tree.AbstractCgClassFile import org.utbot.framework.codegen.model.tree.CgAbstractFieldAccess import org.utbot.framework.codegen.model.tree.CgAbstractMultilineComment import org.utbot.framework.codegen.model.tree.CgAllocateArray @@ -58,6 +61,9 @@ import org.utbot.framework.codegen.model.tree.CgNonStaticRunnable import org.utbot.framework.codegen.model.tree.CgNotNullAssertion import org.utbot.framework.codegen.model.tree.CgParameterDeclaration import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.model.tree.CgRegularClass +import org.utbot.framework.codegen.model.tree.CgRegularClassBody +import org.utbot.framework.codegen.model.tree.CgRegularClassFile import org.utbot.framework.codegen.model.tree.CgReturnStatement import org.utbot.framework.codegen.model.tree.CgSimpleRegion import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation @@ -87,10 +93,16 @@ import org.utbot.framework.codegen.model.tree.CgWhileLoop interface CgVisitor { fun visit(element: CgElement): R + fun visit(element: AbstractCgClassFile<*>): R + fun visit(element: CgRegularClassFile): R fun visit(element: CgTestClassFile): R + fun visit(element: AbstractCgClass<*>): R + fun visit(element: CgRegularClass): R fun visit(element: CgTestClass): R + fun visit(element: AbstractCgClassBody): R + fun visit(element: CgRegularClassBody): R fun visit(element: CgTestClassBody): R fun visit(element: CgStaticsRegion): R diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt index 2b403f8730..4634590e6e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt @@ -1,22 +1,8 @@ package org.utbot.framework.codegen.model.visitor import org.utbot.framework.codegen.StaticImport -import org.utbot.framework.codegen.model.constructor.builtin.arraysDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.createArrayMethodId -import org.utbot.framework.codegen.model.constructor.builtin.createInstanceMethodId -import org.utbot.framework.codegen.model.constructor.builtin.deepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getArrayLengthMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getEnumConstantByNameMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getFieldValueMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getStaticFieldValueMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getUnsafeInstanceMethodId -import org.utbot.framework.codegen.model.constructor.builtin.hasCustomEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.iterablesDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.mapsDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.builtin.setFieldMethodId -import org.utbot.framework.codegen.model.constructor.builtin.setStaticFieldMethodId -import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId -import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.util.importIfNeeded import org.utbot.framework.plugin.api.ClassId @@ -29,33 +15,56 @@ import java.lang.reflect.Modifier import java.util.Arrays import java.util.Objects -internal fun ClassId.utilMethodById(id: MethodId, context: CgContext): String = - with(context) { +private enum class Visibility(val text: String) { + PRIVATE("private"), + @Suppress("unused") + PROTECTED("protected"), + PUBLIC("public"); + + infix fun by(language: CodegenLanguage): String { + if (this == PUBLIC && language == CodegenLanguage.KOTLIN) { + // public is default in Kotlin + return "" + } + return "$text " + } +} + +internal fun UtilMethodProvider.utilMethodTextById( + id: MethodId, + mockFrameworkUsed: Boolean, + mockFramework: MockFramework, + codegenLanguage: CodegenLanguage +): Result = runCatching { + // If util methods are declared in the test class, then they are private. Otherwise, they are public. + val visibility = if (this is TestClassUtilMethodProvider) Visibility.PRIVATE else Visibility.PUBLIC + with(this) { when (id) { - getUnsafeInstanceMethodId -> getUnsafeInstance(codegenLanguage) - createInstanceMethodId -> createInstance(codegenLanguage) - createArrayMethodId -> createArray(codegenLanguage) - setFieldMethodId -> setField(codegenLanguage) - setStaticFieldMethodId -> setStaticField(codegenLanguage) - getFieldValueMethodId -> getFieldValue(codegenLanguage) - getStaticFieldValueMethodId -> getStaticFieldValue(codegenLanguage) - getEnumConstantByNameMethodId -> getEnumConstantByName(codegenLanguage) - deepEqualsMethodId -> deepEquals(codegenLanguage, mockFrameworkUsed, mockFramework) - arraysDeepEqualsMethodId -> arraysDeepEquals(codegenLanguage) - iterablesDeepEqualsMethodId -> iterablesDeepEquals(codegenLanguage) - streamsDeepEqualsMethodId -> streamsDeepEquals(codegenLanguage) - mapsDeepEqualsMethodId -> mapsDeepEquals(codegenLanguage) - hasCustomEqualsMethodId -> hasCustomEquals(codegenLanguage) - getArrayLengthMethodId -> getArrayLength(codegenLanguage) + getUnsafeInstanceMethodId -> getUnsafeInstance(visibility, codegenLanguage) + createInstanceMethodId -> createInstance(visibility, codegenLanguage) + createArrayMethodId -> createArray(visibility, codegenLanguage) + setFieldMethodId -> setField(visibility, codegenLanguage) + setStaticFieldMethodId -> setStaticField(visibility, codegenLanguage) + getFieldValueMethodId -> getFieldValue(visibility, codegenLanguage) + getStaticFieldValueMethodId -> getStaticFieldValue(visibility, codegenLanguage) + getEnumConstantByNameMethodId -> getEnumConstantByName(visibility, codegenLanguage) + deepEqualsMethodId -> deepEquals(visibility, codegenLanguage, mockFrameworkUsed, mockFramework) + arraysDeepEqualsMethodId -> arraysDeepEquals(visibility, codegenLanguage) + iterablesDeepEqualsMethodId -> iterablesDeepEquals(visibility, codegenLanguage) + streamsDeepEqualsMethodId -> streamsDeepEquals(visibility, codegenLanguage) + mapsDeepEqualsMethodId -> mapsDeepEquals(visibility, codegenLanguage) + hasCustomEqualsMethodId -> hasCustomEquals(visibility, codegenLanguage) + getArrayLengthMethodId -> getArrayLength(visibility, codegenLanguage) else -> error("Unknown util method for class $this: $id") } } +} -fun getEnumConstantByName(language: CodegenLanguage): String = +private fun getEnumConstantByName(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object getEnumConstantByName(Class enumClass, String name) throws IllegalAccessException { + ${visibility by language}static Object getEnumConstantByName(Class enumClass, String name) throws IllegalAccessException { java.lang.reflect.Field[] fields = enumClass.getDeclaredFields(); for (java.lang.reflect.Field field : fields) { String fieldName = field.getName(); @@ -72,7 +81,7 @@ fun getEnumConstantByName(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getEnumConstantByName(enumClass: Class<*>, name: String): kotlin.Any? { + ${visibility by language}fun getEnumConstantByName(enumClass: Class<*>, name: String): kotlin.Any? { val fields: kotlin.Array = enumClass.declaredFields for (field in fields) { val fieldName = field.name @@ -89,11 +98,11 @@ fun getEnumConstantByName(language: CodegenLanguage): String = } }.trimIndent() -fun getStaticFieldValue(language: CodegenLanguage): String = +private fun getStaticFieldValue(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object getStaticFieldValue(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { + ${visibility by language}static Object getStaticFieldValue(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { java.lang.reflect.Field field; Class originClass = clazz; do { @@ -116,7 +125,7 @@ fun getStaticFieldValue(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getStaticFieldValue(clazz: Class<*>, fieldName: String): kotlin.Any? { + ${visibility by language}fun getStaticFieldValue(clazz: Class<*>, fieldName: String): kotlin.Any? { var currentClass: Class<*>? = clazz var field: java.lang.reflect.Field do { @@ -139,11 +148,11 @@ fun getStaticFieldValue(language: CodegenLanguage): String = } }.trimIndent() -fun getFieldValue(language: CodegenLanguage): String = +private fun getFieldValue(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object getFieldValue(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { + ${visibility by language}static Object getFieldValue(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { Class clazz = obj.getClass(); java.lang.reflect.Field field; do { @@ -166,7 +175,7 @@ fun getFieldValue(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getFieldValue(any: kotlin.Any, fieldName: String): kotlin.Any? { + ${visibility by language}fun getFieldValue(any: kotlin.Any, fieldName: String): kotlin.Any? { var clazz: Class<*>? = any.javaClass var field: java.lang.reflect.Field do { @@ -189,11 +198,11 @@ fun getFieldValue(language: CodegenLanguage): String = } }.trimIndent() -fun setStaticField(language: CodegenLanguage): String = +private fun setStaticField(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static void setStaticField(Class clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { + ${visibility by language}static void setStaticField(Class clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { java.lang.reflect.Field field; do { @@ -216,7 +225,7 @@ fun setStaticField(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun setStaticField(defaultClass: Class<*>, fieldName: String, fieldValue: kotlin.Any?) { + ${visibility by language}fun setStaticField(defaultClass: Class<*>, fieldName: String, fieldValue: kotlin.Any?) { var field: java.lang.reflect.Field? var clazz = defaultClass @@ -240,11 +249,11 @@ fun setStaticField(language: CodegenLanguage): String = } }.trimIndent() -fun setField(language: CodegenLanguage): String = +private fun setField(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static void setField(Object object, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { + ${visibility by language}static void setField(Object object, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { Class clazz = object.getClass(); java.lang.reflect.Field field; @@ -268,7 +277,7 @@ fun setField(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun setField(any: kotlin.Any, fieldName: String, fieldValue: kotlin.Any?) { + ${visibility by language}fun setField(any: kotlin.Any, fieldName: String, fieldValue: kotlin.Any?) { var clazz: Class<*> = any.javaClass var field: java.lang.reflect.Field? do { @@ -291,11 +300,11 @@ fun setField(language: CodegenLanguage): String = } }.trimIndent() -fun createArray(language: CodegenLanguage): String = +private fun createArray(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object[] createArray(String className, int length, Object... values) throws ClassNotFoundException { + ${visibility by language}static Object[] createArray(String className, int length, Object... values) throws ClassNotFoundException { Object array = java.lang.reflect.Array.newInstance(Class.forName(className), length); for (int i = 0; i < values.length; i++) { @@ -308,7 +317,7 @@ fun createArray(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun createArray( + ${visibility by language}fun createArray( className: String, length: Int, vararg values: kotlin.Any @@ -325,11 +334,11 @@ fun createArray(language: CodegenLanguage): String = } }.trimIndent() -fun createInstance(language: CodegenLanguage): String = +private fun createInstance(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object createInstance(String className) throws Exception { + ${visibility by language}static Object createInstance(String className) throws Exception { Class clazz = Class.forName(className); return Class.forName("sun.misc.Unsafe").getDeclaredMethod("allocateInstance", Class.class) .invoke(getUnsafeInstance(), clazz); @@ -338,7 +347,7 @@ fun createInstance(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun createInstance(className: String): kotlin.Any? { + ${visibility by language}fun createInstance(className: String): kotlin.Any? { val clazz: Class<*> = Class.forName(className) return Class.forName("sun.misc.Unsafe").getDeclaredMethod("allocateInstance", Class::class.java) .invoke(getUnsafeInstance(), clazz) @@ -347,11 +356,11 @@ fun createInstance(language: CodegenLanguage): String = } }.trimIndent() -fun getUnsafeInstance(language: CodegenLanguage): String = +private fun getUnsafeInstance(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object getUnsafeInstance() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + ${visibility by language}static Object getUnsafeInstance() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { java.lang.reflect.Field f = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe"); f.setAccessible(true); return f.get(null); @@ -360,7 +369,7 @@ fun getUnsafeInstance(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getUnsafeInstance(): kotlin.Any? { + ${visibility by language}fun getUnsafeInstance(): kotlin.Any? { val f: java.lang.reflect.Field = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe") f.isAccessible = true return f[null] @@ -375,13 +384,19 @@ fun getUnsafeInstance(language: CodegenLanguage): String = private fun isMockCondition(mockFrameworkUsed: Boolean, mockFramework: MockFramework): String { if (!mockFrameworkUsed) return "" - // TODO for now we have only Mockito but in can be changed in the future - if (mockFramework != MockFramework.MOCKITO) return "" - - return " && !org.mockito.Mockito.mockingDetails(o1).isMock()" + return when (mockFramework) { + MockFramework.MOCKITO -> " && !org.mockito.Mockito.mockingDetails(o1).isMock()" + // in case we will add any other mock frameworks, newer Kotlin compiler versions + // will report a non-exhaustive 'when', so we will not forget to support them here as well + } } -fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramework: MockFramework): String = +private fun deepEquals( + visibility: Visibility, + language: CodegenLanguage, + mockFrameworkUsed: Boolean, + mockFramework: MockFramework +): String = when (language) { CodegenLanguage.JAVA -> { """ @@ -399,20 +414,20 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; FieldsPair that = (FieldsPair) o; - return Objects.equals(o1, that.o1) && Objects.equals(o2, that.o2); + return java.util.Objects.equals(o1, that.o1) && java.util.Objects.equals(o2, that.o2); } @Override public int hashCode() { - return Objects.hash(o1, o2); + return java.util.Objects.hash(o1, o2); } } - private boolean deepEquals(Object o1, Object o2) { + ${visibility by language}static boolean deepEquals(Object o1, Object o2) { return deepEquals(o1, o2, new java.util.HashSet<>()); } - private boolean deepEquals(Object o1, Object o2, java.util.Set visited) { + private static boolean deepEquals(Object o1, Object o2, java.util.Set visited) { visited.add(new FieldsPair(o1, o2)); if (o1 == o2) { @@ -508,7 +523,7 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew } CodegenLanguage.KOTLIN -> { """ - private fun deepEquals(o1: kotlin.Any?, o2: kotlin.Any?): Boolean = deepEquals(o1, o2, hashSetOf()) + ${visibility by language}fun deepEquals(o1: kotlin.Any?, o2: kotlin.Any?): Boolean = deepEquals(o1, o2, hashSetOf()) private fun deepEquals( o1: kotlin.Any?, @@ -583,11 +598,11 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew } } -fun arraysDeepEquals(language: CodegenLanguage): String = +private fun arraysDeepEquals(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean arraysDeepEquals(Object arr1, Object arr2, java.util.Set visited) { + ${visibility by language}static boolean arraysDeepEquals(Object arr1, Object arr2, java.util.Set visited) { final int length = java.lang.reflect.Array.getLength(arr1); if (length != java.lang.reflect.Array.getLength(arr2)) { return false; @@ -605,7 +620,7 @@ fun arraysDeepEquals(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun arraysDeepEquals( + ${visibility by language}fun arraysDeepEquals( arr1: kotlin.Any?, arr2: kotlin.Any?, visited: kotlin.collections.MutableSet> @@ -625,11 +640,11 @@ fun arraysDeepEquals(language: CodegenLanguage): String = } } -fun iterablesDeepEquals(language: CodegenLanguage): String = +private fun iterablesDeepEquals(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean iterablesDeepEquals(Iterable i1, Iterable i2, java.util.Set visited) { + ${visibility by language}static boolean iterablesDeepEquals(Iterable i1, Iterable i2, java.util.Set visited) { final java.util.Iterator firstIterator = i1.iterator(); final java.util.Iterator secondIterator = i2.iterator(); while (firstIterator.hasNext() && secondIterator.hasNext()) { @@ -648,7 +663,7 @@ fun iterablesDeepEquals(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun iterablesDeepEquals( + ${visibility by language}fun iterablesDeepEquals( i1: Iterable<*>, i2: Iterable<*>, visited: kotlin.collections.MutableSet> @@ -665,11 +680,11 @@ fun iterablesDeepEquals(language: CodegenLanguage): String = } } -fun streamsDeepEquals(language: CodegenLanguage): String = +private fun streamsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean streamsDeepEquals( + ${visibility by language}static boolean streamsDeepEquals( java.util.stream.Stream s1, java.util.stream.Stream s2, java.util.Set visited @@ -692,7 +707,7 @@ fun streamsDeepEquals(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun streamsDeepEquals( + ${visibility by language}fun streamsDeepEquals( s1: java.util.stream.Stream<*>, s2: java.util.stream.Stream<*>, visited: kotlin.collections.MutableSet> @@ -709,11 +724,11 @@ fun streamsDeepEquals(language: CodegenLanguage): String = } } -fun mapsDeepEquals(language: CodegenLanguage): String = +private fun mapsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean mapsDeepEquals( + ${visibility by language}static boolean mapsDeepEquals( java.util.Map m1, java.util.Map m2, java.util.Set visited @@ -743,7 +758,7 @@ fun mapsDeepEquals(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun mapsDeepEquals( + ${visibility by language}fun mapsDeepEquals( m1: kotlin.collections.Map<*, *>, m2: kotlin.collections.Map<*, *>, visited: kotlin.collections.MutableSet> @@ -765,11 +780,11 @@ fun mapsDeepEquals(language: CodegenLanguage): String = } } -fun hasCustomEquals(language: CodegenLanguage): String = +private fun hasCustomEquals(visibility: Visibility, language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean hasCustomEquals(Class clazz) { + ${visibility by language}static boolean hasCustomEquals(Class clazz) { while (!Object.class.equals(clazz)) { try { clazz.getDeclaredMethod("equals", Object.class); @@ -786,7 +801,7 @@ fun hasCustomEquals(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun hasCustomEquals(clazz: Class<*>): Boolean { + ${visibility by language}fun hasCustomEquals(clazz: Class<*>): Boolean { var c = clazz while (kotlin.Any::class.java != c) { try { @@ -803,30 +818,36 @@ fun hasCustomEquals(language: CodegenLanguage): String = } } -fun getArrayLength(codegenLanguage: CodegenLanguage) = - when (codegenLanguage) { +private fun getArrayLength(visibility: Visibility, language: CodegenLanguage) = + when (language) { CodegenLanguage.JAVA -> """ - private static int getArrayLength(Object arr) { + ${visibility by language}static int getArrayLength(Object arr) { return java.lang.reflect.Array.getLength(arr); } """.trimIndent() CodegenLanguage.KOTLIN -> """ - private fun getArrayLength(arr: kotlin.Any?): Int = java.lang.reflect.Array.getLength(arr) + ${visibility by language}fun getArrayLength(arr: kotlin.Any?): Int = java.lang.reflect.Array.getLength(arr) """.trimIndent() } internal fun CgContextOwner.importUtilMethodDependencies(id: MethodId) { - for (classId in outerMostTestClass.regularImportsByUtilMethod(id, codegenLanguage)) { + // if util methods come from a separate UtUtils class and not from the test class, + // then we don't need to import any other methods, hence we return from method + val utilMethodProvider = utilMethodProvider as? TestClassUtilMethodProvider ?: return + for (classId in utilMethodProvider.regularImportsByUtilMethod(id, codegenLanguage)) { importIfNeeded(classId) } - for (methodId in outerMostTestClass.staticImportsByUtilMethod(id)) { + for (methodId in utilMethodProvider.staticImportsByUtilMethod(id)) { collectedImports += StaticImport(methodId.classId.canonicalName, methodId.name) } } -private fun ClassId.regularImportsByUtilMethod(id: MethodId, codegenLanguage: CodegenLanguage): List { +private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( + id: MethodId, + codegenLanguage: CodegenLanguage +): List { val fieldClassId = Field::class.id return when (id) { getUnsafeInstanceMethodId -> listOf(fieldClassId) @@ -876,4 +897,4 @@ private fun ClassId.regularImportsByUtilMethod(id: MethodId, codegenLanguage: Co // Note: for now always returns an empty list, because no util method // requires static imports, but this may change in the future @Suppress("unused", "unused_parameter") -private fun ClassId.staticImportsByUtilMethod(id: MethodId): List = emptyList() \ No newline at end of file +private fun TestClassUtilMethodProvider.staticImportsByUtilMethod(id: MethodId): List = emptyList() \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CheckersUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CheckersUtil.kt index 874fd112a1..093d9a73bd 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CheckersUtil.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CheckersUtil.kt @@ -26,6 +26,7 @@ data class TestFrameworkConfiguration( val codegenLanguage: CodegenLanguage, val forceStaticMocking: ForceStaticMocking, val resetNonFinalFieldsAfterClinit: Boolean = true, + val generateUtilClassFile: Boolean, val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.PASS, val enableTestsTimeout: Boolean = false // our tests should not fail due to timeout ) { @@ -83,7 +84,19 @@ val allTestFrameworkConfigurations: List = run { parametrizedTestSource, codegenLanguage, forceStaticMocking, - resetNonFinalFieldsAfterClinit + resetNonFinalFieldsAfterClinit, + generateUtilClassFile = false + ) + possibleConfiguration += TestFrameworkConfiguration( + testFramework, + mockFramework, + mockStrategy, + staticsMocking, + parametrizedTestSource, + codegenLanguage, + forceStaticMocking, + resetNonFinalFieldsAfterClinit, + generateUtilClassFile = true ) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CompilationAndRunUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CompilationAndRunUtils.kt index b476aa4dcb..ddf523586a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CompilationAndRunUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CompilationAndRunUtils.kt @@ -3,7 +3,6 @@ package org.utbot.tests.infrastructure import org.utbot.framework.plugin.api.CodegenLanguage import java.io.File import java.nio.file.Path -import mu.KotlinLogging import org.utbot.common.FileUtil import org.utbot.engine.logger import org.utbot.framework.codegen.Junit5 @@ -15,6 +14,13 @@ data class ClassUnderTest( val generatedTestFile: File ) +fun writeFile(fileContents: String, targetFile: File): File { + val targetDir = targetFile.parentFile + targetDir.mkdirs() + targetFile.writeText(fileContents) + return targetFile +} + fun writeTest( testContents: String, testClassName: String, @@ -27,13 +33,10 @@ fun writeTest( File(buildDirectory.toFile(), "${testClassName.substringAfterLast(".")}${generatedLanguage.extension}") ) - val targetDir = classUnderTest.generatedTestFile.parentFile - targetDir.mkdirs() logger.info { "File size for ${classUnderTest.testClassSimpleName}: ${FileUtil.byteCountToDisplaySize(testContents.length.toLong())}" } - classUnderTest.generatedTestFile.writeText(testContents) - return classUnderTest.generatedTestFile + return writeFile(testContents, classUnderTest.generatedTestFile) } fun compileTests( diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt index 3d0e569696..983dee4675 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt @@ -10,6 +10,9 @@ import org.utbot.framework.codegen.ParametrizedTestSource import org.utbot.framework.codegen.StaticsMocking import org.utbot.framework.codegen.TestFramework import org.utbot.framework.codegen.model.CodeGenerator +import org.utbot.framework.codegen.model.CodeGeneratorResult +import org.utbot.framework.codegen.model.UtilClassKind +import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_CLASS_NAME import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.MockFramework @@ -19,6 +22,8 @@ import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.description import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.withUtContext +import java.io.File +import java.nio.file.Path import kotlin.reflect.KClass private val logger = KotlinLogging.logger {} @@ -75,7 +80,8 @@ class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFram val codegenLanguage = testFrameworkConfiguration.codegenLanguage val parametrizedTestSource = testFrameworkConfiguration.parametrizedTestSource - val testClass = callToCodeGenerator(testSets, classUnderTest) + val codeGenerationResult = callToCodeGenerator(testSets, classUnderTest) + val testClass = codeGenerationResult.generatedCode // actual number of the tests in the generated testClass val generatedMethodsCount = testClass @@ -149,17 +155,33 @@ class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFram val testClassName = classPipeline.retrieveTestClassName("BrokenGeneratedTest") val generatedTestFile = writeTest(testClass, testClassName, buildDirectory, codegenLanguage) + val generatedUtilClassFile = codeGenerationResult.utilClassKind?.writeUtilClassToFile(buildDirectory, codegenLanguage) logger.error("Broken test has been written to the file: [$generatedTestFile]") + if (generatedUtilClassFile != null) { + logger.error("Util class for the broken test has been written to the file: [$generatedUtilClassFile]") + } logger.error("Failed configuration: $testFrameworkConfiguration") throw it } - classPipeline.stageContext = copy(data = testClass, stages = stages + information.completeStage()) + classPipeline.stageContext = copy(data = codeGenerationResult, stages = stages + information.completeStage()) } } + private fun UtilClassKind.writeUtilClassToFile(buildDirectory: Path, language: CodegenLanguage): File { + val utilClassFile = File(buildDirectory.toFile(), "$UT_UTILS_CLASS_NAME${language.extension}") + val utilClassText = getUtilClassText(language) + return writeFile(utilClassText, utilClassFile) + } + + private data class GeneratedTestClassInfo( + val testClassName: String, + val generatedTestFile: File, + val generatedUtilClassFile: File? + ) + @Suppress("UNCHECKED_CAST") private fun processCompilationStages(classesPipelines: List) { val information = StageExecutionInformation(Compilation) @@ -169,24 +191,34 @@ class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFram val codegenLanguage = testFrameworkConfiguration.codegenLanguage val testClassesNamesToTestGeneratedTests = classesPipelines.map { classPipeline -> - val testClass = classPipeline.stageContext.data as String + val codeGeneratorResult = classPipeline.stageContext.data as CodeGeneratorResult//String + val testClass = codeGeneratorResult.generatedCode + val testClassName = classPipeline.retrieveTestClassName("GeneratedTest") val generatedTestFile = writeTest(testClass, testClassName, buildDirectory, codegenLanguage) + val generatedUtilClassFile = codeGeneratorResult.utilClassKind?.writeUtilClassToFile(buildDirectory, codegenLanguage) logger.info("Test has been written to the file: [$generatedTestFile]") + if (generatedUtilClassFile != null) { + logger.info("Util class for the test has been written to the file: [$generatedUtilClassFile]") + } - testClassName to generatedTestFile + GeneratedTestClassInfo(testClassName, generatedTestFile, generatedUtilClassFile) } + val sourceFiles = mutableListOf().apply { + this += testClassesNamesToTestGeneratedTests.map { it.generatedTestFile.absolutePath } + this += testClassesNamesToTestGeneratedTests.mapNotNull { it.generatedUtilClassFile?.absolutePath } + } compileTests( "$buildDirectory", - testClassesNamesToTestGeneratedTests.map { it.second.absolutePath }, + sourceFiles, codegenLanguage ) - testClassesNamesToTestGeneratedTests.zip(classesPipelines) { testClassNameToTest, classPipeline -> + testClassesNamesToTestGeneratedTests.zip(classesPipelines) { generatedTestClassInfo, classPipeline -> classPipeline.stageContext = classPipeline.stageContext.copy( - data = CompilationResult("$buildDirectory", testClassNameToTest.first), + data = CompilationResult("$buildDirectory", generatedTestClassInfo.testClassName), stages = classPipeline.stageContext.stages + information.completeStage() ) } @@ -224,12 +256,13 @@ class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFram private fun callToCodeGenerator( testSets: List, classUnderTest: KClass<*> - ): String { + ): CodeGeneratorResult { val params = mutableMapOf>() val codeGenerator = with(testFrameworkConfiguration) { CodeGenerator( classUnderTest.id, + generateUtilClassFile = generateUtilClassFile, paramNames = params, testFramework = testFramework, staticsMocking = staticsMocking, @@ -243,7 +276,7 @@ class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFram } val testClassCustomName = "${classUnderTest.java.simpleName}GeneratedTest" - return codeGenerator.generateAsString(testSets, testClassCustomName) + return codeGenerator.generateAsStringWithTestReport(testSets, testClassCustomName) } private fun checkPipelinesResults(classesPipelines: List) { @@ -290,6 +323,7 @@ class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFram staticsMocking = StaticsMocking.defaultItem, parametrizedTestSource = ParametrizedTestSource.defaultItem, forceStaticMocking = ForceStaticMocking.defaultItem, + generateUtilClassFile = false ) private const val ERROR_REGION_BEGINNING = "///region Errors" 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 19da58144e..8d3eac68bd 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 @@ -5,12 +5,16 @@ import com.intellij.codeInsight.FileModificationService import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.fileTemplates.FileTemplateUtil import com.intellij.ide.fileTemplates.JavaTemplateUtil +import com.intellij.ide.highlighter.JavaFileType import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction import com.intellij.openapi.command.executeCommand import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.module.Module import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.util.Computable @@ -18,10 +22,13 @@ import com.intellij.openapi.wm.ToolWindowManager import com.intellij.psi.JavaDirectoryService import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiComment import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.codeStyle.JavaCodeStyleManager @@ -31,6 +38,7 @@ import com.intellij.testIntegration.TestIntegrationUtils import com.intellij.util.IncorrectOperationException import com.siyeh.ig.psiutils.ImportUtils import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass +import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.core.util.toPsiDirectory @@ -40,6 +48,7 @@ import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtPsiFactory import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.scripting.resolve.classId import org.utbot.common.HTML_LINE_SEPARATOR @@ -51,7 +60,9 @@ import org.utbot.framework.codegen.ParametrizedTestSource import org.utbot.framework.codegen.RegularImport import org.utbot.framework.codegen.StaticImport import org.utbot.framework.codegen.model.CodeGenerator -import org.utbot.framework.codegen.model.TestsCodeWithTestReport +import org.utbot.framework.codegen.model.CodeGeneratorResult +import org.utbot.framework.codegen.model.UtilClassKind +import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_CLASS_NAME import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ExecutableId @@ -70,6 +81,7 @@ import org.utbot.intellij.plugin.ui.TestsReportNotifier import org.utbot.intellij.plugin.ui.WarningTestsReportNotifier import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots import org.utbot.intellij.plugin.util.RunConfigurationHelper import org.utbot.intellij.plugin.util.extractClassMethodsIncludingNested import org.utbot.intellij.plugin.util.signature @@ -84,6 +96,14 @@ import org.utbot.intellij.plugin.util.IntelliJApiHelper.run object CodeGenerationController { + private class UtilClassListener { + var requiredUtilClassKind: UtilClassKind? = null + + fun onTestClassGenerated(result: CodeGeneratorResult) { + requiredUtilClassKind = maxOfNullable(requiredUtilClassKind, result.utilClassKind) + } + } + fun generateTests( model: GenerateTestsModel, testSetsByClass: Map>, @@ -96,19 +116,29 @@ object CodeGenerationController { val reports = mutableListOf() val testFiles = mutableListOf() + val utilClassListener = UtilClassListener() for (srcClass in testSetsByClass.keys) { val testSets = testSetsByClass[srcClass] ?: continue try { - val classPackageName = if (model.testPackageName.isNullOrEmpty()) - srcClass.containingFile.containingDirectory.getPackage()?.qualifiedName else model.testPackageName + val classPackageName = model.getTestClassPackageNameFor(srcClass) val testDirectory = allTestPackages[classPackageName] ?: baseTestDirectory val testClass = createTestClass(srcClass, testDirectory, model) ?: continue - val file = testClass.containingFile + val testClassFile = testClass.containingFile val cut = psi2KClass[srcClass] ?: error("Didn't find KClass instance for class ${srcClass.name}") runWriteCommandAction(model.project, "Generate tests with UtBot", null, { try { - generateCodeAndReport(srcClass, cut, testClass, file, testSets, model, latch, reports) - testFiles.add(file) + generateCodeAndReport( + srcClass, + cut, + testClass, + testClassFile, + testSets, + model, + latch, + reports, + utilClassListener + ) + testFiles.add(testClassFile) } catch (e: IncorrectOperationException) { showCreatingClassError(model.project, createTestClassName(srcClass)) } @@ -118,6 +148,25 @@ object CodeGenerationController { } } + + run(EDT_LATER) { + waitForCountDown(latch, timeout = 100, timeUnit = TimeUnit.MILLISECONDS) { + val requiredUtilClassKind = utilClassListener.requiredUtilClassKind + ?: return@waitForCountDown // no util class needed + + val existingUtilClass = model.codegenLanguage.getUtilClassOrNull(model.project, model.testModule) + val utilClassKind = newUtilClassKindOrNull(existingUtilClass, requiredUtilClassKind) + if (utilClassKind != null) { + createOrUpdateUtilClass( + testDirectory = baseTestDirectory, + utilClassKind = utilClassKind, + existingUtilClass = existingUtilClass, + model = model + ) + } + } + } + run(READ_ACTION) { val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) run(THREAD_POOL) { @@ -145,10 +194,281 @@ object CodeGenerationController { } } - private fun waitForCountDown(latch: CountDownLatch, action: Runnable) { + /** + * This method decides whether to overwrite an existing util class with a new one. And if so, then with what kind of util class. + * - If no util class exists, then we generate a new one. + * - If existing util class' version is out of date, then we overwrite it with a new one. + * But we use the maximum of two kinds (existing and the new one) to avoid problems with mocks. + * - If existing util class is up-to-date **and** has a greater or equal priority than the new one, + * then we do not need to overwrite it (return null). + * - Lastly, if the new util class kind has a greater priority than the existing one, + * then we do overwrite it with a newer version. + * + * @param existingUtilClass a [PsiFile] representing a file of an existing util class. If it does not exist, then [existingUtilClass] is `null`. + * @param requiredUtilClassKind the kind of the new util class that we attempt to generate. + * @return an [UtilClassKind] of a new util class that will be created or `null`, if no new util class is needed. + */ + private fun newUtilClassKindOrNull(existingUtilClass: PsiFile?, requiredUtilClassKind: UtilClassKind): UtilClassKind? { + if (existingUtilClass == null) { + // If no util class exists, then we should create a new one with the given kind. + return requiredUtilClassKind + } + + val existingUtilClassVersion = existingUtilClass.utilClassVersionOrNull ?: return requiredUtilClassKind + val newUtilClassVersion = requiredUtilClassKind.utilClassVersion + val versionIsUpdated = existingUtilClassVersion != newUtilClassVersion + + val existingUtilClassKind = existingUtilClass.utilClassKindOrNull ?: return requiredUtilClassKind + + if (versionIsUpdated) { + // If an existing util class is out of date, then we must overwrite it with a newer version. + // But we choose the kind with more priority, because it is possible that + // the existing util class needed mocks, but the new one doesn't. + // In this case we still want to support mocks, because the previously generated tests + // expect that the util class does support them. + return maxOfNullable(existingUtilClassKind, requiredUtilClassKind) + } + + if (requiredUtilClassKind <= existingUtilClassKind) { + // If the existing util class kind has a greater or equal priority than the new one we attempt to generate, + // then we should not do anything. The existing util class is already enough. + return null + } + + // The last case. The existing util class has a strictly less priority than the new one. + // So we generate the new one to overwrite the previous one with it. + return requiredUtilClassKind + } + + /** + * If [existingUtilClass] is null (no util class exists), then we create package directories for util class, + * create util class itself, and put it into the corresponding directory. + * Otherwise, we overwrite the existing util class with a new one. + * This is necessary in case if existing util class has no mocks support, but the newly generated tests do use mocks. + * So, we overwrite an util class with a new one that does support mocks. + * + * @param testDirectory root test directory where we will put our generated tests. + * @param utilClassKind kind of util class required by the test class(es) that we generated. + * @param existingUtilClass util class that already exists or null if it does not yet exist. + * @param model [GenerateTestsModel] that contains some useful information for util class generation. + */ + private fun createOrUpdateUtilClass( + testDirectory: PsiDirectory, + utilClassKind: UtilClassKind, + existingUtilClass: PsiFile?, + model: GenerateTestsModel + ) { + val language = model.codegenLanguage + + val utUtilsFile = if (existingUtilClass == null) { + // create a directory to put utils class into + val utilClassDirectory = createUtUtilSubdirectories(testDirectory) + // create util class file and put it into utils directory + createNewUtilClass(utilClassDirectory, language, utilClassKind, model) + } else { + overwriteUtilClass(existingUtilClass, utilClassKind, model) + } + + val utUtilsClass = runReadAction { + // there's only one class in the file + (utUtilsFile as PsiClassOwner).classes.first() + } + + runWriteCommandAction(model.project, "UtBot util class reformatting", null, { + reformat(model, utUtilsFile, utUtilsClass) + }) + + val utUtilsDocument = PsiDocumentManager + .getInstance(model.project) + .getDocument(utUtilsFile) ?: error("Failed to get a Document for UtUtils file") + + unblockDocument(model.project, utUtilsDocument) + } + + private fun overwriteUtilClass( + existingUtilClass: PsiFile, + utilClassKind: UtilClassKind, + model: GenerateTestsModel + ): PsiFile { + val utilsClassDocument = PsiDocumentManager + .getInstance(model.project) + .getDocument(existingUtilClass) + ?: error("Failed to get Document for UtUtils class PsiFile: ${existingUtilClass.name}") + + val utUtilsText = utilClassKind.getUtilClassText(model.codegenLanguage) + + run(EDT_LATER) { + run(WRITE_ACTION) { + unblockDocument(model.project, utilsClassDocument) + executeCommand { + utilsClassDocument.setText(utUtilsText) + } + unblockDocument(model.project, utilsClassDocument) + } + } + return existingUtilClass + } + + /** + * This method creates an util class file and adds it into [utilClassDirectory]. + * + * @param utilClassDirectory directory to put util class into. + * @param language language of util class. + * @param utilClassKind kind of util class required by the test class(es) that we generated. + * @param model [GenerateTestsModel] that contains some useful information for util class generation. + */ + private fun createNewUtilClass( + utilClassDirectory: PsiDirectory, + language: CodegenLanguage, + utilClassKind: UtilClassKind, + model: GenerateTestsModel, + ): PsiFile { + val utUtilsName = language.utilClassFileName + + val utUtilsText = utilClassKind.getUtilClassText(model.codegenLanguage) + + val utUtilsFile = runReadAction { + PsiFileFactory.getInstance(model.project) + .createFileFromText( + utUtilsName, + model.codegenLanguage.fileType, + utUtilsText + ) + } + + // add UtUtils class file into the utils directory + runWriteCommandAction(model.project) { + utilClassDirectory.add(utUtilsFile) + } + + return utUtilsFile + } + + /** + * Util class must have a comment that specifies its version. + * This property represents the version specified by this comment if it exists. Otherwise, the property is `null`. + */ + private val PsiFile.utilClassVersionOrNull: String? + get() = runReadAction { + val utilClass = (this as? PsiClassOwner) + ?.classes + ?.firstOrNull() + ?: return@runReadAction null + + utilClass.getChildrenOfType() + .map { comment -> comment.text } + .firstOrNull { text -> UtilClassKind.UTIL_CLASS_VERSION_COMMENT_PREFIX in text } + ?.substringAfterLast(UtilClassKind.UTIL_CLASS_VERSION_COMMENT_PREFIX) + ?.trim() + } + + /** + * Util class must have a comment that specifies its kind. + * This property obtains the kind specified by this comment if it exists. Otherwise, the property is `null`. + */ + private val PsiFile.utilClassKindOrNull: UtilClassKind? + get() = runReadAction { + val utilClass = (this as? PsiClassOwner) + ?.classes + ?.firstOrNull() + ?: return@runReadAction null + + utilClass.getChildrenOfType() + .map { comment -> comment.text } + .mapNotNull { text -> UtilClassKind.utilClassKindByCommentOrNull(text) } + .firstOrNull() + } + + /** + * @param srcClass class under test + * @return name of the package of a given [srcClass]. + * Null is returned if [PsiDirectory.getPackage] call returns null for the [srcClass] directory. + */ + private fun GenerateTestsModel.getTestClassPackageNameFor(srcClass: PsiClass): String? { + return when { + testPackageName.isNullOrEmpty() -> srcClass.containingFile.containingDirectory.getPackage()?.qualifiedName + else -> testPackageName + } + } + + private val CodegenLanguage.utilClassFileName: String + get() = "$UT_UTILS_CLASS_NAME${this.extension}" + + /** + * @param testDirectory root test directory where we will put our generated tests. + * @return directory for util class if it exists or null otherwise. + */ + private fun getUtilDirectoryOrNull(testDirectory: PsiDirectory): PsiDirectory? { + val directoryNames = UtilClassKind.utilsPackages + var currentDirectory = testDirectory + for (name in directoryNames) { + val subdirectory = runReadAction { currentDirectory.findSubdirectory(name) } ?: return null + currentDirectory = subdirectory + } + return currentDirectory + } + + /** + * @param testDirectory root test directory where we will put our generated tests. + * @return file of util class if it exists or null otherwise. + */ + private fun CodegenLanguage.getUtilClassOrNull(testDirectory: PsiDirectory): PsiFile? { + return runReadAction { + val utilDirectory = getUtilDirectoryOrNull(testDirectory) + utilDirectory?.findFile(this.utilClassFileName) + } + } + + /** + * @param project project whose classes we generate tests for. + * @param testModule module where the generated tests will be placed. + * @return an existing util class from one of the test source roots + * in the given [testModule] or `null` if no util class was found. + */ + private fun CodegenLanguage.getUtilClassOrNull(project: Project, testModule: Module): PsiFile? { + val psiManager = PsiManager.getInstance(project) + + // all test roots for the given test module + val testRoots = runReadAction { + testModule + .suitableTestSourceRoots(this) + .mapNotNull { psiManager.findDirectory(it) } + } + + // return an util class from one of the test source roots or null if no util class was found + return testRoots + .mapNotNull { testRoot -> getUtilClassOrNull(testRoot) } + .firstOrNull() + } + + /** + * Create all package directories for UtUtils class. + * @return the innermost directory - utils from `org.utbot.runtime.utils` + */ + private fun createUtUtilSubdirectories(baseTestDirectory: PsiDirectory): PsiDirectory { + val directoryNames = UtilClassKind.utilsPackages + var currentDirectory = baseTestDirectory + runWriteCommandAction(baseTestDirectory.project) { + for (name in directoryNames) { + currentDirectory = currentDirectory.findSubdirectory(name) ?: currentDirectory.createSubdirectory(name) + } + } + return currentDirectory + } + + /** + * @return Java or Kotlin file type depending on the given [CodegenLanguage] + */ + private val CodegenLanguage.fileType: FileType + get() = when (this) { + CodegenLanguage.JAVA -> JavaFileType.INSTANCE + CodegenLanguage.KOTLIN -> KotlinFileType.INSTANCE + } + + private fun waitForCountDown(latch: CountDownLatch, timeout: Long = 5, timeUnit: TimeUnit = TimeUnit.SECONDS, action: Runnable) { try { - if (!latch.await(5, TimeUnit.SECONDS)) { - run(THREAD_POOL) { waitForCountDown(latch, action) } + if (!latch.await(timeout, timeUnit)) { + run(THREAD_POOL) { waitForCountDown(latch, timeout, timeUnit, action) } } else { action.run() } @@ -254,33 +574,36 @@ object CodeGenerationController { model: GenerateTestsModel, reportsCountDown: CountDownLatch, reports: MutableList, + utilClassListener: UtilClassListener ) { val classMethods = srcClass.extractClassMethodsIncludingNested(false) val paramNames = DumbService.getInstance(model.project) .runReadActionInSmartMode(Computable { findMethodParamNames(classUnderTest, classMethods) }) val codeGenerator = CodeGenerator( - classUnderTest = classUnderTest.id, - paramNames = paramNames.toMutableMap(), - testFramework = model.testFramework, - mockFramework = model.mockFramework, - codegenLanguage = model.codegenLanguage, - parameterizedTestSource = model.parametrizedTestSource, - staticsMocking = model.staticsMocking, - forceStaticMocking = model.forceStaticMocking, - generateWarningsForStaticMocking = model.generateWarningsForStaticMocking, - runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour, - hangingTestsTimeout = model.hangingTestsTimeout, - enableTestsTimeout = true, - testClassPackageName = testClass.packageName - ) + classUnderTest = classUnderTest.id, + generateUtilClassFile = true, + paramNames = paramNames.toMutableMap(), + testFramework = model.testFramework, + mockFramework = model.mockFramework, + codegenLanguage = model.codegenLanguage, + parameterizedTestSource = model.parametrizedTestSource, + staticsMocking = model.staticsMocking, + forceStaticMocking = model.forceStaticMocking, + generateWarningsForStaticMocking = model.generateWarningsForStaticMocking, + runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour, + hangingTestsTimeout = model.hangingTestsTimeout, + enableTestsTimeout = true, + testClassPackageName = testClass.packageName + ) val editor = CodeInsightUtil.positionCursorAtLBrace(testClass.project, file, testClass) //TODO: Use PsiDocumentManager.getInstance(model.project).getDocument(file) // if we don't want to open _all_ new files with tests in editor one-by-one run(THREAD_POOL) { - val testsCodeWithTestReport = codeGenerator.generateAsStringWithTestReport(testSets) - val generatedTestsCode = testsCodeWithTestReport.generatedCode + val codeGenerationResult = codeGenerator.generateAsStringWithTestReport(testSets) + utilClassListener.onTestClassGenerated(codeGenerationResult) + val generatedTestsCode = codeGenerationResult.generatedCode run(EDT_LATER) { run(WRITE_ACTION) { unblockDocument(testClass.project, editor.document) @@ -305,17 +628,18 @@ object CodeGenerationController { unblockDocument(testClassUpdated.project, editor.document) // uploading formatted code - val testsCodeWithTestReportFormatted = - testsCodeWithTestReport.copy(generatedCode = file.text) + val codeGenerationResultFormatted = + codeGenerationResult.copy(generatedCode = file.text) // creating and saving reports - reports += testsCodeWithTestReportFormatted.testsGenerationReport + reports += codeGenerationResultFormatted.testsGenerationReport + run(WRITE_ACTION) { saveSarifReport( testClassUpdated, testSets, model, - testsCodeWithTestReportFormatted, + codeGenerationResultFormatted, ) } unblockDocument(testClassUpdated.project, editor.document) @@ -358,7 +682,7 @@ object CodeGenerationController { testClass: PsiClass, testSets: List, model: GenerateTestsModel, - testsCodeWithTestReport: TestsCodeWithTestReport, + testsCodeWithTestReport: CodeGeneratorResult, ) { val project = model.project val generatedTestsCode = testsCodeWithTestReport.generatedCode @@ -579,4 +903,12 @@ object CodeGenerationController { title = "Failed to Create Class" ) } + + private fun > maxOfNullable(a: T?, b: T?): T? { + return when { + a == null -> b + b == null -> a + else -> maxOf(a, b) + } + } } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt index 66c0a41df7..5c4928a241 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt @@ -48,7 +48,7 @@ data class GenerateTestsModel( var testPackageName: String? = null lateinit var testFramework: TestFramework lateinit var mockStrategy: MockStrategyApi - var mockFramework: MockFramework? = null + lateinit var mockFramework: MockFramework lateinit var staticsMocking: StaticsMocking lateinit var parametrizedTestSource: ParametrizedTestSource lateinit var codegenLanguage: CodegenLanguage diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt index fd2a024f88..9fa4228dcb 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt @@ -511,10 +511,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m // then process force static mocking case model.generateWarningsForStaticMocking = model.staticsMocking is NoStaticMocking if (model.forceStaticMocking == ForceStaticMocking.FORCE) { - // we have to use mock framework to mock statics, no user provided => choose default - if (model.mockFramework == null) { - model.mockFramework = MockFramework.defaultItem - } // we need mock framework extension to mock statics, no user provided => choose default if (model.staticsMocking is NoStaticMocking) { model.staticsMocking = StaticsMocking.defaultItem diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt index af19f7f275..cc82cde6b6 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt @@ -1,12 +1,12 @@ package org.utbot.intellij.plugin.ui.utils import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.util.Patterns import org.utbot.framework.codegen.model.util.patterns import org.utbot.framework.plugin.api.MockFramework import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.roots.LibraryOrderEntry +import org.utbot.framework.codegen.model.util.Patterns fun findFrameworkLibrary( project: Project,