Conversation
Two frames agree when they hold the same rows in the same order with the same values, and the comparison says which of those failed rather than that they differ. A float is compared within a tolerance, since the two paths reach the same number by different arithmetic. A visualization has no frame to compare. What it emits is a figure, and the meaning of a figure is in the numbers behind it, so the figures are compared number by number; a plot that renders as an image is compared as the HTML it emits instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Automated Reviewer SuggestionsBased on the
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8359 +/- ##
=========================================
Coverage 92.76% 92.76%
Complexity 4897 4897
=========================================
Files 1236 1236
Lines 52155 52155
Branches 6409 6409
=========================================
Hits 48380 48380
Misses 2194 2194
Partials 1581 1581
*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:
|
`read_json` infers a type per file, so a column one side wrote as "6" and the other wrote as "6.0" both arrive as the number 6, and a null beside the text "nan" both arrive as NaN. Two different answers compared as one, on every string column of every operator. The engine writes a schema beside its output naming which columns are strings, and both sides are now read that way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`astype(str)` gets three things wrong against `toString`: an empty cell renders as the text "nan", a column holding one has become a float by then so 6 reads "6.0", and a boolean capitalises where the executor writes it lower. The comparison could not see any of it until apache#8359 stopped inferring a type per file, which is why a cast to STRING has been passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three kinds of comment came out. A drawing of the string the code below assembles. A restatement of a branch the reader can see. And the word MVP, which dated the scope to a moment rather than stating it. What replaces them says the same thing shorter, or says what the code cannot: which cases the harness does not drive and why none of them has an operator asking for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
carloea2
left a comment
There was a problem hiding this comment.
The result comparison looks good.
It ignores column order. Row order is settled by --unordered, which lex-sorts both frames unless the operator declares itself order-sensitive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
carloea2
left a comment
There was a problem hiding this comment.
Reviewed alongside the related export and verification PRs. These findings are based on code inspection and focused Python checks, not a full Scala suite run.
A requested column is one the engine declared as a model, so a side that never emitted it is the divergence. Skipping it hid that: the column was dropped from both frames afterwards and a path that produced no model at all compared equal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
carloea2
left a comment
There was a problem hiding this comment.
Follow-up review of the series. Runtime findings were checked locally against the current code; deployment routing was checked from configuration.
The standalone script writes its page through Python's text mode, so on Windows the file holds CRLF while the runtime path's JSONL carries the same markup with LF. Five operators reported a mismatch that was only that. The line ending is the platform writing the file rather than anything the operator chose, so it is normalized away like the Styler uuid already is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
carloea2
left a comment
There was a problem hiding this comment.
Further review with local regression tests and connected execution checks.
The comparison gave every numeric column the tolerance a double needs, so two whole numbers a workflow would never call the same passed it: at rtol=1e-5, LONG 100000 and 100001 compare equal. The declared integer columns are split out and compared exactly; the rest keep the tolerance. Reading them again is the other half. `read_json` parses a column holding a null through float64, so 9007199254740993 is already 9007199254740992 before anything compares it, and pinning the dtype does not help: the rounding happens on the way in. Python's json reads the integer exactly. A value that is not whole in a column the engine declared integral is itself the divergence, so it is reported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It restated its own signature and listed a codec table that lives in TupleIO. The reasons a reader cannot derive stay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@carloea2 all three are fixed: a missing model column now fails, line endings are normalized, and integer columns are compared exactly. Would you take another look when you have a moment? |
The two chart comparators go to a change of their own. They answer a different question and share no code with this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
carloea2
left a comment
There was a problem hiding this comment.
Integer and long columns are now read and compared exactly. The model and HTML checks also look good.
carloea2
left a comment
There was a problem hiding this comment.
The column sets are not compared before selecting loose columns from the actual table. An extra column in the expected table can be ignored. Please compare both column sets first and add an extra column test.
carloea2
left a comment
There was a problem hiding this comment.
Plotly comparison treats booleans as numbers, so true and 1 compare equal. This can hide a schema type change in a chart. Please compare booleans before the numeric tolerance and add a true versus 1 test.
carloea2
left a comment
There was a problem hiding this comment.
Two more comparisons can pass incorrectly. Models with different fitted feature names pass when their predictions match, and a declared string written as JSON number 6 passes against string 6 after coercion. Please compare fitted feature names and preserve raw JSON value types in schema checks.
carloea2
left a comment
There was a problem hiding this comment.
The comparator accepts 1.0 as an integer 1 when the declared type is integer. This hides a real type change. A later string conversion produces 1 in the native path but 1.0 in the exported path. Please reject noninteger JSON values for integer fields and add this case.
carloea2
left a comment
There was a problem hiding this comment.
The comparator ignores column order. Native columns a then b and exported columns b then a pass as equal. Column order is part of the schema and changes positional UDFs, file exports, and code that reads the first column. Please compare columns in order and add a reversed column test.
The plotly comparison read one figure from each side: the first row of the runtime path's output, and the exported script's file as a single figure. An operator that draws a chart per row had every chart after the first go unread, so the exported script could draw the wrong one and the run still agreed. Both sides are read as sequences now. The exported side accepts a lone figure, which is what a one-chart operator writes, or an array. A length difference is reported as one, and a value difference names which chart it was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four cases run compare.py directly, since the comparison lives in the script rather than in Scala: three charts that agree, a third pair whose second chart differs, an exported side that drew fewer than the run, and the lone figure a one-chart operator writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five ways the comparison agreed with itself rather than with the outputs. The column list was never checked. The comparison took its columns from the actual side and selected those out of the expected one, so a column only the expected side carried was never looked at, and `check_like` sorted the columns before comparing, so a reordering passed. Column order decides what a positional UDF reads, what a file export writes, and what code asking for the first column gets, so both halves of the list are compared now. A boolean setting in a Plotly figure fell into the numeric branch, `bool` being a subclass of `int` in Python, and True compared equal to 1 through isclose. A column whose declared type changed from boolean to integer reaches a figure as exactly that difference. A model was asked about its own fitted feature names. Two models fitted on different columns still answer alike on a probe carrying both, so the predictions agreed while the models did not. The names are compared first. A column the schema declares as text was read with `dtype=str`, which settles the two sides on one spelling but turns the JSON number 6 into "6" as readily as it leaves "6" alone. Those columns are re-read from the raw JSON, where a number is still a number. An integer column one side widened to a float passed as the integer the schema asked for. 6 and 6.0 do not survive the same: a later cast to text writes "6" from one and "6.0" from the other. Both sides widening alike is left alone, because where the engine's own path is a Python operator its table goes through pandas too, and the two still agree. A declared boolean column is read the same way, numpy having counted True as 1 under a comparison told not to check dtypes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All five are fixed in |
A chart per row is one of the two shapes a multi-chart operator hands over. The other is a bullet or gauge chart, which draws its charts onto one page and yields that page as a single row, while its script writes the whole set to its page and only the first of them as a figure. Reading one payload per row left every chart after the first on such a page uncompared, and the page the script wrote could not be read at all. Every Plotly.newPlot call on a row now counts, and the expected side may be a page as readily as a figure or a list of them. The one reformat is scalafmt on a line this file already carried. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Ran the full operator suite against this rule. Every failure it produced came from the harness writing its output without conforming to the declared schema, where the engine writes its tuples through it, so a holed integer column left the script as |
carloea2
left a comment
There was a problem hiding this comment.
The comparator now checks column order, JSON value types, model features, and every chart. The earlier findings are addressed. Looks good.
What changes were proposed in this PR?
Two frames agree when they hold the same rows in the same order with the same
values, and the comparison says which of those failed rather than that they
differ. A float is compared within a tolerance, since the two paths reach the
same number by different arithmetic.
A visualization has no frame to compare at all. The two comparators that read
what one draws are in #8509.
Any related issues, documentation, discussions?
Part of #8325, 6 of 27; that issue lists the set in order.
It does not compile until #8357 lands: that entry's build.sbt change puts workflow-operator's test sources on this module's test classpath, and the comparison reuses the
PythonWorkerPoolthat lives there.Closes #8411, the task this change is the whole of.
How was this PR tested?
The tests in this change cover it. The whole set is exercised together once the last piece lands: every operator run through the engine and through its generated script, and the two answers compared.
Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)