diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 53128af7c..b4d8990ed 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -49,7 +49,13 @@ jobs:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v3
- - uses: julia-actions/julia-buildpkg@v1
+ - name: Build package
+ run: |
+ julia --project=. -e '
+ using Pkg
+ Pkg.add(name="BaseModelica", rev="main")
+ Pkg.instantiate()
+ '
- uses: julia-actions/julia-runtest@v1
sanity:
@@ -82,7 +88,13 @@ jobs:
version: '1.12'
arch: x64
- uses: julia-actions/cache@v3
- - uses: julia-actions/julia-buildpkg@v1
+ - name: Build package
+ run: |
+ julia --project=. -e '
+ using Pkg
+ Pkg.add(name="BaseModelica", rev="main")
+ Pkg.instantiate()
+ '
- name: Sanity check ChuaCircuit
run: |
diff --git a/.github/workflows/msl-test.yml b/.github/workflows/msl-test.yml
index 001fb493d..66ad9bc34 100644
--- a/.github/workflows/msl-test.yml
+++ b/.github/workflows/msl-test.yml
@@ -36,6 +36,11 @@ on:
required: false
default: '^(?!Modelica\.Clocked)'
type: string
+ solver:
+ description: 'ODE solver algorithm (any DifferentialEquations.jl algorithm name, e.g. Rodas5P, FBDF)'
+ required: false
+ default: 'Rodas5P'
+ type: string
concurrency:
group: pages-${{ inputs.library || 'Modelica' }}-${{ inputs.lib_version || '4.1.0' }}-${{ inputs.bm_version || 'main' }}
@@ -58,6 +63,7 @@ jobs:
BM_VERSION_INPUT: ${{ inputs.bm_version || 'main' }}
BM_OPTIONS: ${{ inputs.bm_options || 'scalarize,moveBindings,inlineFunctions' }}
FILTER: ${{ inputs.filter || '^(?!Modelica\.Clocked)' }}
+ SOLVER: ${{ inputs.solver || 'Rodas5P' }}
steps:
- name: Checkout source
@@ -123,6 +129,9 @@ jobs:
run: |
julia --project=. -e '
using BaseModelicaLibraryTesting
+ using DifferentialEquations
+ solver_name = get(ENV, "SOLVER", "Rodas5P")
+ configure_simulate!(solver = getproperty(DifferentialEquations, Symbol(solver_name))())
filter_str = get(ENV, "FILTER", "")
main(
library = ENV["LIB_NAME"],
diff --git a/Project.toml b/Project.toml
index 75a73180f..21560b138 100644
--- a/Project.toml
+++ b/Project.toml
@@ -7,6 +7,7 @@ authors = ["AnHeuermann"]
BaseModelica = "a17d5099-185d-4ff5-b5d3-51aa4569e56d"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
+LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
OMJulia = "0f4fe800-344e-11e9-2949-fb537ad918e1"
diff --git a/README.md b/README.md
index 6468562b1..19de3fd2c 100644
--- a/README.md
+++ b/README.md
@@ -55,10 +55,55 @@ main(
Preview the generated HTML report at `main/Modelica/4.1.0/report.html`.
+### Changing the ODE Solver
+
+By default the simulation uses `Rodas5P()`. To switch to a different solver,
+call `configure_simulate!` before `main`:
+
+```julia
+using BaseModelicaLibraryTesting
+using DifferentialEquations
+
+configure_simulate!(solver = FBDF())
+
+main(
+ library = "Modelica",
+ version = "4.1.0",
+ omc_exe = "omc",
+ ref_root = "MAP-LIB_ReferenceResults"
+)
+```
+
+Any SciML-compatible ODE/DAE algorithm (e.g. `QNDF()`, `Rodas4()`) can be
+passed to `solver`.
+
```bash
python -m http.server -d results/main/Modelica/4.1.0/
```
+## GitHub Actions — Manual MSL Test
+
+The [MSL Test & GitHub Pages][msl-action-url] workflow runs automatically every
+day at 03:00 UTC. It can also be triggered manually from the GitHub Actions UI:
+
+1. Go to **Actions → MSL Test & GitHub Pages**
+2. Click **Run workflow**
+3. Fill in the options and click **Run workflow**
+
+The following inputs are available:
+
+| Input | Default | Description |
+| ----- | ------- | ----------- |
+| `library` | `Modelica` | Modelica library name |
+| `lib_version` | `4.1.0` | Library version to test |
+| `bm_version` | `main` | BaseModelica.jl branch, tag, or version |
+| `bm_options` | `scalarize,moveBindings,inlineFunctions` | Comma-separated `--baseModelicaOptions` passed to OpenModelica during Base Modelica export |
+| `filter` | `^(?!Modelica\.Clocked)` | Julia regex to restrict which models are tested (empty string runs all models) |
+| `solver` | `Rodas5P` | Any `DifferentialEquations.jl` algorithm name (e.g. `Rodas5P`, `Rodas5Pr`, `FBDF`) |
+
+Results are published to [GitHub Pages][msl-pages-url] under
+`results////`.
+
## License
This package is available under the [OSMC-PL License][osmc-license-file] and the
@@ -68,6 +113,7 @@ file for details.
[build-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml/badge.svg?branch=main
[build-action-url]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml?query=branch%3Amain
[msl-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/msl-test.yml/badge.svg?branch=main
+[msl-action-url]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/msl-test.yml
[msl-pages-url]: https://openmodelica.github.io/BaseModelicaLibraryTesting.jl/
[openmodelica-url]: https://openmodelica.org/
[basemodelicajl-url]: https://github.com/SciML/BaseModelica.jl
diff --git a/src/BaseModelicaLibraryTesting.jl b/src/BaseModelicaLibraryTesting.jl
index 7610640fa..5921161b6 100644
--- a/src/BaseModelicaLibraryTesting.jl
+++ b/src/BaseModelicaLibraryTesting.jl
@@ -4,7 +4,7 @@ import Pkg
import OMJulia
import OMJulia: sendExpression
import BaseModelica
-import DifferentialEquations: solve, Rodas5P, ReturnCode
+import DifferentialEquations
import ModelingToolkit
import Dates: now
import Printf: @sprintf
@@ -21,11 +21,12 @@ include("pipeline.jl")
# ── Public API ─────────────────────────────────────────────────────────────────
# Shared types and constants
-export ModelResult, CompareSettings, RunInfo
+export ModelResult, CompareSettings, SimulateSettings, RunInfo
export LIBRARY, LIBRARY_VERSION, CMP_REL_TOL, CMP_ABS_TOL
# Comparison configuration
export configure_comparison!, compare_settings
+export configure_simulate!, simulate_settings
# Pipeline phases
export run_export # Phase 1: Base Modelica export via OMC
diff --git a/src/compare.jl b/src/compare.jl
index 5cf987b69..d9b07592f 100644
--- a/src/compare.jl
+++ b/src/compare.jl
@@ -368,6 +368,8 @@ function compare_with_reference(
signals::Vector{String} = String[],
)::Tuple{Int,Int,Int,String}
+ isdir(model_dir) || mkpath(model_dir)
+
times, ref_data = _read_ref_csv(ref_csv_path)
isempty(times) && return 0, 0, 0, ""
diff --git a/src/parse_bm.jl b/src/parse_bm.jl
index e49d40113..dcaa2c935 100644
--- a/src/parse_bm.jl
+++ b/src/parse_bm.jl
@@ -18,6 +18,7 @@ function run_parse(bm_path::String, model_dir::String,
parse_error = ""
ode_prob = nothing
+ isdir(model_dir) || mkpath(model_dir)
log_file = open(joinpath(model_dir, "$(model)_parsing.log"), "w")
stdout_pipe = Pipe()
println(log_file, "Model: $model")
diff --git a/src/pipeline.jl b/src/pipeline.jl
index 4a1815b58..b253f61aa 100644
--- a/src/pipeline.jl
+++ b/src/pipeline.jl
@@ -57,8 +57,12 @@ end
Run the four-phase pipeline for a single model and return its result.
"""
-function test_model(omc::OMJulia.OMCSession, model::String, results_root::String,
- ref_root::String; csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::ModelResult
+function test_model(omc::OMJulia.OMCSession,
+ model::String,
+ results_root::String,
+ ref_root::String;
+ sim_settings ::SimulateSettings = _SIM_SETTINGS,
+ csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::ModelResult
model_dir = joinpath(results_root, "files", model)
mkpath(model_dir)
@@ -93,6 +97,7 @@ function test_model(omc::OMJulia.OMCSession, model::String, results_root::String
# Phase 3 ──────────────────────────────────────────────────────────────────
sim_ok, sim_t, sim_err, sol = run_simulate(ode_prob, model_dir, model;
+ settings = sim_settings,
csv_max_size_mb, cmp_signals)
# Phase 4 (optional) ───────────────────────────────────────────────────────
@@ -132,6 +137,7 @@ function main(;
results_root :: String = "",
ref_root :: String = get(ENV, "MAPLIB_REF", ""),
bm_options :: String = get(ENV, "BM_OPTIONS", "scalarize,moveBindings,inlineFunctions"),
+ sim_settings :: SimulateSettings = _SIM_SETTINGS,
csv_max_size_mb :: Int = CSV_MAX_SIZE_MB,
)
t0 = time()
@@ -200,7 +206,7 @@ function main(;
for (i, model) in enumerate(models)
@info "[$i/$(length(models))] $model"
- result = test_model(omc, model, results_root, ref_root; csv_max_size_mb)
+ result = test_model(omc, model, results_root, ref_root; sim_settings, csv_max_size_mb)
push!(results, result)
phase = if result.sim_success && result.cmp_total > 0
@@ -245,6 +251,9 @@ function main(;
length(cpu_info),
Sys.total_memory() / 1024^3,
time() - t0,
+ let s = _SIM_SETTINGS.solver
+ "$(parentmodule(typeof(s))).$(nameof(typeof(s)))"
+ end,
)
generate_report(results, results_root, info; csv_max_size_mb)
diff --git a/src/report.jl b/src/report.jl
index 008ffc4f6..8ce87a243 100644
--- a/src/report.jl
+++ b/src/report.jl
@@ -159,6 +159,7 @@ function generate_report(results::Vector{ModelResult}, results_root::String,
OpenModelica: $(info.omc_version)
OMC options: $(info.omc_options)
BaseModelica.jl: $(basemodelica_jl_version)
+Solver: $(info.solver)
Filter: $(var_filter)
Reference results: $(ref_results)
CPU: $(info.cpu_model) ($(info.cpu_threads) threads)
diff --git a/src/simulate.jl b/src/simulate.jl
index fe6723aa4..c1f80fc79 100644
--- a/src/simulate.jl
+++ b/src/simulate.jl
@@ -1,14 +1,49 @@
# ── Phase 3: ODE simulation with DifferentialEquations / MTK ──────────────────
-import DifferentialEquations: solve, Rodas5P, ReturnCode
+import DifferentialEquations
import Logging
import ModelingToolkit
import Printf: @sprintf
+"""Module-level default simulation settings. Modify via `configure_simulate!`."""
+const _SIM_SETTINGS = SimulateSettings(solver = DifferentialEquations.Rodas5P())
+
+"""
+ configure_simulate!(; solver, saveat_n) → SimulateSettings
+
+Update the module-level simulation settings in-place and return them.
+
+# Keyword arguments
+- `solver` — any SciML ODE/DAE algorithm instance (e.g. `Rodas5P`, `FBDF()`).
+- `saveat_n` — number of uniform time points for purely algebraic systems.
+
+# Example
+
+```julia
+using OrdinaryDiffEqBDF
+configure_simulate!(solver = FBDF())
+```
+"""
+function configure_simulate!(;
+ solver :: Union{Any,Nothing} = nothing,
+ saveat_n :: Union{Int,Nothing} = nothing,
+)
+ isnothing(solver) || (_SIM_SETTINGS.solver = solver)
+ isnothing(saveat_n) || (_SIM_SETTINGS.saveat_n = saveat_n)
+ return _SIM_SETTINGS
+end
+
+"""
+ simulate_settings() → SimulateSettings
+
+Return the current module-level simulation settings.
"""
- run_simulate(ode_prob, model_dir, model; cmp_signals, csv_max_size_mb) → (success, time, error, sol)
+simulate_settings() = _SIM_SETTINGS
-Solve `ode_prob` with Rodas5P (stiff solver). On success, also writes the
+"""
+ run_simulate(ode_prob, model_dir, model; settings, cmp_signals, csv_max_size_mb) → (success, time, error, sol)
+
+Solve `ode_prob` using the algorithm in `settings.solver`. On success, also writes the
solution as a CSV file `_sim.csv` in `model_dir`.
Writes a `_sim.log` file in `model_dir`.
Returns `nothing` as the fourth element on failure.
@@ -20,21 +55,26 @@ of signals will be compared.
CSV files larger than `csv_max_size_mb` MiB are replaced with a
`_sim.csv.toobig` marker so that the report can note the omission.
"""
-function run_simulate(ode_prob, model_dir::String,
+function run_simulate(ode_prob,
+ model_dir::String,
model::String;
- cmp_signals ::Vector{String} = String[],
- csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}
- sim_success = false
- sim_time = 0.0
- sim_error = ""
- sol = nothing
+ settings ::SimulateSettings = _SIM_SETTINGS,
+ cmp_signals ::Vector{String} = String[],
+ csv_max_size_mb::Int = CSV_MAX_SIZE_MB)::Tuple{Bool,Float64,String,Any}
+
+ sim_success = false
+ sim_time = 0.0
+ sim_error = ""
+ sol = nothing
+ solver_settings_string = ""
log_file = open(joinpath(model_dir, "$(model)_sim.log"), "w")
println(log_file, "Model: $model")
logger = Logging.SimpleLogger(log_file, Logging.Debug)
t0 = time()
+
+ solver = settings.solver
try
- # Rodas5P handles stiff DAE-like systems well.
# Redirect all library log output (including Symbolics/MTK warnings)
# to the log file so they don't clutter stdout.
sol = Logging.with_logger(logger) do
@@ -42,14 +82,47 @@ function run_simulate(ode_prob, model_dir::String,
# For stateless models (no unknowns) the adaptive solver takes no
# internal steps and sol.t would be empty with saveat=[].
# Supply explicit time points so observed variables can be evaluated.
- sys = ode_prob.f.sys
- saveat = isempty(ModelingToolkit.unknowns(sys)) ?
- collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = 500)) :
- Float64[]
- solve(ode_prob, Rodas5P(); saveat = saveat, dense = true)
+ sys = ode_prob.f.sys
+ n_unknowns = length(ModelingToolkit.unknowns(sys))
+
+ kwargs = if n_unknowns == 0
+ # No unknowns at all (e.g. BusUsage):
+ # Supply explicit time points so observed variables can be evaluated.
+ saveat_s = collect(range(ode_prob.tspan[1], ode_prob.tspan[end]; length = settings.saveat_n))
+ (saveat = saveat_s, dense = true)
+ else
+ (saveat = Float64[], dense = true)
+ end
+
+ # Log solver settings — init returns NullODEIntegrator (no .opts)
+ # when the problem has no unknowns (u::Nothing), so only inspect
+ # opts when a real integrator is returned.
+ # Use our own `saveat` vector for the log: integ.opts.saveat is a
+ # BinaryHeap which does not support iterate/minimum/maximum.
+ integ = DifferentialEquations.init(ode_prob, solver; kwargs...)
+ saveat_s = kwargs.saveat
+ solver_settings_string = if hasproperty(integ, :opts)
+ sv_str = isempty(saveat_s) ? "[]" : "$(length(saveat_s)) points in [$(first(saveat_s)), $(last(saveat_s))]"
+ """
+ Solver $(parentmodule(typeof(solver))).$(nameof(typeof(solver)))
+ saveat: $sv_str
+ abstol: $(@sprintf("%.2e", integ.opts.abstol))
+ reltol: $(@sprintf("%.2e", integ.opts.reltol))
+ adaptive: $(integ.opts.adaptive)
+ dense: $(integ.opts.dense)
+ """
+ else
+ sv_str = isempty(saveat_s) ? "[]" : "$(length(saveat_s)) points in [$(first(saveat_s)), $(last(saveat_s))]"
+ "Solver (NullODEIntegrator — no unknowns)
+ saveat: $sv_str
+ dense: true"
+ end
+
+ # Solve
+ DifferentialEquations.solve(ode_prob, solver; kwargs...)
end
sim_time = time() - t0
- if sol.retcode == ReturnCode.Success
+ if sol.retcode == DifferentialEquations.ReturnCode.Success
sys = sol.prob.f.sys
n_vars = length(ModelingToolkit.unknowns(sys))
n_obs = length(ModelingToolkit.observed(sys))
@@ -67,7 +140,8 @@ function run_simulate(ode_prob, model_dir::String,
sim_time = time() - t0
sim_error = sprint(showerror, e, catch_backtrace())
end
- println(log_file, "Time: $(round(sim_time; digits=3)) s")
+ println(log_file, solver_settings_string)
+ println(log_file, "Time: $(round(sim_time; digits=3)) s")
println(log_file, "Success: $sim_success")
isempty(sim_error) || println(log_file, "\n--- Error ---\n$sim_error")
close(log_file)
diff --git a/src/summary.jl b/src/summary.jl
index 53ba785ef..847a9c8e7 100644
--- a/src/summary.jl
+++ b/src/summary.jl
@@ -33,6 +33,7 @@ function write_summary(
print(io, " \"cpu_threads\": $(info.cpu_threads),\n")
print(io, " \"ram_gb\": $(@sprintf "%.2f" info.ram_gb),\n")
print(io, " \"total_time_s\": $(@sprintf "%.2f" info.total_time_s),\n")
+ print(io, " \"solver\": \"$(_esc_json(info.solver))\",\n")
print(io, " \"models\": [\n")
for (i, r) in enumerate(results)
sep = i < length(results) ? "," : ""
@@ -74,6 +75,7 @@ Parsed contents of a single `summary.json` file.
- `cpu_threads` — number of logical CPU threads
- `ram_gb` — total system RAM in GiB
- `total_time_s` — wall-clock duration of the full test run in seconds
+- `solver` — fully-qualified solver name, e.g. `"DifferentialEquations.Rodas5P"`
- `models` — vector of per-model dicts; each has keys
`"name"`, `"export"`, `"parse"`, `"sim"`, `"cmp_total"`, `"cmp_pass"`
"""
@@ -92,6 +94,7 @@ struct RunSummary
cpu_threads :: Int
ram_gb :: Float64
total_time_s :: Float64
+ solver :: String
models :: Vector{Dict{String,Any}}
end
@@ -150,6 +153,7 @@ function load_summary(results_root::String)::Union{RunSummary,Nothing}
_int("cpu_threads"),
_float("ram_gb"),
_float("total_time_s"),
+ _str("solver"),
models,
)
end
diff --git a/src/types.jl b/src/types.jl
index b073c7843..18704c5ae 100644
--- a/src/types.jl
+++ b/src/types.jl
@@ -37,6 +37,25 @@ Base.@kwdef mutable struct CompareSettings
error_fn :: Symbol = :mixed
end
+# ── Simulation settings ────────────────────────────────────────────────────────
+
+"""
+ SimulateSettings
+
+Mutable configuration struct for ODE simulation.
+
+# Fields
+- `solver` — any SciML ODE/DAE algorithm instance. Default: `nothing`,
+ resolved to `Rodas5P()` when the module-level singleton is
+ constructed in `simulate.jl`.
+- `saveat_n` — number of evenly-spaced time points used for purely algebraic
+ systems (all mass-matrix rows zero). Default: `500`.
+"""
+Base.@kwdef mutable struct SimulateSettings
+ solver :: Any = nothing
+ saveat_n :: Int = 500
+end
+
# ── Run metadata ───────────────────────────────────────────────────────────────
"""
@@ -60,6 +79,7 @@ Metadata about a single test run, collected by `main()` and written into both
- `cpu_threads` — number of logical CPU threads
- `ram_gb` — total system RAM in GiB
- `total_time_s` — wall-clock duration of the full test run in seconds
+- `solver` — fully-qualified solver name, e.g. `"DifferentialEquations.Rodas5P"`
"""
struct RunInfo
library :: String
@@ -76,6 +96,7 @@ struct RunInfo
cpu_threads :: Int
ram_gb :: Float64
total_time_s :: Float64
+ solver :: String
end
# ── Result type ────────────────────────────────────────────────────────────────
diff --git a/test/chua_circuit.jl b/test/chua_circuit.jl
new file mode 100644
index 000000000..1f3ffcbcb
--- /dev/null
+++ b/test/chua_circuit.jl
@@ -0,0 +1,31 @@
+@testset "ChuaCircuit pipeline" begin
+ tmpdir = mktempdir()
+ model_dir = joinpath(tmpdir, "files", TEST_MODEL_CHUA)
+ mkpath(model_dir)
+ bm_path = replace(abspath(joinpath(model_dir, "$TEST_MODEL_CHUA.bmo")), "\\" => "/")
+
+ omc = OMJulia.OMCSession(TEST_OMC)
+ try
+ OMJulia.sendExpression(omc, """setCommandLineOptions("--baseModelica --baseModelicaOptions=scalarize,moveBindings -d=evaluateAllParameters")""")
+ ok = OMJulia.sendExpression(omc, """loadModel(Modelica, {"4.1.0"})""")
+ @test ok == true
+
+ exp_ok, _, exp_err = run_export(omc, TEST_MODEL_CHUA, model_dir, bm_path)
+ @test exp_ok
+ exp_ok || @warn "Export error: $exp_err"
+
+ if exp_ok
+ par_ok, _, par_err, ode_prob = run_parse(bm_path, model_dir, TEST_MODEL_CHUA)
+ @test par_ok
+ par_ok || @warn "Parse error: $par_err"
+
+ if par_ok
+ sim_ok, _, sim_err, _ = run_simulate(ode_prob, model_dir, TEST_MODEL_CHUA)
+ @test sim_ok
+ sim_ok || @warn "Simulation error: $sim_err"
+ end
+ end
+ finally
+ OMJulia.quit(omc)
+ end
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index e5b06798f..f542a7d1f 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,11 +1,13 @@
"""
Tests for the BaseModelicaLibraryTesting package.
-Sections:
- 1. Unit tests — pure helper functions, no OMC or simulation needed.
- 2. Integration — full pipeline for Modelica.Electrical.Analog.Examples.ChuaCircuit.
+Files:
+ unit_helpers.jl — pure helper functions, no OMC or simulation needed
+ chua_circuit.jl — full pipeline for ChuaCircuit (requires OMC)
+ bus_usage.jl — parse+simulate from fixture .bmo (no OMC)
+ amplifier_with_op_amp.jl — parse+simulate+verify from fixture .bmo (no OMC)
-Run from the julia/ directory:
+Run from the project directory:
julia --project=. test/runtests.jl
Or via Pkg:
@@ -15,126 +17,16 @@ Environment variables:
OMC_EXE Path to the omc binary (default: system PATH)
"""
-import Test: @test, @testset
+import Test: @test, @testset, @test_broken
import OMJulia
import BaseModelicaLibraryTesting: run_export, run_parse, run_simulate,
+ compare_with_reference,
_clean_var_name, _normalize_var,
_ref_csv_path, _read_ref_csv
-# ── 1. Unit tests ──────────────────────────────────────────────────────────────
+const FIXTURES = joinpath(@__DIR__, "fixtures")
+const TEST_OMC = get(ENV, "OMC_EXE", "omc")
+const TEST_MODEL_CHUA = "Modelica.Electrical.Analog.Examples.ChuaCircuit"
-@testset "Unit tests" begin
-
- @testset "_clean_var_name" begin
- # Standard MTK form: var"name"(t)
- @test _clean_var_name("var\"C1.v\"(t)") == "C1.v"
- # Without (t)
- @test _clean_var_name("var\"C1.v\"") == "C1.v"
- # Plain name with (t) suffix
- @test _clean_var_name("C1.v(t)") == "C1.v"
- # Plain name, no annotation
- @test _clean_var_name("x") == "x"
- # Leading/trailing whitespace is stripped
- @test _clean_var_name(" foo(t) ") == "foo"
- # ₊ hierarchy separator is preserved (it is the job of _normalize_var)
- @test _clean_var_name("var\"C1₊v\"(t)") == "C1₊v"
- end
-
- @testset "_normalize_var" begin
- # Reference-CSV side: plain dot-separated name
- @test _normalize_var("C1.v") == "c1.v"
- @test _normalize_var("L.i") == "l.i"
- # MTK side with ₊ hierarchy separator and (t) annotation
- @test _normalize_var("C1₊v(t)") == "c1.v"
- # MTK side with var"..." quoting
- @test _normalize_var("var\"C1₊v\"(t)") == "c1.v"
- # Already normalized input
- @test _normalize_var("c1.v") == "c1.v"
- # Multi-level hierarchy
- @test _normalize_var("a₊b₊c(t)") == "a.b.c"
- end
-
- @testset "_ref_csv_path" begin
- mktempdir() do dir
- model = "Modelica.Electrical.Analog.Examples.ChuaCircuit"
- csv_dir = joinpath(dir, "Modelica", "Electrical", "Analog",
- "Examples", "ChuaCircuit")
- mkpath(csv_dir)
- csv_file = joinpath(csv_dir, "ChuaCircuit.csv")
- write(csv_file, "")
- @test _ref_csv_path(dir, model) == csv_file
- @test _ref_csv_path(dir, "Modelica.NotExisting") === nothing
- end
- end
-
- @testset "_read_ref_csv" begin
- mktempdir() do dir
- csv = joinpath(dir, "test.csv")
-
- # Quoted headers (MAP-LIB format)
- write(csv, "\"time\",\"C1.v\",\"L.i\"\n0,4,0\n0.5,3.5,0.1\n1,3.0,0.2\n")
- times, data = _read_ref_csv(csv)
- @test times ≈ [0.0, 0.5, 1.0]
- @test data["C1.v"] ≈ [4.0, 3.5, 3.0]
- @test data["L.i"] ≈ [0.0, 0.1, 0.2]
- @test !haskey(data, "\"time\"") # quotes must be stripped from keys
-
- # Unquoted headers
- write(csv, "time,x,y\n0,1,2\n1,3,4\n")
- times2, data2 = _read_ref_csv(csv)
- @test times2 ≈ [0.0, 1.0]
- @test data2["x"] ≈ [1.0, 3.0]
- @test data2["y"] ≈ [2.0, 4.0]
-
- # Empty file → empty collections
- write(csv, "")
- t0, d0 = _read_ref_csv(csv)
- @test isempty(t0)
- @test isempty(d0)
-
- # Blank lines between data rows are ignored
- write(csv, "time,v\n0,1\n\n1,2\n\n")
- times3, data3 = _read_ref_csv(csv)
- @test times3 ≈ [0.0, 1.0]
- @test data3["v"] ≈ [1.0, 2.0]
- end
- end
-
-end # "Unit tests"
-
-# ── 2. Integration test ────────────────────────────────────────────────────────
-
-const TEST_MODEL = "Modelica.Electrical.Analog.Examples.ChuaCircuit"
-const TEST_OMC = get(ENV, "OMC_EXE", "omc")
-
-@testset "ChuaCircuit pipeline" begin
- tmpdir = mktempdir()
- model_dir = joinpath(tmpdir, "files", TEST_MODEL)
- mkpath(model_dir)
- bm_path = replace(abspath(joinpath(model_dir, "$TEST_MODEL.bmo")), "\\" => "/")
-
- omc = OMJulia.OMCSession(TEST_OMC)
- try
- OMJulia.sendExpression(omc, """setCommandLineOptions("--baseModelica --baseModelicaOptions=scalarize,moveBindings -d=evaluateAllParameters")""")
- ok = OMJulia.sendExpression(omc, """loadModel(Modelica, {"4.1.0"})""")
- @test ok == true
-
- exp_ok, _, exp_err = run_export(omc, TEST_MODEL, model_dir, bm_path)
- @test exp_ok
- exp_ok || @warn "Export error: $exp_err"
-
- if exp_ok
- par_ok, _, par_err, ode_prob = run_parse(bm_path, model_dir, TEST_MODEL)
- @test par_ok
- par_ok || @warn "Parse error: $par_err"
-
- if par_ok
- sim_ok, _, sim_err, _ = run_simulate(ode_prob, model_dir, TEST_MODEL)
- @test sim_ok
- sim_ok || @warn "Simulation error: $sim_err"
- end
- end
- finally
- OMJulia.quit(omc)
- end
-end
+include("unit_helpers.jl")
+include("chua_circuit.jl")
diff --git a/test/unit_helpers.jl b/test/unit_helpers.jl
new file mode 100644
index 000000000..2a8e71f94
--- /dev/null
+++ b/test/unit_helpers.jl
@@ -0,0 +1,78 @@
+@testset "Unit tests" begin
+
+ @testset "_clean_var_name" begin
+ # Standard MTK form: var"name"(t)
+ @test _clean_var_name("var\"C1.v\"(t)") == "C1.v"
+ # Without (t)
+ @test _clean_var_name("var\"C1.v\"") == "C1.v"
+ # Plain name with (t) suffix
+ @test _clean_var_name("C1.v(t)") == "C1.v"
+ # Plain name, no annotation
+ @test _clean_var_name("x") == "x"
+ # Leading/trailing whitespace is stripped
+ @test _clean_var_name(" foo(t) ") == "foo"
+ # ₊ hierarchy separator is preserved (it is the job of _normalize_var)
+ @test _clean_var_name("var\"C1₊v\"(t)") == "C1₊v"
+ end
+
+ @testset "_normalize_var" begin
+ # Reference-CSV side: plain dot-separated name
+ @test _normalize_var("C1.v") == "c1.v"
+ @test _normalize_var("L.i") == "l.i"
+ # MTK side with ₊ hierarchy separator and (t) annotation
+ @test _normalize_var("C1₊v(t)") == "c1.v"
+ # MTK side with var"..." quoting
+ @test _normalize_var("var\"C1₊v\"(t)") == "c1.v"
+ # Already normalized input
+ @test _normalize_var("c1.v") == "c1.v"
+ # Multi-level hierarchy
+ @test _normalize_var("a₊b₊c(t)") == "a.b.c"
+ end
+
+ @testset "_ref_csv_path" begin
+ mktempdir() do dir
+ model = "Modelica.Electrical.Analog.Examples.ChuaCircuit"
+ csv_dir = joinpath(dir, "Modelica", "Electrical", "Analog",
+ "Examples", "ChuaCircuit")
+ mkpath(csv_dir)
+ csv_file = joinpath(csv_dir, "ChuaCircuit.csv")
+ write(csv_file, "")
+ @test _ref_csv_path(dir, model) == csv_file
+ @test _ref_csv_path(dir, "Modelica.NotExisting") === nothing
+ end
+ end
+
+ @testset "_read_ref_csv" begin
+ mktempdir() do dir
+ csv = joinpath(dir, "test.csv")
+
+ # Quoted headers (MAP-LIB format)
+ write(csv, "\"time\",\"C1.v\",\"L.i\"\n0,4,0\n0.5,3.5,0.1\n1,3.0,0.2\n")
+ times, data = _read_ref_csv(csv)
+ @test times ≈ [0.0, 0.5, 1.0]
+ @test data["C1.v"] ≈ [4.0, 3.5, 3.0]
+ @test data["L.i"] ≈ [0.0, 0.1, 0.2]
+ @test !haskey(data, "\"time\"") # quotes must be stripped from keys
+
+ # Unquoted headers
+ write(csv, "time,x,y\n0,1,2\n1,3,4\n")
+ times2, data2 = _read_ref_csv(csv)
+ @test times2 ≈ [0.0, 1.0]
+ @test data2["x"] ≈ [1.0, 3.0]
+ @test data2["y"] ≈ [2.0, 4.0]
+
+ # Empty file → empty collections
+ write(csv, "")
+ t0, d0 = _read_ref_csv(csv)
+ @test isempty(t0)
+ @test isempty(d0)
+
+ # Blank lines between data rows are ignored
+ write(csv, "time,v\n0,1\n\n1,2\n\n")
+ times3, data3 = _read_ref_csv(csv)
+ @test times3 ≈ [0.0, 1.0]
+ @test data3["v"] ≈ [1.0, 2.0]
+ end
+ end
+
+end # "Unit tests"