Fast PR CI: small hostile-test caps, heavy tests in a separate job (#43)

* test: hostile-params test runs under small session work caps

Session gains Limits, the defaults of mesh.tet.generate's max_steiner and
max_tets (production values unchanged; a parameter still overrides them;
file.open keeps them). The registry-wide hostile-params test runs on
sessions with small caps, so the needle-triangle tessellation it meshes is
refused at once instead of meshing to the 50k Steiner cap: the engine test
binary drops from ~350 s to ~20 s locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* ci: move heavy tests to a heavy job on main, nightly and on demand

Four slow tests are #[ignore = "heavy: ..."] (every corpus recipe, the
cantilever convergence study, the explicit-vs-modal cantilever period, the
generic-predicate rational cross-check); fast representative tests of each
area stay in the PR gate. cargo xtask test --heavy runs every ignored test
(benchmarks included) and the doc tests; cargo xtask test and ci run the
rest without the doc tests. The new heavy CI job runs it on pushes to main,
nightly and on workflow_dispatch. Branch jobs restore main's rust cache and
no longer save their own. AGENTS.md documents the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* test: the three slow LBM verification cases run in the heavy job

The Stokes-drag sphere array, the staircase-pipe convergence study and the
developing-channel case are heavy (21 s, 11 s, 6 s alone); plane
Poiseuille, Couette, Taylor-Green, reproducibility and the stability
limits stay in the PR gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 21:21:49 -04:00
1 parent 7dc7a0e36d
commit 5e5a2ae251
13 files changed
+113 -18

No files matched your search

+3 -1
View File
@@ -154,9 +154,11 @@ impl Session {
(session, warnings)
}
/// Replace this session with `opened`, keeping its undo memory limit; the history starts empty.
/// Replace this session with `opened`, keeping its undo memory limit and its work caps; the
/// history starts empty.
fn replace_with(&mut self, mut opened: Session) {
opened.history = std::mem::take(&mut self.history);
opened.limits = self.limits;
opened.clear_history();
*self = opened;
}
+22 -3
View File
@@ -126,10 +126,9 @@ fn surface_options(p: &Value, mesh: &SurfaceMesh) -> Result<SurfaceOptions> {
}
fn tet_generate(s: &mut Session, p: &Value) -> Result<Value> {
let defaults = CdtOptions::default();
let cdt_options = CdtOptions {
max_steiner: optional_count(p, "max_steiner", defaults.max_steiner as u64, 0..=TET_MAX_STEINER)?,
max_tets: optional_count(p, "max_tets", defaults.max_tets as u64, 1..=TET_MAX_TETS)?,
max_steiner: optional_count(p, "max_steiner", s.limits.max_steiner as u64, 0..=TET_MAX_STEINER)?,
max_tets: optional_count(p, "max_tets", s.limits.max_tets as u64, 1..=TET_MAX_TETS)?,
};
let (surface, body, face_names) = {
let Input { mesh, body, faces } = surface_input(s, p)?;
@@ -648,6 +647,26 @@ mod tests {
assert_eq!((s.journal.len(), &s.model), (journal, &model), "failures change nothing");
}
/// The session's caps are the defaults of `max_steiner` and `max_tets`; a parameter overrides them.
#[test]
fn tet_generate_honours_the_session_caps() {
use std::sync::Arc;
use modelcraft_geometry::open_design;
use modelcraft_geometry::sample::{Plate, plate_design};
use crate::{EngineError, Limits};
let design = Arc::new(open_design(&plate_design(&Plate::default()).unwrap()).unwrap());
let mut s = Session { design: Some(design), limits: Limits { max_tets: 10, ..Limits::default() }, ..Session::default() };
let plate = json!({"body": "Body1", "tolerance": 0.5});
let refused = s.execute("mesh.tet.generate", &plate).unwrap_err();
assert!(matches!(refused, EngineError::Mesh(modelcraft_mesh::Error::Limit(_))), "{refused}");
assert!(s.model.elements().is_empty());
let r = s.execute("mesh.tet.generate", &json!({"body": "Body1", "tolerance": 0.5, "max_tets": 1_000_000})).unwrap();
assert!(r["elements"].as_u64().unwrap() > 0, "{r}");
}
#[test]
fn tet_generate_on_an_imported_surface_and_an_open_one() {
use std::sync::Arc;
+1
View File
@@ -792,6 +792,7 @@ pub(crate) mod tests {
/// bending energy, so without hourglass stiffness (viscous control) the period is longer by
/// (1 − 1/n²)^{−½} (3.3% for n = 4); the stiffness form restores part of it.
#[test]
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
fn explicit_cantilever_period_matches_the_modal_frequency() {
let (length, n) = (10.0, [40usize, 4usize]);
let mut s = Session::default();
+29 -4
View File
@@ -124,6 +124,25 @@ pub(crate) fn next_revision() -> u64 {
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
/// The defaults of the work caps that commands take as parameters: what a command may spend
/// when its parameters name no cap of their own. A parameter still overrides them within the
/// command's documented range. The defaults are the production ones; a host may lower them (the
/// tests do, so hostile input is refused quickly).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
/// `mesh.tet.generate`'s `max_steiner`.
pub max_steiner: usize,
/// `mesh.tet.generate`'s `max_tets`.
pub max_tets: usize,
}
impl Default for Limits {
fn default() -> Self {
let cdt = modelcraft_mesh::cdt::CdtOptions::default();
Limits { max_steiner: cdt.max_steiner, max_tets: cdt.max_tets }
}
}
/// The model being edited, the geometry it is built on and the commands that built it.
#[derive(Debug, Clone, Default)]
pub struct Session {
@@ -153,6 +172,8 @@ pub struct Session {
results: BTreeMap<String, (u64, Arc<StepResult>)>,
/// What the results display shows (`results.*`); `None` shows the model.
pub results_view: Option<results::ResultsView>,
/// The default work caps of commands; a file replacing the session keeps them.
pub limits: Limits,
}
impl Session {
@@ -400,16 +421,20 @@ mod tests {
json!({"geometry": {"body": "Bar"}, "elements": [1, 99], "faces": [{"element": 1, "face": 200}]}),
json!({"geometry": {"body": "Bar", "vertex": "v"}, "nodes": [1], "edges": [{"element": 1, "edge": 0}]}),
];
// Small work caps, so input that would mesh up to the production caps (needle triangles
// need Steiner points all along them) is refused at once. The caps themselves are what
// `tet_generate_honours_the_session_caps` checks.
let capped = || Session { limits: Limits { max_steiner: 64, max_tets: 100_000 }, ..Session::default() };
for spec in commands() {
for p in &hostile {
let mut s = Session::default();
let mut s = capped();
let _ = s.execute(spec.id, &p.clone());
}
}
assert!(Session::default().execute("no.such", &json!({})).is_err());
assert!(capped().execute("no.such", &json!({})).is_err());
// The same against a defined model, so edits and deletes reach existing definitions.
let mut defined = Session::default();
let mut defined = capped();
crate::cmd::define::tests::defined_bar(&mut defined);
for spec in commands() {
for p in &hostile {
@@ -423,7 +448,7 @@ mod tests {
// The same against an open design.
let (_dir, path) = sample_design_file(&Plate::default());
let mut opened = Session::default();
let mut opened = capped();
opened.execute("geometry.open", &json!({"path": path})).unwrap();
let tess = [
json!({"body": "Body1"}),