Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/actions/coverage-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ runs:
shell: bash
run: julia --color=yes --project=OM.jl/OMLibraryTesting.jl -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()'

# OMFrontend bundles MSL 3.2.3 and 4.0.0; another version is the installed
# library: Modelica, ModelicaServices and Complex (a single file in the
# repository, a folder with its package.mo when installed).
# OMFrontend bundles MSL 3.2.3 and 4.1.0 as single files; another version is
# the installed library: Modelica, ModelicaServices and Complex (a folder with
# its package.mo when installed). 4.1.0 is installed too: a bundle has no
# Resources (the files models read through modelica:// URIs).
- name: Install MSL ${{ inputs.msl }}
if: inputs.msl != '3.2.3' && inputs.msl != '4.0.0'
if: inputs.msl != '3.2.3'
shell: bash
env:
MSL_VERSION: ${{ inputs.msl }}
Expand Down
8 changes: 4 additions & 4 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "OMLibraryTesting"
uuid = "4866894a-ca78-4bef-9a5c-0d35f2f81ce1"
version = "0.3.2"
version = "0.3.3"
authors = ["John Tinnerholm <john.tinnerholm@liu.se>"]

[deps]
Expand Down Expand Up @@ -46,9 +46,9 @@ DoubleEnded = "0.2.3"
ImmutableList = "0.4.2"
ListUtil = "0.4.1"
MetaModelica = "0.4.2"
OM = "0.6.1"
OMBackend = "0.7.0"
OMFrontend = "1.3.1"
OM = "0.7.0"
OMBackend = "0.8.0"
OMFrontend = "1.7.5"
OMParser = "0.2.1"
SCode = "1.2.0"

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ results = OMLibraryTesting.run_coverage(domain = "Thermal", msl_version = "MSL:3
results = OMLibraryTesting.run_coverage(model = "Modelica.Thermal.HeatTransfer.Examples.TwoMasses")

# MSL 4.1.0: its experiment list, registry (models/models_4.1.0.toml) and reference data
# (reference/4.1.0/); OMFrontend bundles 3.2.3 and 4.0.0, so 4.1.0 is the installed library
# (~/.openmodelica/libraries: Modelica, ModelicaServices and Complex 4.1.0)
# (reference/4.1.0/); OMFrontend bundles 3.2.3 and 4.1.0 as single files (the resources a model
# reads come from the installed library: ~/.openmodelica/libraries, Modelica 4.1.0)
results = OMLibraryTesting.run_coverage(msl_version = "MSL:4.1.0")

# Generate an HTML report
Expand Down Expand Up @@ -189,8 +189,8 @@ Selected keywords (`run_testsuite`):

Notes and limitations:

* `:omjl` mode only runs cases that load a **bundled** MSL version (3.2.x or 4.0.0)
and target a `Modelica.*` class. Cases using MSL 4.1.0/trunk, third-party libraries,
* `:omjl` mode only runs cases that load a **bundled** MSL version (3.2.x, or 4.x as 4.1.0)
and target a `Modelica.*` class. Cases using MSL trunk, third-party libraries,
or inline-defined models are reported as skipped with a reason. It stops at
`SIMULATE` by default because the testsuite ships no validation CSVs in this harness.
* `:rtest` mode needs `omc-diff` in the scaffolded OPENMODELICAHOME. `fetch_testsuite`
Expand Down
8 changes: 7 additions & 1 deletion models/models_4.1.0.toml
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ atol = 1.0
domain = "Electrical.Analog"
name = "Modelica.Electrical.Analog.Examples.CharacteristicIdealDiodes"
reltol = 0.05
solver = "FBDF"
issue = "2026-10-22: the FBDF override (2026-05-22, Rodas5 Unstable at the diodes' switching) removed: FBDF stops with MaxIters, the default solver validates 3/3."

[models.ChuaCircuit]
atol = 5.0
Expand Down Expand Up @@ -778,3 +778,9 @@ name = "Modelica.Mechanics.MultiBody.Examples.Rotational3DEffects.BevelGear1D"
domain = "Mechanics.MultiBody"
stopTime = 0.4
issue = "2026-10-13: the MSL 4.1.0 reference ends at 0.4 s (experiment StopTime 1): compared over the reference's span."

[models.Mechanics_MultiBody_Examples_Elementary_PointGravityWithPointMasses2]
name = "Modelica.Mechanics.MultiBody.Examples.Elementary.PointGravityWithPointMasses2"
domain = "Mechanics.MultiBody"
stopTime = 2.0
issue = "2026-10-20: the MSL 4.1.0 reference ends at 2.0 s (experiment StopTime 3): compared over the reference's span."
10 changes: 7 additions & 3 deletions src/comparison.jl
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,15 @@ end
_event_limits(ref_time, ref_values, t) -> Union{Nothing, Tuple{Float64, Float64}}

The values before and after an event the reference recorded at `t` (a time
that appears more than once: a result file stores both), else `nothing`.
that appears more than once: a result file stores both), else `nothing`. The
reference files store their times as Float32: the event's time is `t` to
Float32's rounding (MSL HBridge_RL's switching at t = 0.099 is stored as
0.098999995, at 0.0996 as 0.099600002).
"""
function _event_limits(ref_time::Vector{Float64}, ref_values::Vector{Float64}, t::Float64)
first_row = searchsortedfirst(ref_time, t)
last_row = searchsortedlast(ref_time, t)
local rounding = Float64(eps(Float32(max(abs(t), floatmin(Float32)))))
first_row = searchsortedfirst(ref_time, t - rounding)
last_row = searchsortedlast(ref_time, t + rounding)
last_row > first_row || return nothing
return (ref_values[first_row], ref_values[last_row])
end
Expand Down
57 changes: 55 additions & 2 deletions src/runner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,61 @@ function run_model(spec::ModelSpec, mgr::WorkerManager;
return ModelResult(spec, PhaseResult[], BROKEN,
Dates.format(Dates.now(), "yyyy-mm-dd HH:MM"))
end
return run_on_worker(mgr, spec, effective_phases; timeout = timeout,
check_sim_code = check_sim_code)
local result = run_on_worker(mgr, spec, effective_phases; timeout = timeout,
check_sim_code = check_sim_code)
recycleLargeWorker!(mgr)
return result
end

"""
recycleLargeWorker!(mgr)

Restarts the worker of `mgr` before its next model when its memory (`workerMemoryGB`) is over
`OMJL_COVERAGE_RECYCLE_GB` gigabytes (unset or 0: never). A worker keeps what the models before it
left (compiled code, caches, freed memory the allocator holds), and a footprint watchdog killed it
under whichever model came next (coverage 21: MSL QuasiStatic EddyCurrentLosses, under 3 GB in a
fresh process).
"""
function recycleLargeWorker!(mgr::WorkerManager)::Nothing
local limit = something(tryparse(Float64, get(ENV, "OMJL_COVERAGE_RECYCLE_GB", "0")), 0.0)
(limit > 0 && mgr.os_pid !== nothing && mgr.pid !== nothing && mgr.pid in workers()) || return nothing
local gb = workerMemoryGB(mgr.os_pid)
if gb > limit
@info "Recycling worker $(mgr.pid): memory $(round(gb; digits = 1)) GB, over $(limit) GB"
kill_worker!(mgr)
end
return nothing
end

"""
workerMemoryGB(os_pid) -> Float64

A process's memory in gigabytes as a footprint watchdog measures it: on macOS `top`'s MEM (the
physical footprint), elsewhere `ps`'s resident size. On macOS the two differ in both directions
(compressed pages count in the footprint, freed pages the allocator marked reusable do not; one
process: 11.5 GB resident, 7.7 GB footprint), and recycling by the resident size let coverage 26's
workers pass the 8 GB footprint watchdog below 6 GB resident (FullRobot,
ThreePhaseTransformerWithRectifier killed; under 6 GB each alone). Freed memory from many medium
arrays stays in the footprint (a worker at 0.6 GB live heap: 7.5 GB). 0 where neither reads.
"""
function workerMemoryGB(os_pid::Integer)::Float64
if Sys.isapple()
local top = try
read(`top -l 1 -pid $(os_pid) -stats mem`, String)
catch e
e isa InterruptException && rethrow()
""
end
local m = match(r"([0-9.]+)([KMG])[+-]?\s*$", strip(top))
m === nothing || return parse(Float64, m[1]) * (m[2] == "G" ? 1.0 : m[2] == "M" ? 1 / 1024 : 1 / 2^20)
end
local kb = try
something(tryparse(Int, strip(read(`ps -o rss= -p $(os_pid)`, String))), 0)
catch e
e isa InterruptException && rethrow()
0
end
return kb / 2^20
end

"""
Expand Down
5 changes: 3 additions & 2 deletions src/testsuite.jl
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,12 @@ end
# ---------------------------------------------------------------------------

# Map an upstream loadModel version to the bundled MSL_Version the OM.jl
# by-name pipeline understands. Only 3.2.x and 4.0.0 are bundled.
# by-name pipeline understands. 3.2.3 and 4.1.0 are bundled; 4.0 is 4.1.0
# (4.1.0 is backwards compatible with 4.0.0).
function _bundled_msl_version(version::String)::Union{Nothing, String}
isempty(version) && return nothing
startswith(version, "3.2") && return "MSL:3.2.3"
startswith(version, "4.0") && return "MSL:4.0.0"
(startswith(version, "4.0") || startswith(version, "4.1")) && return "MSL:4.1.0"
return nothing
end

Expand Down
8 changes: 8 additions & 0 deletions test/comparisonTests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ end
#= Neither limit: a reset that did not happen. =#
local noReset = OLT.compare_signal(SignalOf(t -> t <= 0.5 ? t : t + 0.1), ref, "x", 1.0; npoints = 5)
@test !noReset.passed
#= 2026-10-22: the reference files store their times as Float32, an event at 0.099 as 0.098999995 (MSL
HBridge_RL's switching, 4.1.0): the event at the sample instant 0.099 all the same. =#
local t32 = Float64(Float32(0.099))
local stored = OLT.ReferenceData([0.0, 0.05, t32, t32, 0.1], Dict("x" => [0.0, 0.05, 0.099, -0.099, -0.1]))
@test t32 < 0.099
@test OLT._event_limits(stored.time, stored.signals["x"], 0.099) == (0.099, -0.099)
@test OLT._event_limits(stored.time, stored.signals["x"], 0.05) === nothing
@test OLT.compare_signal(SignalOf(t -> t <= 0.099 ? t : -t), stored, "x", 0.1; npoints = 1001).passed
end

@testset "comparison: a stopTime past the reference's end" begin
Expand Down
2 changes: 1 addition & 1 deletion test/registryTests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ end
@test all(e -> e["name"] in experiments, values(OLT.TOML.parsefile(OLT.default_models_path("4.1.0"))["models"]))
#= Bundled versions by MSL_Version; others are installed libraries (not loaded here). =#
@test OLT._msl_kwargs("MSL:3.2.3") == (; MSL_Version = "MSL:3.2.3")
@test OLT._msl_kwargs("MSL:4.0.0") == (; MSL_Version = "MSL:4.0.0")
@test OLT._msl_kwargs("MSL:4.1.0") == (; MSL_Version = "MSL:4.1.0")
end
15 changes: 15 additions & 0 deletions test/runnerTests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,18 @@ end
@test [r.spec.name for r in merged] == [s.name for s in specs] # run_coverage's order (domain, name)
@test_throws ErrorException OLT.merge_shards([result.(s1), result.(s1[1:1])])
end

#= A worker is recycled by the measure the footprint watchdog kills by (top's MEM on macOS), not ps's
resident size: they differ in both directions, and coverage 26's workers passed the 8 GB watchdog below
6 GB resident. =#
@testset "workerMemoryGB: the footprint watchdog's measure" begin
local gb = OLT.workerMemoryGB(getpid())
@test 0.05 < gb < 1024
if Sys.isapple()
local top = strip(last(split(strip(read(`top -l 1 -pid $(getpid()) -stats mem`, String)), '\n')))
local m = match(r"([0-9.]+)([KMG])", top)
local topGB = parse(Float64, m[1]) * (m[2] == "G" ? 1.0 : m[2] == "M" ? 1 / 1024 : 1 / 2^20)
@test isapprox(gb, topGB; rtol = 0.2)
end
@test redirect_stderr(devnull) do; OLT.workerMemoryGB(999_999); end == 0.0
end
Loading