Skip to content

test(verify): compare what the two paths produced - #8359

Draft
kz930 wants to merge 14 commits into
apache:mainfrom
kz930:feat/verify-compare-two-answers
Draft

kz930 wants to merge 14 commits into
apache:mainfrom
kz930:feat/verify-compare-two-answers

Conversation

@kz930

@kz930 kz930 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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 PythonWorkerPool that 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)

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>
@github-actions github-actions Bot added feature platform Non-amber Scala service paths labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • No candidates found from git blame history.

@codecov-commenter

codecov-commenter commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.76%. Comparing base (e7d1676) to head (b54fc2d).

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           
Flag Coverage Δ *Carryforward flag
access-control-service 71.78% <ø> (ø)
agent-service 99.16% <ø> (ø) Carriedforward from b17651b
amber 88.59% <ø> (ø) Carriedforward from b17651b
computing-unit-managing-service 55.20% <ø> (ø)
config-service 87.37% <ø> (ø)
file-service 81.53% <ø> (ø)
frontend 96.57% <ø> (ø) Carriedforward from b17651b
notebook-migration-service 83.73% <ø> (ø)
pyamber 98.41% <ø> (ø) Carriedforward from b17651b
workflow-compiling-service 74.09% <ø> (ø) Carriedforward from b17651b

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

`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>
kz930 added a commit to kz930/texera that referenced this pull request Sep 3, 2026
`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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result comparison looks good.

@kz930
kz930 marked this pull request as draft September 4, 2026 17:44
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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread workflow-compiling-service/src/test/resources/python/compare.py Outdated
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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further review with local regression tests and connected execution checks.

Comment thread workflow-compiling-service/src/test/resources/python/compare.py
kz930 and others added 2 commits September 10, 2026 13:01
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>
@kz930

kz930 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

@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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer and long columns are now read and compared exactly. The model and HTML checks also look good.

@carloea2 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

kz930 and others added 4 commits September 18, 2026 15:56
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>
@kz930

kz930 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

All five are fixed in b17651bef. The integer rule fails only where the two sides wrote the column differently, 6 against 6.0, because when the engine's own path is a Python operator both sides widen alike and still agree.

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>
@kz930

kz930 commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

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 6.0 and the engine as 6; that is fixed in #8358 and the suite is back to 145 of 145 with the rule in place. It also caught two genuine column-order divergences, the Machine Learning Scorer and the JSONL source, both now fixed.

@carloea2 carloea2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparator now checks column order, JSON value types, model features, and every chart. The earlier findings are addressed. Looks good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature platform Non-amber Scala service paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compare what the two paths produced

3 participants