diff --git a/psiminer-core/src/test/kotlin/BasePsiRequiredTest.kt b/psiminer-core/src/test/kotlin/BasePsiRequiredTest.kt index d4c8034..7a0896e 100644 --- a/psiminer-core/src/test/kotlin/BasePsiRequiredTest.kt +++ b/psiminer-core/src/test/kotlin/BasePsiRequiredTest.kt @@ -15,7 +15,6 @@ abstract class BasePsiRequiredTest(private val psiSourceFile: File) : BasePlatfo abstract val handler: LanguageHandler private val methods = mutableMapOf() - private class ResourceException(resourceRoot: String) : RuntimeException("Can't find resources in $resourceRoot") // We should define the root resources folder @@ -35,8 +34,8 @@ abstract class BasePsiRequiredTest(private val psiSourceFile: File) : BasePlatfo val methodName = handler.methodProvider.getNameNode(it).text methods[methodName] = it } - } } + } } @AfterAll @@ -58,6 +57,7 @@ abstract class BasePsiRequiredTest(private val psiSourceFile: File) : BasePlatfo open class JavaPsiRequiredTest(source: String) : BasePsiRequiredTest(dataFolder.resolve("$source.$ext")) { override val handler: LanguageHandler = JavaHandler() + companion object { val dataFolder = File("java") const val ext = "java" @@ -66,6 +66,7 @@ open class JavaPsiRequiredTest(source: String) : BasePsiRequiredTest(dataFolder. open class KotlinPsiRequiredTest(source: String) : BasePsiRequiredTest(dataFolder.resolve("$source.$ext")) { override val handler: LanguageHandler = KotlinHandler() + companion object { val dataFolder = File("kotlin") const val ext = "kt" @@ -74,6 +75,7 @@ open class KotlinPsiRequiredTest(source: String) : BasePsiRequiredTest(dataFolde open class PhpPsiRequiredTest(source: String) : BasePsiRequiredTest(dataFolder.resolve("$source.$ext")) { override val handler: LanguageHandler = PhpHandler() + companion object { val dataFolder = File("php") const val ext = "php" diff --git a/psiminer-core/src/test/kotlin/psi/graphs/BaseGraphTest.kt b/psiminer-core/src/test/kotlin/psi/graphs/BaseGraphTest.kt new file mode 100644 index 0000000..b916cd1 --- /dev/null +++ b/psiminer-core/src/test/kotlin/psi/graphs/BaseGraphTest.kt @@ -0,0 +1,86 @@ +package psi.graphs + +import BasePsiRequiredTest +import com.intellij.openapi.editor.Document +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.psi.elements.impl.MethodImpl +import org.junit.jupiter.api.TestInstance +import psi.language.LanguageHandler +import psi.language.PhpHandler +import java.io.File + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class BaseGraphTest(private val psiSourceFile: File) : BasePsiRequiredTest(psiSourceFile) { + + fun countOutgoingEdges(edges: List): Map = + edges.groupBy { edge -> edge.from } + .mapKeys { (psiElement, _) -> Vertex(psiElement.toString(), getPositionInFunction(psiElement)) } + .mapValues { (_, toVertices) -> toVertices.size } + + fun countIncomingEdges(edges: List): Map = + edges.groupBy { edge -> edge.to } + .mapKeys { (psiElement, _) -> Vertex(psiElement.toString(), getPositionInFunction(psiElement)) } + .mapValues { (_, fromVertices) -> fromVertices.size } + + abstract fun PsiElement.methodRoot(): PsiElement + + private fun Document.getLineNumber(psiElement: PsiElement) = + this.getLineNumber(psiElement.textOffset) + + /** + * For PsiElement returns its position relatively to the function declaration. + * + * Function declaration corresponds to lineNumber=0. + * Column number is also 0-based. + */ + private fun getPositionInFunction(psiElement: PsiElement): Pair { + val document = getDocument(psiElement) + val functionDeclarationLineNumber = document.getLineNumber(psiElement.methodRoot()) + val lineNumber = document.getLineNumber(psiElement) - functionDeclarationLineNumber + val prevLineEndOffset = document.getLineEndOffset(lineNumber + functionDeclarationLineNumber - 1) + val columnNumber = psiElement.textOffset - prevLineEndOffset - 1 + return Pair(lineNumber, columnNumber) + } + + private fun getDocument(psiElement: PsiElement): Document { + val containingFile = psiElement.containingFile + val project = containingFile.project + val psiDocumentManager = PsiDocumentManager.getInstance(project) + return psiDocumentManager.getDocument(containingFile) + ?: throw DocumentNotFoundException(psiSourceFile.name, psiElement.text) + } + + data class Vertex(val elementText: String, val positionInFile: Pair) + + data class CorrectNumberOfIncomingAndOutgoingEdges( + val incoming: Map>, + val outgoing: Map> + ) + + private class DocumentNotFoundException(fileName: String, psiElementText: String) : + RuntimeException("Can't find document for PsiElement $psiElementText in file $fileName") + + class MethodRootNotFoundException(psiElementText: String) : + RuntimeException("Can't find method root for $psiElementText") + + class CorrectValueNotProvidedException(methodName: String, edgeType: String) : + RuntimeException("No correct number of $edgeType edges for method $methodName provided") +} + +open class PhpGraphTest(source: String) : BaseGraphTest(dataFolder.resolve("$source.$ext")) { + + override fun PsiElement.methodRoot(): PsiElement = + when (this) { + is MethodImpl -> this + else -> this.parentOfType() as PsiElement? ?: throw MethodRootNotFoundException(this.text) + } + + override val handler: LanguageHandler = PhpHandler() + + companion object { + val dataFolder = File("php") + const val ext = "php" + } +} diff --git a/psiminer-core/src/test/kotlin/psi/graphs/edgeProviders/php/PhpControlFlowEdgeProviderTest.kt b/psiminer-core/src/test/kotlin/psi/graphs/edgeProviders/php/PhpControlFlowEdgeProviderTest.kt index d766a0f..9c9d2d8 100644 --- a/psiminer-core/src/test/kotlin/psi/graphs/edgeProviders/php/PhpControlFlowEdgeProviderTest.kt +++ b/psiminer-core/src/test/kotlin/psi/graphs/edgeProviders/php/PhpControlFlowEdgeProviderTest.kt @@ -1,26 +1,19 @@ package psi.graphs.edgeProviders.php -import PhpPsiRequiredTest import com.intellij.openapi.application.ReadAction -import com.intellij.psi.PsiElement import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import psi.graphs.EdgeType -import psi.graphs.forward +import psi.graphs.* import psi.graphs.graphMiners.PhpGraphMiner -import psi.graphs.withType -internal class PhpControlFlowEdgeProviderTest : PhpPsiRequiredTest("PhpFlowMethods") { +internal class PhpControlFlowEdgeProviderTest : PhpGraphTest("PhpFlowMethods") { @ParameterizedTest @ValueSource( strings = [ "straightWriteMethod", - "straightReadWriteMethod", "ifMethod", - "forMethod", - "foreachMethod", - "multipleReturns" + "forMethod" ] ) fun `test control flow extraction from PHP methods`(methodName: String) { @@ -30,10 +23,16 @@ internal class PhpControlFlowEdgeProviderTest : PhpPsiRequiredTest("PhpFlowMetho val codeGraph = graphMiner.mine(psiRoot) val controlFlowEdges = codeGraph.edges.withType(EdgeType.ControlFlow).flatMap { (_, edges) -> edges.forward() } - val textRepresentation = controlFlowEdges.map { - Pair(it.from.shortText(), it.to.shortText()) - }.toSet() - println(textRepresentation) + assertContainsElements( + countIncomingEdges(controlFlowEdges).entries, + correctNumberOfControlFlowEdges.incoming[methodName]?.entries + ?: throw CorrectValueNotProvidedException(methodName, "control flow") + ) + assertContainsElements( + countOutgoingEdges(controlFlowEdges).entries, + correctNumberOfControlFlowEdges.outgoing[methodName]?.entries + ?: throw CorrectValueNotProvidedException(methodName, "control flow") + ) } } @@ -41,11 +40,8 @@ internal class PhpControlFlowEdgeProviderTest : PhpPsiRequiredTest("PhpFlowMetho @ValueSource( strings = [ "straightWriteMethod", - "straightReadWriteMethod", "ifMethod", - "forMethod", - "foreachMethod", - "multipleReturns" + "forMethod" ] ) fun `test control element extraction from PHP methods`(methodName: String) { @@ -53,46 +49,87 @@ internal class PhpControlFlowEdgeProviderTest : PhpPsiRequiredTest("PhpFlowMetho val graphMiner = PhpGraphMiner() ReadAction.run { val codeGraph = graphMiner.mine(psiRoot) - val controlElementEdges = + val controlFlowEdges = codeGraph.edges.withType(EdgeType.ControlElement).flatMap { (_, edges) -> edges.forward() } - val textRepresentation = controlElementEdges.map { - Pair(it.from.shortText(), it.to.shortText()) - }.toSet() - println(textRepresentation) + assertContainsElements( + countIncomingEdges(controlFlowEdges).entries, + correctNumberOfControlElementEdges.incoming[methodName]?.entries + ?: throw CorrectValueNotProvidedException(methodName, "control element") + ) + assertContainsElements( + countOutgoingEdges(controlFlowEdges).entries, + correctNumberOfControlElementEdges.outgoing[methodName]?.entries + ?: throw CorrectValueNotProvidedException(methodName, "control element") + ) } } - @ParameterizedTest - @ValueSource( - strings = [ - "straightWriteMethod", - "straightReadWriteMethod", - "ifMethod", - "forMethod", - "foreachMethod", - "multipleReturns" - ] - ) - fun `test return element extraction from PHP methods`(methodName: String) { - val psiRoot = getMethod(methodName) - val graphMiner = PhpGraphMiner() - ReadAction.run { - val codeGraph = graphMiner.mine(psiRoot) - val controlElementEdges = - codeGraph.edges.withType(EdgeType.ReturnsTo).flatMap { (_, edges) -> edges.forward() } - val textRepresentation = controlElementEdges.map { - Pair(it.from.shortText(), it.to.shortText()) - }.toSet() - println(textRepresentation) - } - } + companion object { - private fun PsiElement.shortText(): String { - val lines = text.lines() - return if (lines.size <= 1) { - text - } else { - lines[0] + "...${lines.size}" - } + val correctNumberOfControlFlowEdges = CorrectNumberOfIncomingAndOutgoingEdges( + incoming = mapOf( + "straightWriteMethod" to mapOf( + Vertex("VariableImpl: a", Pair(2, 8)) to 1, + Vertex("VariableImpl: b", Pair(3, 8)) to 1, + Vertex("VariableImpl: c", Pair(4, 8)) to 1 + ), + "ifMethod" to mapOf( + Vertex("BinaryExpressionImpl: \$a > 1", Pair(3, 12)) to 1, + Vertex("VariableImpl: b", Pair(4, 12)) to 1, + Vertex("Statement", Pair(10, 8)) to 3 + ), + "forMethod" to mapOf( + Vertex("VariableImpl: i", Pair(2, 13)) to 1, + Vertex("VariableImpl: i", Pair(2, 21)) to 2, + Vertex("Break", Pair(4, 16)) to 1 + ) + ), + outgoing = mapOf( + "straightWriteMethod" to mapOf( + Vertex("MethodImpl: straightWriteMethod", Pair(0, 20)) to 1, + Vertex("VariableImpl: a", Pair(2, 8)) to 1, + Vertex("VariableImpl: b", Pair(3, 8)) to 1 + ), + "ifMethod" to mapOf( + Vertex("BinaryExpressionImpl: \$a > 1", Pair(3, 12)) to 2, + Vertex("BinaryExpressionImpl: \$a < 0", Pair(5, 19)) to 2, + Vertex("VariableImpl: c", Pair(6, 12)) to 1 + ), + "forMethod" to mapOf( + Vertex("VariableImpl: i", Pair(2, 13)) to 1, + Vertex("BinaryExpressionImpl: \$i == 1", Pair(3, 16)) to 2 + ) + ) + ) + + val correctNumberOfControlElementEdges = CorrectNumberOfIncomingAndOutgoingEdges( + incoming = mapOf( + "straightWriteMethod" to mapOf(), + "ifMethod" to mapOf( + Vertex("If", Pair(3, 8)) to 1, // $a = 1 -> if ($a > 1)... + Vertex("VariableImpl: a", Pair(3, 12)) to 1, // if ($a > 1)... -> $a + Vertex("If", Pair(5, 15)) to 1, // if ($a > 1)... -> else if ($a < 0)... + Vertex("VariableImpl: a", Pair(5, 19)) to 1 // else if ($a < 0)... -> $a + ), + "forMethod" to mapOf( + Vertex("For", Pair(2, 8)) to 3, // 1. forMethod -> for () 2. $i -> for() 3. break -> for() + Vertex("VariableImpl: i", Pair(2, 13)) to 1, // for () -> $i = 0 + Vertex("BinaryExpressionImpl: \$i < 2", Pair(2, 21)) to 1 // for () -> $i < 2 + ) + ), + outgoing = mapOf( + "straightWriteMethod" to mapOf(), + "ifMethod" to mapOf( + Vertex("VariableImpl: a", Pair(2, 8)) to 1, // $a = 1 -> if ($a > 1)... + Vertex("If", Pair(3, 8)) to 1 // if ($a > 1)... -> $a + ), + "forMethod" to mapOf( + Vertex("MethodImpl: forMethod", Pair(0, 20)) to 1, // forMethod -> for() + Vertex("For", Pair(2, 8)) to 2, // 1. for() -> $i = 0 2. for() -> $i < 2 + Vertex("VariableImpl: i", Pair(2, 21)) to 1, // $i -> $i < 2 + Vertex("Break", Pair(4, 16)) to 1 // break -> for() + ) + ) + ) } } diff --git a/psiminer-core/src/test/resources/data/php/PhpFlowMethods.php b/psiminer-core/src/test/resources/data/php/PhpFlowMethods.php index 906b8ed..7557ac1 100644 --- a/psiminer-core/src/test/resources/data/php/PhpFlowMethods.php +++ b/psiminer-core/src/test/resources/data/php/PhpFlowMethods.php @@ -80,6 +80,6 @@ public function multipleReturns(): int return 1; } } - return 2; + return 2; } } \ No newline at end of file