1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
name: Rust build and test
on:
push:
branches:
pull_request:
branches:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The OPF tests and tests/injection_hessian_test.rs read the pglib-opf
# cases from benchmark-grids and convert them with numpy
# (tests/data/pglib_opf.py). CGMES-Test-Configurations stays unfetched.
- name: Fetch benchmark-grids
run: git submodule update --init --depth 1 tests/data/benchmark-grids
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install numpy
run: pip install numpy
# Dependencies and the previous run's build, keyed on Cargo.lock. Without
# it every run compiles every crate from scratch, once per profile.
- uses: Swatinem/rust-cache@v2
# **Each feature set CI covers is compiled on its own, and tested once,
# together.** Testing them one at a time ran the default suite eight times
# over and relinked every test binary for each feature set; what the
# separate builds actually guard is a missing or wrong `#[cfg]`, which a
# compile check — `--all-targets`, so tests and examples included — finds
# just as well, with no codegen and no link.
#
# Not here at all: `klu` (a C compiler and libclang), `pardiso` (a
# separately-licensed Intel oneMKL, `MKLROOT` set — not on hosted runners;
# see docs/src/solvers/backends.md), `opf-highs` and `opf-ipopt` (system
# libraries no runner has, so `tests/opf_cross_test.rs`, which holds HiGHS
# against the in-house solver, runs where HiGHS is installed), and
# `smallsignal-arpack`. Everything below is pure Rust or fetched by cargo:
#
# - `opf`: the interior-point QP solver in `src/opf/ipm.rs`, and `opf::bnb`
# branching over it for the mixed-integer path.
# - `ucte`: fixed-column text, no dependency. Its suite checks that each
# network solves to pypowsybl's answer, committed as JSON under
# `tests/data/ucte/`, so the runner needs no pypowsybl.
# - `iidm`: one crate (quick-xml). Checked with `ucte` too, because its
# strongest test reads the same network through both parsers.
# - `dynamics`: pure Rust on this crate's sparse backends; small-signal
# analysis rides along on it.
# - `rao`: serde, which the crate already has. Its external gate,
# `tests/rao_cucumber_test.rs`, reads powsybl-open-rao's own Cucumber
# suite from benchmark-grids and needs no Java.
# - `capi`: pure Rust plus cbindgen.
# - `matpower`: plain text, no dependency. Its suite reads the cases in
# benchmark-grids and checks each solution against the case's own
# equations.
- name: Check each feature set on its own
run: |
for features in "" opf ucte iidm iidm,ucte dynamics dynamics,iidm \
rao rao,ucte,iidm capi matpower; do
echo "::group::cargo check --features '$features'"
cargo check --all-targets --features "$features"
echo "::endgroup::"
done
# Feature *unification* is its own failure mode, which the checks above
# cannot see: `iidm` and `cgmes` share quick-xml, and cimmodel enables its
# `encoding` feature — which changes the shape of a public type. That
# combination compiled in no single-feature step and broke in the one
# nobody ran. This is that step.
- name: Check every CI-covered feature together
run: cargo check --all-targets --features cgmes,ucte,iidm,opf,rao,dynamics,capi,matpower
# Tested on the union, *less `cgmes`*: its tests read
# CGMES-Test-Configurations (158 MB), which CI does not fetch, and nine of
# them fail rather than skip without it. CI has never run them; fetching
# the submodule and adding `cgmes` here is what would change that.
#
# The integration tests are one binary (`tests/all.rs`), so this links once.
- name: Test every CI-covered feature together
run: cargo test --features ucte,iidm,opf,rao,dynamics,capi,matpower
# The library is split into crates (see crates/), and a plain `cargo test`
# at the root tests only the root package. The others' own unit tests —
# the KLU port's among them — run here.
- name: Test the workspace's other crates
run: cargo test --workspace --exclude gridoxide
- name: Build the dynamics scaling examples
run: |
cargo build --release --features dynamics --example dynamics_scale
cargo build --release --features dynamics --example smallsignal_scale
# The two example programs are the part `cargo test` cannot cover. They are
# what proves the generated header actually parses as C11 and C++17, that
# the `#[repr(C)]` layouts agree with what a real compiler computes, and
# that the exported symbols link — none of which a Rust test can check,
# since it never sees the header at all.
- name: Compile and run the C and C++ examples
run: |
cargo build --release --features capi
cc -std=c11 -Wall -Wextra -Werror -Iinclude \
capi/examples/powerflow.c target/release/libgridoxide.a \
-o /tmp/powerflow_c -lpthread -ldl -lm
c++ -std=c++17 -Wall -Wextra -Werror -Iinclude \
capi/examples/powerflow.cpp target/release/libgridoxide.a \
-o /tmp/powerflow_cpp -lpthread -ldl -lm
python tests/data/pglib_opf.py /tmp/pglib-opf
/tmp/powerflow_c /tmp/pglib-opf/pglib_opf_case14_ieee.json
/tmp/powerflow_cpp /tmp/pglib-opf/pglib_opf_case14_ieee.json
# The header is committed so a prebuilt-artifact consumer needs no Rust
# toolchain. Nothing keeps it in step with the code unless something checks,
# and building with `capi` regenerates it — so a diff here means a
# contributor changed the ABI without committing the regenerated header.
- name: Check the committed header is not stale
run: git diff --exit-code -- include/gridoxide.h