diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt index a44941c8a0..f79ee02149 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt @@ -183,6 +183,11 @@ object UtSettings { var testName by getBooleanProperty(true) var testDisplayName by getBooleanProperty(true) + /** + * Generate summaries using plugin's custom JavaDoc tags. + */ + var useCustomJavaDocTags by getBooleanProperty(false) + /** * Enable the machine learning module to generate summaries for methods under test. * True by default. diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt index 143b9d5d48..18489e9094 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt @@ -1276,6 +1276,9 @@ class DocPreTagStatement(content: List) : DocTagStatement(content) override fun hashCode(): Int = content.hashCode() } +data class DocCustomTagStatement(val statements: List) : DocTagStatement(statements) { + override fun toString(): String = content.joinToString(separator = "") +} open class DocClassLinkStmt(val className: String) : DocStatement() { override fun toString(): String = className 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 ec78d73dea..4c84ce2a8c 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 @@ -11,6 +11,7 @@ import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.DocClassLinkStmt import org.utbot.framework.plugin.api.DocCodeStmt +import org.utbot.framework.plugin.api.DocCustomTagStatement import org.utbot.framework.plugin.api.DocMethodLinkStmt import org.utbot.framework.plugin.api.DocPreTagStatement import org.utbot.framework.plugin.api.DocRegularStmt @@ -52,6 +53,7 @@ interface CgElement { is CgMultilineComment -> visit(element) is CgDocumentationComment -> visit(element) is CgDocPreTagStatement -> visit(element) + is CgCustomTagStatement -> visit(element) is CgDocCodeStmt -> visit(element) is CgDocRegularStmt -> visit(element) is CgDocClassLinkStmt -> visit(element) @@ -335,6 +337,11 @@ class CgDocPreTagStatement(content: List) : CgDocTagStatement(co override fun hashCode(): Int = content.hashCode() } +/** + * Represents a type for statements containing custom JavaDoc tags. + */ +data class CgCustomTagStatement(val statements: List) : CgDocTagStatement(statements) + class CgDocCodeStmt(val stmt: String) : CgDocStatement() { override fun isEmpty(): Boolean = stmt.isEmpty() @@ -379,6 +386,10 @@ fun convertDocToCg(stmt: DocStatement): CgDocStatement { val stmts = stmt.content.map { convertDocToCg(it) } CgDocPreTagStatement(content = stmts) } + is DocCustomTagStatement -> { + val stmts = stmt.content.map { convertDocToCg(it) } + CgCustomTagStatement(statements = stmts) + } is DocRegularStmt -> CgDocRegularStmt(stmt = stmt.stmt) is DocClassLinkStmt -> CgDocClassLinkStmt(className = stmt.className) is DocMethodLinkStmt -> CgDocMethodLinkStmt(methodName = stmt.methodName, stmt = stmt.className) 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 92e4aebf78..77b739d174 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 @@ -16,6 +16,7 @@ import org.utbot.framework.codegen.model.tree.CgComment import org.utbot.framework.codegen.model.tree.CgCommentedAnnotation import org.utbot.framework.codegen.model.tree.CgComparison import org.utbot.framework.codegen.model.tree.CgContinueStatement +import org.utbot.framework.codegen.model.tree.CgCustomTagStatement import org.utbot.framework.codegen.model.tree.CgDeclaration import org.utbot.framework.codegen.model.tree.CgDecrement import org.utbot.framework.codegen.model.tree.CgDoWhileLoop @@ -309,11 +310,19 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: } override fun visit(element: CgDocPreTagStatement) { if (element.content.all { it.isEmpty() }) return - println("
")
         for (stmt in element.content) stmt.accept(this)
         println("
") } + + override fun visit(element: CgCustomTagStatement) { + if (element.statements.all { it.isEmpty() }) return + + for (stmt in element.statements) { + stmt.accept(this) + } + } + override fun visit(element: CgDocCodeStmt) { if (element.isEmpty()) return 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 2d341a51d1..30c269e88c 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 @@ -22,6 +22,7 @@ import org.utbot.framework.codegen.model.tree.CgDocClassLinkStmt import org.utbot.framework.codegen.model.tree.CgDocCodeStmt import org.utbot.framework.codegen.model.tree.CgDocMethodLinkStmt import org.utbot.framework.codegen.model.tree.CgDocPreTagStatement +import org.utbot.framework.codegen.model.tree.CgCustomTagStatement import org.utbot.framework.codegen.model.tree.CgDocRegularStmt import org.utbot.framework.codegen.model.tree.CgDocumentationComment import org.utbot.framework.codegen.model.tree.CgElement @@ -122,6 +123,7 @@ interface CgVisitor { // Comment statements fun visit(element: CgDocPreTagStatement): R + fun visit(element: CgCustomTagStatement): R fun visit(element: CgDocCodeStmt): R fun visit(element: CgDocRegularStmt): R fun visit(element: CgDocClassLinkStmt): R diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/UtValueTestCaseChecker.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/UtValueTestCaseChecker.kt index db442b9604..a72f2b446b 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/UtValueTestCaseChecker.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/UtValueTestCaseChecker.kt @@ -31,6 +31,7 @@ import org.utbot.framework.coverage.toAtLeast import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.DocClassLinkStmt import org.utbot.framework.plugin.api.DocCodeStmt +import org.utbot.framework.plugin.api.DocCustomTagStatement import org.utbot.framework.plugin.api.DocMethodLinkStmt import org.utbot.framework.plugin.api.DocPreTagStatement import org.utbot.framework.plugin.api.DocRegularStmt @@ -2746,6 +2747,7 @@ private fun flattenDocStatements(summary: List): List flatten.add(s) is DocCodeStmt -> flatten.add(s) is DocRegularStmt -> flatten.add(s) + is DocCustomTagStatement -> flatten.add(s) } } return flatten diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt new file mode 100644 index 0000000000..c82e37b027 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt @@ -0,0 +1,32 @@ +package org.utbot.intellij.plugin.javadoc + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiReference +import com.intellij.psi.javadoc.CustomJavadocTagProvider +import com.intellij.psi.javadoc.JavadocTagInfo +import com.intellij.psi.javadoc.PsiDocTagValue +import org.utbot.summary.comment.CustomJavaDocTag +import org.utbot.summary.comment.CustomJavaDocTagProvider + +/** + * Provides plugin's custom JavaDoc tags to make test summaries structured. + */ +class UtCustomJavaDocTagProvider : CustomJavadocTagProvider { + override fun getSupportedTags(): List = + CustomJavaDocTagProvider().getPluginCustomTags().map { UtCustomTagInfo(it) } + + class UtCustomTagInfo(private val tag: CustomJavaDocTag) : JavadocTagInfo { + override fun getName(): String = tag.name + + fun getMessage(): String = tag.message + + override fun isInline() = false + + override fun checkTagValue(value: PsiDocTagValue?): String? = null + + override fun getReference(value: PsiDocTagValue?): PsiReference? = null + + override fun isValidInContext(element: PsiElement?): Boolean = element is PsiMethod + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtDocumentationProvider.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtDocumentationProvider.kt new file mode 100644 index 0000000000..e8b9be9757 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtDocumentationProvider.kt @@ -0,0 +1,46 @@ +package org.utbot.intellij.plugin.javadoc + +import com.intellij.codeInsight.javadoc.JavaDocExternalFilter +import com.intellij.codeInsight.javadoc.JavaDocInfoGeneratorFactory +import com.intellij.lang.java.JavaDocumentationProvider +import com.intellij.psi.PsiDocCommentBase +import com.intellij.psi.PsiJavaDocumentedElement + +/** + * To render UtBot custom JavaDoc tags messages, we need to override basic behaviour of [JavaDocumentationProvider]. + * The IJ platform knows only custom tag names, so we need to add their messages in rendered comments to make it look nice. + */ +class UtDocumentationProvider : JavaDocumentationProvider() { + override fun generateRenderedDoc(comment: PsiDocCommentBase): String? { + val target = comment.owner ?: comment + + if (target !is PsiJavaDocumentedElement) { + return "" + } + + val baseJavaDocInfoGenerator = JavaDocInfoGeneratorFactory.getBuilder(target.getProject()) + .setPsiElement(target) + .setIsGenerationForRenderedDoc(true) + .create() + + val finalDocContent = replaceTagNamesWithMessages(baseJavaDocInfoGenerator.generateRenderedDocInfo()) + + return JavaDocExternalFilter.filterInternalDocInfo(finalDocContent) + } + + /** + * Replaces names of plugin's custom JavaDoc tags with their messages in the comment generated by the IJ platform. + * Example: utbot.MethodUnderTest -> Method under test. + */ + private fun replaceTagNamesWithMessages(comment: String?) = + comment?.let { + val docTagProvider = UtCustomJavaDocTagProvider() + docTagProvider.supportedTags.fold(it) { result, tag -> + if (result.contains(tag.name)) { + result.replace(tag.name, "${tag.getMessage()}:") + } else { + result + } + } + } ?: "" +} \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/plugin.xml b/utbot-intellij/src/main/resources/META-INF/plugin.xml index 87742b96f5..ee853869d9 100644 --- a/utbot-intellij/src/main/resources/META-INF/plugin.xml +++ b/utbot-intellij/src/main/resources/META-INF/plugin.xml @@ -34,6 +34,9 @@ + + + diff --git a/utbot-summary-tests/src/test/kotlin/examples/CustomJavaDocTagsEnabler.kt b/utbot-summary-tests/src/test/kotlin/examples/CustomJavaDocTagsEnabler.kt new file mode 100644 index 0000000000..5929caad89 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/CustomJavaDocTagsEnabler.kt @@ -0,0 +1,25 @@ +package examples + +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.utbot.framework.UtSettings + +/** + * Controls the value of useCustomJavaDocTags global variable. + * + * Should be used in summary tests containing custom JavaDoc tags. + * To use it, add an annotation @ExtendWith(CustomJavaDocTagsEnabler::class) under test class. + */ +class CustomJavaDocTagsEnabler(private val enable: Boolean = true) : BeforeEachCallback, AfterEachCallback { + private var previousValue = false + + override fun beforeEach(context: ExtensionContext?) { + previousValue = UtSettings.useCustomJavaDocTags + UtSettings.useCustomJavaDocTags = enable + } + + override fun afterEach(context: ExtensionContext?) { + UtSettings.useCustomJavaDocTags = previousValue + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryConditionsTest.kt b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryConditionsTest.kt new file mode 100644 index 0000000000..79947a0817 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryConditionsTest.kt @@ -0,0 +1,54 @@ +package examples.controlflow + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.controlflow.Conditions +import org.utbot.framework.plugin.api.MockStrategyApi + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class SummaryConditionsTest : SummaryTestCaseGeneratorTest( + Conditions::class +) { + @Test + fun testSimpleCondition() { + val summary1 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#simpleCondition(boolean)}\n" + + "@utbot.executesCondition {@code (condition): False}\n" + + "@utbot.returnsFrom {@code return 0;}" + + val summary2 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#simpleCondition(boolean)}\n" + + "@utbot.executesCondition {@code (condition): True}\n" + + "@utbot.returnsFrom {@code return 1;}" + + val methodName1 = "testSimpleCondition_NotCondition" + val methodName2 = "testSimpleCondition_Condition" + + val displayName1 = "condition : False -> return 0" + val displayName2 = "condition : True -> return 1" + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + val method = Conditions::simpleCondition + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionClusteringExamplesTest.kt b/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionClusteringExamplesTest.kt new file mode 100644 index 0000000000..bc51997a1e --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionClusteringExamplesTest.kt @@ -0,0 +1,77 @@ +package examples.exceptions + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.exceptions.ExceptionClusteringExamples +import org.utbot.framework.plugin.api.MockStrategyApi + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class SummaryExceptionClusteringExamplesTest : SummaryTestCaseGeneratorTest( + ExceptionClusteringExamples::class +) { + @Test + fun testDifferentExceptions() { + val summary1 = "@utbot.classUnderTest {@link ExceptionClusteringExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionClusteringExamples#differentExceptions(int)}\n" + + "@utbot.executesCondition {@code (i == 0): True}\n" + + "@utbot.throwsException {@link java.lang.ArithmeticException} in: return 100 / i;" + + val summary2 = "@utbot.classUnderTest {@link ExceptionClusteringExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionClusteringExamples#differentExceptions(int)}\n" + + "@utbot.executesCondition {@code (i == 0): False},\n" + + "{@code (i == 1): True}\n" + + "@utbot.throwsException {@link org.utbot.examples.exceptions.MyCheckedException} after condition: i == 1" + val summary3 = "@utbot.classUnderTest {@link ExceptionClusteringExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionClusteringExamples#differentExceptions(int)}\n" + + "@utbot.executesCondition {@code (i == 0): False},\n" + + "{@code (i == 1): False},\n" + + "{@code (i == 2): True}\n" + + "@utbot.throwsException {@link java.lang.IllegalArgumentException} after condition: i == 2" + val summary4 = "@utbot.classUnderTest {@link ExceptionClusteringExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionClusteringExamples#differentExceptions(int)}\n" + + "@utbot.executesCondition {@code (i == 0): False},\n" + + "{@code (i == 1): False},\n" + + "{@code (i == 2): False}\n" + + "@utbot.returnsFrom {@code return i * 2;}\n" + + val methodName1 = "testDifferentExceptions_IEqualsZero" + val methodName2 = "testDifferentExceptions_IEquals1" + val methodName3 = "testDifferentExceptions_IEquals2" + val methodName4 = "testDifferentExceptions_INotEquals2" + + val displayName1 = "return 100 / i : True -> ThrowArithmeticException" + val displayName2 = "i == 1 -> ThrowMyCheckedException" + val displayName3 = "i == 2 -> ThrowIllegalArgumentException" + val displayName4 = "i == 0 : False -> return i * 2" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + val method = ExceptionClusteringExamples::differentExceptions + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/structures/SummaryMinStackTest.kt b/utbot-summary-tests/src/test/kotlin/examples/structures/SummaryMinStackTest.kt new file mode 100644 index 0000000000..5cfca1201b --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/structures/SummaryMinStackTest.kt @@ -0,0 +1,205 @@ +package examples.structures + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.structures.MinStack +import org.utbot.framework.plugin.api.MockStrategyApi + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class SummaryMinStackTest : SummaryTestCaseGeneratorTest( + MinStack::class +) { + @Test + fun testGetMin() { + val summary1 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#getMin()}\n" + + "@utbot.throwsException {@link java.lang.ArrayIndexOutOfBoundsException} in: return minStack[size - 1];" + + val summary2 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#getMin()}\n" + + "@utbot.throwsException {@link java.lang.NullPointerException} in: return minStack[size - 1];" + + val summary3 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#getMin()}\n" + + "@utbot.returnsFrom {@code return minStack[size - 1];}\n" + + val methodName1 = "testGetMin_ThrowArrayIndexOutOfBoundsException" + val methodName2 = "testGetMin_ThrowNullPointerException" + val methodName3 = "testGetMin_ReturnSize1OfMinStack" + + val displayName1 = "return minStack[size - 1] : True -> ThrowArrayIndexOutOfBoundsException" + val displayName2 = "return minStack[size - 1] : True -> ThrowNullPointerException" + val displayName3 = "-> return minStack[size - 1]" + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + val method = MinStack::getMin + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testRemoveValue() { + val summary1 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#removeValue()}\n" + + "@utbot.executesCondition {@code (size <= 0): True}\n" + + "@utbot.throwsException {@link java.lang.RuntimeException} after condition: size <= 0" + + val summary2 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#removeValue()}\n" + + "@utbot.executesCondition {@code (size <= 0): False}\n" + + val methodName1 = "testRemoveValue_SizeLessOrEqualZero" + val methodName2 = "testRemoveValue_SizeGreaterThanZero" + + val displayName1 = "size <= 0 -> ThrowRuntimeException" + val displayName2 = "-> size <= 0 : False" + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + val method = MinStack::removeValue + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testAddValue() { + val summary1 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.throwsException {@link java.lang.ArrayIndexOutOfBoundsException} in: stack[size] = value;" + + val summary2 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.throwsException {@link java.lang.NullPointerException} in: stack[size] = value;" + + val summary3 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.executesCondition {@code (size == 0): True}\n" + + "@utbot.throwsException {@link java.lang.ArrayIndexOutOfBoundsException} in: minStack[size] = value;" + + val summary4 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.executesCondition {@code (size == 0): True}\n" + + "@utbot.throwsException {@link java.lang.NullPointerException} in: minStack[size] = value;" + + val summary5 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.executesCondition {@code (size == 0): False}\n" + + "@utbot.throwsException {@link java.lang.NullPointerException} in: minStack[size] = Math.min(minStack[size - 1], value);" + + val summary6 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.executesCondition {@code (size == 0): False}\n" + + "@utbot.throwsException {@link java.lang.ArrayIndexOutOfBoundsException} in: minStack[size] = Math.min(minStack[size - 1], value);" + val summary7 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.executesCondition {@code (size == 0): False}\n" + + "@utbot.invokes {@code {@link java.lang.Math#min(long,long)}}\n" + + "@utbot.throwsException {@link java.lang.ArrayIndexOutOfBoundsException} in: minStack[size] = Math.min(minStack[size - 1], value);" + val summary8 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.executesCondition {@code (size == 0): True}\n" + val summary9 = "@utbot.classUnderTest {@link MinStack}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#addValue(long)}\n" + + "@utbot.executesCondition {@code (size == 0): False}\n" + + "@utbot.invokes {@code {@link java.lang.Math#min(long,long)}}\n" + + val methodName1 = "testAddValue_ThrowArrayIndexOutOfBoundsException" + val methodName2 = "testAddValue_ThrowNullPointerException" + val methodName3 = "testAddValue_ThrowArrayIndexOutOfBoundsException_1" + val methodName4 = "testAddValue_ThrowNullPointerException_1" + val methodName5 = "testAddValue_ThrowNullPointerException_2" + val methodName6 = "testAddValue_ThrowArrayIndexOutOfBoundsException_2" + val methodName7 = "testAddValue_MathMin" + val methodName8 = "testAddValue_SizeEqualsZero" + val methodName9 = "testAddValue_SizeNotEqualsZero" + + val displayName1 = "stack[size] = value -> ThrowArrayIndexOutOfBoundsException" + val displayName2 = "stack[size] = value -> ThrowNullPointerException" + val displayName3 = "minStack[size] = value -> ThrowArrayIndexOutOfBoundsException" + val displayName4 = "minStack[size] = value -> ThrowNullPointerException" + val displayName5 = "minStack[size] = Math.min(minStack[size - 1], value) -> ThrowNullPointerException" + val displayName6 = "minStack[size] = Math.min(minStack[size - 1], value) -> ThrowArrayIndexOutOfBoundsException" + val displayName7 = "minStack[size] = Math.min(minStack[size - 1], value) -> ThrowArrayIndexOutOfBoundsException" + val displayName8 = "-> size == 0 : True" + val displayName9 = "size == 0 : False -> MathMin" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6, + summary7, + summary8, + summary9 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6, + displayName7, + displayName8, + displayName9 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6, + methodName7, + methodName8, + methodName9, + ) + + val method = MinStack::addValue + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt index b247d9c978..133fa77ec6 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt @@ -28,6 +28,7 @@ import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.UtFuzzedExecution import org.utbot.summary.fuzzer.names.MethodBasedNameSuggester import org.utbot.summary.fuzzer.names.ModelBasedNameSuggester +import org.utbot.summary.comment.CustomJavaDocCommentBuilder import soot.SootMethod private val logger = KotlinLogging.logger {} @@ -155,8 +156,13 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List = listOf(), + var invokes: List = listOf(), + var iterates: List = listOf(), + var returnsFrom: String = "", + var throwsException: String = "" +) \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocCommentBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocCommentBuilder.kt new file mode 100644 index 0000000000..81e351485f --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocCommentBuilder.kt @@ -0,0 +1,86 @@ +package org.utbot.summary.comment + +import org.utbot.framework.plugin.api.DocCustomTagStatement +import org.utbot.framework.plugin.api.DocStatement +import org.utbot.framework.plugin.api.exceptionOrNull +import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.tag.TraceTagWithoutExecution +import soot.SootMethod + +/** + * Builds JavaDoc comments for generated tests using plugin's custom JavaDoc tags. + */ +class CustomJavaDocCommentBuilder( + traceTag: TraceTagWithoutExecution, + sootToAST: MutableMap +) : SimpleCommentBuilder(traceTag, sootToAST, stringTemplates = StringsTemplatesPlural()) { + + /** + * Collects statements for final JavaDoc comment. + */ + fun buildDocStatements(method: SootMethod): List { + val comment = buildCustomJavaDocComment(method) + val docStatementList = + CustomJavaDocTagProvider().getPluginCustomTags().mapNotNull { it.generateDocStatement(comment) } + return listOf(DocCustomTagStatement(docStatementList)) + } + + private fun buildCustomJavaDocComment(currentMethod: SootMethod): CustomJavaDocComment { + val methodReference = getMethodReference( + currentMethod.declaringClass.name, + currentMethod.name, + currentMethod.parameterTypes + ) + val classReference = getClassReference(currentMethod.declaringClass.javaStyleName) + + val customJavaDocComment = CustomJavaDocComment( + classUnderTest = classReference, + methodUnderTest = methodReference, + ) + + val rootSentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) + skippedIterations() + buildSentenceBlock(traceTag.rootStatementTag, rootSentenceBlock, currentMethod) + rootSentenceBlock.squashStmtText() + + // builds Throws exception section + val thrownException = traceTag.result.exceptionOrNull() + val exceptionThrow: String? = if (thrownException == null) { + traceTag.result.exceptionOrNull()?.let { it::class.qualifiedName } + } else { + val exceptionName = thrownException.javaClass.name + val reason = findExceptionReason(currentMethod, thrownException) + "{@link $exceptionName} $reason" + } + + if (exceptionThrow != null) { + customJavaDocComment.throwsException = exceptionThrow + } + + // builds Iterates section + rootSentenceBlock.iterationSentenceBlocks.forEach { (loopDesc, sentenceBlocks) -> + customJavaDocComment.iterates += stringTemplates.iterationSentence.format( + stringTemplates.codeSentence.format(loopDesc), + numberOccurrencesToText( + sentenceBlocks.size + ) + ) + } + + // builds Invoke, Execute, Return sections + generateSequence(rootSentenceBlock) { it.nextBlock }.forEach { + for (statement in it.stmtTexts) { + when (statement.stmtType) { + StmtType.Invoke -> customJavaDocComment.invokes += "{@code ${statement.description}}" + StmtType.Condition -> customJavaDocComment.executesCondition += "{@code ${statement.description}}" + StmtType.Return -> customJavaDocComment.returnsFrom = "{@code ${statement.description}}" + else -> { + //TODO: see [issue-773](https://github.com/UnitTestBot/UTBotJava/issues/773) + } + } + } + } + + return customJavaDocComment + } +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocTagProvider.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocTagProvider.kt new file mode 100644 index 0000000000..b1db88048b --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocTagProvider.kt @@ -0,0 +1,60 @@ +package org.utbot.summary.comment + +import org.utbot.framework.plugin.api.DocRegularStmt + +/** + * Provides a list of supported custom JavaDoc tags. + */ +class CustomJavaDocTagProvider { + // The tags' order is important because plugin builds final JavaDoc comment according to it. + fun getPluginCustomTags(): List = + listOf( + CustomJavaDocTag.ClassUnderTest, + CustomJavaDocTag.MethodUnderTest, + CustomJavaDocTag.ExpectedResult, + CustomJavaDocTag.ActualResult, + CustomJavaDocTag.Executes, + CustomJavaDocTag.Invokes, + CustomJavaDocTag.Iterates, + CustomJavaDocTag.ReturnsFrom, + CustomJavaDocTag.ThrowsException, + ) +} + +sealed class CustomJavaDocTag( + val name: String, + val message: String, + private val valueRetriever: (CustomJavaDocComment) -> Any +) { + object ClassUnderTest : + CustomJavaDocTag("utbot.classUnderTest", "Class under test", CustomJavaDocComment::classUnderTest) + + object MethodUnderTest : + CustomJavaDocTag("utbot.methodUnderTest", "Method under test", CustomJavaDocComment::methodUnderTest) + + object ExpectedResult : + CustomJavaDocTag("utbot.expectedResult", "Expected result", CustomJavaDocComment::expectedResult) + + object ActualResult : CustomJavaDocTag("utbot.actualResult", "Actual result", CustomJavaDocComment::actualResult) + object Executes : + CustomJavaDocTag("utbot.executesCondition", "Executes condition", CustomJavaDocComment::executesCondition) + + object Invokes : CustomJavaDocTag("utbot.invokes", "Invokes", CustomJavaDocComment::invokes) + object Iterates : CustomJavaDocTag("utbot.iterates", "Iterates", CustomJavaDocComment::iterates) + object ReturnsFrom : CustomJavaDocTag("utbot.returnsFrom", "Returns from", CustomJavaDocComment::returnsFrom) + object ThrowsException : + CustomJavaDocTag("utbot.throwsException", "Throws exception", CustomJavaDocComment::throwsException) + + fun generateDocStatement(comment: CustomJavaDocComment): DocRegularStmt? = + when (val value = valueRetriever.invoke(comment)) { + is String -> value.takeIf { it.isNotEmpty() }?.let { + DocRegularStmt("@$name $value\n") + } + is List<*> -> value.takeIf { it.isNotEmpty() }?.let { + val valueToString = value.joinToString(separator = ",\n", postfix = "\n") + + DocRegularStmt("@$name $valueToString") + } + else -> null + } +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleClusterCommentBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleClusterCommentBuilder.kt index 829308ed02..3c1328e1ad 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleClusterCommentBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleClusterCommentBuilder.kt @@ -63,7 +63,7 @@ class SimpleClusterCommentBuilder( * Builds sentence blocks as parent one, * but ignores few types of statementTag that are considered in SimpleCommentBuilder */ - private fun buildSentenceBlock( + override fun buildSentenceBlock( statementTag: StatementTag?, sentenceBlock: SimpleSentenceBlock, currentMethod: SootMethod @@ -93,7 +93,7 @@ class SimpleClusterCommentBuilder( sentenceInvoke.squashStmtText() if (!sentenceInvoke.isEmpty()) { sentenceBlock.invokeSentenceBlock = - Pair(invokeDescription(className, methodName, methodParameterTypes), sentenceInvoke) + Pair(getMethodReference(className, methodName, methodParameterTypes), sentenceInvoke) createNextBlock = true invokeRegistered = true } diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleCommentBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleCommentBuilder.kt index eab1d0d3e6..3a57924058 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleCommentBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleCommentBuilder.kt @@ -42,15 +42,7 @@ open class SimpleCommentBuilder( */ open fun buildString(currentMethod: SootMethod): String { val root = SimpleSentenceBlock(stringTemplates = stringTemplates) - - val thrownException = traceTag.result.exceptionOrNull() - if (thrownException == null) { - root.exceptionThrow = traceTag.result.exceptionOrNull()?.let { it::class.qualifiedName } - } else { - val exceptionName = thrownException.javaClass.simpleName - val reason = findExceptionReason(currentMethod, thrownException) - root.exceptionThrow = "$exceptionName $reason" - } + buildThrownExceptionInfo(root, currentMethod) skippedIterations() buildSentenceBlock(traceTag.rootStatementTag, root, currentMethod) var sentence = toSentence(root) @@ -61,12 +53,10 @@ open class SimpleCommentBuilder( return "
\n$sentence
".replace(CARRIAGE_RETURN, "") } - /** - * Creates List from SimpleSentenceBlock - */ - open fun buildDocStmts(currentMethod: SootMethod): List { - val root = SimpleSentenceBlock(stringTemplates = stringTemplates) - + private fun buildThrownExceptionInfo( + root: SimpleSentenceBlock, + currentMethod: SootMethod + ) { val thrownException = traceTag.result.exceptionOrNull() if (thrownException == null) { root.exceptionThrow = traceTag.result.exceptionOrNull()?.let { it::class.qualifiedName } @@ -75,9 +65,14 @@ open class SimpleCommentBuilder( val reason = findExceptionReason(currentMethod, thrownException) root.exceptionThrow = "$exceptionName $reason" } - skippedIterations() - buildSentenceBlock(traceTag.rootStatementTag, root, currentMethod) - val docStmts = toDocStmts(root) + } + + /** + * Creates List<[DocStatement]> from [SimpleSentenceBlock]. + */ + open fun buildDocStmts(currentMethod: SootMethod): List { + val sentenceBlock = buildSentenceBlock(currentMethod) + val docStmts = toDocStmts(sentenceBlock) if (docStmts.isEmpty()) { return listOf(DocRegularStmt(genWarnNotification())) //TODO SAT-1310 @@ -88,6 +83,14 @@ open class SimpleCommentBuilder( return listOf(DocPreTagStatement(docStmts)) } + private fun buildSentenceBlock(currentMethod: SootMethod): SimpleSentenceBlock { + val rootSentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) + buildThrownExceptionInfo(rootSentenceBlock, currentMethod) + skippedIterations() + buildSentenceBlock(traceTag.rootStatementTag, rootSentenceBlock, currentMethod) + return rootSentenceBlock + } + protected fun genWarnNotification(): String = " " //why is it empty? /** @@ -114,7 +117,7 @@ open class SimpleCommentBuilder( return stmts } - private fun findExceptionReason(currentMethod: SootMethod, thrownException: Throwable): String { + protected fun findExceptionReason(currentMethod: SootMethod, thrownException: Throwable): String { val path = traceTag.path if (path.isEmpty()) { if (thrownException is ConcreteExecutionFailureException) { @@ -161,7 +164,7 @@ open class SimpleCommentBuilder( /** * Sentence blocks are built based on unique and partly unique statement tags. */ - private fun buildSentenceBlock( + open fun buildSentenceBlock( statementTag: StatementTag?, sentenceBlock: SimpleSentenceBlock, currentMethod: SootMethod @@ -191,7 +194,7 @@ open class SimpleCommentBuilder( sentenceInvoke.squashStmtText() if (!sentenceInvoke.isEmpty()) { sentenceBlock.invokeSentenceBlock = - Pair(invokeDescription(className, methodName, methodParameterTypes), sentenceInvoke) + Pair(getMethodReference(className, methodName, methodParameterTypes), sentenceInvoke) createNextBlock = true invokeRegistered = true } @@ -317,7 +320,7 @@ open class SimpleCommentBuilder( sentenceBlock.stmtTexts.add( StmtDescription( StmtType.Invoke, - invokeDescription(className, methodName, methodParameterTypes), + getMethodReference(className, methodName, methodParameterTypes), frequency ) ) @@ -349,7 +352,7 @@ open class SimpleCommentBuilder( * In case when an enclosing class in nested, we need to replace '$' with '.' * to render the reference. */ - fun invokeDescription(className: String, methodName: String, methodParameterTypes: List): String { + fun getMethodReference(className: String, methodName: String, methodParameterTypes: List): String { val prettyClassName: String = className.replace("$", ".") return if (methodParameterTypes.isEmpty()) { @@ -360,6 +363,14 @@ open class SimpleCommentBuilder( } } + /** + * Returns a reference to the class. + * Replaces '$' with '.' in case a class is nested. + */ + fun getClassReference(fullClasName: String): String { + return "{@link ${fullClasName.replace("$", ".")}}" + } + protected fun buildIterationsBlock( iterations: List, activatedStep: Step, diff --git a/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt b/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt index 39b995ccad..ba1d82e02b 100644 --- a/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt +++ b/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt @@ -67,7 +67,7 @@ class SimpleCommentBuilderTest { @Test fun `builds inline link for method`() { val commentBuilder = SimpleCommentBuilder(traceTag, sootToAst) - val methodReference = commentBuilder.invokeDescription("org.utbot.ClassName", "methodName", listOf()) + val methodReference = commentBuilder.getMethodReference("org.utbot.ClassName", "methodName", listOf()) val expectedMethodReference = "{@link org.utbot.ClassName#methodName()}" assertEquals(methodReference, expectedMethodReference) } @@ -76,7 +76,7 @@ class SimpleCommentBuilderTest { fun `builds inline link for method in nested class`() { val commentBuilder = SimpleCommentBuilder(traceTag, sootToAst) val methodReference = - commentBuilder.invokeDescription("org.utbot.ClassName\$NestedClassName", "methodName", listOf()) + commentBuilder.getMethodReference("org.utbot.ClassName\$NestedClassName", "methodName", listOf()) val expectedMethodReference = "{@link org.utbot.ClassName.NestedClassName#methodName()}" assertEquals(methodReference, expectedMethodReference) }