test(pybuilder): make the bad-neighbour boundary sweep actually compile its snippets - #8405
Conversation
…le its snippets
`PythonTemplateBuilderSpec`'s exhaustive bad-neighbour test passed
vacuously. Its `assertToolboxDoesNotCompile` helper wrapped each snippet
in a `package ... { }` clause before handing it to a runtime `ToolBox`,
and a ToolBox cannot wrap a `PackageDef` into its synthetic `__wrapper`
method: `tb.compile` dies during typer with a bare
`java.lang.AssertionError: assertion failed: method wrapper` for any
input, the empty string included. The helper's `intercept[Throwable]`
therefore succeeded unconditionally, and all 130 assertions in the sweep
passed without the `pyb` macro ever expanding.
Rebuild the sweep on the technique `BoundaryValidatorSpec` documents:
compile a block rather than a package clause and assert on the captured
`ToolBoxError` message, which distinguishes a `validateCompileTime`
abort (carrying the specific boundary reason) from a benign expansion
whose only failure is the `private[amber]` `fromInterpolated` access
error. Each of the 65 bad-neighbour characters is now required to abort
with its own templated reason and its own character, on both sides:
130 real macro expansions.
Also fix a second vacuity in the same test: it derived its character set
from `PythonLexerUtils.isBadNeighbor`, the predicate it claims to pin,
so shrinking the predicate would have shrunk the sweep's input instead
of failing it. Both sets are now spelled out as literals, and a new
complementary test requires the 29 safe-neighbour characters to produce
the benign outcome (57 cases), so the assertion discriminates in both
directions. A third new test pins `#` as a left neighbour aborting for
the comment rule rather than the neighbour rule.
Drop `inPybuilderPkg`, `assertToolboxDoesNotCompile` and
`scalaUnicodeEscape`, which had no other user. The escape helper was
broken too: it emitted `\u0041` with two backslashes, nothing rewrites
that inside the generated triple-quoted literal, and the abort message
shows the macro's neighbour was the digit `1`, never `A`.
Test-only change. 184 -> 186 tests in the module, 0 failures on both
sides, nothing removed or renamed.
Automated Reviewer SuggestionsBased on the
|
There was a problem hiding this comment.
🟢 Approval recommended
The change is isolated to tests, aligns with existing ToolBox-based patterns in the module, and the only feedback is minor spelling consistency nits in new test text.
Pull request overview
This PR fixes a vacuous/exhaustive “bad-neighbour” boundary sweep in PythonTemplateBuilderSpec by changing the runtime ToolBox harness so the pyb macro actually expands (and therefore can meaningfully distinguish boundary aborts from the expected post-expansion fromInterpolated access failure).
Changes:
- Replaced the old
package ... { ... }ToolBox compilation approach with a block-based compilation harness that reaches macro expansion and capturesToolBoxErrormessages. - Rewrote the exhaustive bad-neighbor sweep to assert on the boundary abort marker plus the specific left/right adjacency reason per character.
- Added discriminating coverage for “safe” neighbor characters (benign outcome) and a dedicated
#-left-neighbor test to pin the comment-rule precedence.
File summaries
| File | Description |
|---|---|
| common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala | Reworks ToolBox-based macro-expansion testing and adds non-vacuous exhaustive boundary sweeps with message-based assertions. |
Review details
Suppressed comments (3)
common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala:547
- These new test names/comments use “neighbour”, while the surrounding APIs/tests use “neighbor” (e.g., isBadNeighbor). Aligning the spelling makes grep/search and terminology consistent across the suite.
test("no safe-neighbour character aborts direct UI adjacency (the sweep discriminates)") {
// The counterpart of the sweep above: without this, weakening the neighbour rule to "always
// abort" would leave that sweep green. `#` on the left is excluded because the comment rule
// legitimately fires first there; the test below pins that case separately.
common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala:572
- Spelling consistency: update “safe-neighbour” to “safe-neighbor” (matches isBadNeighbor / BoundaryValidator wording).
s"${failures.size} of $checked safe-neighbour cases were not benign:\n" +
common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala:577
- Spelling consistency: switch “neighbour” -> “neighbor” in this test name to match the rest of the pybuilder terminology (isBadNeighbor, badLeftNeighbor, etc.).
test("'#' as a left neighbour aborts for the comment rule, not the neighbour rule") {
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| val failures = mismatches.result() | ||
| assert( | ||
| failures.isEmpty, | ||
| s"${failures.size} of $checked adjacency cases did not abort with the neighbour reason:\n" + |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8405 +/- ##
============================================
- Coverage 94.03% 94.02% -0.01%
+ Complexity 4821 4820 -1
============================================
Files 1204 1204
Lines 48991 48991
Branches 5956 5956
============================================
- Hits 46067 46066 -1
Misses 1458 1458
- Partials 1466 1467 +1
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 512 | 0.313 | 18,452/27,763/27,763 us | 🔴 -7.9% / 🔴 +73.7% |
| 🟢 | bs=100 sw=10 sl=64 | 1,154 | 0.705 | 84,856/123,368/123,368 us | 🟢 -7.7% / 🟢 +18.3% |
| ⚪ | bs=1000 sw=10 sl=64 | 1,393 | 0.85 | 719,287/776,090/776,090 us | ⚪ within ±5% / 🟢 +38.4% |
Baseline details
Latest main 1cbe857 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 512 tuples/sec | 556 tuples/sec | 755.36 tuples/sec | -7.9% | -32.2% |
| bs=10 sw=10 sl=64 | MB/s | 0.313 MB/s | 0.339 MB/s | 0.461 MB/s | -7.7% | -32.1% |
| bs=10 sw=10 sl=64 | p50 | 18,452 us | 17,696 us | 12,938 us | +4.3% | +42.6% |
| bs=10 sw=10 sl=64 | p95 | 27,763 us | 28,556 us | 15,980 us | -2.8% | +73.7% |
| bs=10 sw=10 sl=64 | p99 | 27,763 us | 28,556 us | 19,233 us | -2.8% | +44.4% |
| bs=100 sw=10 sl=64 | throughput | 1,154 tuples/sec | 1,135 tuples/sec | 976.3 tuples/sec | +1.7% | +18.2% |
| bs=100 sw=10 sl=64 | MB/s | 0.705 MB/s | 0.693 MB/s | 0.596 MB/s | +1.7% | +18.3% |
| bs=100 sw=10 sl=64 | p50 | 84,856 us | 86,599 us | 102,340 us | -2.0% | -17.1% |
| bs=100 sw=10 sl=64 | p95 | 123,368 us | 133,674 us | 109,262 us | -7.7% | +12.9% |
| bs=100 sw=10 sl=64 | p99 | 123,368 us | 133,674 us | 118,827 us | -7.7% | +3.8% |
| bs=1000 sw=10 sl=64 | throughput | 1,393 tuples/sec | 1,395 tuples/sec | 1,006 tuples/sec | -0.1% | +38.4% |
| bs=1000 sw=10 sl=64 | MB/s | 0.85 MB/s | 0.851 MB/s | 0.614 MB/s | -0.1% | +38.4% |
| bs=1000 sw=10 sl=64 | p50 | 719,287 us | 715,813 us | 999,855 us | +0.5% | -28.1% |
| bs=1000 sw=10 sl=64 | p95 | 776,090 us | 792,355 us | 1,042,833 us | -2.1% | -25.6% |
| bs=1000 sw=10 sl=64 | p99 | 776,090 us | 792,355 us | 1,070,722 us | -2.1% | -27.5% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,390.36,200,128000,512,0.313,18451.57,27762.89,27762.89
1,100,10,64,20,1732.65,2000,1280000,1154,0.705,84856.35,123367.61,123367.61
2,1000,10,64,20,14354.38,20000,12800000,1393,0.850,719287.09,776089.87,776089.87
What changes were proposed in this PR?
PythonTemplateBuilderSpec's exhaustive bad-neighbour test passed vacuously. Its private helper wrapped every snippet in a package clause before handing it to a runtimeToolBox:A
ToolBoxcannot wrap aPackageDefinto its synthetic__wrappermethod, sotb.compiledies during typer with a barejava.lang.AssertionErrorfor any input. Note the type: notToolBoxError, which is whyintercept[Throwable]was needed for the helper to pass.I ran the helper against inputs that obviously must not be rejected. Every one "does not compile":
tb.parsetb.compile""(the empty string)PackageDefjava.lang.AssertionError: assertion failed: method wrapperobject Trivial { val x = 1 }PackageDefpyb"foo $ui bar"(whitespace neighbours)PackageDef,as a left neighbour — a good neighbourPackageDefToolBoxErrorThe measured message body is
assertion failed: / method wrapper / while compiling: <no file> / during phase: typer / library version: version 2.13.18 / compiler version: version 2.13.18 / reconstructed args: / last tree to typer: .... The compilation dies wrapping the tree, before macro expansion.tb.parseon the same input succeeds, so the failure is entirely intb.compile. Compiling the identical body as a block instead of a package clause reaches the macro and aborts properly, which is what the rewrite relies on.The helper was used at two call sites, inside the test that iterates the
isBadNeighborsubset of printable ASCII and asserts left- and right-adjacency per character. All 130 of those assertions passed without thepybmacro ever expanding.Before -> after:
The rewrite uses the technique
BoundaryValidatorSpecalready documents in its header: compile a block, not a package; the snippet always fails (the expansion calls theprivate[amber]fromInterpolated, which the ToolBox's__wrapperpackage cannot reach), but the macro fully expands first, so the two outcomes are distinguishable by message — avalidateCompileTimeabort carrying the specific boundary reason, versus a benign expansion whose only failure is thefromInterpolatedaccess error.Concretely:
must not be immediately adjacent to 'z' on the left. 130 real macro expansions.#on the left is excluded and pinned separately). Without this, weakening the neighbour rule to "always abort" would leave the first sweep green.#as a left neighbour aborts for the comment rule, not the neighbour rule.PythonLexerUtils.isBadNeighbor. Deriving them was a second, subtler vacuity: shrinking the predicate would silently shrink the sweep's input instead of failing it.inPybuilderPkg,assertToolboxDoesNotCompile, andscalaUnicodeEscape, which had no other user. The escape helper was broken on its own terms too: it emitted\\u0041— two backslashes — into the generated source, nothing rewrites that inside the generated triple-quoted literal, and the abort message proves what the macro actually saw: the left neighbour was the digit1, neverA. So the old sweep fed the same wrong neighbour for all 65 characters. The new snippets embed the raw character instead.What this PR does not do: it touches no production code and adds no tests to any other area. Measured from the JUnit XML, the file goes from 65 to 67 test cases: 1 test rewritten, 2 added, 0 removed and 0 renamed, so 64 tests, their names and the file's structure are untouched. The file contains nine
assertDoesNotCompiletests using ScalaTest's own macro (not the broken helper), four of them single-case adjacency tests; those nine are unchanged, and three of the four adjacency ones demonstrably fire under Mutation C below.Any related issues, documentation, discussions?
Closes #8401
How was this PR tested?
Baseline on
1cbe857007,PyBuilder/test: 184 tests, 5 suites, 0 failures. After: 186 tests, 5 suites, 0 failures. Comparing test-case identities fromcommon/pybuilder/target/test-reports/TEST-*.xmlrather than counts: 0 removed, 0 renamed, and the 2 additions are exactly the new tests named above. Every suite reportsfailures="0"on both sides.PyBuilder/scalafmtCheck,PyBuilder/Test/scalafmtCheckandPyBuilder/scalafixAll --checkall pass.Non-vacuity, both directions, measured:
fromInterpolatedpresent)#left neighbourBoth sweeps carry loop-ran guards (
assert(checked == 130)/assert(checked == 57)) and set-size guards (assert(size == 65)/assert(size == 29)), and report every mismatching case rather than failing fast, so a regression names the characters.Mutation A —
PythonLexerUtils.isBadNeighborchanged to... || (isIdentChar(c) && c != 'z'):That contrast is the clearest evidence here: under a mutation that removes one character from the bad-neighbour set, the old test and every other suite in the module stayed green; the rewritten sweep names the exact character, both sides, and quotes the benign message as the reason.
PythonLexerUtilsSpecdoes not catch it either — it only samples the predicate (',",a,Z,0,_, plus two negatives) and never testsz. The rewritten sweep catches it only because the character set is no longer derived from the predicate.Mutation B — the right-neighbour abort in
BoundaryValidator.validateCompileTimedisabled (if (false && isBadNeighbor(rightNeighbor))):Mutation C — both compile-time neighbour arms disabled:
The fourth adjacency test ("PyString (EncodableString) glued to identifier on the left") stays green under Mutation C — it is pinned through a different path, not the compile-time neighbour arms.
All three mutations were reverted by copying back pre-mutation file copies, never
git checkout/git restore.git diff 1cbe857007 -- '*/src/main/*'is empty on the committed branch.Corrections after review. Three claims in an earlier draft of this description were wrong and are fixed above; recording them rather than editing them away:
assertDoesNotCompiletests, 4 of them adjacency; "two" was the count that happened to fire under Mutation B, not a property of the fileA... as six literal characters"\\u0041, two backslashes; the macro's neighbour was the digit1A fourth: an earlier draft put the sweep runtime at "~4.2 s". Wall-clock for ToolBox work is not stable enough to quote — three runs on the same machine measured the two sweeps at 4.02 s, 4.90 s and 5.70 s — so the figure is dropped in favour of the compilation count, which is exact.
One thing worth recording, because it looks like a bug and is not:
pyb"pre ${ui}\" post"— a"written as a Scala escape — is not rejected.StringContext.partsfor a custom interpolator are raw, so both the validator andfromInterpolatedsee\as the neighbour, andfromInterpolatedis documented as taking raw parts and does not callprocessEscapes. The validator and the renderer agree, so there is nothing to fix. The new snippets sidestep it by using triple-quoted Scala literals, where the character is verbatim.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)