#![forbid(unsafe_code)]
#[cfg(feature = "cli")]
mod cli_tests {
use approx::assert_abs_diff_eq;
use ciborium::value::Value as CborValue;
use delaunay::prelude::checkpoint::{
DELAUNAY_CHECKPOINT_SCHEMA_VERSION, DelaunayCheckpointManifest,
};
use delaunay::prelude::construction::DelaunayTriangulation;
use delaunay::prelude::geometry::RobustKernel;
use serde_json::{Value, from_slice, from_value};
use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
process::{Command, Output},
time::{SystemTime, UNIX_EPOCH},
};
fn delaunay_command() -> Command {
Command::new(env!("CARGO_BIN_EXE_delaunay"))
}
fn pachner_stress_command() -> Command {
Command::new(env!("CARGO_BIN_EXE_pachner-stress"))
}
fn run_cli(args: &[&str]) -> Output {
delaunay_command()
.args(args)
.output()
.expect("delaunay binary should run")
}
fn run_pachner_stress(args: &[&str]) -> Output {
pachner_stress_command()
.args(args)
.output()
.expect("pachner-stress binary should run")
}
fn output_text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
fn assert_success(output: &Output) {
assert!(
output.status.success(),
"expected success, got status {:?}\nstderr:\n{}",
output.status.code(),
output_text(&output.stderr)
);
}
fn assert_exit_code(output: &Output, code: i32) {
assert_eq!(
output.status.code(),
Some(code),
"unexpected status\nstdout:\n{}\nstderr:\n{}",
output_text(&output.stdout),
output_text(&output.stderr)
);
}
fn stdout_json(output: &Output) -> Value {
from_slice(&output.stdout).expect("stdout should contain JSON")
}
fn assert_stderr_contains(output: &Output, expected: &str) {
let stderr = output_text(&output.stderr);
assert!(
stderr.contains(expected),
"stderr should contain {expected:?}, got:\n{stderr}"
);
}
fn generated_triangulation_tds(json: &Value) -> CborValue {
assert_eq!(json["schema_version"], DELAUNAY_CHECKPOINT_SCHEMA_VERSION);
assert_eq!(json["manifest"]["manifest_version"], 1);
assert_eq!(json["manifest"]["digest"]["version"], 1);
assert_eq!(json["manifest"]["digest"]["algorithm"], "sha256");
assert_eq!(
json["manifest"]["digest"]["value"]
.as_str()
.expect("triangulation manifest should include a digest")
.len(),
64
);
let bytes = json["tds"]
.as_array()
.expect("triangulation JSON should include embedded TDS bytes")
.iter()
.map(|value| {
u8::try_from(value.as_u64().expect("TDS byte should be unsigned"))
.expect("TDS byte should fit u8")
})
.collect::<Vec<_>>();
ciborium::de::from_reader(bytes.as_slice()).expect("embedded TDS should decode")
}
fn cbor_field<'a>(value: &'a CborValue, name: &str) -> &'a CborValue {
value
.as_map()
.expect("embedded TDS record should be a map")
.iter()
.find_map(|(key, value)| (key.as_text() == Some(name)).then_some(value))
.unwrap_or_else(|| panic!("embedded TDS record should contain {name}"))
}
fn assert_generated_triangulation_json(json: &Value, dimension: usize, vertex_count: usize) {
let tds = generated_triangulation_tds(json);
assert_eq!(json["manifest"]["dimension"], dimension);
assert_eq!(
json["manifest"]["f_vector"]
.as_array()
.expect("triangulation manifest should include an f-vector")
.len(),
dimension + 1
);
let vertices = cbor_field(&tds, "vertices")
.as_array()
.expect("triangulation JSON should include vertices");
assert_eq!(vertices.len(), vertex_count);
let vertex_ids: HashSet<_> = vertices
.iter()
.map(|vertex| {
let coordinates = cbor_field(vertex, "point")
.as_array()
.expect("vertex JSON should include coordinates");
assert_eq!(coordinates.len(), dimension);
assert!(
coordinates
.iter()
.all(|coordinate| coordinate.as_float().is_some_and(f64::is_finite))
);
cbor_field(vertex, "uuid")
.as_bytes()
.expect("vertex CBOR should include UUID bytes")
.clone()
})
.collect();
assert_eq!(vertex_ids.len(), vertex_count);
let simplex_vertices = cbor_field(&tds, "simplex_vertices")
.as_map()
.expect("triangulation JSON should include simplex vertex references");
assert!(!simplex_vertices.is_empty());
for (_, references) in simplex_vertices {
let references = references
.as_array()
.expect("simplex vertex references should be an array");
assert_eq!(references.len(), dimension + 1);
let referenced_ids: HashSet<_> = references
.iter()
.map(|reference| {
reference
.as_bytes()
.expect("simplex vertex reference should be UUID bytes")
.clone()
})
.collect();
assert_eq!(referenced_ids.len(), dimension + 1);
assert!(referenced_ids.is_subset(&vertex_ids));
}
}
fn target_artifact_path(label: &str, extension: &str) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after UNIX epoch")
.as_nanos();
PathBuf::from("target")
.join("cli-tests")
.join(format!("{label}-{stamp}.{extension}"))
}
fn target_json_path(label: &str) -> PathBuf {
target_artifact_path(label, "json")
}
fn file_json(path: &Path) -> Value {
let bytes = fs::read(path).expect("JSON output file should be readable");
from_slice(&bytes).expect("output file should contain JSON")
}
#[test]
fn binary_help_exposes_each_binarys_direct_workflows() {
let output = run_cli(&["--help"]);
assert_success(&output);
let help = output_text(&output.stdout);
assert!(help.contains("generate"));
assert!(help.contains("spherical-hero"));
assert!(help.contains("validation-demo"));
let diagnostic_help = run_pachner_stress(&["--help"]);
assert_success(&diagnostic_help);
let diagnostic_help = output_text(&diagnostic_help.stdout);
assert!(diagnostic_help.contains("Run validated Pachner-move stress diagnostics"));
assert!(diagnostic_help.contains("--dimension"));
assert!(diagnostic_help.contains("Workload steps"));
assert!(diagnostic_help.contains("round-trip mode"));
assert!(!diagnostic_help.contains("Attempted Pachner moves"));
}
#[test]
fn redundant_command_groups_are_rejected() {
for name in ["artifact", "diagnose"] {
let output = run_cli(&[name, "--help"]);
assert_exit_code(&output, 2);
assert_stderr_contains(&output, "unrecognized subcommand");
}
}
#[test]
fn generate_triangulation_emits_json_to_stdout() {
let output = run_cli(&[
"generate",
"triangulation",
"--dimension",
"2",
"--vertices",
"4",
"--seed",
"1",
]);
assert_success(&output);
let json = stdout_json(&output);
assert_generated_triangulation_json(&json, 2, 4);
}
#[test]
fn generate_ball_distribution_emits_points_inside_unit_ball() {
let output = run_cli(&[
"generate",
"triangulation",
"--dimension",
"3",
"--vertices",
"8",
"--distribution",
"ball",
"--seed",
"11",
]);
assert_success(&output);
let json = stdout_json(&output);
let tds = generated_triangulation_tds(&json);
let vertices = cbor_field(&tds, "vertices")
.as_array()
.expect("triangulation JSON should include vertices");
assert_eq!(vertices.len(), 8);
for vertex in vertices {
let point = cbor_field(vertex, "point")
.as_array()
.expect("vertex JSON should include coordinates");
assert_eq!(point.len(), 3);
let norm_sq: f64 = point
.iter()
.map(|coordinate| {
let coordinate = coordinate
.as_float()
.expect("coordinate should be a CBOR float");
coordinate * coordinate
})
.sum();
assert!(
norm_sq <= 1.0 + 1.0e-12,
"ball distribution emitted point outside unit ball: norm_sq={norm_sq}"
);
}
}
#[test]
fn generate_convex_hull_emits_schema_json_to_stdout() {
let output = run_cli(&[
"generate",
"convex-hull",
"--dimension",
"3",
"--vertices",
"6",
"--seed",
"1",
]);
assert_success(&output);
let json = stdout_json(&output);
let facets = json["facets"]
.as_array()
.expect("convex-hull JSON should include facets");
assert_eq!(json["schema"], "delaunay.convex_hull");
assert_eq!(json["schema_version"], 1);
assert_eq!(json["dimension"], 3);
assert_eq!(json["vertex_count"], 6);
assert_eq!(
json["facet_count"].as_u64(),
Some(u64::try_from(facets.len()).expect("facet count should fit in u64"))
);
assert!(!facets.is_empty());
}
#[test]
fn generate_visualization_emits_generic_schema_json_to_stdout() {
let output = run_cli(&[
"generate",
"visualization",
"--dimension",
"3",
"--vertices",
"6",
"--seed",
"1",
]);
assert_success(&output);
let json = stdout_json(&output);
let metadata = &json["metadata"];
let simplices = json["simplices"]
.as_array()
.expect("visualization JSON should include simplices");
let adjacency = json["adjacency"]
.as_array()
.expect("visualization JSON should include adjacency");
assert_eq!(metadata["schema"], "delaunay.simplicial_complex");
assert_eq!(metadata["schema_version"], 1);
assert_eq!(metadata["dimension"], 3);
assert_eq!(metadata["vertex_count"], 6);
assert!(!simplices.is_empty());
assert_eq!(adjacency.len(), simplices.len() * 4);
assert!(
json["vertices"]
.as_array()
.is_some_and(|vertices| vertices.iter().all(|vertex| {
vertex["id"].is_string()
&& vertex["coordinates"]
.as_array()
.is_some_and(|coordinates| coordinates.len() == 3)
}))
);
}
#[test]
fn spherical_hero_emits_valid_s2_triangles_to_stdout() {
let output = run_cli(&["spherical-hero", "--vertices", "8"]);
assert_success(&output);
let json = stdout_json(&output);
let vertices = json["vertices"]
.as_array()
.expect("spherical hero JSON should include vertices");
let simplices = json["simplices"]
.as_array()
.expect("spherical hero JSON should include simplices");
assert_eq!(json["schema"], "delaunay.spherical_hero");
assert_eq!(json["schema_version"], 1);
assert_eq!(json["intrinsic_dimension"], 2);
assert_eq!(json["ambient_dimension"], 3);
assert_eq!(vertices.len(), 8);
let exported_vertex_count =
u64::try_from(vertices.len()).expect("small fixture count should fit u64");
assert!(vertices.iter().all(|vertex| {
let coordinates = vertex
.as_array()
.expect("spherical hero vertex should be a coordinate array");
let squared_norm = coordinates
.iter()
.map(|coordinate| {
let value = coordinate
.as_f64()
.expect("spherical hero coordinate should be finite f64 JSON");
value * value
})
.sum::<f64>();
coordinates.len() == 3 && (squared_norm - 1.0).abs() <= 1.0e-12
}));
assert_eq!(simplices.len(), 2 * vertices.len() - 4);
assert!(simplices.iter().all(|simplex| {
simplex.as_array().is_some_and(|vertex_indices| {
vertex_indices.len() == 3
&& vertex_indices.iter().all(|index| {
index
.as_u64()
.is_some_and(|index| index < exported_vertex_count)
})
&& vertex_indices[0] != vertex_indices[1]
&& vertex_indices[0] != vertex_indices[2]
&& vertex_indices[1] != vertex_indices[2]
})
}));
let repeated = run_cli(&["spherical-hero", "--vertices", "8"]);
assert_success(&repeated);
assert_eq!(output.stdout, repeated.stdout);
}
#[test]
fn spherical_hero_accepts_minimum_vertex_count() {
let output = run_cli(&["spherical-hero", "--vertices", "4"]);
assert_success(&output);
let json = stdout_json(&output);
assert_eq!(json["vertices"].as_array().map(Vec::len), Some(4));
assert_eq!(json["simplices"].as_array().map(Vec::len), Some(4));
}
#[test]
fn spherical_hero_writes_requested_json_artifact() {
let path = target_json_path("spherical-hero");
let output = run_cli(&[
"spherical-hero",
"--vertices",
"8",
"--output",
path.to_str().expect("target path should be UTF-8"),
]);
assert_success(&output);
assert!(
output.stdout.is_empty(),
"--output should keep spherical hero JSON out of stdout"
);
let json = file_json(&path);
assert_eq!(json["schema"], "delaunay.spherical_hero");
assert_eq!(json["schema_version"], 1);
assert_eq!(json["intrinsic_dimension"], 2);
assert_eq!(json["ambient_dimension"], 3);
assert_eq!(json["vertices"].as_array().map(Vec::len), Some(8));
}
#[test]
fn spherical_hero_rejects_too_few_vertices() {
for vertices in ["0", "3"] {
let output = run_cli(&["spherical-hero", "--vertices", vertices]);
assert_exit_code(&output, 1);
assert_stderr_contains(
&output,
&format!(
"S^2 spherical-hero generation requires at least 4 vertices, got {vertices}"
),
);
}
}
#[test]
fn spherical_hero_rejects_empty_output_path_during_parsing() {
let output = run_cli(&["spherical-hero", "--vertices", "8", "--output", ""]);
assert_exit_code(&output, 2);
assert_stderr_contains(&output, "a value is required");
assert_stderr_contains(&output, "--output <OUTPUT>");
}
#[test]
fn generate_triangulation_writes_requested_json_artifact() {
let path = target_json_path("generate-triangulation");
let output = run_cli(&[
"generate",
"triangulation",
"--dimension",
"2",
"--vertices",
"4",
"--seed",
"1",
"--output",
path.to_str().expect("target path should be UTF-8"),
]);
assert_success(&output);
assert!(
output.stdout.is_empty(),
"--output should keep generated triangulation JSON out of stdout"
);
let json = file_json(&path);
assert_generated_triangulation_json(&json, 2, 4);
}
#[test]
fn generate_triangulation_supports_dimension_four() {
let path = target_json_path("generate-triangulation-4d");
let output = run_cli(&[
"generate",
"triangulation",
"--dimension",
"4",
"--vertices",
"6",
"--seed",
"4",
"--output",
path.to_str().expect("target path should be UTF-8"),
]);
assert_success(&output);
let json = file_json(&path);
assert_generated_triangulation_json(&json, 4, 6);
let expected_manifest: DelaunayCheckpointManifest =
from_value(json["manifest"].clone()).unwrap();
let restored: DelaunayTriangulation<RobustKernel<f64>, (), (), 4> =
from_slice(&fs::read(&path).expect("4D checkpoint should be readable")).unwrap();
assert_eq!(restored.number_of_vertices(), 6);
assert!(restored.number_of_simplices() > 1);
assert_eq!(restored.checkpoint_manifest().unwrap(), expected_manifest);
restored.validate().unwrap();
}
#[test]
fn generate_triangulation_supports_dimension_five() {
let path = target_json_path("generate-triangulation-5d");
let output = run_cli(&[
"generate",
"triangulation",
"--dimension",
"5",
"--vertices",
"7",
"--seed",
"5",
"--output",
path.to_str().expect("target path should be UTF-8"),
]);
assert_success(&output);
let json = file_json(&path);
assert_generated_triangulation_json(&json, 5, 7);
let expected_manifest: DelaunayCheckpointManifest =
from_value(json["manifest"].clone()).unwrap();
let restored: DelaunayTriangulation<RobustKernel<f64>, (), (), 5> =
from_slice(&fs::read(&path).expect("5D checkpoint should be readable")).unwrap();
assert_eq!(restored.number_of_vertices(), 7);
assert!(restored.number_of_simplices() > 1);
assert_eq!(restored.checkpoint_manifest().unwrap(), expected_manifest);
restored.validate().unwrap();
}
#[test]
fn validation_demo_writes_requested_json_artifact() {
let path = target_json_path("validation-demo");
let output = run_cli(&[
"validation-demo",
"--output",
path.to_str().expect("target path should be UTF-8"),
]);
assert_success(&output);
assert!(
output.stdout.is_empty(),
"--output should keep validation-demo JSON out of stdout"
);
let json = file_json(&path);
let cases = json["cases"]
.as_array()
.expect("validation-demo JSON should include cases");
assert_eq!(json["schema"], "delaunay.validation_demo");
assert_eq!(json["schema_version"], 1);
assert_eq!(json["dimension"], 2);
assert_eq!(json["valid_baseline"]["status"], "passed");
assert_eq!(cases.len(), 5);
assert_eq!(cases[3]["layer"], "Valid realization");
}
#[test]
fn artifact_open_errors_name_the_requested_destination() {
let path = target_artifact_path("validation-demo-directory", "dir");
fs::create_dir_all(&path).expect("output-directory fixture should be created");
let output = run_cli(&[
"validation-demo",
"--output",
path.to_str().expect("target path should be UTF-8"),
]);
assert_exit_code(&output, 1);
assert_stderr_contains(&output, "failed to open artifact");
assert_stderr_contains(
&output,
path.file_name()
.and_then(|name| name.to_str())
.expect("fixture file name should be UTF-8"),
);
}
#[test]
fn pachner_stress_accepts_small_quiet_summary_run() {
let summary_path = target_json_path("pachner-stress-summary");
let progress_path = target_artifact_path("pachner-stress-progress", "csv");
let output = run_pachner_stress(&[
"--dimension",
"3d",
"--mode",
"round-trip",
"--vertices",
"5",
"--attempts",
"2",
"--validate-every",
"1",
"--key-refresh-every",
"1",
"--retry-attempts",
"4",
"--seed",
"7",
"--quiet",
"--progress-csv",
progress_path.to_str().expect("target path should be UTF-8"),
"--summary-json",
summary_path.to_str().expect("target path should be UTF-8"),
]);
assert_success(&output);
assert!(
output.stdout.is_empty(),
"--quiet should suppress Pachner stress telemetry"
);
let json = file_json(&summary_path);
assert_eq!(json["schema"], "delaunay.pachner_stress");
assert_eq!(json["schema_version"], 2);
assert_eq!(json["dimension"], 3);
assert_eq!(json["label"], "3d");
assert_eq!(json["mode"], "round-trip");
assert_eq!(json["validation_scope"], "topology");
assert_eq!(json["configured_vertices"], 5);
assert_eq!(json["configured_steps"], 2);
assert_eq!(json["validate_every"], 1);
assert_eq!(json["key_refresh_every"], 1);
assert_eq!(json["retry_attempts"], 4);
assert_eq!(json["seed"], 7);
assert_eq!(json["source"]["mode"], "round-trip");
assert_eq!(json["source"]["validation_scope"], "topology");
assert_eq!(json["report"]["completed_steps"], 2);
let proposal_attempts = json["report"]["proposal_attempts"]
.as_u64()
.expect("proposal attempts should be an unsigned count");
let accepted_mutations = json["report"]["accepted_mutations"]
.as_u64()
.expect("accepted mutations should be an unsigned count");
let proposal_rejections = json["report"]["proposal_rejections"]
.as_u64()
.expect("proposal rejections should be an unsigned count");
assert_eq!(proposal_attempts, accepted_mutations + proposal_rejections);
assert!(proposal_attempts <= 4);
assert!(json.get("attempts").is_none());
assert!(json["report"].get("accepted").is_none());
let progress = fs::read_to_string(progress_path).expect("progress CSV should be readable");
let rows: Vec<_> = progress.lines().collect();
assert_eq!(
rows.len(),
3,
"expected one CSV header and two progress rows"
);
assert_eq!(
rows[0],
"schema_version,dimension,label,mode,validation_scope,sequence,completed_steps,configured_steps,\
proposal_attempts,accepted_mutations,candidate_misses,proposal_rejections,validations,\
validation_nanos,acceptance_rate,vertices,simplices"
);
let fields: Vec<_> = rows[1].split(',').collect();
assert_eq!(fields.len(), 17);
assert_eq!(fields[0], "2");
assert_eq!(fields[1], "3");
assert_eq!(fields[2], "3d");
assert_eq!(fields[3], "round-trip");
assert_eq!(fields[4], "topology");
assert_eq!(fields[5], "1");
assert_eq!(fields[6], "1");
assert_eq!(fields[7], "2");
assert_eq!(fields[12], "1");
let completed_proposals = fields[8]
.parse::<u32>()
.expect("progress proposal attempts should parse");
let accepted = fields[9]
.parse::<u32>()
.expect("progress accepted mutations should parse");
let acceptance_rate = fields[14]
.parse::<f64>()
.expect("progress acceptance rate should parse");
assert!(completed_proposals > 0);
assert_abs_diff_eq!(
acceptance_rate,
f64::from(accepted) / f64::from(completed_proposals),
epsilon = 1.0e-6
);
}
#[test]
fn pachner_stress_accepts_small_random_walk_summary_run() {
let summary_path = target_json_path("pachner-stress-random-walk-summary");
let output = run_pachner_stress(&[
"--dimension",
"3d",
"--mode",
"random-walk",
"--vertices",
"5",
"--attempts",
"2",
"--validate-every",
"1",
"--key-refresh-every",
"1",
"--retry-attempts",
"4",
"--seed",
"7",
"--quiet",
"--summary-json",
summary_path.to_str().expect("target path should be UTF-8"),
]);
assert_success(&output);
assert!(
output.stdout.is_empty(),
"--quiet should suppress Pachner stress telemetry"
);
let json = file_json(&summary_path);
assert_eq!(json["dimension"], 3);
assert_eq!(json["label"], "3d");
assert_eq!(json["mode"], "random-walk");
assert_eq!(json["validation_scope"], "topology");
assert_eq!(json["configured_vertices"], 5);
assert_eq!(json["schema_version"], 2);
assert_eq!(json["configured_steps"], 2);
assert_eq!(json["validate_every"], 1);
assert_eq!(json["source"]["mode"], "random-walk");
assert_eq!(json["source"]["validation_scope"], "topology");
assert_eq!(json["report"]["completed_steps"], 2);
assert_eq!(json["report"]["validations"], 2);
let proposal_attempts = json["report"]["proposal_attempts"]
.as_u64()
.expect("proposal attempts should be an unsigned count");
let accepted_mutations = json["report"]["accepted_mutations"]
.as_u64()
.expect("accepted mutations should be an unsigned count");
let proposal_rejections = json["report"]["proposal_rejections"]
.as_u64()
.expect("proposal rejections should be an unsigned count");
assert_eq!(proposal_attempts, accepted_mutations + proposal_rejections);
assert!(proposal_attempts <= 2);
}
#[test]
fn pachner_stress_emits_setup_stage_telemetry() {
let output = run_pachner_stress(&[
"--dimension",
"3d",
"--mode",
"round-trip",
"--vertices",
"5",
"--attempts",
"1",
"--validate-every",
"1",
"--key-refresh-every",
"1",
"--retry-attempts",
"4",
"--seed",
"7",
]);
assert_success(&output);
let stdout = output_text(&output.stdout);
assert!(stdout.contains("pachner_stress_stage"));
assert!(stdout.contains("validation_scope=topology"));
assert!(stdout.contains("stage=generate_points_start"));
assert!(stdout.contains("stage=construction_start"));
assert!(stdout.contains("stage=initial_topology_validation_done"));
assert!(stdout.contains("pachner_stress_source"));
assert!(stdout.contains("pachner_stress_progress"));
assert!(stdout.contains("pachner_stress_metric"));
assert!(stdout.contains("schema_version=2"));
assert!(stdout.contains("completed_steps=1 configured_steps=1"));
assert!(stdout.contains("proposal_attempts="));
assert!(stdout.contains("accepted_mutations="));
assert!(!stdout.contains(" attempts="));
assert!(!stdout.contains(" accepted="));
}
#[test]
fn generate_rejects_unsupported_dimension_after_parsing() {
let output = run_cli(&["generate", "--dimension", "6", "--vertices", "8"]);
assert_exit_code(&output, 1);
assert_stderr_contains(&output, "generate supports dimensions 2 through 5, got 6");
}
#[test]
fn generate_rejects_too_few_vertices_for_dimension() {
let output = run_cli(&["generate", "--dimension", "3", "--vertices", "3"]);
assert_exit_code(&output, 1);
assert_stderr_contains(
&output,
"3D Euclidean generation requires at least 4 vertices, got 3",
);
}
#[test]
fn generate_rejects_zero_vertices() {
let output = run_cli(&["generate", "--dimension", "3", "--vertices", "0"]);
assert_exit_code(&output, 1);
assert_stderr_contains(
&output,
"3D Euclidean generation requires at least 4 vertices, got 0",
);
}
#[test]
fn generate_rejects_empty_output_path_during_parsing() {
let output = run_cli(&[
"generate",
"triangulation",
"--dimension",
"3",
"--vertices",
"4",
"--output",
"",
]);
assert_exit_code(&output, 2);
assert_stderr_contains(&output, "a value is required");
assert_stderr_contains(&output, "--output <OUTPUT>");
}
#[test]
fn generate_rejects_invalid_distribution_value() {
let output = run_cli(&["generate", "--dimension", "3", "--distribution", "sphere"]);
assert_exit_code(&output, 2);
assert_stderr_contains(&output, "invalid value 'sphere'");
assert_stderr_contains(&output, "[possible values: cube, ball]");
}
#[test]
fn pachner_stress_rejects_unsupported_dimension_value() {
let output = run_pachner_stress(&["--dimension", "2d"]);
assert_exit_code(&output, 2);
assert_stderr_contains(&output, "invalid value '2d'");
assert_stderr_contains(&output, "[possible values: 3d, 4d]");
}
#[test]
fn pachner_stress_rejects_empty_artifact_paths_during_parsing() {
for argument in ["--progress-csv", "--summary-json"] {
let output = run_pachner_stress(&[argument, ""]);
assert_exit_code(&output, 2);
assert_stderr_contains(&output, "a value is required");
assert_stderr_contains(&output, argument);
}
}
#[test]
fn pachner_stress_rejects_zero_validated_arguments() {
for (argument, expected) in [
("--attempts", "--attempts must be positive"),
("--validate-every", "--validate-every must be positive"),
(
"--key-refresh-every",
"--key-refresh-every must be positive",
),
("--retry-attempts", "--retry-attempts must be positive"),
] {
let output = run_pachner_stress(&[argument, "0", "--quiet"]);
assert_exit_code(&output, 1);
let stderr = output_text(&output.stderr);
assert!(
stderr.contains(expected),
"{argument} stderr should contain {expected:?}, got:\n{stderr}"
);
}
}
#[test]
fn pachner_stress_rejects_too_few_vertices_for_dimension() {
let output = run_pachner_stress(&["--dimension", "3d", "--vertices", "3"]);
assert_exit_code(&output, 1);
assert_stderr_contains(&output, "3D stress requires at least 4 vertices, got 3");
}
#[test]
fn pachner_stress_rejects_zero_vertices() {
let output = run_pachner_stress(&["--dimension", "3d", "--vertices", "0"]);
assert_exit_code(&output, 1);
assert_stderr_contains(&output, "3D stress requires at least 4 vertices, got 0");
}
#[test]
fn pachner_stress_rejects_duplicate_artifact_paths() {
let path = target_json_path("pachner-stress-duplicate-artifact");
let path = path.to_str().expect("target path should be UTF-8");
let output =
run_pachner_stress(&["--quiet", "--progress-csv", path, "--summary-json", path]);
assert_exit_code(&output, 1);
assert_stderr_contains(
&output,
"progress CSV and summary JSON must use different paths",
);
}
#[test]
fn pachner_stress_rejects_lexically_aliased_artifact_paths() {
let direct = target_json_path("pachner-stress-aliased-artifact");
let alias = direct
.parent()
.expect("target artifact should have a parent")
.join("missing")
.join("..")
.join(
direct
.file_name()
.expect("target artifact should have a file name"),
);
let output = run_pachner_stress(&[
"--quiet",
"--progress-csv",
direct.to_str().expect("target path should be UTF-8"),
"--summary-json",
alias.to_str().expect("target path should be UTF-8"),
]);
assert_exit_code(&output, 1);
assert_stderr_contains(&output, "progress CSV");
assert_stderr_contains(&output, "summary JSON");
assert_stderr_contains(&output, "must use different paths");
}
#[test]
fn unsupported_topology_style_flags_are_rejected() {
for (args, expected) in [
(
&["generate", "--spherical"][..],
"unexpected argument '--spherical'",
),
(
&["validation-demo", "--hyperbolic"][..],
"unexpected argument '--hyperbolic'",
),
] {
let output = run_cli(args);
assert_exit_code(&output, 2);
assert_stderr_contains(&output, expected);
}
}
}