Skip to content

Commit

Permalink
Merge pull request #41 from JetBrains-Research/better-tests
Browse files Browse the repository at this point in the history
Another view on how tests should like
  • Loading branch information
egor-bogomolov authored Jan 26, 2023
2 parents 5eb3877 + 47e13d1 commit bee0af8
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 57 deletions.
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;
}
}

0 comments on commit bee0af8

Please sign in to comment.