New feature: node graph comparison
See original GitHub issueI’m developing a library that does annotation processing and generates code using KotlinPoet. While before I had this working by just comparing the raw generated text against that of a golden file, I’m now using kastree to parse both to rule out trivial differences like comments and whitespace.
To improve my ability to spot where the generated and expected code diverge in case the test fails, I made a SyntaxTreeComparer
object that traverses two node graphs in tandem and returns the first divergence found between them, if any. I can then pretty-print this in my failure messages.
For example, given this golden file to compare against:
// Package statement and various imports...
class MinAndMax<T: Comparable<T>>(id: String) : ElementaryNode(id) {
private val `_values`: SinglePortInputBoundary<Iterable<T>> = inputBoundary("values")
val values: InputPort<Iterable<T>> = `_values`.exposed
private val `_min`: SinglePortOutputBoundary<T?> = outputBoundary("min")
val min: OutputPort<T?> = `_min`.exposed
private val `_max`: SinglePortOutputBoundary<T?> = outputBoundary("max")
val max: OutputPort<T?> = `_max`.exposed
override suspend fun executeOnce() {
val foo = "Bar" // Shouldn't really be in generated code
coroutineScope {
val values = async {
`_values`.inner.yield()
}
val output = nodeBody(values.await())
`_min`.inner.forward(output.min)
`_max`.inner.forward(output.max)
}
}
private fun nodeBody(values: Iterable<T>): ReturnValue<T> = ReturnValue(values.min(), values.max())
private class ReturnValue<T>(val min: T?, val max: T?)
}
And a test case that involves this custom kotlintest matcher, where diff
passes both node graphs to SyntaxTreeComparer
:
fun matchTree(expectedTree: Node) = object : Matcher<Node> {
override fun test(value: Node): Result {
val divergence = value diff expectedTree
val ifShouldMatch = "Trees should match, but diverge:\n${divergence?.prettyPrint()}"
val ifShouldNotMatch = "Trees should not match, but they do"
return Result(divergence == null, ifShouldMatch, ifShouldNotMatch)
}
}
I get this failure message:
Trees should match, but diverge:
EXPECTED
{
val foo = "Bar"
coroutineScope {
val values = async {
`_values`.inner.yield()
}
val output = nodeBody(values.await())
`_min`.inner.forward(output.min)
`_max`.inner.forward(output.max)
}
}
ACTUAL
{
coroutineScope {
val values = async {
`_values`.inner.yield()
}
val output = nodeBody(values.await())
`_min`.inner.forward(output.min)
`_max`.inner.forward(output.max)
}
}
Anyway, point is - would you be interested in incorporating something like this into your library? If so, I can open a PR with the code for the syntax tree comparer and add some unit tests later on.
Issue Analytics
- State:
- Created 5 years ago
- Comments:6 (6 by maintainers)
Here’s something I hacked together that shows the idea. The differ:
A main test:
Returns:
Now, there are little bugs and what not and if I were developing a full solution, I’d change it up and add tests and what not. But in general, the idea of keeping paths of classes and then comparing at the end is reasonable, and future proof when I add more AST classes as more language features get developed.
@TAGC - Note, it may not work on top-level changes and you need to add some test cases. Basically, it just keeps a path of the classes it has traversed and then compares paths at the end.