Skip to content

[End to End Testing] Implement must change. - #3182

Open
matthew7838 wants to merge 27 commits into
devfrom
e2e-must-change
Open

[End to End Testing] Implement must change.#3182
matthew7838 wants to merge 27 commits into
devfrom
e2e-must-change

Conversation

@matthew7838

@matthew7838 matthew7838 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Adds "must change" support to end-to-end testing: an SME can flag variables that are expected to differ from the recorded expected results, and E2E now fails if they don't.

Context

Issue(s) closed by this pull request: closes #3158

What

  • Each input set gets a *_must_change_variables.json file (in input/data/end_to_end_testing/) listing variables that must differ from the recorded expected results.
  • Flagged variables are graded inversely: they must change (beyond the domain tolerance) or the domain fails; their differences no longer count as regular failures.
  • Comparison results now include changed_variables, a compiled list of the differing variable names, so an SME can evaluate them and flag the legitimate ones.

Why

E2E filters are generated at version X; when a model change lands, every intended difference in X+ shows up as a failure. An SME often knows a variable must be different without knowing its new value, so this is a binary flag, not a new expected value. The changed_variables list gives the SME a clean list to review instead of raw diff paths.

How

  • New must_change_variables_path key on every entry in end_to_end_testing_result_paths.json points to the input set's file (schema added in properties/default.json).
  • In E2ETestResultsHandler, flagged variables are excluded from the regular DeepDiff and each one is checked individually with the same DeepDiff settings and tolerance filtering; outcomes are recorded as must_change_satisfied / must_change_violations, and end_to_end_testing_passing requires no regular differences and no violations.
  • Flagged names that exist in no domain's expected results raise a configuration error, so typos cannot silently pass.
  • The lists are user-managed and default to empty; they are not cleared automatically. After updating expected results, empty the list yourself — leftover flags will fail the next run.

Test plan

  • Unit tests for file loading/validation, the per-variable change check, changed-name extraction, and the full comparison over real temp files (pass, violation, missing variable, unexpected change, unknown name).
  • Verified against the real input pipeline: loaded the animals_only e2e metadata through InputManager, synthesized actual results from the filter file, and ran compare_actual_and_expected_test_results for all five scenarios above.
  • Flake8, Black, and mypy clean (no new mypy errors vs dev).

Complete workflow tests:

  1. Flag something that doesn't change → violation. Change nothing else; add to the flag file: "AnimalModuleReporter.report_animal_population_statistics.population_number_of_cows". Run. Animal domain fails; the results file shows must_change_violations with "value still matches the expected results within the tolerance".

  2. Change an input, flag nothing → the SME review list. Empty the flag list; in the animal file set animal_config.management_decisions.milk_fat_percent from 4 to 4.5. Run. Animal fails, and Animal.changed_variables in the results file is the compiled name list an SME would review.

  3. Declare the change → pass. Keep the 4.5 tweak; copy the changed_variables array from scenario 2's results file straight into must_change_variables (same JSON shape on purpose). Run. Animal passes, with everything under must_change_satisfied. This is the intended workflow end to end.

  4. Typo protection. Add "AnimalModuleReporter.not_a_real_variable" to the flag list. Run. You get the configuration error "Must-change variables not found in the expected results of any domain".

Input Changes

  • Added freestall/open_lot/field_and_feed/animals_only _must_change_variables.json files (empty lists by default).
  • Added must_change_variables_path to every entry in end_to_end_testing_result_paths.json, with matching schema in RUFAS/input/metadata/properties/default.json.

Output Changes

  • {Domain}.changed_variables: names of unflagged variables whose values differ from the expected results.
  • {Domain}.must_change_satisfied: flagged variables that did change.
  • {Domain}.must_change_violations: flagged variables that did not change (or are missing), with the reason.

Filter

No new filter needed — the existing e2e_comparison_* filters ({Domain}.* patterns) already capture the new keys.

@matthew7838 matthew7838 self-assigned this Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on e2e-must-change branch: 1134
Mypy errors on dev branch: 1135
1 fewer errors on e2e-must-change branch

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Please update the changelog. This PR cannot be merged until changelog_WIP.md is updated.
🚨 Flake8 linting errors were found. Please fix the linting issues.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on e2e-must-change branch: 1134
Mypy errors on dev branch: 1135
1 fewer errors on e2e-must-change branch

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Please update the changelog. This PR cannot be merged until changelog_WIP.md is updated.
🚨 Flake8 linting errors were found. Please fix the linting issues.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on e2e-must-change branch: 1134
Mypy errors on dev branch: 1135
1 fewer errors on e2e-must-change branch

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Please update the changelog. This PR cannot be merged until changelog_WIP.md is updated.
🚨 Flake8 linting errors were found. Please fix the linting issues.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on e2e-must-change branch: 1134
Mypy errors on dev branch: 1135
1 fewer errors on e2e-must-change branch

@matthew7838
matthew7838 marked this pull request as ready for review August 10, 2026 14:29
@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on e2e-must-change branch: 1133
Mypy errors on dev branch: 1134
1 fewer errors on e2e-must-change branch

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on e2e-must-change branch: 1133
Mypy errors on dev branch: 1134
1 fewer errors on e2e-must-change branch

@allisterakun allisterakun left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

@ew3361zh ew3361zh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems close but is complex! I may need a walk through on this if possible. I left some notes on things I'm confused on or could use clarification with.

Also had trouble with multiple must-change variables across multiple domains working properly.

Here are the 2 I used:

"AnimalModuleReporter.report_animal_population_statistics.population_number_of_cows", "PurchasedFeedStorage.report_stored_purchased_feeds.stored_feed_202.balance_storage_levels"

I added them both to all must_change_variables jsons and then changed # of cows in the animal input thinking that would change the cow number and the feed balance since a different number of cows would be eating a different amount of feed.

I got:
Freestall

  • Animal - must_change_violation for population_number_of_cows
  • Feed - must_change_satisfied for 202.balance_storage_levels

Animals only:

  • Animal - must_change_violation for population_number_of_cows

Field and Feed:

  • Feed - passing, nothing about 202 so I'm guessing it wasn't found to be matching. Maybe that should be reported?

Open Lot:

  • Animal - must_change_violation for population_number_of_cows
  • Feed - must_change_violation for 202.balance_storage_levels - this is the one that threw me because even though open lot uses a different feed input, it still uses 202 in rations so I would've expected it to pass.

@@ -0,0 +1,4 @@
{
"description": "Variables listed in 'must_change_variables' are expected to differ from the recorded expected results, e.g. because of a known model change whose new values are not yet known. During an end-to-end testing run, each listed variable must differ from its recorded expected value beyond the domain tolerance: a matching value is reported as a failure, and differences in listed variables are not. Variable names must exactly match keys in the 'expected_results' of this input set's e2e_json_*_filter.json files; the 'changed_variables' list in the end-to-end comparison results shows which variables currently differ. Managing this list is the user's responsibility and it defaults to empty: after updating the expected results (UPDATE_E2E_TEST_RESULTS), empty this list yourself, since the freshly recorded expected results already reflect the change and leftover entries will fail the next run.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do you think we could have this utilize regex-pattern-matching? I think of the variables that have the simulation day attached or a soil layer number attached and having the ability to match a series of variables across a range seems like a very likely scenario. If you like the idea, we could implement in a separate issue.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also what do you think about automatically clearing out these sections of must-change variables during an e2e expected results update?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's a great idea to have regex pattern matching. I will create an issue for it.

I'd prefer not to auto-clear. The must-change file records the SME's expectation, and I don't want the update task silently rewriting it. A leftover flag fails loudly on the next run with a self-explanatory message, while auto-clearing could silently discard a flag someone set for a change that hasn't landed yet.

Comment on lines +421 to +427
if not path.exists():
om.add_error(
"End-to-end testing must-change configuration error",
f"Must-change variables file not found: {path}",
info_map,
)
raise FileNotFoundError(f"E2E testing error: Must-change variables file not found: {path}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wonder if the validity of this path (and other paths referenced after e2e is complete) should be validated at the start somehow? Just thinking about how long e2e testing will take once the averaging results piece is factored in and these end-of-task validation errors seem brutal.

@matthew7838 matthew7838 Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, and all the inputs the comparison needs exist before the sim runs, so we can check them up front. This could also fit into a separate issue.

"function": E2ETestResultsHandler.compare_actual_and_expected_test_results.__name__,
}
test_result_path_sets = E2ETestResultsHandler._get_test_result_paths(output_prefix)
must_change_variables = E2ETestResultsHandler._load_must_change_variables(test_result_path_sets)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you explain a little about this set-up?

If you gather the must_change_variables for all domains outside the test_result_path_sets loop but then check this entire set against each domain within that loop, does that mean the user should expect all those variables to change in whichever domain set of results they're found?

Are there circumstances where we'd expect a variable that's found in multiple domains to change in some simulations and not in others?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems like this should maybe be within the loop and done at each iteration through for each individual path_set but it's a little confusing to me so maybe I have it wrong.

@matthew7838 matthew7838 Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Each task calls this once. All the domain entries of a set point at the same must-change file(freestall's Animal, CropAndSoil, Manure, and Feed entries all reference freestall_must_change_variables.json), so moving the load inside the loop would just read the same file four times.

On your other questions: within one input set every domain is just a filter over the same simulation, so a variable has one value per run, so if two domains both recorded it, they'd agree on whether it changed.


diff = DeepDiff(expected_results, actual_results, ignore_order=True, verbose_level=2, significant_digits=3)
domain_must_change_variables = sorted(name for name in must_change_variables if name in expected_results)
matched_must_change_variables.update(domain_must_change_variables)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same with this - should matched_must_change_variables be path_set specific (i.e. be created new within each loop through test_result_path_sets?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It deliberately survives the whole loop. matched_must_change_variables is the memory of "which flagged names have been found in at least one domain so far".

Comment on lines +171 to +182
if is_difference_in_results:
om.add_error(
f"End-to-end testing failed for {domain}",
"Identified differences between actual and expected results.",
info_map,
)
if must_change_violations:
om.add_error(
f"End-to-end testing failed for {domain}",
f"Must-change variables did not change: {sorted(must_change_violations)}",
info_map,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wondering if these could be distinguished in the e2e summary at the end of the e2e runs - e.g.

freestall_e2e results:
  Animal: Failing - no differences, yes must change violations
  CropAndSoil: Passing
  Manure: Failing - yes differences, no must change violations
  Feed: Passing

Not required obviously but just a thought for improving down the line.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also quick note that part of the reason I'm suggesting this is that these errors are getting more difficult to catch with the volume of logs and warnings that occur through all the simulations and now that an error can be multiple things, getting a summary of that at the end would be helpful. Or maybe we should think about changing the default verbosity of the e2e tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I like this. I'd rather make it its own issue than add it to this PR, though. The useful version is one summary printed after all the runs finish, and since the sets run in parallel workers, that means the main process has to collect results from the tasks; that's complicated. This new issue's scope can include both the final reporting and the verbosity (As we discussed and brought up by Allister, it still needs to extract some log info related to the simulation status).

Comment on lines +193 to +195
if domain_must_change_variables:
comparison_results["must_change_satisfied"] = must_change_satisfied
comparison_results["must_change_violations"] = must_change_violations

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel like it could be helpful to have the amount the variable changed by for reference reported here - maybe the change is a small fraction below or above the threshold and that would be helpful information to the SME. Or maybe it's must-change but the factor by which it changed is greatly out of proportion with the SME's expected change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I would say this is a lot more complicated than it seems. One main problem I can think of is that most flagged variables are a year-long series (feed 202's balance was 13 values; daily variables are 365) or lists of dictionaries. What "the amount" will be is hard to define. For example, what's the biggest percent change? The count of days beyond tolerance? The full diff? etc.

So this is something that I think might not be worth the hassle, since with such complexity in the outputs, I'm not sure if it's going to be that helpful in the end.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on e2e-must-change branch: 1133
Mypy errors on dev branch: 1134
1 fewer errors on e2e-must-change branch

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[E2E] Introduce Must Change

3 participants