Skip to content

Test every library of an FMI job with FMI - #309

Merged
adrpo merged 1 commit into
masterfrom
fix-phase-without-simulator
Aug 12, 2026
Merged

Test every library of an FMI job with FMI#309
adrpo merged 1 commit into
masterfrom
fix-phase-without-simulator

Conversation

@adrpo

@adrpo adrpo commented Aug 12, 2026

Copy link
Copy Markdown
Member

The master-fmi job of build 11369 ran for seven hours and twenty minutes and then threw all of it away:

Traceback (most recent call last):
  File "./test.py", line 1153, in <module>
    (lambda filename_prefix, errPrefix, diff:
  File "./test.py", line 1158, in <lambda>
    ("%s (%d verified)" % (timeSeconds(diff.get("time")), diff.get("numCompared"))) if s[3]["phase"]>=7 else (...
AttributeError: 'NoneType' object has no attribute 'get'

A model reported that it had been verified while having nothing to show for the comparison. Chasing that turned up an older bug underneath it.

Four libraries have not been testing FMI at all

The configuration loop prepares a reference-file directory once and lets the libraries that share it reuse the result:

if c["referenceFiles"]["destination"] in preparedReferenceDirs:
  (c["referenceFiles"],c["referenceFilesURL"]) = preparedReferenceDirs[destination]
  continue

That continue skips the rest of the loop body — and the only thing after it is the line that turns FMI on:

if allTestsFmi:
  c["fmi"] = "2.0"

So in configs/conf.json four libraries have been running as ordinary native models inside the FMI jobs, each of them the second library to want a directory another had already cloned:

library shares the reference files of
PowerGrids_symb_jac PowerGrids
PowerGrids_dev PowerGrids
ClaRa_dev ClaRa
ScalableTestSuite_noopt ScalableTestSuite

The results say so plainly. Each of the four verifies exactly the same number of models in master and in master-fmi, while every library that prepares its own directory drops as an FMI job should:

library master master-fmi
PowerGrids_symb_jac 66 66 never built an FMU
PowerGrids_dev 19 19 never built an FMU
ClaRa_dev 72 72 never built an FMU
ScalableTestSuite_noopt 244 244 never built an FMU
PowerGrids 65 31
ScalableTestSuite 242 176
ClaRa 72 62

The same three lines also looked the directory up under destination — a variable assigned after the lookup, so it held whatever the previous library left behind. It has been reading the right entry only because every destination in the configuration happens to be spelled in normalised form.

Both are fixed by computing the normalised destination first and putting the preparation in an else branch, so nothing in the loop body is unreachable any more.

Why it started crashing

Before #297 this only mistested those libraries quietly. Now a job can run several simulators against one FMU, and a model that was never built as an FMU has no per-simulator results at all — so the second simulator's branch fell back to the phase the first simulator reached. That is 7, verified, with no comparison to go with it, and the report generator trusts phase >= 7 to mean there is one.

Two changes:

  • a simulator that never ran a model now reports the phase the shared build reached, never a phase another simulator went on to reach with the same FMU. The database row and the report agree, because both go through the same helper.
  • a model with nothing to compare renders as an empty cell whatever phase it claims. A mismatch should not be able to throw away a finished run.

Testing

The report cell was evaluated directly, before and after, by extracting the real expression from test.py:

before   diff=None, phase=7 -> AttributeError: 'NoneType' object has no attribute 'get'
before   verified          -> '1.50 (12 verified)'
before   failed vars       -> '1.50 (<a href="files/L_M.diff.html">2/12 failed</a>)'
after    diff=None, phase=7 -> '&nbsp;'
after    verified          -> '1.50 (12 verified)'
after    failed vars       -> '1.50 (<a href="files/L_M.diff.html">2/12 failed</a>)'

The reference-file loop was executed as it stands, with git and omc stubbed, over three libraries sharing one directory. Before, the third lost fmi; after, all three keep it, and each still ends up with the same prepared reference files.

resultValues and dataForSimulator were exercised over the shapes that matter: a simulator with no results of its own (phase capped at the build, no diff, no simulation time, and the row records the same), a build that failed (keeps its own lower phase), a simulator with its own results (untouched), one that ran and failed (reports its own failure, not the other's success), and the primary branch (passed through unchanged).

Related to #297, which turned this from a silent mistest into a crash.


Generated by Claude Code.

A library whose reference files are already prepared by another library took a
`continue` that skipped the rest of the configuration loop - and the only thing
after it is the line that turns FMI on:

    if allTestsFmi:
      c["fmi"] = "2.0"

So four libraries of configs/conf.json have been running as ordinary native
models inside the FMI jobs: PowerGrids_symb_jac, PowerGrids_dev, ClaRa_dev and
ScalableTestSuite_noopt, each of them the second library to want a reference
directory that PowerGrids, ClaRa and ScalableTestSuite had already cloned. They
verify identically in master and in master-fmi - 66, 19, 72 and 244 models -
while every library that prepares its own directory drops as an FMI job should
(242 to 176 for ScalableTestSuite, 65 to 31 for PowerGrids).

The reuse also looked the directory up under a key it had not computed yet, so
it took whatever `destination` the previous library left behind. Both are fixed
by computing the normalised destination first and putting the preparation in an
else branch, which leaves nothing after the loop's body unreachable.

Since #297 this crashed the run rather than quietly mistesting it. Such a model
has no per-simulator results, so the second simulator's branch reported the
phase the *first* simulator reached - 7, verified - with no comparison to go
with it, and building the report died on it after seven hours:

    ("%s (%d verified)" % (timeSeconds(diff.get("time")), ...)) if s[3]["phase"]>=7
    AttributeError: 'NoneType' object has no attribute 'get'

A simulator that never ran a model now reports the phase the shared build
reached and never a phase another simulator went on to reach with the same FMU,
in the report and in the database row alike; and a model with nothing to compare
renders as an empty cell whatever phase it claims, so a mismatch can never again
throw away a finished run.

Fixes the master-fmi failure of build 11369.
@adrpo
adrpo merged commit 4dc7387 into master Aug 12, 2026
7 checks passed
@sjoelund
sjoelund deleted the fix-phase-without-simulator branch August 12, 2026 19:38
adrpo added a commit that referenced this pull request Aug 12, 2026
An FMI job that runs several simulators over one FMU (#297, #309) gives the
first of them a veto over the rest. When it fails, the TimeoutError handler
calls writeResultAndExit, the process is gone, and the loop that runs
fmisimulators[1:] never starts -- but a row is still written for every
simulator, recording the phase the first one stopped at. The other tools are
marked as having failed at a model they were never given.

checkOutputTimeout raises TimeoutError for a command that exits non-zero as
well as for one that runs out of time, so this covers an ordinary failure and
not just a slow one.

In the master-fmi run of 2026-08-12 this cost 433 models. They had never
simulated under OMSimulator -- phase 5 in every master-fmi run back to
2026-08-08 -- but FMPy ran them, and the previous master-fmi-fmpy run has them
at phase 6 or 7. In the first combined run they are phase 5 in both tables,
with FMPy's simulation time recorded as exactly 0: OMSimulator failed in under
a second for 344 of them, and FMPy never started. They show up as 433 of the
653 regressions on the comparison page, next to the 213 that PR #309 really did
find. It is also why that page reports OMSimulator's version, and its JSON
parse warning, in place of "FMPy version 0.3.29": FMPy never ran to say what it
was.

The first simulator now records its failure and falls through to the others,
which is what every simulator after it already did. Nothing changes when only
one simulator is configured: that case still ends the model where it always
has, so the non-FMI paths and the single-simulator FMI jobs are untouched.

Verified on MyLibrary.Blocks.Examples.PID_Controller with the first simulator
replaced by /bin/false:

  before   phase 5, "simulators": {}
  after    phase 5, "simulators": {"fmpy": {"sim": 0.64, "phase": 7}}

and, unchanged in both:

  one failing simulator     phase 5, os._exit as before
  both simulators working   OMSimulator phase 7, fmpy phase 7


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

Co-authored-by: Claude Opus 5 (1M context) <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