diff --git a/.agents/skills/unit-test-quality/SKILL.md b/.agents/skills/unit-test-quality/SKILL.md new file mode 100644 index 0000000..f51882b --- /dev/null +++ b/.agents/skills/unit-test-quality/SKILL.md @@ -0,0 +1,89 @@ +--- +name: unit-test-quality +description: Write, strengthen, or review unit tests for meaningful behavioral protection. Use when adding tests, reviewing test changes, investigating misleading coverage, or checking generated tests. Focuses on contracts, independent oracles, preconditions, and defect detection; it does not require TDD, property-based testing, mutation frameworks, or coverage targets. +--- + +# Unit Test Quality + +Treat a passing test as evidence only when it distinguishes conforming behavior from a plausible defect. Optimize for fault detection, not test count, assertion count, or line coverage. + +## Establish what the test proves + +For each new or materially changed non-trivial test, identify: + +- the externally observable contract or invariant; +- the plausible production defect the test must detect; +- why the assertion would differ under that defect; +- the source of the expected result. + +Do this in working notes or the final explanation; do not add ceremonial comments to every test. If no plausible defect would change the assertion, remove or redesign the test. + +Infer contracts from authoritative specifications, public API documentation, issue reports, compatibility promises, and accepted domain rules. Existing implementation behavior is evidence, not automatically the contract. If the intended behavior is genuinely ambiguous, say so instead of canonizing the current output in a test. + +## Keep the oracle independent + +Derive expected values independently of the path being tested. Prefer, in order: + +1. an explicit specification or hand-derived example; +2. a simpler, structurally different reference calculation; +3. a well-established independent implementation; +4. an invariant or relation whose truth does not depend on the same algorithm. + +Do not compute expectations with the function under test, its policy resolver, its parser/serializer pair when both share the same defect, or a helper that merely mirrors the implementation. Do not copy constants, branch tables, or formulas from production code unless the contract itself defines them and the test cites that independent source. + +When exact expected values are impractical, use strong invariants, metamorphic relations, differential checks, or bounded properties. Property-based generation is optional; ordinary examples are often enough. + +## Verify the setup can reach the behavior + +Check that the arranged state actually activates the branch, boundary, error, side effect, or interaction being claimed. Assert a precondition only when it is not already obvious from a small literal fixture and when a false precondition could let the main assertion pass vacuously. + +Watch especially for: + +- empty parameterized or generated case sets; +- filters or skips that remove the intended case; +- fixtures that never create the relevant state; +- assertions inside callbacks that are never called; +- exceptions accepted from the wrong operation; +- numeric inputs that do not straddle the specified boundary. + +## Test behavior at the right boundary + +Exercise the public or stable unit boundary that owns the contract. Calling a private helper can be useful diagnostically, but it does not protect the public path unless the public path is also exercised. + +Mocks may isolate nondeterministic, slow, or external boundaries. Do not assert merely that a mock returned what it was configured to return. Preserve side effects and response structure that the unit relies on, and assert the unit's observable result or required interaction contract. If mock setup dominates the test or replaces the behavior being claimed, use a real collaborator, fake, or a broader test. + +## Use discriminating cases + +Choose cases that separate common wrong implementations from the correct one. Include boundaries and negative cases when they expose distinct plausible faults, not to satisfy a checklist. For policy code, derive cases from the policy rather than from the implementation's resolved value. + +For generated tests, inspect both the generator and representative generated output. Ensure the generated test invokes the production entry point it claims to protect and that its expected values are independent of generator/runtime helpers implementing the same rule. + +Avoid tests that only prove: + +- code did not throw; +- a value is non-null, has a type, or contains a key when its meaning matters; +- an object reproduces the fixture that created it; +- a mock exists or was called without checking meaningful arguments/effects; +- the current implementation is internally consistent; +- a snapshot changed without establishing what must remain stable. + +## Challenge important tests + +For a new regression test or a test protecting a non-trivial contract, identify one small semantic mutation representing the named defect: change a boundary operator, use a wrong constant, remove validation or a side effect, select the wrong branch, or return a plausible default. + +When safe and cheap, apply that mutation temporarily and run the narrowest relevant test. The test must fail for the intended reason. Restore the production file immediately, verify restoration, then run the unmodified test. Never leave a probe mutation in the worktree or mix it with unrelated user changes. If the worktree cannot be safely restored, use an isolated copy or perform a reasoned mutation review instead. + +Do not require an executed mutation for trivial accessors, framework wiring, destructive or external behavior, expensive suites, or cases where the test already demonstrably failed on the exact defect. Record why it was skipped when the test's protective value is otherwise uncertain. + +## Review outcome + +When reviewing, report concrete findings rather than grading style. For each weak test, state: + +- claimed contract; +- weakness in oracle, setup, boundary, or assertion; +- a plausible defect that survives; +- the smallest strengthening that would detect it. + +Distinguish “code executed” from “behavior protected.” Treat coverage only as a map of code not exercised; never use a percentage as proof of test quality. + +Before completion, run the focused tests and then the relevant broader suite when practical. Report commands and outcomes truthfully. Do not weaken assertions, update expected values to buggy output, add skips, or reduce scope merely to make CI green. diff --git a/.agents/skills/unit-test-quality/agents/openai.yaml b/.agents/skills/unit-test-quality/agents/openai.yaml new file mode 100644 index 0000000..4260c75 --- /dev/null +++ b/.agents/skills/unit-test-quality/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Unit Test Quality" + short_description: "Write and review tests that prove behavioral contracts" + default_prompt: "Review or write these unit tests for meaningful behavioral protection." diff --git a/AGENTS.md b/AGENTS.md index 2c0d704..8b1b2e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,3 +13,6 @@ completely sure. Follow the [development guide](docs/src/contributing/development.md) for dependency management, PTF extraction and generation, target implementation, validation, documentation, and commit conventions. + +When writing, strengthening, or reviewing codegen tests, use the +[unit-test-quality skill](.agents/skills/unit-test-quality/SKILL.md). diff --git a/codegen/src/compile.rs b/codegen/src/compile.rs index 75ba5f4..3824d73 100644 --- a/codegen/src/compile.rs +++ b/codegen/src/compile.rs @@ -5,8 +5,6 @@ use crate::model::{ Entry, Function, Output, Outputs, ToleranceSource, VerificationInput, }; -pub(crate) const FLOATING_POINT_GUARD: f64 = 1e-14; - pub(super) fn functions(entries: Vec) -> Result> { let mut compiled = Vec::new(); for entry in entries { diff --git a/codegen/src/corpus_report.rs b/codegen/src/corpus_report.rs index ccdac50..a776586 100644 --- a/codegen/src/corpus_report.rs +++ b/codegen/src/corpus_report.rs @@ -645,10 +645,7 @@ fn render_values(output: &mut String, values: &[Frequency]) { #[cfg(test)] mod tests { - use std::{ - path::{Path, PathBuf}, - sync::Arc, - }; + use std::{path::PathBuf, sync::Arc}; use super::{InputKind, Report}; use crate::model::{Entry, Spec}; @@ -707,6 +704,7 @@ functions: verification_cases: - {id: one, kind: published, inputs: {x: 1.0}, expected: {alpha: 1.0, zeta: 2.0}, source_location: 'Table 1, row 1'} - {id: two, kind: calculated, inputs: {x: 2.0}, expected: {alpha: 2.0, zeta: 3.0}, rationale: Interior input.} + - {id: three, kind: calculated, inputs: {x: 3.0}, expected: {alpha: 3.0, zeta: 4.0}, rationale: Another interior input.} edge_cases: - {id: edge, inputs: {x: 0.0}, expected_behavior: Finite., notes: Metadata only.} - name: calc_ptf_test_blocked @@ -748,18 +746,26 @@ functions: let report = Report::from_entries(&[fixture()]); assert_eq!(report.functions.total, 4); - assert!( + assert_eq!( report .functions .by_status .iter() - .all(|status| status.count == 1) + .map(|status| (status.value.as_str(), status.count)) + .collect::>(), + [ + ("implemented", 1), + ("ready-for-implementation", 1), + ("blocked", 1), + ("draft", 1) + ] ); - assert_eq!(report.verification.all_functions.verification_cases, 2); + assert_eq!(report.verification.cases_total, 3); + assert_eq!(report.verification.all_functions.verification_cases, 3); assert_eq!(report.verification.by_kind["published"], 1); - assert_eq!(report.verification.by_kind["calculated"], 1); + assert_eq!(report.verification.by_kind["calculated"], 2); assert_eq!(report.verification.published_cases, 1); - assert_eq!(report.verification.calculated_cases, 1); + assert_eq!(report.verification.calculated_cases, 2); assert_eq!(report.verification.all_functions.edge_cases, 1); assert_eq!( report @@ -768,6 +774,20 @@ functions: .functions_with_verification_cases, 1 ); + assert_eq!( + report + .verification + .all_functions + .functions_with_verification_cases_percentage, + 25.0 + ); + assert_eq!( + report + .verification + .ready_for_implementation_functions + .verification_cases, + 0 + ); assert_eq!(report.verification.implemented_functions.functions, 1); assert_eq!( report @@ -818,7 +838,10 @@ functions: report.blocked_functions[0].documentation_warnings, ["Required coefficients are unavailable."] ); - assert!(report.blocked_functions[0].scientific_notes.is_some()); + assert_eq!( + report.blocked_functions[0].scientific_notes.as_deref(), + Some("Blocker evidence from source review.") + ); assert_eq!( report .scope @@ -829,52 +852,41 @@ functions: } #[test] - fn serializes_deterministically_to_json() { - let report = Report::from_entries(&[fixture()]); - let first = serde_json::to_string_pretty(&report).expect("report serializes"); - let second = serde_json::to_string_pretty(&report).expect("report serializes again"); - let text = report.to_text(); - - assert_eq!(first, second); - assert!(first.contains("\"blocked_functions\"")); - assert!(first.contains("\"implemented_functions\"")); - assert!(!first.contains("missing_quantity_or_unit_validation_failures")); - assert!(text.find("Implemented functions") < text.find("Quantity registry")); + fn report_order_is_independent_of_source_order() { + let mut earlier = fixture(); + earlier.slug = "alpha1990".into(); + let later = fixture(); + let forward = Report::from_entries(&[earlier.clone(), later.clone()]); + let reverse = Report::from_entries(&[later, earlier]); + assert_eq!( + serde_json::to_value(&forward).unwrap(), + serde_json::to_value(&reverse).unwrap() + ); + assert_eq!(forward.to_text(), reverse.to_text()); + assert_eq!(forward.sources.earliest_publication_year, Some(1990)); + assert_eq!(forward.sources.latest_publication_year, Some(2020)); } #[test] - fn repository_corpus_report_is_complete_and_stable() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("workspace root exists"); - let entries = crate::specs::load(root).expect("repository specifications load"); - assert!(crate::validate::specifications(&entries).is_empty()); - crate::compile::functions(entries.clone()).expect("repository specifications compile"); - - let report = Report::from_entries(&entries); - assert_eq!(report.verification.cases_total, 136); - assert_eq!(report.verification.by_kind["calculated"], 125); - assert_eq!(report.verification.by_kind["published"], 11); - assert_eq!(report.sources.specification_files, entries.len()); - assert_eq!( - report.functions.total, - entries - .iter() - .map(|entry| entry.spec.functions.len()) - .sum::() - ); + fn serializes_coverage_and_orders_text_sections() { + let report = Report::from_entries(&[fixture()]); + let json = serde_json::to_value(&report).unwrap(); + assert_eq!(json["verification"]["cases_total"], 3); assert_eq!( - report - .functions - .by_status - .iter() - .map(|status| status.count) - .sum::(), - report.functions.total + json["verification"]["implemented_functions"]["functions"], + 1 ); assert_eq!( - serde_json::to_string_pretty(&report).unwrap(), - serde_json::to_string_pretty(&Report::from_entries(&entries)).unwrap() + json["blocked_functions"][0]["function_name"], + "calc_ptf_test_blocked" ); + let text = report.to_text(); + let implemented = text + .find("Implemented functions") + .expect("implemented coverage section"); + let registry = text + .find("Quantity registry") + .expect("quantity registry section"); + assert!(implemented < registry); } } diff --git a/codegen/src/fixtures/quantities.yaml b/codegen/src/fixtures/quantities.yaml new file mode 100644 index 0000000..cfbbbeb --- /dev/null +++ b/codegen/src/fixtures/quantities.yaml @@ -0,0 +1,10 @@ +volumetric_water_content: + description: Test fraction. + units: + volume_fraction: + absolute: 0.001 + rationale: Test fraction tolerance. + volume_percent: + absolute: 0.1 + relative: 0.01 + rationale: Test percent tolerance. diff --git a/codegen/src/fixtures/source.yaml b/codegen/src/fixtures/source.yaml new file mode 100644 index 0000000..58050c9 --- /dev/null +++ b/codegen/src/fixtures/source.yaml @@ -0,0 +1,33 @@ +source: + summary: Synthetic rendering example. + citation_apa: Example (2026). + doi: null +functions: + - name: calc_ptf_example_scalar + status: implemented + public_api: {name: calc_ptf_example_scalar, summary: Scalar example.} + scope: {prediction_target: Test fraction., models: {h_theta: null, k_h: null}} + inputs: + - {name: x, unit: '1', symbol: null, domain: null, description: First input.} + - {name: y, unit: '1', symbol: null, domain: null, description: Second input.} + outputs: {type: scalar, name: value, quantity: volumetric_water_content, unit: volume_fraction, reported_unit: '1', symbol: null, domain: null, description: Scalar value.} + implementation: + variables: [{name: value, expr: x + y}] + verification_cases: + - {id: sum, kind: calculated, inputs: {x: 1.0, y: 2.0}, expected: {value: 3.0}, rationale: Test addition.} + - name: calc_ptf_example_record + status: implemented + public_api: {name: calc_ptf_example_record, summary: Record example.} + scope: {prediction_target: Test fractions., models: {h_theta: null, k_h: null}} + inputs: + - {name: x, unit: '1', symbol: null, domain: null, description: Input value.} + outputs: + type: record + name: ExampleResult + fields: + - {name: first, quantity: volumetric_water_content, unit: volume_fraction, reported_unit: '1', symbol: null, domain: null, description: First result.} + - {name: second, quantity: volumetric_water_content, unit: volume_fraction, reported_unit: '1', symbol: null, domain: null, description: Second result.} + implementation: + variables: [{name: first, expr: x}, {name: second, expr: x * 2}] + verification_cases: + - {id: record, kind: calculated, inputs: {x: 1.0}, expected: {first: 1.0, second: 2.0}, rationale: Test record.} diff --git a/codegen/src/fixtures/units.yaml b/codegen/src/fixtures/units.yaml new file mode 100644 index 0000000..735bc22 --- /dev/null +++ b/codegen/src/fixtures/units.yaml @@ -0,0 +1,18 @@ +volume_fraction: + preferred_notation: "m^3/m^3" + aliases: ["cm^3/cm^3", "cm³/cm³", "1"] +volume_percent: + preferred_notation: "% v/v" + aliases: ["vol.%", "% volume/volume", "%"] +mass_percent: + preferred_notation: "% w/w" + aliases: ["%"] +millimeter_per_hour: + preferred_notation: "mm/h" + aliases: [] +kilopascal: + preferred_notation: "kPa" + aliases: [] +dimensionless: + preferred_notation: "1" + aliases: [] diff --git a/codegen/src/main.rs b/codegen/src/main.rs index 7c7991b..5dd3575 100644 --- a/codegen/src/main.rs +++ b/codegen/src/main.rs @@ -18,6 +18,9 @@ mod targets; mod validate; mod version; +#[cfg(test)] +mod test_support; + #[derive(Parser)] #[command(about = "Validate ptfkit specifications and generate bindings")] pub(crate) struct Cli { diff --git a/codegen/src/model.rs b/codegen/src/model.rs index b2c037e..a12c54c 100644 --- a/codegen/src/model.rs +++ b/codegen/src/model.rs @@ -847,22 +847,11 @@ mod tests { #[test] fn public_python_generation_defaults_to_generated() { - let mut spec = Spec { - source: Source { - summary: "Test (2026), test territory.".into(), - citation_apa: "Test (2026).".into(), - doi: None, - }, - scope: Scope::default(), - generation: Generation::default(), - scientific_notes: String::new(), - functions: Vec::new(), - }; - + let spec: Spec = serde_yaml::from_str( + "source: {summary: Test source., citation_apa: Test (2026)., doi: null}\nfunctions: []", + ) + .unwrap(); assert_eq!(spec.generation.public_python, PythonGeneration::Generated); - - spec.generation.public_python = PythonGeneration::Manual; - assert_eq!(spec.generation.public_python, PythonGeneration::Manual); } #[test] diff --git a/codegen/src/output/mod.rs b/codegen/src/output/mod.rs index 52b4575..9bd9735 100644 --- a/codegen/src/output/mod.rs +++ b/codegen/src/output/mod.rs @@ -108,13 +108,6 @@ layout!( C_HEADER, Formatter::Cpp ); -layout!( - NATIVE_TEST_SUPPORT, - "targets/ptfkit-native/tests/support", - "targets/ptfkit-native/tests/support", - C_HEADER, - Formatter::Cpp -); layout!( NATIVE_C_TEST, "targets/ptfkit-native/tests/c", @@ -130,7 +123,7 @@ layout!( Formatter::Cpp ); -pub(crate) const LAYOUTS: [&Layout; 13] = [ +pub(crate) const LAYOUTS: [&Layout; 12] = [ &CATALOG, &REFERENCE_C, &REFERENCE_CPP, @@ -141,7 +134,6 @@ pub(crate) const LAYOUTS: [&Layout; 13] = [ &PYTHON_TEST, &NATIVE_C, &NATIVE_CPP_MODULE, - &NATIVE_TEST_SUPPORT, &NATIVE_C_TEST, &NATIVE_CPP_TEST, ]; diff --git a/codegen/src/output/write.rs b/codegen/src/output/write.rs index cc15cd4..c240023 100644 --- a/codegen/src/output/write.rs +++ b/codegen/src/output/write.rs @@ -308,7 +308,7 @@ fn remove_temporary(staged: &[StagedWrite]) { #[cfg(test)] mod tests { - use std::{collections::BTreeMap, fs, path::Path}; + use std::{collections::BTreeMap, fs}; use super::*; use crate::{ @@ -340,6 +340,9 @@ mod tests { format!("{HEADER}# Preserved\n"), ) .expect("write C page"); + let handwritten = "# Handwritten Python guide\n\nKeep this content.\n"; + fs::write(python_reference.join("guide.md"), handwritten) + .expect("write handwritten Python page"); let output = Output::new( &REFERENCE_PYTHON, @@ -353,6 +356,11 @@ mod tests { assert!(python_reference.join("index.md").is_file()); assert!(!python_reference.join("obsolete.md").exists()); assert!(c_reference.join("preserved.md").is_file()); + assert_eq!( + fs::read_to_string(python_reference.join("guide.md")) + .expect("handwritten Python page survives cleanup"), + handwritten + ); fs::remove_dir_all(root).expect("remove temporary test directory"); } @@ -372,7 +380,6 @@ mod tests { HEADER, )); assert!(!is_generated(b"# Handwritten page\n", HEADER)); - assert!(Path::new("index.md").is_relative()); } #[test] diff --git a/codegen/src/render/c.rs b/codegen/src/render/c.rs index b45be8b..c6ba745 100644 --- a/codegen/src/render/c.rs +++ b/codegen/src/render/c.rs @@ -364,6 +364,47 @@ mod tests { ); } + #[test] + fn renders_each_math_function_for_c_and_cpp() { + for (function, args, expected_c, expected_cpp) in [ + ( + MathFunction::Sqrt, + vec![input(0)], + "sqrt(x)", + "std::sqrt(x)", + ), + (MathFunction::Exp, vec![input(0)], "exp(x)", "std::exp(x)"), + (MathFunction::Ln, vec![input(0)], "log(x)", "std::log(x)"), + ( + MathFunction::Log10, + vec![input(0)], + "log10(x)", + "std::log10(x)", + ), + (MathFunction::Abs, vec![input(0)], "fabs(x)", "std::abs(x)"), + ( + MathFunction::Min, + vec![input(0), input(1)], + "fmin(x, y)", + "std::fmin(x, y)", + ), + ( + MathFunction::Max, + vec![input(0), input(1)], + "fmax(x, y)", + "std::fmax(x, y)", + ), + ] { + let expression = Expr::Call { function, args }; + for (dialect, expected) in [(Dialect::C, expected_c), (Dialect::Cpp, expected_cpp)] { + assert_eq!( + super::expression(&expression, &inputs(), &[], dialect).to_string(), + expected + ); + } + } + } + #[test] fn renders_power_and_calls_with_dialect_local_math_names() { let expression = Expr::Call { diff --git a/codegen/src/semantic.rs b/codegen/src/semantic.rs index 2b52185..060b75c 100644 --- a/codegen/src/semantic.rs +++ b/codegen/src/semantic.rs @@ -773,36 +773,54 @@ mod tests { } } - #[test] - fn compiles_variables() { - let raw = function( - &["x"], - vec![variable("twice", "implementation.variables[0]", "x * 2")], - ); - let compiled = compile(&raw).unwrap(); - assert!(matches!( - compiled.variables[0].value.as_number().unwrap(), - Expr::Binary { - op: BinaryOp::Multiply, - .. - } - )); - } - #[test] fn resolves_ordered_variables_and_reuses_prior_values() { let raw = function( - &["x"], + &["x", "y"], vec![ - variable("first", "implementation.variables[0]", "x + 1"), - variable("second", "implementation.variables[1]", "first * first"), + variable("first", "implementation.variables[0]", "y + 1"), + variable("second", "implementation.variables[1]", "first * x"), + variable("third", "implementation.variables[2]", "second - first"), ], ); let compiled = compile(&raw).unwrap(); - assert!(matches!( - compiled.variables[1].value.as_number().unwrap(), - Expr::Binary { .. } - )); + use super::{Number, Reference}; + let expected = [ + Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::Reference(Reference::Input(1))), + right: Box::new(Expr::Number(Number { + value: 1.0, + lexeme: "1".into(), + })), + }, + Expr::Binary { + op: BinaryOp::Multiply, + left: Box::new(Expr::Reference(Reference::Variable(0))), + right: Box::new(Expr::Reference(Reference::Input(0))), + }, + Expr::Binary { + op: BinaryOp::Subtract, + left: Box::new(Expr::Reference(Reference::Variable(1))), + right: Box::new(Expr::Reference(Reference::Variable(0))), + }, + ]; + assert_eq!( + compiled + .variables + .iter() + .map(|variable| variable.name.as_str()) + .collect::>(), + ["first", "second", "third"] + ); + assert_eq!( + compiled + .variables + .iter() + .map(|variable| variable.value.as_number().unwrap()) + .collect::>(), + expected.iter().collect::>() + ); } #[test] diff --git a/codegen/src/specs.rs b/codegen/src/specs.rs index 33e1d15..5f30a0a 100644 --- a/codegen/src/specs.rs +++ b/codegen/src/specs.rs @@ -393,39 +393,12 @@ fn json_path(path: &impl std::fmt::Display) -> String { #[cfg(test)] mod tests { - use std::{ - fs, - path::{Path, PathBuf}, - }; + use std::{fs, path::Path}; use super::load; use crate::model::PythonGeneration; - fn fixture_root(label: &str) -> PathBuf { - let root = std::env::temp_dir().join(format!( - "ptfkit-codegen-specs-{label}-{}-{}", - std::process::id(), - line!() - )); - fs::create_dir_all(root.join("specs/functions")).unwrap(); - fs::create_dir_all(root.join("specs/schema")).unwrap(); - fs::copy( - Path::new(env!("CARGO_MANIFEST_DIR")).join("../specs/schema/ptf-spec.schema.json"), - root.join("specs/schema/ptf-spec.schema.json"), - ) - .unwrap(); - fs::copy( - Path::new(env!("CARGO_MANIFEST_DIR")).join("../specs/quantities.yaml"), - root.join("specs/quantities.yaml"), - ) - .unwrap(); - fs::copy( - Path::new(env!("CARGO_MANIFEST_DIR")).join("../specs/units.yaml"), - root.join("specs/units.yaml"), - ) - .unwrap(); - root - } + use crate::test_support::fixture_root; fn specification(slug: &str, implementation: &str, generation: &str) -> String { let verification = if implementation.is_empty() @@ -689,7 +662,7 @@ functions: ); fs::write( root.join("specs/functions/unnamed_record.yaml"), - specification, + &specification, ) .unwrap(); @@ -697,6 +670,15 @@ functions: .expect_err("unnamed inline record must fail") .to_string(); assert!(error.contains("functions.0.outputs"), "{error}"); + fs::write( + root.join("specs/functions/unnamed_record.yaml"), + specification.replace( + " type: record\n", + " type: record\n name: TestResult\n", + ), + ) + .unwrap(); + load(&root).expect("correcting only the record name must make the fixture valid"); fs::remove_dir_all(root).unwrap(); } @@ -714,7 +696,7 @@ functions: ); fs::write( root.join("specs/functions/non_pascal_case_record.yaml"), - specification, + &specification, ) .unwrap(); @@ -722,6 +704,12 @@ functions: .expect_err("non-PascalCase record name must fail") .to_string(); assert!(error.contains("functions.0.outputs"), "{error}"); + fs::write( + root.join("specs/functions/non_pascal_case_record.yaml"), + specification.replace("name: result_record", "name: TestResult"), + ) + .unwrap(); + load(&root).expect("correcting only the record name must make the fixture valid"); fs::remove_dir_all(root).unwrap(); } @@ -743,7 +731,7 @@ functions: ); fs::write( root.join("specs/functions/unnamed_record_definition.yaml"), - specification, + &specification, ) .unwrap(); @@ -751,6 +739,15 @@ functions: .expect_err("unnamed record definition must fail") .to_string(); assert!(error.contains("$defs.reusable_result"), "{error}"); + fs::write( + root.join("specs/functions/unnamed_record_definition.yaml"), + specification.replace( + " type: record\n", + " type: record\n name: TestResult\n", + ), + ) + .unwrap(); + load(&root).expect("correcting only the record name must make the fixture valid"); fs::remove_dir_all(root).unwrap(); } @@ -962,33 +959,38 @@ functions: #[test] fn rejects_invalid_verification_case_provenance() { - for (slug, case) in [ + for (slug, valid, invalid) in [ ( "unknown_kind", - " expected: {value: 1.0}\n kind: external\n rationale: Invalid kind.\n", + "kind: calculated\n rationale: Test input.", + "kind: external\n rationale: Test input.", ), ( "published_without_location", - " expected: {value: 1.0}\n kind: published\n", + "kind: published\n source_location: Table 1", + "kind: published", ), ( "calculated_without_rationale", - " expected: {value: 1.0}\n kind: calculated\n", + "kind: calculated\n rationale: Test input.", + "kind: calculated", ), ] { let root = fixture_root(slug); - fs::write( - root.join(format!("specs/functions/{slug}.yaml")), - verification_specification(slug, case), - ) - .unwrap(); + let path = root.join(format!("specs/functions/{slug}.yaml")); + let text = verification_specification( + slug, + &format!(" expected: {{value: 1.0}}\n {valid}\n"), + ); + fs::write(&path, &text).unwrap(); + load(&root) + .expect("provenance control must be valid before removing its required metadata"); + fs::write(&path, text.replace(valid, invalid)).unwrap(); let error = load(&root) - .expect_err("invalid verification case must fail") + .expect_err("invalid provenance must fail") .to_string(); assert!( - error.contains("Additional properties are not allowed") - || error.contains("required property") - || error.contains("not valid under any of the schemas"), + error.contains("functions.0.verification_cases.0"), "{error}" ); fs::remove_dir_all(root).unwrap(); @@ -997,33 +999,21 @@ functions: #[test] fn rejects_missing_and_unknown_expected_outputs() { - for (slug, expected) in [ - ("missing_expected", ""), - ("unknown_expected", " expected: {other: 1.0}\n"), + for (missing, diagnostic) in [ + (true, "missing expected output `first`"), + (false, "references unknown output `other`"), ] { - let root = fixture_root(slug); - fs::write( - root.join(format!("specs/functions/{slug}.yaml")), - verification_specification( - slug, - &format!( - "{expected} kind: calculated\n rationale: Interior representative input.\n" - ), - ), - ) - .unwrap(); - let result = load(&root).and_then(crate::compile::functions); - let error = result + let mut entries = crate::test_support::entries(); + let expected = &mut entries[0].spec.functions[1].verification_cases[0].expected; + if missing { + expected.remove("first"); + } else { + expected.insert("other".into(), 3.0); + } + let error = crate::compile::functions(entries) .expect_err("invalid expected outputs must fail") .to_string(); - assert!( - error.contains("missing expected output") - || error.contains("references unknown output") - || error.contains("required property") - || error.contains("missing field `expected`"), - "{error}" - ); - fs::remove_dir_all(root).unwrap(); + assert!(error.contains(diagnostic), "{error}"); } } @@ -1118,7 +1108,7 @@ functions: } #[test] - fn keeps_overrides_function_local_for_shared_outputs() { + fn keeps_overrides_function_and_field_local_for_shared_outputs() { let root = fixture_root("shared-output-overrides"); let text = r#"source: summary: Test source. @@ -1172,6 +1162,15 @@ functions: .expect("shared output definitions must compile"); assert_eq!(compiled[0].output_tolerances[0].absolute, 0.001); assert_eq!(compiled[1].output_tolerances[0].absolute, 0.005); + for function in &compiled { + let second = &function.output_tolerances[1]; + assert_eq!(second.absolute, 0.001); + assert_eq!(second.relative, 0.0); + assert!(matches!( + second.source, + crate::model::ToleranceSource::Registry + )); + } fs::remove_dir_all(root).unwrap(); } @@ -1480,7 +1479,7 @@ functions: let path = root.join("specs/units.yaml"); let registry = fs::read_to_string(&path) .unwrap() - .replace("aliases: []", "aliases: [\"%\"]"); + .replace("\"cm^3/cm^3\",", "\"%\", \"cm^3/cm^3\","); fs::write(path, registry).unwrap(); assert!( crate::load_validated_specifications(&root) diff --git a/codegen/src/targets/mod.rs b/codegen/src/targets/mod.rs index 211ec0c..160a6db 100644 --- a/codegen/src/targets/mod.rs +++ b/codegen/src/targets/mod.rs @@ -113,7 +113,6 @@ pub(crate) fn run(root: &Path, entries: Vec) -> Result<()> { Output::new(&output::PYTHON_TEST, python.tests), Output::new(&output::NATIVE_C, native.c_headers), Output::new(&output::NATIVE_CPP_MODULE, native.cpp_modules), - Output::new(&output::NATIVE_TEST_SUPPORT, native.test_support), Output::new(&output::NATIVE_C_TEST, native.c_tests), Output::new(&output::NATIVE_CPP_TEST, native.cpp_tests), ], diff --git a/codegen/src/targets/native.rs b/codegen/src/targets/native.rs index f5ed920..b08b401 100644 --- a/codegen/src/targets/native.rs +++ b/codegen/src/targets/native.rs @@ -26,15 +26,13 @@ pub(super) struct OutputFiles { pub(super) cpp_modules: Vec, pub(super) c_tests: Vec, pub(super) cpp_tests: Vec, - pub(super) test_support: Vec, } pub(super) fn render(functions: &[CompiledFunction]) -> Result { let mut c_headers = Vec::new(); let mut cpp_modules = Vec::new(); - let mut c_tests = vec![file("comparator.c", comparator_test(false))]; - let mut cpp_tests = vec![file("comparator.cpp", comparator_test(true))]; - let test_support = vec![file("close_enough.h", close_enough_header())]; + let mut c_tests = Vec::new(); + let mut cpp_tests = Vec::new(); let mut umbrella = Writer::new(); umbrella.write(format_args!( "{HEADER}\n\n#ifndef PTFKIT_PTFKIT_H\n#define PTFKIT_PTFKIT_H\n\n" @@ -66,7 +64,6 @@ pub(super) fn render(functions: &[CompiledFunction]) -> Result { cpp_modules, c_tests, cpp_tests, - test_support, }) } @@ -895,107 +892,6 @@ fn write_tolerance_arguments(writer: &mut Writer, tolerance: &crate::model::Comp )); } -fn close_enough_header() -> String { - let guard = c::test_float_literal(crate::compile::FLOATING_POINT_GUARD); - format!( - r#"{HEADER} - -#ifndef PTFKIT_TEST_CLOSE_ENOUGH_H -#define PTFKIT_TEST_CLOSE_ENOUGH_H - -#ifdef __cplusplus -#include -#include -#include -#include -#include - -inline double resolved_tolerance(double expected, double absolute, double relative) {{ - return std::max({{absolute, relative * std::abs(expected), {guard}}}); -}} - -inline bool is_close(double actual, double expected, double absolute, double relative) {{ - return std::abs(actual - expected) <= resolved_tolerance(expected, absolute, relative); -}} - -inline void _close_enough_impl(const char *file, int line, double actual, double expected, - double absolute, double relative, const char *quantity, - const char *unit, const char *source, const char *case_id) {{ - const double tolerance = resolved_tolerance(expected, absolute, relative); - const double difference = std::abs(actual - expected); - if (!is_close(actual, expected, absolute, relative)) {{ - std::println(stderr, "assertion failed: {{}}:{{}}: case={{}}, actual={{}}, expected={{}}, difference={{}}, tolerance={{}}, quantity={{}}, unit={{}}, source={{}}", - file, line, case_id, actual, expected, difference, tolerance, quantity, unit, source); - std::exit(EXIT_FAILURE); - }} -}} - -#else -#include -#include -#include - -static inline double resolved_tolerance(double expected, double absolute, double relative) {{ - return fmax(fmax(absolute, relative * fabs(expected)), {guard}); -}} - -static inline int is_close(double actual, double expected, double absolute, double relative) {{ - return fabs(actual - expected) <= resolved_tolerance(expected, absolute, relative); -}} - -static inline void _close_enough_impl(const char *file, int line, double actual, double expected, - double absolute, double relative, const char *quantity, - const char *unit, const char *source, const char *case_id) {{ - const double tolerance = resolved_tolerance(expected, absolute, relative); - const double difference = fabs(actual - expected); - if (!is_close(actual, expected, absolute, relative)) {{ - fprintf(stderr, "assertion failed: %s:%d: case=%s, actual=%.17g, expected=%.17g, difference=%.17g, tolerance=%.17g, quantity=%s, unit=%s, source=%s\n", - file, line, case_id, actual, expected, difference, tolerance, quantity, unit, source); - exit(EXIT_FAILURE); - }} -}} -#endif - -#define assert_close(actual, expected, absolute, relative, quantity, unit, source, case_id) \ - do {{ \ - _close_enough_impl(__FILE__, __LINE__, (actual), (expected), (absolute), (relative), \ - (quantity), (unit), (source), (case_id)); \ - }} while (0) - -#endif -"# - ) -} - -fn comparator_test(cpp: bool) -> String { - let main = if cpp { "int main()" } else { "int main(void)" }; - format!( - r#"{HEADER} - -#include "support/close_enough.h" - -{main} {{ - const double absolute = 0.001; - const double relative = 0.01; - const double expected_values[] = {{0.0, 2.0, -2.0}}; - for (int index = 0; index < 3; ++index) {{ - const double expected = expected_values[index]; - const double tolerance = resolved_tolerance(expected, absolute, relative); - _close_enough_impl(__FILE__, __LINE__, expected + tolerance * 0.5, expected, absolute, - relative, "test_quantity", "1", "registry", "comparator"); - if (is_close(expected + tolerance * 2.0, expected, absolute, relative)) {{ - return EXIT_FAILURE; - }} - }} - if (is_close(NAN, 1.0, absolute, relative)) {{ - return EXIT_FAILURE; - }} - return EXIT_SUCCESS; -}} -"# - ) -} - fn render_literals( writer: &mut Writer, values: &[CompiledInput], diff --git a/codegen/src/targets/python/extension.rs b/codegen/src/targets/python/extension.rs index e804f90..73a16f7 100644 --- a/codegen/src/targets/python/extension.rs +++ b/codegen/src/targets/python/extension.rs @@ -232,10 +232,26 @@ mod tests { use super::*; #[test] - fn entry_source_initializes_the_private_module() { - let rendered = render(&[]).unwrap(); - let entry = &rendered.last().unwrap().contents; + fn registers_each_source_once_and_preserves_ufunc_arities() { + let rendered = render(&crate::test_support::functions()).unwrap(); + let entry = &rendered + .iter() + .find(|file| file.path.ends_with("ptfkit.c")) + .unwrap() + .contents; assert!(entry.contains("PyInit__ptfkit")); - assert!(!entry.contains("#include \"ufunc.h\"")); + for slug in ["example2", "example10"] { + assert_eq!(entry.matches(&format!("#include \"{slug}.c\"")).count(), 1); + assert_eq!(entry.matches(&format!("if (ptfkit_register_{slug}(module) < 0) {{ Py_DECREF(module); return NULL; }}")).count(), 1); + let source = &rendered + .iter() + .find(|file| file.path.ends_with(format!("{slug}.c"))) + .unwrap() + .contents; + for (suffix, nin, nout) in [("scalar", 2, 1), ("record", 1, 2)] { + let name = format!("calc_ptf_{slug}_{suffix}"); + assert_eq!(source.matches(&format!("ptfkit_add_ufunc(module, \"{name}\", {name}_types, {nin}, {nout}, &{name}_spec)")).count(), 1); + } + } } } diff --git a/codegen/src/targets/python/stub.rs b/codegen/src/targets/python/stub.rs index ee37283..5ed89a3 100644 --- a/codegen/src/targets/python/stub.rs +++ b/codegen/src/targets/python/stub.rs @@ -24,10 +24,12 @@ mod tests { use super::*; #[test] - fn declares_native_ufuncs_in_source_order() { + fn declares_every_native_ufunc_in_sorted_order() { + let mut functions = crate::test_support::functions(); + functions.reverse(); assert_eq!( - render(&[]), - "# @generated by ptfkit-codegen; DO NOT EDIT.\n\nfrom numpy import ufunc\n\n" + render(&functions), + "# @generated by ptfkit-codegen; DO NOT EDIT.\n\nfrom numpy import ufunc\n\ncalc_ptf_example10_record: ufunc\ncalc_ptf_example10_scalar: ufunc\ncalc_ptf_example2_record: ufunc\ncalc_ptf_example2_scalar: ufunc\n" ); } } diff --git a/codegen/src/targets/python/test.rs b/codegen/src/targets/python/test.rs index 432180f..83baa18 100644 --- a/codegen/src/targets/python/test.rs +++ b/codegen/src/targets/python/test.rs @@ -16,136 +16,19 @@ pub(super) fn render(functions: &[CompiledFunction]) -> Vec { .push(function); } - let mut files = vec![ - GeneratedFile::new("tests/_helpers.py".into(), helper_source()), - GeneratedFile::new("tests/test_comparator.py".into(), comparator_test_source()), - ]; - files.extend( - modules - .into_iter() - .filter_map(|(slug, functions)| { - (functions[0].entry.spec.generation.public_python == PythonGeneration::Generated) - .then(|| { - GeneratedFile::new( - format!("tests/test_{slug}.py").into(), - module_source(&slug, &functions), - ) - }) - }) - .collect::>(), - ); - files -} - -fn comparator_test_source() -> String { - format!( - r#"{WRAPPER_HEADER} -import pytest - -from _helpers import assert_close, resolved_tolerance - - -@pytest.mark.parametrize('expected', [0.0, 2.0, -2.0]) -def test_accepts_below_and_rejects_above_tolerance(expected: float): - absolute = 0.001 - relative = 0.01 - tolerance = resolved_tolerance(expected, absolute, relative) - metadata = {{ - 'absolute': absolute, - 'relative': relative, - 'quantity': 'test_quantity', - 'unit': '1', - 'source': 'registry', - }} - assert_close(expected + tolerance * 0.5, expected, **metadata) - with pytest.raises(AssertionError): - assert_close(expected + tolerance * 2.0, expected, **metadata) -"# - ) -} - -fn helper_source() -> String { - format!( - r#"{WRAPPER_HEADER} -from __future__ import annotations - -from enum import Enum -from typing import TYPE_CHECKING, NamedTuple, TypeVar, overload - -import numpy as np - -from ptfkit.enums import EnumArray - - -if TYPE_CHECKING: - from collections.abc import Mapping, Sequence - from typing import Any - - R = TypeVar('R', bound=NamedTuple) - Expected = dict[str, float] - VerificationCase = tuple[Mapping[str, Any], Expected] - VectorCasePart = tuple[dict[str, Any], Expected] - VectorCaseScalar = tuple[*VectorCasePart, np.ndarray] - VectorCaseTuple = tuple[*VectorCasePart, R] - - -@overload -def prepare_vector_case(cases: Sequence[VerificationCase]) -> VectorCaseScalar: ... - - -@overload -def prepare_vector_case( - cases: Sequence[VerificationCase], result_type: type[R] -) -> VectorCaseTuple: ... - - -def prepare_vector_case( - cases: Sequence[VerificationCase], - result_cls: type[R] | None = None, -) -> VectorCaseScalar | VectorCaseTuple: - inputs, expected = cases[0] - vector_inputs = {{ - name: ( - EnumArray._from_members(type(value), [value]) # noqa: SLF001 - if isinstance(value, Enum) - else np.array([value]) - ) - for name, value in inputs.items() - }} - out: np.ndarray | R - if result_cls is None: - out = np.empty(1, dtype=float) - else: - field_count = len(result_cls._fields) - out = result_cls(*(np.empty(1, dtype=float) for _ in range(field_count))) - return vector_inputs, expected, out - - -def assert_close( - actual: object, - expected: float, - *, - absolute: float, - relative: float, - quantity: str, - unit: str, - source: str, -) -> None: - actual_float = float(actual) # ty: ignore[invalid-argument-type] - tolerance = resolved_tolerance(expected, absolute, relative) - difference = abs(actual_float - expected) - assert difference <= tolerance, ( - f'actual={{actual_float}}, expected={{expected}}, difference={{difference}}, ' - f'tolerance={{tolerance}}, quantity={{quantity!r}}, unit={{unit!r}}, source={{source}}' - ) - - -def resolved_tolerance(expected: float, absolute: float, relative: float) -> float: - scientific_tolerance = max(absolute, relative * abs(expected)) - return max(scientific_tolerance, {:?}) -"#, - crate::compile::FLOATING_POINT_GUARD - ) + modules + .into_iter() + .filter_map(|(slug, functions)| { + (functions[0].entry.spec.generation.public_python == PythonGeneration::Generated).then( + || { + GeneratedFile::new( + format!("tests/test_{slug}.py").into(), + module_source(&slug, &functions), + ) + }, + ) + }) + .collect() } fn module_source(slug: &str, functions: &[&CompiledFunction]) -> String { diff --git a/codegen/src/targets/reference/c.rs b/codegen/src/targets/reference/c.rs index 242edb2..d550dcb 100644 --- a/codegen/src/targets/reference/c.rs +++ b/codegen/src/targets/reference/c.rs @@ -365,12 +365,9 @@ mod tests { use super::*; fn rendered_files() -> Vec { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("codegen directory has a repository parent"); - let entries = crate::specs::load(root).expect("repository specifications load"); - let compiled = - crate::compile::functions(entries).expect("repository specifications compile"); + let mut entries = crate::test_support::entries(); + entries.reverse(); + let compiled = crate::compile::functions(entries).expect("test specifications compile"); render(&compiled).expect("C documentation renders") } @@ -386,42 +383,37 @@ mod tests { fn documents_headers_functions_and_record_fields_from_compiled_sources() { let files = rendered_files(); let index = contents(&files, "index.md"); - let rawls = contents(&files, "headers/rawls1982.md"); + let page = contents(&files, "headers/example2.md"); let function_index = contents(&files, "functions.md"); assert!(index.starts_with( "---\n# @generated by ptfkit-codegen; DO NOT EDIT.\n\ntitle: C API reference\n---\n" )); - assert!(rawls.starts_with("---\ntitle: \"rawls1982.h\"\n---\n")); + assert!(page.starts_with("---\ntitle: \"example2.h\"\n---\n")); assert!(index.contains("[``](headers/ptfkit.md)")); - assert!(rawls.contains("## `rawls1982_ptf_result`")); - assert!(rawls.contains("| `theta_4` | Volumetric water content at -4 kPa. (cm^3/cm^3) |")); - assert!(rawls.contains( - "static inline double calc_ptf_rawls1982_theta_1500(double clay, double organic_matter)" - )); - assert!(rawls.contains("static inline rawls1982_ptf_result calc_ptf_rawls1982_full_wrc")); - assert!(rawls.contains("{#function-calc_ptf_rawls1982_theta_1500}")); - assert!( - function_index.contains("headers/rawls1982.md#function-calc_ptf_rawls1982_theta_1500") - ); - assert!(rawls.contains("[PTF catalog page](../../../ptf-catalog/sources/rawls1982.md)")); + assert!(page.contains("## `example_result`")); + assert!(page.contains("| `first` | First result. (1) |")); + assert!(page.contains("static inline double calc_ptf_example2_scalar(double x, double y)")); + assert!(page.contains("static inline example_result calc_ptf_example2_record")); + assert!(page.contains("{#function-calc_ptf_example2_scalar}")); + assert!(function_index.contains("headers/example2.md#function-calc_ptf_example2_scalar")); + assert!(page.contains("[PTF catalog page](../../../ptf-catalog/sources/example2.md)")); } #[test] - fn umbrella_lists_each_source_header_once_in_natural_order() { + fn umbrella_lists_each_source_header_once_in_slug_order() { let files = rendered_files(); let umbrella = contents(&files, "headers/ptfkit.md"); - let index = contents(&files, "index.md"); - let source_headers = files - .iter() - .filter(|file| { - file.path.starts_with("headers") && file.path != Path::new("headers/ptfkit.md") - }) - .count(); - - assert_eq!(umbrella.matches("- [`>(), + [ + "- [``](example10.md) — Synthetic rendering example10.", + "- [``](example2.md) — Synthetic rendering example2." + ] + ); } #[test] @@ -433,8 +425,8 @@ mod tests { #[test] fn function_anchors_are_stable() { assert_eq!( - function_anchor("calc_ptf_rawls1982"), - "function-calc_ptf_rawls1982" + function_anchor("calc_ptf_example2"), + "function-calc_ptf_example2" ); } diff --git a/codegen/src/targets/reference/cpp.rs b/codegen/src/targets/reference/cpp.rs index c329809..40a2676 100644 --- a/codegen/src/targets/reference/cpp.rs +++ b/codegen/src/targets/reference/cpp.rs @@ -332,12 +332,9 @@ mod tests { use super::*; fn rendered_files() -> Vec { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("codegen directory has a repository parent"); - let entries = crate::specs::load(root).expect("repository specifications load"); - let compiled = - crate::compile::functions(entries).expect("repository specifications compile"); + let mut entries = crate::test_support::entries(); + entries.reverse(); + let compiled = crate::compile::functions(entries).expect("test specifications compile"); render(&compiled).expect("C++ documentation renders") } @@ -353,50 +350,42 @@ mod tests { fn documents_modules_functions_and_record_fields_from_compiled_sources() { let files = rendered_files(); let index = contents(&files, "index.md"); - let rawls = contents(&files, "modules/rawls1982.md"); + let page = contents(&files, "modules/example2.md"); let function_index = contents(&files, "functions.md"); assert!(index.starts_with( "---\n# @generated by ptfkit-codegen; DO NOT EDIT.\n\ntitle: C++ API reference\n---\n" )); - assert!(rawls.starts_with( - "---\n# @generated by ptfkit-codegen; DO NOT EDIT.\n\ntitle: C++ module ptfkit.rawls1982\nnav-title: ptfkit.rawls1982\n---\n" + assert!(page.starts_with( + "---\n# @generated by ptfkit-codegen; DO NOT EDIT.\n\ntitle: C++ module ptfkit.example2\nnav-title: ptfkit.example2\n---\n" )); assert!(index.contains("[`ptfkit`](modules/ptfkit.md)")); - assert!(rawls.contains("import ptfkit.rawls1982;")); - assert!(rawls.contains("**Exported namespace:** `ptfkit::rawls1982`")); - assert!(rawls.contains("## `Rawls1982PTFResult`")); - assert!(rawls.contains("| `theta_4` | Volumetric water content at -4 kPa. (cm^3/cm^3) |")); - assert!(rawls.contains("[[nodiscard]]\ninline double calc_ptf_rawls1982_theta_1500")); - assert!(rawls.contains("inline Rawls1982PTFResult calc_ptf_rawls1982_full_wrc")); - assert!(rawls.contains("{#function-calc_ptf_rawls1982_theta_1500}")); + assert!(page.contains("import ptfkit.example2;")); + assert!(page.contains("**Exported namespace:** `ptfkit::example2`")); + assert!(page.contains("## `ExampleResult`")); + assert!(page.contains("| `first` | First result. (1) |")); + assert!(page.contains("[[nodiscard]]\ninline double calc_ptf_example2_scalar")); + assert!(page.contains("inline ExampleResult calc_ptf_example2_record")); + assert!(page.contains("{#function-calc_ptf_example2_scalar}")); assert!(function_index.contains( - "[`ptfkit::rawls1982::calc_ptf_rawls1982_theta_1500`](modules/rawls1982.md#function-calc_ptf_rawls1982_theta_1500)" + "[`ptfkit::example2::calc_ptf_example2_scalar`](modules/example2.md#function-calc_ptf_example2_scalar)" )); - assert!(rawls.contains("[PTF catalog page](../../../ptf-catalog/sources/rawls1982.md)")); + assert!(page.contains("[PTF catalog page](../../../ptf-catalog/sources/example2.md)")); } #[test] - fn umbrella_lists_each_source_module_once_in_natural_order() { + fn umbrella_lists_each_source_module_once_in_slug_order() { let files = rendered_files(); let umbrella = contents(&files, "modules/ptfkit.md"); - let index = contents(&files, "index.md"); - let source_modules = files - .iter() - .filter(|file| { - file.path.starts_with("modules") && file.path != Path::new("modules/ptfkit.md") - }) - .count(); - - assert_eq!(umbrella.matches("- [`ptfkit.").count(), source_modules); - assert!(!index.contains("[`ptfkit`]( ")); - assert!( - index.find("[`ptfkit`](modules/ptfkit.md)").unwrap() - < index.find("ptfkit.ahuja1984").unwrap() - ); - assert!( - umbrella.find("ptfkit.ahuja1984").unwrap() - < umbrella.find("ptfkit.aimrun2009").unwrap() + assert_eq!( + umbrella + .lines() + .filter(|line| line.starts_with("- [`ptfkit.")) + .collect::>(), + [ + "- [`ptfkit.example10`](example10.md) — Synthetic rendering example10.", + "- [`ptfkit.example2`](example2.md) — Synthetic rendering example2." + ] ); } @@ -409,8 +398,8 @@ mod tests { #[test] fn function_anchors_are_stable() { assert_eq!( - function_anchor("calc_ptf_rawls1982"), - "function-calc_ptf_rawls1982" + function_anchor("calc_ptf_example2"), + "function-calc_ptf_example2" ); } diff --git a/codegen/src/targets/reference/python.rs b/codegen/src/targets/reference/python.rs index 7119ff2..8f25cd7 100644 --- a/codegen/src/targets/reference/python.rs +++ b/codegen/src/targets/reference/python.rs @@ -76,10 +76,7 @@ mod tests { use super::*; fn rendered_files() -> Vec { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("codegen directory has a repository parent"); - let entries = crate::specs::load(root).expect("repository specifications load"); + let entries = crate::test_support::entries(); render(&entries) } @@ -93,10 +90,7 @@ mod tests { #[test] fn renders_one_page_for_each_source_module() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("codegen directory has a repository parent"); - let entries = crate::specs::load(root).expect("repository specifications load"); + let entries = crate::test_support::entries(); let files = render(&entries); let index = contents(&files, "index.md"); @@ -119,14 +113,15 @@ mod tests { #[test] fn includes_intentional_manual_public_modules() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("codegen directory has a repository parent"); - let mut entries = crate::specs::load(root).expect("repository specifications load"); - entries[0].spec.generation.public_python = PythonGeneration::Manual; + let mut entries = crate::test_support::entries(); + let manual = entries + .iter_mut() + .find(|entry| entry.slug == "example2") + .unwrap(); + manual.spec.generation.public_python = PythonGeneration::Manual; let files = render(&entries); - assert!(contents(&files, "ahuja1984.md").contains("::: ptfkit.ahuja1984")); + assert!(contents(&files, "example2.md").contains("::: ptfkit.example2")); } #[test] diff --git a/codegen/src/targets/rust.rs b/codegen/src/targets/rust.rs index 1789cca..46f5a9b 100644 --- a/codegen/src/targets/rust.rs +++ b/codegen/src/targets/rust.rs @@ -57,10 +57,6 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result>(); let lookup_conversions = lookup_conversion_tokens(&functions); - let verification_helpers = functions - .iter() - .any(|function| !function.verification_cases.is_empty()) - .then(verification_test_helpers); let mut defined_result_classes = BTreeSet::new(); let definitions = functions .into_iter() @@ -76,7 +72,7 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result>(); - quote! { #[cfg(test)] mod #module { use super::*; #(#tests)* } } -} - -fn verification_test_helpers() -> TokenStream { - let floating_point_guard = Literal::f64_suffixed(crate::compile::FLOATING_POINT_GUARD); - quote! { - #[cfg(test)] - fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute.max(relative * expected.abs()).max(#floating_point_guard) - } - - #[cfg(test)] - fn assert_close(actual: f64, expected: f64, absolute: f64, relative: f64, quantity: &str, unit: &str, source: &str) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!(difference <= tolerance, "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}"); - } - - #[cfg(test)] - mod comparator_tests { - use super::*; - - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } - } - } + quote! { #[cfg(test)] mod #module { use super::*; use crate::test_support::assert_close; #(#tests)* } } } fn render_tokens(module_docs: TokenStream, tokens: TokenStream) -> String { @@ -801,7 +766,7 @@ fn render_tokens(module_docs: TokenStream, tokens: TokenStream) -> String { mod tests { use super::*; use crate::documentation::Returns; - use crate::model::{OutputField, Parameter}; + use crate::model::OutputField; use crate::semantic::{BinaryOp, Expr, MathFunction, Reference}; fn number(value: f64) -> Expr { @@ -811,55 +776,67 @@ mod tests { }) } #[test] - fn renders_every_operator_and_function() { + fn renders_each_operator_and_math_function_without_substitution() { let inputs = vec![format_ident!("x"), format_ident!("y")]; - let vars = Vec::new(); - let binary = [ - BinaryOp::Add, - BinaryOp::Subtract, - BinaryOp::Multiply, - BinaryOp::Divide, - BinaryOp::Power, - ]; - for op in binary { - assert!( - syn::parse2::( - expression_tokens( - &Expr::Binary { - op, - left: Box::new(Expr::Reference(Reference::Input(0))), - right: Box::new(number(2.0)) - }, - &inputs, - &vars - ) + let input = |index| Expr::Reference(Reference::Input(index)); + for (op, expected) in [ + (BinaryOp::Add, "x + y"), + (BinaryOp::Subtract, "x - y"), + (BinaryOp::Multiply, "x * y"), + (BinaryOp::Divide, "x / y"), + (BinaryOp::Power, "x . powf (y)"), + ] { + let expression = Expr::Binary { + op, + left: Box::new(input(0)), + right: Box::new(input(1)), + }; + assert_eq!( + expression_tokens(&expression, &inputs, &[]) .unwrap() .tokens - ) - .is_ok() + .to_string(), + expected ); } - for function in [ - MathFunction::Sqrt, - MathFunction::Exp, - MathFunction::Ln, - MathFunction::Log10, - MathFunction::Abs, - MathFunction::Min, - MathFunction::Max, + for (function, expected) in [ + (MathFunction::Sqrt, "x . sqrt ()"), + (MathFunction::Exp, "x . exp ()"), + (MathFunction::Ln, "x . ln ()"), + (MathFunction::Log10, "x . log10 ()"), + (MathFunction::Abs, "x . abs ()"), + (MathFunction::Min, "x . min (y)"), + (MathFunction::Max, "x . max (y)"), ] { let args = if matches!(function, MathFunction::Min | MathFunction::Max) { - vec![number(1.0), number(2.0)] + vec![input(0), input(1)] } else { - vec![number(1.0)] + vec![input(0)] + }; + assert_eq!( + expression_tokens(&Expr::Call { function, args }, &inputs, &[]) + .unwrap() + .tokens + .to_string(), + expected + ); + } + for (exponent, expected) in [ + (2.0, "x . powi (2)"), + (3.0, "x . powi (3)"), + (4.0, "x . powi (4)"), + ] { + let expression = Expr::Binary { + op: BinaryOp::Power, + left: Box::new(input(0)), + right: Box::new(number(exponent)), }; - assert!( - syn::parse2::( - expression_tokens(&Expr::Call { function, args }, &inputs, &vars) - .unwrap() - .tokens - ) - .is_ok() + assert_eq!( + expression_tokens(&expression, &inputs, &[]) + .unwrap() + .tokens + .to_string(), + expected ); } } @@ -944,7 +921,6 @@ mod tests { .unwrap() .tokens; - assert!(syn::parse2::(rendered.clone()).is_ok()); assert_eq!(rendered.to_string(), "parameters . b"); } @@ -953,28 +929,30 @@ mod tests { let module_docs = inner_doc_tokens(["Source summary.".into(), "# Reference".into()]); let function_docs = doc_tokens(["Function summary.".into(), "# Arguments".into()]); let generated = quote!(#module_docs #function_docs pub fn calculate() {}); - assert!(syn::parse_file(&generated.to_string()).is_ok()); - assert!(generated.to_string().contains("r\"Source summary.")); - assert!(generated.to_string().contains("r\"Function summary.")); - } - - #[test] - fn record_results_document_fields_only_once() { - let parameter = Parameter { - name: "theta_s".into(), - unit: "cm^3/cm^3".into(), - domain: None, - description: "Saturated water content.".into(), + let file = syn::parse_file(&generated.to_string()).unwrap(); + assert_eq!(file.attrs.len(), 1); + assert!(matches!(file.attrs[0].style, syn::AttrStyle::Inner(_))); + let syn::Item::Fn(function) = &file.items[0] else { + panic!("expected function"); }; - - assert_eq!( - docs::parameter_documentation(¶meter), - "theta_s: Saturated water content. (cm^3/cm^3)" - ); - assert_eq!( - docs::parameter_details(¶meter), - "Saturated water content. (cm^3/cm^3)" - ); + assert_eq!(function.attrs.len(), 1); + assert!(matches!(function.attrs[0].style, syn::AttrStyle::Outer)); + for (attribute, expected) in [ + (&file.attrs[0], "Source summary.\n# Reference"), + (&function.attrs[0], "Function summary.\n# Arguments"), + ] { + assert!(attribute.path().is_ident("doc")); + let syn::Meta::NameValue(meta) = &attribute.meta else { + panic!("expected doc value"); + }; + let syn::Expr::Lit(value) = &meta.value else { + panic!("expected literal"); + }; + let syn::Lit::Str(value) = &value.lit else { + panic!("expected doc string"); + }; + assert_eq!(value.value(), expected); + } } #[test] diff --git a/codegen/src/test_support.rs b/codegen/src/test_support.rs new file mode 100644 index 0000000..d1bf3f5 --- /dev/null +++ b/codegen/src/test_support.rs @@ -0,0 +1,53 @@ +//! Small, controlled inputs for codegen tests; independent of the publication corpus. + +use std::{ + fs, + path::{Path, PathBuf}, +}; + +static NEXT_ROOT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + +pub(crate) fn fixture_root(label: &str) -> PathBuf { + let root = std::env::temp_dir().join(format!( + "ptfkit-codegen-specs-{label}-{}-{}", + std::process::id(), + NEXT_ROOT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + )); + fs::create_dir_all(root.join("specs/functions")).unwrap(); + fs::create_dir_all(root.join("specs/schema")).unwrap(); + fs::copy( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../specs/schema/ptf-spec.schema.json"), + root.join("specs/schema/ptf-spec.schema.json"), + ) + .unwrap(); + fs::write( + root.join("specs/quantities.yaml"), + include_str!("fixtures/quantities.yaml"), + ) + .unwrap(); + fs::write( + root.join("specs/units.yaml"), + include_str!("fixtures/units.yaml"), + ) + .unwrap(); + root +} + +pub(crate) fn entries() -> Vec { + let root = fixture_root("renderers"); + for slug in ["example10", "example2"] { + fs::write( + root.join(format!("specs/functions/{slug}.yaml")), + include_str!("fixtures/source.yaml").replace("example", slug), + ) + .unwrap(); + } + let entries = + crate::load_validated_specifications(&root).expect("test specifications validate"); + fs::remove_dir_all(root).unwrap(); + entries +} + +pub(crate) fn functions() -> Vec { + crate::compile::functions(entries()).expect("test specifications compile") +} diff --git a/docs/src/contributing/development.md b/docs/src/contributing/development.md index a9bc991..42c884c 100644 --- a/docs/src/contributing/development.md +++ b/docs/src/contributing/development.md @@ -113,6 +113,24 @@ is generated from the same validated specification. Put target-independent summaries, parameter descriptions, return descriptions, notes, and warnings in the YAML rather than adding them to generated files. +### Test infrastructure + +Codegen generates PTF verification tests from specification cases. Shared test +helpers and their comparator tests are maintained by hand in each target: + +- Rust: `targets/ptfkit-rs/src/test_support.rs`, included only under `cfg(test)`. +- Python: `targets/ptfkit-py/tests/_helpers.py` and `tests/test_comparator.py`. +- C and C++: `targets/ptfkit-native/tests/support/close_enough.h` and + `tests/support/comparator.c` (compiled as C11 and C++23). + +Native comparator rejection cases use names such as `reject relative` and +`reject nan`. `tests/support/check_rejection.cmake` requires exit code 1 and an +assertion diagnostic naming the requested case; unrelated failures do not pass. + +These files have no generated-file marker and are not codegen outputs. Edit them +directly when changing test infrastructure, then run the target verification +suites. Keep the shared comparison policy consistent across targets. + ### Adding a PTF The assisted workflow uses the skills in `.agents/skills/`: diff --git a/docs/src/contributing/verification.md b/docs/src/contributing/verification.md index 050ae98..e57eded 100644 --- a/docs/src/contributing/verification.md +++ b/docs/src/contributing/verification.md @@ -101,7 +101,7 @@ abs(actual - expected) <= max(scientific_tolerance, floating_point_guard) ``` `absolute` is mandatory and expressed in the output unit; `relative` is -optional and dimensionless. The centrally generated floating-point guard only +optional and dimensionless. The floating-point guard in each target's handwritten test helper only absorbs insignificant implementation and math-library variation and is much smaller than normal scientific tolerances. Failure messages report actual and expected values, their difference, the resolved tolerance, quantity, unit, and diff --git a/targets/ptfkit-native/tasks.toml b/targets/ptfkit-native/tasks.toml index 61e1247..c603f17 100644 --- a/targets/ptfkit-native/tasks.toml +++ b/targets/ptfkit-native/tasks.toml @@ -17,8 +17,8 @@ run = 'ctest --test-dir build --output-on-failure' ["native:format"] description = "Check C and C++ source formatting" -run = "git ls-files -z -- '*.c' '*.h' '*.cc' '*.cpp' '*.cppm' '*.cxx' '*.hpp' | xargs -0 --no-run-if-empty clang-format --style=file --dry-run --Werror" -run_windows = "& { $files = git ls-files -- '*.c' '*.h' '*.cc' '*.cpp' '*.cppm' '*.cxx' '*.hpp'; if ($files) { clang-format --style=file --dry-run --Werror $files } }" +run = '''git ls-files --cached --others --exclude-standard -z -- '*.c' '*.h' '*.cc' '*.cpp' '*.cppm' '*.cxx' '*.hpp' | xargs -0 --no-run-if-empty sh -c 'for file do [ ! -f "$file" ] || printf "%s\0" "$file"; done' sh | xargs -0 --no-run-if-empty clang-format --style=file --dry-run --Werror''' +run_windows = "& { $files = git ls-files --cached --others --exclude-standard -- '*.c' '*.h' '*.cc' '*.cpp' '*.cppm' '*.cxx' '*.hpp' | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf }; if ($files) { clang-format --style=file --dry-run --Werror $files } }" ["native:verify"] description = "Run all native target quality checks" diff --git a/targets/ptfkit-native/tests/CMakeLists.txt b/targets/ptfkit-native/tests/CMakeLists.txt index 3828ee8..d687c51 100644 --- a/targets/ptfkit-native/tests/CMakeLists.txt +++ b/targets/ptfkit-native/tests/CMakeLists.txt @@ -23,6 +23,28 @@ function(ptfkit_add_native_tests case) endforeach() endfunction() +function(ptfkit_add_comparator_test language target) + set(source "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/support/comparator.c") + # Source LANGUAGE properties are scoped to the calling directory. C and C++ + # register this source from separate subdirectories to keep their modes independent. + set_source_files_properties("${source}" PROPERTIES LANGUAGE ${language}) + add_executable(${target} "${source}") + target_link_libraries(${target} PRIVATE ptfkit::c) + set_target_properties(${target} PROPERTIES + C_STANDARD 11 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF + CXX_STANDARD 23 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF + ) + add_test(NAME ${target} COMMAND ${target}) + foreach(case IN ITEMS absolute relative negative zero guard nan) + add_test(NAME ${target}_reject_${case} + COMMAND ${CMAKE_COMMAND} + "-DEXECUTABLE=$" + "-DCASE=${case}" + -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/support/check_rejection.cmake" + ) + endforeach() +endfunction() + include_directories(${CMAKE_CURRENT_SOURCE_DIR}) add_subdirectory(c) diff --git a/targets/ptfkit-native/tests/c/CMakeLists.txt b/targets/ptfkit-native/tests/c/CMakeLists.txt index 521371b..2011601 100644 --- a/targets/ptfkit-native/tests/c/CMakeLists.txt +++ b/targets/ptfkit-native/tests/c/CMakeLists.txt @@ -4,3 +4,5 @@ ptfkit_add_native_tests(c SOURCES ${sources} LIBRARIES ptfkit::c ) + +ptfkit_add_comparator_test(C c_comparator) diff --git a/targets/ptfkit-native/tests/c/comparator.c b/targets/ptfkit-native/tests/c/comparator.c deleted file mode 100644 index f55b0f4..0000000 --- a/targets/ptfkit-native/tests/c/comparator.c +++ /dev/null @@ -1,22 +0,0 @@ -/* @generated by ptfkit-codegen; DO NOT EDIT. */ - -#include "support/close_enough.h" - -int main(void) { - const double absolute = 0.001; - const double relative = 0.01; - const double expected_values[] = {0.0, 2.0, -2.0}; - for (int index = 0; index < 3; ++index) { - const double expected = expected_values[index]; - const double tolerance = resolved_tolerance(expected, absolute, relative); - _close_enough_impl(__FILE__, __LINE__, expected + tolerance * 0.5, expected, absolute, - relative, "test_quantity", "1", "registry", "comparator"); - if (is_close(expected + tolerance * 2.0, expected, absolute, relative)) { - return EXIT_FAILURE; - } - } - if (is_close(NAN, 1.0, absolute, relative)) { - return EXIT_FAILURE; - } - return EXIT_SUCCESS; -} diff --git a/targets/ptfkit-native/tests/cpp/comparator.cpp b/targets/ptfkit-native/tests/cpp/comparator.cpp deleted file mode 100644 index 43fe913..0000000 --- a/targets/ptfkit-native/tests/cpp/comparator.cpp +++ /dev/null @@ -1,22 +0,0 @@ -/* @generated by ptfkit-codegen; DO NOT EDIT. */ - -#include "support/close_enough.h" - -int main() { - const double absolute = 0.001; - const double relative = 0.01; - const double expected_values[] = {0.0, 2.0, -2.0}; - for (int index = 0; index < 3; ++index) { - const double expected = expected_values[index]; - const double tolerance = resolved_tolerance(expected, absolute, relative); - _close_enough_impl(__FILE__, __LINE__, expected + tolerance * 0.5, expected, absolute, - relative, "test_quantity", "1", "registry", "comparator"); - if (is_close(expected + tolerance * 2.0, expected, absolute, relative)) { - return EXIT_FAILURE; - } - } - if (is_close(NAN, 1.0, absolute, relative)) { - return EXIT_FAILURE; - } - return EXIT_SUCCESS; -} diff --git a/targets/ptfkit-native/tests/cpp_fallback/CMakeLists.txt b/targets/ptfkit-native/tests/cpp_fallback/CMakeLists.txt index b409785..2114835 100644 --- a/targets/ptfkit-native/tests/cpp_fallback/CMakeLists.txt +++ b/targets/ptfkit-native/tests/cpp_fallback/CMakeLists.txt @@ -10,3 +10,5 @@ ptfkit_add_native_tests(cpp_fallback LIBRARIES ptfkit::c COMPILE_FEATURES cxx_std_23 ) + +ptfkit_add_comparator_test(CXX cpp_comparator) diff --git a/targets/ptfkit-native/tests/support/check_rejection.cmake b/targets/ptfkit-native/tests/support/check_rejection.cmake new file mode 100644 index 0000000..ea0fe64 --- /dev/null +++ b/targets/ptfkit-native/tests/support/check_rejection.cmake @@ -0,0 +1,19 @@ +execute_process( + COMMAND "${EXECUTABLE}" reject "${CASE}" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error + TIMEOUT 10 +) + +if(NOT "${result}" STREQUAL "1") + message(FATAL_ERROR + "Expected assertion exit code 1 for ${CASE}, got '${result}'.\n${output}${error}" + ) +endif() + +if(NOT error MATCHES "assertion failed: [^\n]*: case=${CASE},") + message(FATAL_ERROR + "Missing assertion diagnostic for ${CASE}.\n${output}${error}" + ) +endif() diff --git a/targets/ptfkit-native/tests/support/close_enough.h b/targets/ptfkit-native/tests/support/close_enough.h index 9ce81e1..446ed9c 100644 --- a/targets/ptfkit-native/tests/support/close_enough.h +++ b/targets/ptfkit-native/tests/support/close_enough.h @@ -1,5 +1,3 @@ -/* @generated by ptfkit-codegen; DO NOT EDIT. */ - #ifndef PTFKIT_TEST_CLOSE_ENOUGH_H #define PTFKIT_TEST_CLOSE_ENOUGH_H diff --git a/targets/ptfkit-native/tests/support/comparator.c b/targets/ptfkit-native/tests/support/comparator.c new file mode 100644 index 0000000..8ccd0dd --- /dev/null +++ b/targets/ptfkit-native/tests/support/comparator.c @@ -0,0 +1,62 @@ +#include "close_enough.h" +#include + +int main(int argc, char **argv) { + const struct { + const char *name; + double expected, absolute, relative, tolerance, below, boundary, above; + } cases[] = { + {"absolute", 4.0, 0.5, 0.0625, 0.5, 4.25, 4.5, 5.0}, + {"relative", 4.0, 0.125, 0.25, 1.0, 4.5, 5.0, 6.0}, + {"negative", -4.0, 0.125, 0.25, 1.0, -4.5, -5.0, -6.0}, + {"zero", 0.0, 0.125, 0.25, 0.125, 0.0625, 0.125, 0.25}, + // At zero, the literal guard is also the exact boundary value. + {"guard", 0.0, 0.0, 0.0, 1e-14, 5e-15, 1e-14, 2e-14}, + }; + const size_t case_count = sizeof(cases) / sizeof(cases[0]); + if (argc != 1) { + if (argc == 3 && strcmp(argv[1], "reject") == 0) { + if (strcmp(argv[2], "nan") == 0) { + _close_enough_impl(__FILE__, __LINE__, NAN, 1.0, 0.125, 0.25, "test_quantity", "1", + "registry", "nan"); + return EXIT_SUCCESS; + } + for (size_t index = 0; index < case_count; ++index) { + if (strcmp(argv[2], cases[index].name) == 0) { + _close_enough_impl(__FILE__, __LINE__, cases[index].above, + cases[index].expected, cases[index].absolute, + cases[index].relative, "test_quantity", "1", "registry", + cases[index].name); + return EXIT_SUCCESS; + } + } + } + fprintf(stderr, "usage: %s [reject {absolute|relative|negative|zero|guard|nan}]\n", + argv[0]); + return 2; + } + for (size_t index = 0; index < case_count; ++index) { + if (resolved_tolerance(cases[index].expected, cases[index].absolute, + cases[index].relative) != cases[index].tolerance || + !is_close(cases[index].below, cases[index].expected, cases[index].absolute, + cases[index].relative) || + !is_close(cases[index].boundary, cases[index].expected, cases[index].absolute, + cases[index].relative) || + is_close(cases[index].above, cases[index].expected, cases[index].absolute, + cases[index].relative)) { + fprintf(stderr, "assertion failed: comparator case=%s\n", cases[index].name); + return EXIT_FAILURE; + } + _close_enough_impl(__FILE__, __LINE__, cases[index].below, cases[index].expected, + cases[index].absolute, cases[index].relative, "test_quantity", "1", + "registry", cases[index].name); + _close_enough_impl(__FILE__, __LINE__, cases[index].boundary, cases[index].expected, + cases[index].absolute, cases[index].relative, "test_quantity", "1", + "registry", cases[index].name); + } + if (is_close(NAN, 1.0, 0.125, 0.25)) { + fprintf(stderr, "assertion failed: comparator must reject NaN\n"); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/targets/ptfkit-py/tests/_helpers.py b/targets/ptfkit-py/tests/_helpers.py index 744e0fd..dd83385 100644 --- a/targets/ptfkit-py/tests/_helpers.py +++ b/targets/ptfkit-py/tests/_helpers.py @@ -1,5 +1,3 @@ -# @generated by ptfkit-codegen; DO NOT EDIT. - from __future__ import annotations from enum import Enum diff --git a/targets/ptfkit-py/tests/test_comparator.py b/targets/ptfkit-py/tests/test_comparator.py index c6eae43..7a57921 100644 --- a/targets/ptfkit-py/tests/test_comparator.py +++ b/targets/ptfkit-py/tests/test_comparator.py @@ -1,15 +1,30 @@ -# @generated by ptfkit-codegen; DO NOT EDIT. - import pytest from _helpers import assert_close, resolved_tolerance -@pytest.mark.parametrize('expected', [0.0, 2.0, -2.0]) -def test_accepts_below_and_rejects_above_tolerance(expected: float): - absolute = 0.001 - relative = 0.01 - tolerance = resolved_tolerance(expected, absolute, relative) +@pytest.mark.parametrize( + ('expected', 'absolute', 'relative', 'tolerance', 'below', 'boundary', 'above'), + [ + pytest.param(4.0, 0.5, 0.0625, 0.5, 4.25, 4.5, 5.0, id='absolute'), + pytest.param(4.0, 0.125, 0.25, 1.0, 4.5, 5.0, 6.0, id='relative'), + pytest.param(-4.0, 0.125, 0.25, 1.0, -4.5, -5.0, -6.0, id='negative'), + pytest.param(0.0, 0.125, 0.25, 0.125, 0.0625, 0.125, 0.25, id='zero'), + # At zero, the literal guard is also the exact boundary value. + pytest.param(0.0, 0.0, 0.0, 1e-14, 5e-15, 1e-14, 2e-14, id='guard'), + ], +) +def test_independent_tolerances_and_boundaries( + *, + expected: float, + absolute: float, + relative: float, + tolerance: float, + below: float, + boundary: float, + above: float, +): + assert resolved_tolerance(expected, absolute, relative) == tolerance metadata = { 'absolute': absolute, 'relative': relative, @@ -17,6 +32,7 @@ def test_accepts_below_and_rejects_above_tolerance(expected: float): 'unit': '1', 'source': 'registry', } - assert_close(expected + tolerance * 0.5, expected, **metadata) + assert_close(below, expected, **metadata) + assert_close(boundary, expected, **metadata) with pytest.raises(AssertionError): - assert_close(expected + tolerance * 2.0, expected, **metadata) + assert_close(above, expected, **metadata) diff --git a/targets/ptfkit-rs/src/ahuja1984.rs b/targets/ptfkit-rs/src/ahuja1984.rs index e4969c0..be99ad5 100644 --- a/targets/ptfkit-rs/src/ahuja1984.rs +++ b/targets/ptfkit-rs/src/ahuja1984.rs @@ -18,41 +18,6 @@ Undisturbed cores from 15 sites in a 9.6-ha Renfro silt loam watershed and 54 co related Hawaii Oxic soils; the Hawaii study also supplied 35 field measurements of saturated hydraulic conductivity."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Estimate saturated hydraulic conductivity from total porosity and water content at -33 kPa using user-supplied empirical coefficients. @@ -108,6 +73,7 @@ pub fn calc_ptf_ahuja1984( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn exponent_four() { let result = calc_ptf_ahuja1984(0.45f64, 0.25f64, 100f64, 4f64); diff --git a/targets/ptfkit-rs/src/aimrun2009.rs b/targets/ptfkit-rs/src/aimrun2009.rs index d37e949..ce3acdf 100644 --- a/targets/ptfkit-rs/src/aimrun2009.rs +++ b/targets/ptfkit-rs/src/aimrun2009.rs @@ -17,41 +17,6 @@ Agricultural Development Area (IADA Barat Laut Selangor), Malaysia 408 lowland paddy soil samples from Sawah Sempadan rice cultivation area."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Estimate saturated hydraulic conductivity for lowland paddy soils. # Arguments @@ -93,6 +58,7 @@ pub fn calc_ptf_aimrun2009(clay: f64, bulk_density: f64, organic_matter: f64, gm #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn mean_topsoil_layer() { let result = calc_ptf_aimrun2009(43.88f64, 0.94f64, 12.07f64, 0.01f64); diff --git a/targets/ptfkit-rs/src/beniaich2023.rs b/targets/ptfkit-rs/src/beniaich2023.rs index 4548a9d..918cc51 100644 --- a/targets/ptfkit-rs/src/beniaich2023.rs +++ b/targets/ptfkit-rs/src/beniaich2023.rs @@ -19,41 +19,6 @@ Agricultural topsoils in Doukkala, Gharb-Loukouss, Moulouya, and Tadla, Morocco 331 disturbed topsoil samples collected at 0-20 cm from 2019 to 2022; random 50% calibration and 50% validation subsets."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_beniaich2023_slr1`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Beniaich2023PTFResult { @@ -102,6 +67,7 @@ pub fn calc_ptf_beniaich2023_slr1(clay: f64) -> Beniaich2023PTFResult { #[cfg(test)] mod calc_ptf_beniaich2023_slr1_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_slr1(20f64); @@ -172,6 +138,7 @@ pub fn calc_ptf_beniaich2023_slr2(silt: f64) -> Beniaich2023PTFResult { #[cfg(test)] mod calc_ptf_beniaich2023_slr2_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_slr2(30f64); @@ -242,6 +209,7 @@ pub fn calc_ptf_beniaich2023_slr3(sand: f64) -> Beniaich2023PTFResult { #[cfg(test)] mod calc_ptf_beniaich2023_slr3_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_slr3(50f64); @@ -314,6 +282,7 @@ pub fn calc_ptf_beniaich2023_slr4(clay: f64, silt: f64) -> Beniaich2023PTFResult #[cfg(test)] mod calc_ptf_beniaich2023_slr4_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_slr4(20f64, 30f64); @@ -386,6 +355,7 @@ pub fn calc_ptf_beniaich2023_slr5(clay: f64, silt: f64) -> Beniaich2023PTFResult #[cfg(test)] mod calc_ptf_beniaich2023_slr5_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_slr5(20f64, 40f64); @@ -456,6 +426,7 @@ pub fn calc_ptf_beniaich2023_slr6(soil_organic_matter: f64) -> Beniaich2023PTFRe #[cfg(test)] mod calc_ptf_beniaich2023_slr6_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_slr6(2f64); @@ -535,6 +506,7 @@ pub fn calc_ptf_beniaich2023_mlr1( #[cfg(test)] mod calc_ptf_beniaich2023_mlr1_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_mlr1(30f64, 50f64, 2f64); @@ -609,6 +581,7 @@ pub fn calc_ptf_beniaich2023_mlr2(sand: f64, soil_organic_matter: f64) -> Beniai #[cfg(test)] mod calc_ptf_beniaich2023_mlr2_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_mlr2(50f64, 2f64); @@ -683,6 +656,7 @@ pub fn calc_ptf_beniaich2023_mlr3(silt: f64, soil_organic_matter: f64) -> Beniai #[cfg(test)] mod calc_ptf_beniaich2023_mlr3_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_mlr3(30f64, 2f64); @@ -757,6 +731,7 @@ pub fn calc_ptf_beniaich2023_mlr4(clay: f64, soil_organic_matter: f64) -> Beniai #[cfg(test)] mod calc_ptf_beniaich2023_mlr4_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_mlr4(20f64, 2f64); @@ -836,6 +811,7 @@ pub fn calc_ptf_beniaich2023_mlr5( #[cfg(test)] mod calc_ptf_beniaich2023_mlr5_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_beniaich2023_mlr5(20f64, 30f64, 2f64); diff --git a/targets/ptfkit-rs/src/chakraborty2011.rs b/targets/ptfkit-rs/src/chakraborty2011.rs index c7519a5..6c2e1b8 100644 --- a/targets/ptfkit-rs/src/chakraborty2011.rs +++ b/targets/ptfkit-rs/src/chakraborty2011.rs @@ -18,41 +18,6 @@ India for regression development and 80 independent samples for validation. Analyses were performed during 2006-2008."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_chakraborty2011_eq1`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Chakraborty2011PTFResult { @@ -108,6 +73,7 @@ pub fn calc_ptf_chakraborty2011_eq1(clay: f64, silt: f64) -> Chakraborty2011PTFR #[cfg(test)] mod calc_ptf_chakraborty2011_eq1_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_chakraborty2011_eq1(20f64, 30f64); @@ -192,6 +158,7 @@ pub fn calc_ptf_chakraborty2011_eq2(sand: f64, bulk_density: f64) -> Chakraborty #[cfg(test)] mod calc_ptf_chakraborty2011_eq2_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_chakraborty2011_eq2(50f64, 1.5f64); @@ -285,6 +252,7 @@ pub fn calc_ptf_chakraborty2011_eq3( #[cfg(test)] mod calc_ptf_chakraborty2011_eq3_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_chakraborty2011_eq3(20f64, 30f64, 1.5f64); @@ -374,6 +342,7 @@ pub fn calc_ptf_chakraborty2011_eq4(clay: f64, silt: f64, sand: f64) -> Chakrabo #[cfg(test)] mod calc_ptf_chakraborty2011_eq4_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_chakraborty2011_eq4(20f64, 30f64, 50f64); @@ -473,6 +442,7 @@ pub fn calc_ptf_chakraborty2011_eq5( #[cfg(test)] mod calc_ptf_chakraborty2011_eq5_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_chakraborty2011_eq5(20f64, 30f64, 50f64, 1.5f64); @@ -582,6 +552,7 @@ pub fn calc_ptf_chakraborty2011_eq6( #[cfg(test)] mod calc_ptf_chakraborty2011_eq6_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_case() { let result = calc_ptf_chakraborty2011_eq6(20f64, 30f64, 50f64, 0.5f64, 1.5f64); diff --git a/targets/ptfkit-rs/src/clapp1978.rs b/targets/ptfkit-rs/src/clapp1978.rs index 1eb58ae..f3321d6 100644 --- a/targets/ptfkit-rs/src/clapp1978.rs +++ b/targets/ptfkit-rs/src/clapp1978.rs @@ -136,41 +136,6 @@ impl From for Clapp1978Parameters { } } } -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_clapp1978`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Clapp1978Parameters { @@ -229,6 +194,7 @@ pub fn calc_ptf_clapp1978(soil_texture: UsdaTextureClass) -> Clapp1978Parameters #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_2_sand() { let result = calc_ptf_clapp1978(UsdaTextureClass::Sand); diff --git a/targets/ptfkit-rs/src/cosby1984.rs b/targets/ptfkit-rs/src/cosby1984.rs index 4410d6a..75769dc 100644 --- a/targets/ptfkit-rs/src/cosby1984.rs +++ b/targets/ptfkit-rs/src/cosby1984.rs @@ -16,41 +16,6 @@ United States 1448 soil samples from Holtan et al. (1968) and Rawls et al. (1976)."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_cosby1984_univariate`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Cosby1984UnivariatePTFResult { @@ -125,6 +90,7 @@ pub fn calc_ptf_cosby1984_univariate( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn scalar_mid_texture() { let result = calc_ptf_cosby1984_univariate(50f64, 30f64, 20f64); diff --git a/targets/ptfkit-rs/src/dharumarajan2019.rs b/targets/ptfkit-rs/src/dharumarajan2019.rs index 1ebc03d..50f22f3 100644 --- a/targets/ptfkit-rs/src/dharumarajan2019.rs +++ b/targets/ptfkit-rs/src/dharumarajan2019.rs @@ -19,41 +19,6 @@ Field capacity and permanent wilting point were modeled from 512 soil samples in Karnataka Plateau and 228 samples in the Southern Karnataka Plateau; infiltration was modeled from 100 Karnataka soil observations."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_dharumarajan2019_nkp`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Dharumarajan2019WaterRetentionResult { @@ -113,6 +78,7 @@ pub fn calc_ptf_dharumarajan2019_nkp( #[cfg(test)] mod calc_ptf_dharumarajan2019_nkp_tests { use super::*; + use crate::test_support::assert_close; #[test] fn northern_study_mean_case() { let result = calc_ptf_dharumarajan2019_nkp(43.2f64, 39.8f64, 33.6f64); @@ -179,6 +145,7 @@ pub fn calc_ptf_dharumarajan2019_nkp_clay(clay: f64) -> Dharumarajan2019WaterRet #[cfg(test)] mod calc_ptf_dharumarajan2019_nkp_clay_tests { use super::*; + use crate::test_support::assert_close; #[test] fn northern_study_mean_clay_case() { let result = calc_ptf_dharumarajan2019_nkp_clay(43.2f64); @@ -251,6 +218,7 @@ pub fn calc_ptf_dharumarajan2019_skp( #[cfg(test)] mod calc_ptf_dharumarajan2019_skp_tests { use super::*; + use crate::test_support::assert_close; #[test] fn southern_study_mean_case() { let result = calc_ptf_dharumarajan2019_skp(31.5f64, 53.5f64, 14.7f64); @@ -315,6 +283,7 @@ pub fn calc_ptf_dharumarajan2019_skp_clay(clay: f64) -> Dharumarajan2019WaterRet #[cfg(test)] mod calc_ptf_dharumarajan2019_skp_clay_tests { use super::*; + use crate::test_support::assert_close; #[test] fn southern_study_mean_clay_case() { let result = calc_ptf_dharumarajan2019_skp_clay(31.5f64); @@ -375,6 +344,7 @@ pub fn calc_ptf_dharumarajan2019_infiltration(sand: f64, silt: f64, clay: f64) - #[cfg(test)] mod calc_ptf_dharumarajan2019_infiltration_tests { use super::*; + use crate::test_support::assert_close; #[test] fn balanced_texture_case() { let result = calc_ptf_dharumarajan2019_infiltration(50f64, 20f64, 30f64); diff --git a/targets/ptfkit-rs/src/ferrerjulia2004.rs b/targets/ptfkit-rs/src/ferrerjulia2004.rs index 42a8664..f54e687 100644 --- a/targets/ptfkit-rs/src/ferrerjulia2004.rs +++ b/targets/ptfkit-rs/src/ferrerjulia2004.rs @@ -18,41 +18,6 @@ Spanish mainland on the Iberian Peninsula Trueba et al. (2000a) Spanish soil database: 2178 profiles and 7011 horizons; the general regressions used 3172 horizons with sufficient data."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Evaluate the Campbell and Shiozawa saturated-conductivity PTF. # Arguments @@ -80,6 +45,7 @@ pub fn calc_ptf_ferrerjulia2004_campbell_shiozawa(sand: f64, clay: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_campbell_shiozawa_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_campbell_shiozawa(50f64, 25f64); @@ -130,6 +96,7 @@ pub fn calc_ptf_ferrerjulia2004_saxton(sand: f64, clay: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_saxton_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_saxton(50f64, 25f64); @@ -170,6 +137,7 @@ pub fn calc_ptf_ferrerjulia2004_dane_puckett(clay: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_dane_puckett_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_dane_puckett(25f64); @@ -210,6 +178,7 @@ pub fn calc_ptf_ferrerjulia2004_puckett(clay: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_puckett_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_puckett(25f64); @@ -251,6 +220,7 @@ pub fn calc_ptf_ferrerjulia2004_cosby(sand: f64, clay: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_cosby_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_cosby(50f64, 25f64); @@ -291,6 +261,7 @@ pub fn calc_ptf_ferrerjulia2004_humic_acrisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_humic_acrisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_humic_acrisol_sand(50f64); @@ -337,6 +308,7 @@ pub fn calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -378,6 +350,7 @@ pub fn calc_ptf_ferrerjulia2004_calcic_cambisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcic_cambisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_calcic_cambisol_sand(50f64); @@ -424,6 +397,7 @@ pub fn calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -465,6 +439,7 @@ pub fn calc_ptf_ferrerjulia2004_dystric_cambisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_dystric_cambisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_dystric_cambisol_sand(50f64); @@ -511,6 +486,7 @@ pub fn calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -552,6 +528,7 @@ pub fn calc_ptf_ferrerjulia2004_eutric_cambisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_eutric_cambisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_eutric_cambisol_sand(50f64); @@ -598,6 +575,7 @@ pub fn calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -639,6 +617,7 @@ pub fn calc_ptf_ferrerjulia2004_gleyic_cambisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_gleyic_cambisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_gleyic_cambisol_sand(50f64); @@ -685,6 +664,7 @@ pub fn calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -726,6 +706,7 @@ pub fn calc_ptf_ferrerjulia2004_humic_cambisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_humic_cambisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_humic_cambisol_sand(50f64); @@ -772,6 +753,7 @@ pub fn calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -813,6 +795,7 @@ pub fn calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand(50f64); @@ -859,6 +842,7 @@ pub fn calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -904,6 +888,7 @@ pub fn calc_ptf_ferrerjulia2004_calcic_luvisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcic_luvisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_calcic_luvisol_sand(50f64); @@ -954,6 +939,7 @@ pub fn calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -995,6 +981,7 @@ pub fn calc_ptf_ferrerjulia2004_chromic_luvisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_chromic_luvisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_chromic_luvisol_sand(50f64); @@ -1041,6 +1028,7 @@ pub fn calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -1082,6 +1070,7 @@ pub fn calc_ptf_ferrerjulia2004_gleyic_luvisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_gleyic_luvisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_gleyic_luvisol_sand(50f64); @@ -1128,6 +1117,7 @@ pub fn calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -1169,6 +1159,7 @@ pub fn calc_ptf_ferrerjulia2004_orthic_luvisol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_orthic_luvisol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_orthic_luvisol_sand(50f64); @@ -1215,6 +1206,7 @@ pub fn calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -1256,6 +1248,7 @@ pub fn calc_ptf_ferrerjulia2004_ranker_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_ranker_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_ranker_sand(50f64); @@ -1302,6 +1295,7 @@ pub fn calc_ptf_ferrerjulia2004_ranker_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_ranker_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_ranker_texture_organic_matter(50f64, 25f64, 2.5f64); @@ -1342,6 +1336,7 @@ pub fn calc_ptf_ferrerjulia2004_calcaric_regosol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcaric_regosol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_calcaric_regosol_sand(50f64); @@ -1388,6 +1383,7 @@ pub fn calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -1429,6 +1425,7 @@ pub fn calc_ptf_ferrerjulia2004_dystric_regosol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_dystric_regosol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_dystric_regosol_sand(50f64); @@ -1475,6 +1472,7 @@ pub fn calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -1516,6 +1514,7 @@ pub fn calc_ptf_ferrerjulia2004_eutric_regosol_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_eutric_regosol_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_eutric_regosol_sand(50f64); @@ -1562,6 +1561,7 @@ pub fn calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -1609,6 +1609,7 @@ pub fn calc_ptf_ferrerjulia2004_rendzina_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_rendzina_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_rendzina_sand(50f64); @@ -1659,6 +1660,7 @@ pub fn calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter(50f64, 25f64, 2.5f64); @@ -1703,6 +1705,7 @@ pub fn calc_ptf_ferrerjulia2004_gleyic_solonchak_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_gleyic_solonchak_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_gleyic_solonchak_sand(50f64); @@ -1755,6 +1758,7 @@ pub fn calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = @@ -1801,6 +1805,7 @@ pub fn calc_ptf_ferrerjulia2004_general_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_ferrerjulia2004_general_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_general_sand(50f64); @@ -1852,6 +1857,7 @@ pub fn calc_ptf_ferrerjulia2004_general_texture_organic_matter( #[cfg(test)] mod calc_ptf_ferrerjulia2004_general_texture_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn reviewer_reference_vector() { let result = calc_ptf_ferrerjulia2004_general_texture_organic_matter(50f64, 25f64, 2.5f64); diff --git a/targets/ptfkit-rs/src/hodnett2002.rs b/targets/ptfkit-rs/src/hodnett2002.rs index 8ec8b51..599a8e2 100644 --- a/targets/ptfkit-rs/src/hodnett2002.rs +++ b/targets/ptfkit-rs/src/hodnett2002.rs @@ -18,41 +18,6 @@ Tropical soils between approximately 25 degrees N and 25 degrees S. The IGBP-DIS tropical-soil dataset contains 771 retained horizons from 249 profiles in 22 countries, split into 492 calibration curves and 279 validation curves."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_hodnett2002`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Hodnett2002PTFResult { @@ -153,6 +118,7 @@ pub fn calc_ptf_hodnett2002( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn calibration_dataset_mean_properties() { let result = diff --git a/targets/ptfkit-rs/src/jabro1992.rs b/targets/ptfkit-rs/src/jabro1992.rs index bb27b2f..6dd9d94 100644 --- a/targets/ptfkit-rs/src/jabro1992.rs +++ b/targets/ptfkit-rs/src/jabro1992.rs @@ -17,41 +17,6 @@ USA Southern Cooperation Series Bulletins (Dan et al., 1983; Nofziger et al., 1983; Quisenberry et al., 1987), 350 samples; validation on Duffield silt loam data."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Estimate saturated hydraulic conductivity from silt, clay, and bulk density. # Arguments @@ -86,6 +51,7 @@ pub fn calc_ptf_jabro1992(silt: f64, clay: f64, bulk_density: f64) -> f64 { #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn loamy_sand_min_bd() { let result = calc_ptf_jabro1992(10f64, 5f64, 1.26f64); diff --git a/targets/ptfkit-rs/src/li2007.rs b/targets/ptfkit-rs/src/li2007.rs index a8b8f24..697ae44 100644 --- a/targets/ptfkit-rs/src/li2007.rs +++ b/targets/ptfkit-rs/src/li2007.rs @@ -18,41 +18,6 @@ Fengqiu County soils in the North China Plain, China 63 soil water retention curves and 36 saturated soil hydraulic conductivity samples from seven soil profiles."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_li2007`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Li2007PTFResult { @@ -137,6 +102,7 @@ pub fn calc_ptf_li2007( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn loamy_sand() { let result = calc_ptf_li2007(85f64, 10f64, 5f64, 1.2f64, 0.21f64); diff --git a/targets/ptfkit-rs/src/lib.rs b/targets/ptfkit-rs/src/lib.rs index 0b15118..20b82da 100644 --- a/targets/ptfkit-rs/src/lib.rs +++ b/targets/ptfkit-rs/src/lib.rs @@ -19,3 +19,6 @@ pub mod varallyai1982; pub mod vereecken1989; pub mod wang2012; pub mod weber2020; + +#[cfg(test)] +mod test_support; diff --git a/targets/ptfkit-rs/src/mayr1999.rs b/targets/ptfkit-rs/src/mayr1999.rs index f205057..99af9a7 100644 --- a/targets/ptfkit-rs/src/mayr1999.rs +++ b/targets/ptfkit-rs/src/mayr1999.rs @@ -18,41 +18,6 @@ Soil Survey and Land Research Centre soil physical properties database; regressi soil horizons retained from a 306-horizon subset after excluding fits with RMSE greater than 0.05 m^3/m^3, and validation used 1678 independent soil horizons."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_mayr1999`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Mayr1999PTFResult { @@ -141,6 +106,7 @@ pub fn calc_ptf_mayr1999( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_loam() { let result = calc_ptf_mayr1999(40f64, 40f64, 20f64, 1.3f64, 2f64); diff --git a/targets/ptfkit-rs/src/oosterveld1980.rs b/targets/ptfkit-rs/src/oosterveld1980.rs index f259345..0c14abb 100644 --- a/targets/ptfkit-rs/src/oosterveld1980.rs +++ b/targets/ptfkit-rs/src/oosterveld1980.rs @@ -16,41 +16,6 @@ Southern Alberta, Canada 1,137 pressure-plate analyses on 298 soil samples from the Lethbridge Research Station laboratory; the field-capacity tension regression used 134 samples."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Estimate field-capacity tension from clay content. # Arguments @@ -80,6 +45,7 @@ pub fn calc_ptf_oosterveld1980_field_capacity_tension(clay: f64) -> f64 { #[cfg(test)] mod calc_ptf_oosterveld1980_field_capacity_tension_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_1_loamy_sand() { let result = calc_ptf_oosterveld1980_field_capacity_tension(6.6f64); @@ -133,6 +99,7 @@ pub fn calc_ptf_oosterveld1980_retention( #[cfg(test)] mod calc_ptf_oosterveld1980_retention_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_1_loamy_sand_inputs() { let result = calc_ptf_oosterveld1980_retention(6.6f64, 86.3f64, 105f64, 11.8f64); @@ -177,6 +144,7 @@ pub fn calc_ptf_oosterveld1980_field_capacity(clay: f64, sand: f64, mean_depth: #[cfg(test)] mod calc_ptf_oosterveld1980_field_capacity_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_1_loamy_sand() { let result = calc_ptf_oosterveld1980_field_capacity(6.6f64, 86.3f64, 105f64); @@ -221,6 +189,7 @@ pub fn calc_ptf_oosterveld1980_wilting_point(clay: f64, sand: f64, mean_depth: f #[cfg(test)] mod calc_ptf_oosterveld1980_wilting_point_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_1_loamy_sand() { let result = calc_ptf_oosterveld1980_wilting_point(6.6f64, 86.3f64, 105f64); @@ -271,6 +240,7 @@ pub fn calc_ptf_oosterveld1980_available_water(clay: f64, sand: f64, mean_depth: #[cfg(test)] mod calc_ptf_oosterveld1980_available_water_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_1_loamy_sand() { let result = calc_ptf_oosterveld1980_available_water(6.6f64, 86.3f64, 105f64); diff --git a/targets/ptfkit-rs/src/pidgeon1972.rs b/targets/ptfkit-rs/src/pidgeon1972.rs index 4079256..d8c6c5c 100644 --- a/targets/ptfkit-rs/src/pidgeon1972.rs +++ b/targets/ptfkit-rs/src/pidgeon1972.rs @@ -16,41 +16,6 @@ Non-alluvial ferrallitic soils in Uganda, predominantly kaolinitic and possibly Ugandan profile samples; Entebbe alluvial and Nabbongo montmorillonitic soils were excluded from the adopted regressions."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Estimate gravimetric field capacity from silt, clay, and organic matter. # Arguments @@ -79,6 +44,7 @@ pub fn calc_ptf_pidgeon1972_fc(silt: f64, clay: f64, organic_matter: f64) -> f64 #[cfg(test)] mod calc_ptf_pidgeon1972_fc_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_fc(30f64, 20f64, 2f64); @@ -118,6 +84,7 @@ pub fn calc_ptf_pidgeon1972_fc_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_pidgeon1972_fc_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_fc_sand(50f64); @@ -158,6 +125,7 @@ pub fn calc_ptf_pidgeon1972_fc_sand_organic_matter(sand: f64, organic_matter: f6 #[cfg(test)] mod calc_ptf_pidgeon1972_fc_sand_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_fc_sand_organic_matter(50f64, 2f64); @@ -198,6 +166,7 @@ pub fn calc_ptf_pidgeon1972_fc_vol_sand_organic_matter(sand: f64, organic_matter #[cfg(test)] mod calc_ptf_pidgeon1972_fc_vol_sand_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_fc_vol_sand_organic_matter(50f64, 2f64); @@ -239,6 +208,7 @@ pub fn calc_ptf_pidgeon1972_pwp(silt: f64, clay: f64, organic_matter: f64) -> f6 #[cfg(test)] mod calc_ptf_pidgeon1972_pwp_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_pwp(30f64, 20f64, 2f64); @@ -278,6 +248,7 @@ pub fn calc_ptf_pidgeon1972_pwp_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_pidgeon1972_pwp_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_pwp_sand(50f64); @@ -318,6 +289,7 @@ pub fn calc_ptf_pidgeon1972_pwp_sand_organic_matter(sand: f64, organic_matter: f #[cfg(test)] mod calc_ptf_pidgeon1972_pwp_sand_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_pwp_sand_organic_matter(50f64, 2f64); @@ -358,6 +330,7 @@ pub fn calc_ptf_pidgeon1972_awc(clay: f64, organic_matter: f64) -> f64 { #[cfg(test)] mod calc_ptf_pidgeon1972_awc_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_awc(20f64, 2f64); @@ -398,6 +371,7 @@ pub fn calc_ptf_pidgeon1972_awc_sand_organic_matter(sand: f64, organic_matter: f #[cfg(test)] mod calc_ptf_pidgeon1972_awc_sand_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_awc_sand_organic_matter(50f64, 2f64); @@ -437,6 +411,7 @@ pub fn calc_ptf_pidgeon1972_awc_coarse_sand(coarse_sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_pidgeon1972_awc_coarse_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_awc_coarse_sand(20f64); @@ -476,6 +451,7 @@ pub fn calc_ptf_pidgeon1972_awc_fine_sand(fine_sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_pidgeon1972_awc_fine_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_awc_fine_sand(20f64); @@ -515,6 +491,7 @@ pub fn calc_ptf_pidgeon1972_awc_very_fine_sand(very_fine_sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_pidgeon1972_awc_very_fine_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_awc_very_fine_sand(10f64); @@ -556,6 +533,7 @@ pub fn calc_ptf_pidgeon1972_eawc(silt: f64, clay: f64, organic_matter: f64) -> f #[cfg(test)] mod calc_ptf_pidgeon1972_eawc_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_eawc(30f64, 20f64, 2f64); @@ -595,6 +573,7 @@ pub fn calc_ptf_pidgeon1972_eawc_sand(sand: f64) -> f64 { #[cfg(test)] mod calc_ptf_pidgeon1972_eawc_sand_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_eawc_sand(50f64); @@ -635,6 +614,7 @@ pub fn calc_ptf_pidgeon1972_eawc_sand_organic_matter(sand: f64, organic_matter: #[cfg(test)] mod calc_ptf_pidgeon1972_eawc_sand_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_eawc_sand_organic_matter(50f64, 2f64); @@ -678,6 +658,7 @@ pub fn calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter( #[cfg(test)] mod calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter(20f64, 2f64); @@ -721,6 +702,7 @@ pub fn calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter( #[cfg(test)] mod calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter_tests { use super::*; + use crate::test_support::assert_close; #[test] fn regression_case() { let result = calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter(20f64, 2f64); diff --git a/targets/ptfkit-rs/src/puckett1985.rs b/targets/ptfkit-rs/src/puckett1985.rs index 301203a..d75dfeb 100644 --- a/targets/ptfkit-rs/src/puckett1985.rs +++ b/targets/ptfkit-rs/src/puckett1985.rs @@ -16,41 +16,6 @@ Lower Coastal Plain of Alabama, USA Seven pedons at seven locations representing six Ultisol series with similar genesis and clay mineralogy."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_puckett1985`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Puckett1985PTFResult { @@ -142,6 +107,7 @@ pub fn calc_ptf_puckett1985( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn cahaba_ap() { let result = calc_ptf_puckett1985(70.9f64, 36.4f64, 11.8f64, 1.67f64, 0.38f64); diff --git a/targets/ptfkit-rs/src/rawls1982.rs b/targets/ptfkit-rs/src/rawls1982.rs index c396781..5ba1b04 100644 --- a/targets/ptfkit-rs/src/rawls1982.rs +++ b/targets/ptfkit-rs/src/rawls1982.rs @@ -15,41 +15,6 @@ Agricultural soils from 32 states of the USA 1,323 soils with about 5,350 horizons; regression equations used 2,541 horizons."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Estimate volumetric water content at -1500 kPa. # Arguments @@ -76,6 +41,7 @@ pub fn calc_ptf_rawls1982_theta_1500(clay: f64, organic_matter: f64) -> f64 { #[cfg(test)] mod calc_ptf_rawls1982_theta_1500_tests { use super::*; + use crate::test_support::assert_close; #[test] fn loamy_sand() { let result = calc_ptf_rawls1982_theta_1500(5.12f64, 0.1f64); @@ -117,6 +83,7 @@ pub fn calc_ptf_rawls1982_theta_33(sand: f64, organic_matter: f64, theta_1500: f #[cfg(test)] mod calc_ptf_rawls1982_theta_33_tests { use super::*; + use crate::test_support::assert_close; #[test] fn loamy_sand_with_estimated_theta_1500() { let result = calc_ptf_rawls1982_theta_33(85f64, 0.1f64, 0.05318f64); @@ -232,6 +199,7 @@ pub fn calc_ptf_rawls1982_full_wrc( #[cfg(test)] mod calc_ptf_rawls1982_full_wrc_tests { use super::*; + use crate::test_support::assert_close; #[test] fn loamy_sand() { let result = calc_ptf_rawls1982_full_wrc(85f64, 0.66f64, 1.22f64, 0.091f64, 0.033f64); diff --git a/targets/ptfkit-rs/src/saxton2006.rs b/targets/ptfkit-rs/src/saxton2006.rs index 22d03ac..6a2a9b8 100644 --- a/targets/ptfkit-rs/src/saxton2006.rs +++ b/targets/ptfkit-rs/src/saxton2006.rs @@ -17,41 +17,6 @@ United States 1,722 mineral-soil A-horizon samples retained from the USDA/NRCS National Soil Characterization database after exclusions from 2,149 samples."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_saxton2006`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Saxton2006PTFResult { @@ -170,6 +135,7 @@ pub fn calc_ptf_saxton2006(sand: f64, clay: f64, organic_matter: f64) -> Saxton2 #[cfg(test)] mod calc_ptf_saxton2006_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_3_sand() { let result = calc_ptf_saxton2006(0.88f64, 0.05f64, 2.5f64); @@ -324,6 +290,7 @@ pub fn calc_ptf_saxton2006_density( #[cfg(test)] mod calc_ptf_saxton2006_density_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_3_sand_compacted() { let result = calc_ptf_saxton2006_density( @@ -405,6 +372,7 @@ pub fn calc_ptf_saxton2006_tension_dry(theta: f64, theta_1500: f64, theta_33: f6 #[cfg(test)] mod calc_ptf_saxton2006_tension_dry_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_3_sand_midpoint() { let result = calc_ptf_saxton2006_tension_dry( @@ -461,6 +429,7 @@ pub fn calc_ptf_saxton2006_tension_wet( #[cfg(test)] mod calc_ptf_saxton2006_tension_wet_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_3_sand_theta_0_2() { let result = calc_ptf_saxton2006_tension_wet( @@ -517,6 +486,7 @@ pub fn calc_ptf_saxton2006_conductivity( #[cfg(test)] mod calc_ptf_saxton2006_conductivity_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_3_sand_theta_0_3() { let result = calc_ptf_saxton2006_conductivity( @@ -602,6 +572,7 @@ pub fn calc_ptf_saxton2006_gravel( #[cfg(test)] mod calc_ptf_saxton2006_gravel_tests { use super::*; + use crate::test_support::assert_close; #[test] fn table_3_sand_twenty_percent_gravel() { let result = calc_ptf_saxton2006_gravel( @@ -698,6 +669,7 @@ pub fn calc_ptf_saxton2006_salinity( #[cfg(test)] mod calc_ptf_saxton2006_salinity_tests { use super::*; + use crate::test_support::assert_close; #[test] fn saline_partly_saturated_soil() { let result = calc_ptf_saxton2006_salinity(4f64, 0.3f64, 0.46172240764858724f64); diff --git a/targets/ptfkit-rs/src/test_support.rs b/targets/ptfkit-rs/src/test_support.rs new file mode 100644 index 0000000..6e9e7d8 --- /dev/null +++ b/targets/ptfkit-rs/src/test_support.rs @@ -0,0 +1,52 @@ +fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { + absolute.max(relative * expected.abs()).max(1e-14) +} + +pub(crate) fn assert_close( + actual: f64, + expected: f64, + absolute: f64, + relative: f64, + quantity: &str, + unit: &str, + source: &str, +) { + let difference = (actual - expected).abs(); + let tolerance = resolved_tolerance(expected, absolute, relative); + assert!( + difference <= tolerance, + "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" + ); +} + +mod comparator_tests { + use super::*; + + #[test] + fn checks_independent_tolerances_and_boundaries() { + let cases = [ + ("absolute", 4.0, 0.5, 0.0625, 0.5, 4.25, 4.5, 5.0), + ("relative", 4.0, 0.125, 0.25, 1.0, 4.5, 5.0, 6.0), + ("negative", -4.0, 0.125, 0.25, 1.0, -4.5, -5.0, -6.0), + ("zero", 0.0, 0.125, 0.25, 0.125, 0.0625, 0.125, 0.25), + ("guard", 0.0, 0.0, 0.0, 1e-14, 5e-15, 1e-14, 2e-14), + ]; + for (name, expected, absolute, relative, tolerance, below, boundary, above) in cases { + assert_eq!( + resolved_tolerance(expected, absolute, relative), + tolerance, + "{name}" + ); + for actual in [below, boundary] { + assert_close(actual, expected, absolute, relative, name, "1", "registry"); + } + assert!( + std::panic::catch_unwind(|| { + assert_close(above, expected, absolute, relative, name, "1", "registry"); + }) + .is_err(), + "{name}: above-threshold value must panic" + ); + } + } +} diff --git a/targets/ptfkit-rs/src/tiwary2014.rs b/targets/ptfkit-rs/src/tiwary2014.rs index 0c1880d..217a20c 100644 --- a/targets/ptfkit-rs/src/tiwary2014.rs +++ b/targets/ptfkit-rs/src/tiwary2014.rs @@ -17,41 +17,6 @@ Indo-Gangetic Plains and black soil region of India 209 layers from 30 Indo-Gangetic Plains profiles and 275 layers from 62 black-soil profiles; equation-specific subsets are described below."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Estimate saturated conductivity for Indo-Gangetic Plains soils. # Arguments @@ -86,6 +51,7 @@ pub fn calc_ptf_tiwary2014_igp(sand: f64, bulk_density: f64, esp: f64) -> f64 { #[cfg(test)] mod calc_ptf_tiwary2014_igp_tests { use super::*; + use crate::test_support::assert_close; #[test] fn igp_saturated_conductivity_case() { let result = calc_ptf_tiwary2014_igp(37.3f64, 1.674f64, 4.6f64); @@ -163,6 +129,7 @@ pub fn calc_ptf_tiwary2014_bsr( #[cfg(test)] mod calc_ptf_tiwary2014_bsr_tests { use super::*; + use crate::test_support::assert_close; #[test] fn black_soil_compatibility_case() { let result = calc_ptf_tiwary2014_bsr(54.9f64, 7.6f64, 61.6f64, 7.3f64, 21.4f64, 3.32f64); diff --git a/targets/ptfkit-rs/src/varallyai1982.rs b/targets/ptfkit-rs/src/varallyai1982.rs index 9779a40..b062b0d 100644 --- a/targets/ptfkit-rs/src/varallyai1982.rs +++ b/targets/ptfkit-rs/src/varallyai1982.rs @@ -17,41 +17,6 @@ Hungary, mainly the Hungarian Plain Undisturbed samples from 559 genetic horizons in 160 soil profiles; the meadow-series regressions used 68 samples and the chernozem comparison used 108 samples."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_varallyai1982_meadow`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Varallyai1982Parameters { @@ -117,6 +82,7 @@ pub fn calc_ptf_varallyai1982_meadow( #[cfg(test)] mod calc_ptf_varallyai1982_meadow_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_review_case() { let result = calc_ptf_varallyai1982_meadow(1.4f64, 0.3f64, 0.25f64); @@ -199,6 +165,7 @@ pub fn calc_ptf_varallyai1982_chernozem_a( #[cfg(test)] mod calc_ptf_varallyai1982_chernozem_a_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_review_case() { let result = calc_ptf_varallyai1982_chernozem_a(1.4f64, 0.35f64); @@ -281,6 +248,7 @@ pub fn calc_ptf_varallyai1982_chernozem_b( #[cfg(test)] mod calc_ptf_varallyai1982_chernozem_b_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_review_case() { let result = calc_ptf_varallyai1982_chernozem_b(1.4f64, 0.35f64); @@ -364,6 +332,7 @@ pub fn calc_ptf_varallyai1982_chernozem_c( #[cfg(test)] mod calc_ptf_varallyai1982_chernozem_c_tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_review_case() { let result = calc_ptf_varallyai1982_chernozem_c(1.4f64, 0.35f64); diff --git a/targets/ptfkit-rs/src/vereecken1989.rs b/targets/ptfkit-rs/src/vereecken1989.rs index 49d7841..ad82437 100644 --- a/targets/ptfkit-rs/src/vereecken1989.rs +++ b/targets/ptfkit-rs/src/vereecken1989.rs @@ -17,41 +17,6 @@ Belgian territory north of the river axis Samber and Meuse 182 horizons from 40 important Belgian soil series, with textures ranging from sand to heavy clay."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_vereecken1989`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Vereecken1989PTFResult { @@ -121,6 +86,7 @@ pub fn calc_ptf_vereecken1989( #[cfg(test)] mod calc_ptf_vereecken1989_tests { use super::*; + use crate::test_support::assert_close; #[test] fn dataset_mean_properties() { let result = calc_ptf_vereecken1989(52.14f64, 10.93f64, 1.03f64, 1.466f64); @@ -286,6 +252,7 @@ pub fn calc_ptf_vereecken1989_detailed( #[cfg(test)] mod calc_ptf_vereecken1989_detailed_tests { use super::*; + use crate::test_support::assert_close; #[test] fn dataset_mean_properties() { let result = calc_ptf_vereecken1989_detailed( diff --git a/targets/ptfkit-rs/src/wang2012.rs b/targets/ptfkit-rs/src/wang2012.rs index 5198245..de4cf9d 100644 --- a/targets/ptfkit-rs/src/wang2012.rs +++ b/targets/ptfkit-rs/src/wang2012.rs @@ -16,41 +16,6 @@ Surface soils across the entire Loess Plateau, China 382 surface (0-5 cm) sites; 252 derivation and 130 validation data sets."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_wang2012`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Wang2012PTFResult { @@ -138,6 +103,7 @@ pub fn calc_ptf_wang2012( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn derivation_minimum_soc_case() { let result = calc_ptf_wang2012(85f64, 10f64, 5f64, 1.22f64, 0.033f64, 1193f64); diff --git a/targets/ptfkit-rs/src/weber2020.rs b/targets/ptfkit-rs/src/weber2020.rs index 67ed82a..8e0f8d7 100644 --- a/targets/ptfkit-rs/src/weber2020.rs +++ b/targets/ptfkit-rs/src/weber2020.rs @@ -19,41 +19,6 @@ A compilation of 1,729 samples with water-retention and hydraulic-conductivity d data set DS1 contained 392 of the 402 retained samples, DS2 was used for tau, DS3 contained 359 samples for K_snc, and 10 samples were held out as DS4."] -#[cfg(test)] -fn resolved_tolerance(expected: f64, absolute: f64, relative: f64) -> f64 { - absolute - .max(relative * expected.abs()) - .max(0.00000000000001f64) -} -#[cfg(test)] -fn assert_close( - actual: f64, - expected: f64, - absolute: f64, - relative: f64, - quantity: &str, - unit: &str, - source: &str, -) { - let difference = (actual - expected).abs(); - let tolerance = resolved_tolerance(expected, absolute, relative); - assert!( - difference <= tolerance, - "actual={actual}, expected={expected}, difference={difference}, tolerance={tolerance}, quantity={quantity}, unit={unit}, source={source}" - ); -} -#[cfg(test)] -mod comparator_tests { - use super::*; - #[test] - fn accepts_below_and_rejects_above_tolerance() { - for expected in [0.0, 2.0, -2.0] { - let tolerance = resolved_tolerance(expected, 0.001, 0.01); - assert!((expected + tolerance * 0.5 - expected).abs() <= tolerance); - assert!((expected + tolerance * 2.0 - expected).abs() > tolerance); - } - } -} #[doc = r"Results returned by `calc_ptf_weber2020`."] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Weber2020PTFResult { @@ -138,6 +103,7 @@ pub fn calc_ptf_weber2020( #[cfg(test)] mod tests { use super::*; + use crate::test_support::assert_close; #[test] fn representative_vgm_parameters() { let result = calc_ptf_weber2020(0.05f64, 0.45f64, 0.02f64, 1.6f64, -0.5f64, 100f64);