GraphQuerySmokeTest.kt
src/jvmMain/kotlin/afield/data/query/graph/GraphQuerySmokeTest.kt · 409 lines
package afield.data.query.graph
FIELD_TEST id: graphquery.core.smoke runner: jvm-main entrypoint: afield.data.query.graph.GraphQuerySmokeTest tags: [smoke, graphquery, reflection] timeoutSec: 60 expect: exitCode: 0 stdoutContains: - "GraphQuery smoke passed"
Smoke test for the reflective path layer in afield.data.query.graph, the XPath-style query
engine over arbitrary live object graphs.
It exercises only the generic core, which is what lets it run as a plain jvm-main
entrypoint: the core touches no Runloop, spawns no threads and opens no devices, so the JVM exits
naturally. The synth-coupled parameter head (modulation, suppliers, GraphBindingRuntime)
does register a runloop hook and is covered by the jvmTest suites instead.
The central case is testFastPathAgreesWithFullWalk: the literal-prefix optimization must
return exactly what a full traversal plus glob filter returns.
object GraphQuerySmokeTest
The graphquery.core.smoke entrypoint; prints "GraphQuery smoke passed" on success.
object GraphQuerySmokeTest {
fun main(args: Array<String>)
@JvmStatic
fun main(args: Array<String>) {
check(runTests()) { "GraphQuery smoke tests failed" }
}
fun runTests(): Boolean
fun runTests(): Boolean = try {
println("Running GraphQuery smoke...")
testWalkShapeAndOrdering()
testGlobSemantics()
testCycleAndAliasHandling()
testWritability()
testPredicates()
testSlashOperator()
testTypedLenses()
testCompiledPatterns()
testFastPathAgreesWithFullWalk()
testFastPathCost()
testCustomEdges()
testPolicyKnobs()
testHtmlRendering()
println("GraphQuery smoke passed")
true
} catch (t: Throwable) {
t.printStackTrace()
false
}
fixtures
private enum class Mode { OFF, ON }
private class Osc(var freq: Float, var gain: Float, var label: String, var enabled: Boolean, var mode: Mode)
private class Voice(val carrier: Osc, val modulator: Osc, var name: String)
private class Nested(val inner: Voice, var depth: Int)
private class Backref(var label: String) {
var up: Any? = null
}
private class Custom(var real: Float) : LiveGraphInspectable {
var hidden: Float = 9f
override fun liveGraphChildren(): Iterable<LiveGraphEdge> = listOf(
LiveGraphEdge("synthetic", { hidden }, { v -> hidden = (v as Number).toFloat() })
)
}
private fun osc(freq: Float = 220f, gain: Float = 0.5f) = Osc(freq, gain, "osc", true, Mode.ON)
private fun voice() = Voice(osc(220f, 0.8f), osc(440f, 0.2f), "voice")
private fun require(condition: Boolean, message: () -> String) {
if (!condition) throw AssertionError(message())
}
private fun <T> requireEquals(expected: T, actual: T, what: String) {
if (expected != actual) throw AssertionError("$what: expected <$expected> but was <$actual>")
}
cases
Depth-first, interior edges recorded, properties in primary-constructor order.
source — 69 lines, from line 94
private fun testWalkShapeAndOrdering() {
requireEquals(
listOf(
"carrier", "carrier/freq", "carrier/gain", "carrier/label", "carrier/enabled", "carrier/mode",
"modulator", "modulator/freq", "modulator/gain", "modulator/label", "modulator/enabled",
"modulator/mode", "name"
),
voice().graphPaths("**").paths(),
"walk order"
)
println(" ok: walk shape and constructor ordering")
}
private fun testGlobSemantics() {
val v = voice()
requireEquals(listOf("carrier", "modulator", "name"), v.graphPaths("*").paths(), "single star")
requireEquals(
listOf("carrier/freq", "modulator/freq"),
v.graphPaths("**/freq").paths(),
"double star"
)
requireEquals(listOf("carrier/freq"), v.graphPaths("carrier/freq").paths(), "literal")
requireEquals(0, v.graphPaths("nope").size, "no match")
// ** absorbs zero segments
requireEquals(listOf("freq"), osc().graphPaths("**/freq").paths(), "zero-segment double star")
// A `*` inside a segment globs within that one name, and never across a `/`.
requireEquals(
listOf("carrier/gain", "modulator/gain"),
v.graphPaths("**/" + "*ain").paths(),
"glob inside a segment"
)
requireEquals(listOf("carrier", "modulator"), v.graphPaths("*a*r").paths(), "glob at one level")
requireEquals(0, v.graphPaths("*req").size, "a within-segment glob is still one segment")
requireEquals(0, v.graphPaths("carrier*freq").size, "a within-segment glob cannot cross a /")
// Names are matched ignoring case, literal segments included.
requireEquals(listOf("carrier/freq"), v.graphPaths("CARRIER/Freq").paths(), "case-insensitive literal")
requireEquals(
listOf("carrier/gain", "modulator/gain"),
v.graphPaths("**/" + "*AIN").paths(),
"case-insensitive glob"
)
// Two spellings of one name are two slots, and one pattern finds both. The literal
// fast path has to notice that and defer to the walk, which is what this pins.
val twins = mutableMapOf<String, Any?>("freq" to 1f, "Freq" to 2f)
requireEquals(listOf("freq", "Freq"), twins.graphPaths("freq").paths(), "case-variant siblings")
// `|` unions whole patterns: one walk, each slot once, in walk order.
requireEquals(
listOf("carrier/freq", "carrier/label", "modulator/freq", "modulator/label"),
v.graphPaths("**/freq | **/label").paths(),
"alternation"
)
requireEquals(
v.graphPaths("**").paths(),
v.graphPaths("**/freq | **").paths(),
"overlapping branches report each slot once"
)
requireEquals(
listOf("carrier", "modulator/gain"),
v.graphPaths("modulator/gain|carrier").paths(),
"branch order does not change walk order"
)
requireEquals(0, v.graphPaths("|").size, "nothing but bars matches nothing")
println(" ok: glob semantics")
}
Path-scoped cycle detection: cycles stop, aliases still expand under each address.
source — 124 lines, from line 165
private fun testCycleAndAliasHandling() {
val a = Backref("a")
val b = Backref("b")
a.up = b
b.up = a
requireEquals(
listOf("label", "up", "up/label", "up/up"),
a.graphPaths("**").paths(),
"cycle termination"
)
val shared = osc(100f)
class Aliased(val x: Osc, val y: Osc)
val paths = Aliased(shared, shared).graphPaths("**").paths()
require(paths.contains("x/freq") && paths.contains("y/freq")) {
"both aliases must expand, got $paths"
}
val viaSecond = Aliased(shared, shared).graphPath("y/freq")
require(viaSecond != null && viaSecond.get() == 100f) { "second alias must be addressable" }
println(" ok: cycles terminate and aliases expand")
}
private fun testWritability() {
val v = voice()
v.graphPath("carrier/freq")!!.set(330f)
requireEquals(330f, v.carrier.freq, "write through a ref")
class Fixed(val a: Float, var b: Float)
val set = Fixed(1f, 2f).graphPaths("**")
requireEquals(listOf("b"), set.writable().paths(), "val is read-only")
// Arrays are writable, consistently with mutable collections.
val arr = arrayOf<Any?>(1f, 2f)
arr.graphPaths("0").first().set(7f)
requireEquals(7f, arr[0], "array element write")
val list = mutableListOf<Any?>(1f)
list.graphPaths("0").first().set(5f)
requireEquals(5f, list[0], "list element write")
val map = mutableMapOf<String, Any?>("k" to 1f)
map.graphPaths("k").first().set(3f)
requireEquals(3f, map["k"], "map entry write")
println(" ok: writability across properties, arrays, lists and maps")
}
private fun testPredicates() {
val v = voice()
requireEquals(
listOf("modulator/freq"),
v.graphPaths("**").whereValueIs<Float> { it > 300f }.paths(),
"typed value predicate"
)
requireEquals(
listOf("carrier/gain"),
v.graphPaths("**/gain").whereValue { (it as? Float ?: 0f) > 0.5f }.paths(),
"value predicate"
)
require(v.graphPaths("**").ownedBy(v.carrier).all { it.owner === v.carrier }) { "ownedBy" }
requireEquals(
listOf("carrier", "modulator", "name"),
v.graphPaths("**").atDepth(1..1).paths(),
"atDepth"
)
val all = v.graphPaths("**")
requireEquals(
all.size,
all.excluding("**/gain").size + all.matching("**/gain").size,
"excluding is the complement of matching"
)
println(" ok: predicates")
}
private fun testSlashOperator() {
val v = voice()
// The sugar is the same query.
requireEquals(
v.graphPaths("**/freq").paths(),
(v / "**/freq").paths(),
"slash is graphPaths"
)
// A chain splits the pattern without losing the address it was reached by.
requireEquals(listOf("carrier/freq"), (v / "carrier" / "freq").paths(), "chained slash")
(v / "carrier" / "freq").lenses<Float>().set(330f)
requireEquals(330f, v.carrier.freq, "write through a chained slash")
// A typed leaf has nothing below it, so this reports nothing rather than the
// LensSet's own fields.
require(((v / "**/freq").lenses<Float>() / "**").isEmpty) { "slash off a typed leaf" }
println(" ok: slash operator")
}
private fun testTypedLenses() {
val v = voice()
requireEquals(3, v.graphPaths("**").lenses<String>().set("x"), "string lenses written")
requireEquals("x", v.carrier.label, "lens write through")
v.graphPaths("**").lenses<Boolean>().update { !it }
requireEquals(false, v.carrier.enabled, "boolean lens update")
v.graphPaths("**").lenses<Mode>().set(Mode.OFF)
requireEquals(Mode.OFF, v.carrier.mode, "enum lens set")
// A lens only ever sees values of its own type, so this cannot mis-cast.
requireEquals(2, v.graphPaths("**/freq").lenses<Float>().size, "float lenses")
requireEquals(0, v.graphPaths("**/freq").lenses<String>().size, "no string freqs")
println(" ok: typed lenses over String, Boolean, enum and Float")
}
private fun testCompiledPatterns() {
val a = GraphQuery.compile("**/freq")
require(a === GraphQuery.compile("**/freq")) { "compiled patterns must be cached" }
require(GraphQuery.compile("carrier/freq").isStatic) { "literal pattern is static" }
require(!GraphQuery.compile("**").isStatic) { "glob pattern is not static" }
val v = voice()
requireEquals(listOf("carrier/freq", "modulator/freq"), a.resolve(v).paths(), "reuse a plan")
v.carrier.freq = 1f
requireEquals(listOf<Any?>(1f, 440f), a.resolve(v).values(), "plans read live")
println(" ok: compiled patterns")
}
The optimized resolver must be indistinguishable from a full traversal plus filter. Includes a back-reference graph, which is where a naive prefix descent diverges.
private fun testFastPathAgreesWithFullWalk() {
val backref = Backref("root").also { root ->
root.up = Backref("child").also { it.up = root }
}
val roots: List<Pair<String, Any>> = listOf(
"voice" to voice(),
"nested" to Nested(voice(), 2),
"map" to mutableMapOf<String, Any?>("a" to osc(), "b" to osc(), "n" to null),
"list" to mutableListOf<Any?>(osc(), osc()),
"array" to arrayOf<Any?>(osc(), osc()),
"backref" to backref
)
val patterns = listOf(
"**", "*", "**/freq", "*/freq",
"carrier", "carrier/freq", "carrier/" + "*", "carrier/" + "**",
"inner/carrier/freq", "inner/" + "*" + "/freq", "inner/" + "**",
"a/freq", "a/" + "*", "0/freq", "n",
"up/" + "**", "up/" + "*", "up/up",
"nope", "carrier/nope", "**/nope"
)
val options = GraphWalkOptions()
var checked = 0
roots.forEach { (rootName, root) ->
patterns.forEach { pattern ->
val slow = GraphWalker.scan(root, options).matching(pattern).paths()
val fast = root.graphPaths(pattern, options).paths()
requireEquals(slow, fast, "pattern '$pattern' on $rootName")
checked++
}
}
println(" ok: fast path matches full walk across $checked root/pattern combinations")
}
A literal path reads one getter per named segment; a full walk reads every edge.
source — 81 lines, from line 328
private fun testFastPathCost() {
val reads = intArrayOf(0)
class Counted(val label: String) : LiveGraphInspectable {
val kids = linkedMapOf<String, Counted>()
var leaf: Float = 1f
override fun liveGraphChildren(): Iterable<LiveGraphEdge> =
kids.map { (name, child) -> LiveGraphEdge(name, { reads[0]++; child }) } +
LiveGraphEdge("leaf", { reads[0]++; leaf }, { v -> leaf = (v as Number).toFloat() })
}
fun build(depth: Int, breadth: Int, label: String): Counted {
val node = Counted(label)
if (depth > 0) repeat(breadth) { i -> node.kids["k$i"] = build(depth - 1, breadth, "$label.$i") }
return node
}
val root = build(depth = 3, breadth = 5, label = "root")
reads[0] = 0
requireEquals(1, root.graphPaths("k1/k2/k3/leaf").size, "literal path resolves")
val staticReads = reads[0]
requireEquals(4, staticReads, "one getter per named segment")
reads[0] = 0
val everything = root.graphPaths("**")
val fullReads = reads[0]
require(everything.size > 100) { "graph should be large, got ${everything.size}" }
require(fullReads > staticReads * 20) {
"full walk ($fullReads) should cost far more than a literal path ($staticReads)"
}
println(" ok: literal path cost $staticReads edge reads vs $fullReads for a full walk")
}
private fun testCustomEdges() {
val subject = Custom(1f)
val paths = subject.graphPaths("**").paths()
requireEquals("synthetic", paths.first(), "custom edges come first")
require(paths.contains("real")) { "reflected properties survive: $paths" }
subject.graphPath("synthetic")!!.set(42f)
requireEquals(42f, subject.hidden, "custom setter writes through")
println(" ok: LiveGraphInspectable edges merge ahead of reflection")
}
private fun testPolicyKnobs() {
val v = voice()
requireEquals(
listOf("carrier", "modulator"),
v.graphPaths("**", GraphWalkOptions(maxDepth = 0)).paths().take(2),
"maxDepth clips"
)
require(v.graphPaths("**", GraphWalkOptions(maxRefs = 3)).size <= 3) { "maxRefs bounds" }
// Pruning keeps the built-in guards, so no reflection into String.
val shallow = v.graphPaths(
"**",
GraphWalkOptions(descendInto = { value, byDefault -> byDefault && value !is Osc })
)
requireEquals(listOf("carrier", "modulator", "name"), shallow.paths(), "descendInto prunes")
class Maybe(var value: Float?, var other: Float)
requireEquals(
listOf("other"),
Maybe(null, 1f).graphPaths("**", GraphWalkOptions(includeNulls = false)).paths(),
"includeNulls filters"
)
println(" ok: maxDepth, maxRefs, descendInto and includeNulls")
}
private fun testHtmlRendering() {
val html = voice().graphPaths("**").toHTML()
require(html.contains("PathSet") && html.contains("<table")) { "PathSet renders a table" }
val hostile = mutableMapOf<String, Any?>("<script>" to 1f).graphPaths("**").toHTML()
require(hostile.contains("<script>") && !hostile.contains("<script>")) {
"markup must be escaped: $hostile"
}
println(" ok: HTML rendering and escaping")
}
}