Skip to content

Run the scheduler constraint tests under both backends - #2385

Merged
Flix6x merged 4 commits into
mainfrom
test/solver-coverage-hole
Aug 3, 2026
Merged

Run the scheduler constraint tests under both backends#2385
Flix6x merged 4 commits into
mainfrom
test/solver-coverage-hole

Conversation

@Flix6x

@Flix6x Flix6x commented Aug 3, 2026

Copy link
Copy Markdown
Member

Description

Since #2364 the schedulers build the same model twice — once through Pyomo, once directly in HiGHS — so a test of scheduler behaviour only means something under one backend if the two agree, which is exactly what cannot be assumed.

Only test_solver.py was parametrized over solvers. Everything else ran under the configured default and no other.

That is not hypothetical: on #2355 a fix passed on the Pyomo path and failed on the direct one, and test_commitments.py could not have caught it, because it only ever runs under one solver.

  • Modules opt in with RUN_UNDER_EACH_SOLVER = True; an autouse fixture in conftest.py does the switching, so no test signatures change
  • Enabled on test_group_constraints.py and test_operation_modes.py (27 tests → 54)
  • test_solver_coverage.py makes the remaining hole explicit

What is deliberately not enabled, and why

test_commitments.py and test_storage.py build assets with fixed names, so running each test twice in one fixture scope violates generic_asset's unique-name constraint. I tried it; it fails on both backends with UniqueViolation, which is a fixture problem rather than a backend disagreement. Parametrizing them means making those fixtures unique per parameter first — a larger change than this one, and better done on its own.

Rather than leave that silent, test_solver_coverage.py asserts that every planning test module either opts into the matrix or appears on an EXEMPT list with a reason, and that no EXEMPT entry is stale (module gone, or since opted in). Adding a module then forces a deliberate choice instead of quietly widening the gap.

How to test

pytest flexmeasures/data/models/planning/tests

test_group_constraints.py and test_operation_modes.py now report [appsi_highs] / [highspy] variants.

Proven capable of failing (per #2384): removing RUN_UNDER_EACH_SOLVER from a module makes test_scheduler_modules_run_under_each_solver_or_are_exempt fail with that module named; adding a module to EXEMPT while it still opts in makes test_exempt_list_has_no_stale_entries fail.

Further improvements

  • Make the DB fixtures in test_commitments.py / test_storage.py unique per parameter, then drop those two from EXEMPT.

Related items

Since #2364 the schedulers build the same model twice, once through Pyomo and
once directly in HiGHS, so a test of scheduler behaviour only means something
under one backend if the two agree -- which is the thing that cannot be assumed.
Only test_solver.py was parametrized, so most constraint behaviour was verified
under the configured default and no other. That is how a fix could pass on the
Pyomo path and fail on the direct one, as happened on #2355.

A module now opts in with RUN_UNDER_EACH_SOLVER = True and an autouse fixture in
conftest does the switching, so no test signature changes. Enabled on
test_group_constraints.py and test_operation_modes.py (27 tests, now 54).

test_commitments.py and test_storage.py are NOT enabled, and the reason is worth
recording: they build assets with fixed names, so running each test twice in one
fixture scope violates generic_asset's unique-name constraint. Parametrizing
them means making those fixtures unique per parameter first, which is a larger
change than this one.

Rather than leave that hole silent, test_solver_coverage.py asserts every
planning test module either opts in or appears on an EXEMPT list with a reason,
and that no EXEMPT entry is stale. A new module is then a decision someone makes
on purpose instead of a gap nobody notices.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2jLjsQzwztDc7Nxz1ozmQ
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@read-the-docs-community

read-the-docs-community Bot commented Aug 3, 2026

Copy link
Copy Markdown

Documentation build overview

📚 flexmeasures | 🛠️ Build #33897274 | 📁 Comparing f223676 against latest (fc0b7bd)

  🔍 Preview build  

1 file changed
± api/v3_0.html

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

At least show that the exempt modules would still succeed when all their tests would use the other solver.

The exempt modules could not be parametrized in-process, but that left their
behaviour under the non-default backend simply unknown, which is a weaker
position than it needed to be.

Adds a --lp-solver option pinning a whole run to one backend, so those modules
can be run again under the other one. (An environment variable would not do:
TestingConfig does not read FLEXMEASURES_LP_SOLVER.)

Result: test_commitments.py, test_storage.py and test_process.py -- 68 tests --
all pass under appsi_highs, the non-default backend. So the exemption costs
per-test granularity, not coverage.

Checked the flag actually bites rather than silently doing nothing: running with
--lp-solver=definitely_not_a_solver fails with Pyomo's UnknownSolver, so the
option does reach solver selection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2jLjsQzwztDc7Nxz1ozmQ
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x

Flix6x commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Good call — "cannot be parametrized" had quietly become "unverified", which is a weaker claim than the situation warranted.

Added a --lp-solver option that pins a whole run to one backend, so the exempt modules can simply be run again under the other one. An environment variable would not have worked here: TestingConfig does not read FLEXMEASURES_LP_SOLVER.

Result — all three exempt modules pass under appsi_highs, the non-default backend:

pytest flexmeasures/data/models/planning/tests/test_commitments.py \
       flexmeasures/data/models/planning/tests/test_storage.py \
       flexmeasures/data/models/planning/tests/test_process.py --lp-solver=appsi_highs
68 passed

So the exemption costs per-test granularity, not coverage: those modules are green under both backends, just not within one run.

I also checked the flag actually bites rather than silently doing nothing — --lp-solver=definitely_not_a_solver fails with Pyomo's UnknownSolver, so the option really does reach solver selection. Without that check, "68 passed" would have been equally consistent with the flag being ignored.

The command is recorded next to the EXEMPT entries so it can be re-run.

Every docstring and comment block added here broke lines mid-phrase, against
the convention this repo documents and that #2384 had just restated -- "run
once per / backend", "under one backend / if the two agree", "set from the /
environment".

Reflowed so each physical line ends after punctuation. Text only.

Checked mechanically rather than by eye this time: inside a multi-line docstring
or comment block, every line but the last must end in punctuation. The only
remaining hits in the added lines are the two continuation lines of an example
shell command, which end in a backslash by nature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2jLjsQzwztDc7Nxz1ozmQ
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x

Flix6x commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Reflowed in b59d1444e — you were right, and it was every block I added: "run once per / backend", "under one backend / if the two agree", "set from the / environment". Text only; 319 passed, 3 xfailed.

I've stopped checking this by eye, since eyeballing is what failed twice. The rule is mechanical — inside a multi-line docstring or comment block, every line but the last must end in punctuation — so I checked it that way, and separated hits in lines this PR adds from pre-existing ones. The only remaining hits in added lines are the two continuation lines of an example shell command, which end in a backslash by nature. The pre-existing hits in conftest.py I left alone; reflowing unrelated prose does not belong in this PR.

If it would be useful, that check is about 30 lines and could go in as a pre-commit hook, so the convention stops depending on a reviewer noticing. Happy to open it separately — it would have caught this, #2364's, and #2218's.

Comment thread flexmeasures/data/models/planning/tests/test_solver_coverage.py Outdated
Comment thread flexmeasures/data/models/planning/tests/test_solver_coverage.py Outdated
A code comment telling the reader to consult a pull request description ages
badly and does not belong in the tree. Replaced with the fact it was pointing
at: these modules pass under the other backend when a run is pinned with
--lp-solver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2jLjsQzwztDc7Nxz1ozmQ
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x added a commit that referenced this pull request Aug 3, 2026
The docstring convention -- break lines only after punctuation -- has been
missed in three PRs in a row (#2364, #2218, #2385), each time caught by a
reviewer rather than by the author. #2384 documented the rule harder, and the
very next PR broke it, so documentation is evidently not the missing piece.

Adds .claude/hooks/reflow_check.py and wires it as a PreToolUse hook on
`git commit`, next to the existing pre-commit and worktree-guard hooks.

Two deliberate choices:

It is an agent hook, not a pre-commit hook. The misses have been an agent's,
and contributors should not pay a false-positive tax for that. It is also the
only form that works: the check is a heuristic, and a heuristic gate that a
human hits on embedded OpenAPI YAML gets disabled, whereas an agent can read
"line 2028 may break mid-phrase", look, and judge.

It is advisory and never blocks. It exits 0 always, and only inspects lines
being added, so a legacy file is not a wall -- the repo has ~4350 candidate
hits, and reflowing unrelated prose is not a thing a commit should do.

Suppressing non-prose (bullets, RST directives, doctests, embedded YAML and
JSON, shell continuations) cuts the noise substantially where docstrings carry
API specs: sensors.py 494 hits to 59, assets.py 385 to 15. Files that are
genuinely prose-heavy stay high (storage.py 199 to 173), which is the honest
answer rather than a tuned one.

Self-tested both directions: a docstring broken mid-phrase is reported, a
correctly reflowed one is silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2jLjsQzwztDc7Nxz1ozmQ
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x
Flix6x merged commit fa012cf into main Aug 3, 2026
13 checks passed
@Flix6x
Flix6x deleted the test/solver-coverage-hole branch August 3, 2026 22:59
Flix6x added a commit that referenced this pull request Aug 3, 2026
The docstring convention -- break lines only after punctuation -- has been
missed in three PRs in a row (#2364, #2218, #2385), each time caught by a
reviewer rather than by the author. #2384 documented the rule harder, and the
very next PR broke it, so documentation is evidently not the missing piece.

Adds .claude/hooks/reflow_check.py and wires it as a PreToolUse hook on
`git commit`, next to the existing pre-commit and worktree-guard hooks.

Two deliberate choices:

It is an agent hook, not a pre-commit hook. The misses have been an agent's,
and contributors should not pay a false-positive tax for that. It is also the
only form that works: the check is a heuristic, and a heuristic gate that a
human hits on embedded OpenAPI YAML gets disabled, whereas an agent can read
"line 2028 may break mid-phrase", look, and judge.

It is advisory and never blocks. It exits 0 always, and only inspects lines
being added, so a legacy file is not a wall -- the repo has ~4350 candidate
hits, and reflowing unrelated prose is not a thing a commit should do.

Suppressing non-prose (bullets, RST directives, doctests, embedded YAML and
JSON, shell continuations) cuts the noise substantially where docstrings carry
API specs: sensors.py 494 hits to 59, assets.py 385 to 15. Files that are
genuinely prose-heavy stay high (storage.py 199 to 173), which is the honest
answer rather than a tuned one.

Self-tested both directions: a docstring broken mid-phrase is reported, a
correctly reflowed one is silent.


Claude-Session: https://claude.ai/code/session_01X2jLjsQzwztDc7Nxz1ozmQ

Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant