Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Another view on how tests should like #41

Merged
merged 3 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions psiminer-core/src/test/kotlin/BasePsiRequiredTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ abstract class BasePsiRequiredTest(private val psiSourceFile: File) : BasePlatfo

abstract val handler: LanguageHandler
private val methods = mutableMapOf<String, PsiElement>()

private class ResourceException(resourceRoot: String) : RuntimeException("Can't find resources in $resourceRoot")

// We should define the root resources folder
Expand All @@ -35,8 +34,8 @@ abstract class BasePsiRequiredTest(private val psiSourceFile: File) : BasePlatfo
val methodName = handler.methodProvider.getNameNode(it).text
methods[methodName] = it
}
}
}
}
}

@AfterAll
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down
86 changes: 86 additions & 0 deletions psiminer-core/src/test/kotlin/psi/graphs/BaseGraphTest.kt
Original file line number Diff line number Diff line change
@@ -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<Edge>): Map<Vertex, Int> =
edges.groupBy { edge -> edge.from }
.mapKeys { (psiElement, _) -> Vertex(psiElement.toString(), getPositionInFunction(psiElement)) }
.mapValues { (_, toVertices) -> toVertices.size }

fun countIncomingEdges(edges: List<Edge>): Map<Vertex, Int> =
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<Int, Int> {
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<Int, Int>)

data class CorrectNumberOfIncomingAndOutgoingEdges(
val incoming: Map<String, Map<Vertex, Int>>,
val outgoing: Map<String, Map<Vertex, Int>>
)

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<MethodImpl>() as PsiElement? ?: throw MethodRootNotFoundException(this.text)
}

override val handler: LanguageHandler = PhpHandler()

companion object {
val dataFolder = File("php")
const val ext = "php"
}
}
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -30,69 +23,113 @@ 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")
)
}
}

@ParameterizedTest
@ValueSource(
strings = [
"straightWriteMethod",
"straightReadWriteMethod",
"ifMethod",
"forMethod",
"foreachMethod",
"multipleReturns"
"forMethod"
]
)
fun `test control element extraction from PHP methods`(methodName: String) {
val psiRoot = getMethod(methodName)
val graphMiner = PhpGraphMiner()
ReadAction.run<Exception> {
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<Exception> {
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()
)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@ public function multipleReturns(): int
return 1;
}
}
return 2;
return 2;
}
}