From 351c2ae015cb271186a586d5c4a7dd120c0fa607 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 19 Oct 2026 21:24:34 -0700 Subject: [PATCH 1/2] fix: strengthen correctness checks and repository scan coverage - Include ordinary Rust and Python tests in production Semgrep scans while keeping deliberate rule fixtures in their dedicated checks. - Reject unexpected errors in mixed-exponent determinant properties and verify stale release targets cannot upload artifacts. - Keep scanner environments out of benchmark fixtures and align test code with the expanded static-analysis coverage. - Correct exact row-scaling documentation, benchmark capacity estimates, coverage tool ownership, and the released v0.5.0 roadmap. --- .semgrepignore | 9 ++++ REFERENCES.md | 11 +++-- docs/BENCHMARKING.md | 8 ++-- docs/MEASURING_COVERAGE.md | 7 +-- docs/code_organization.md | 4 ++ docs/roadmap.md | 46 +++++++++--------- justfile | 2 +- scripts/tests/test_maintenance_integration.py | 31 ++++++++++++ scripts/tests/test_performance_workflow.py | 6 ++- scripts/tests/test_release_baseline.py | 3 ++ semgrep.yaml | 5 +- src/exact.rs | 18 ++----- src/lib.rs | 2 +- tests/proptest_exact.rs | 47 +++++++++++++------ tests/rational_allocations.rs | 2 +- 15 files changed, 131 insertions(+), 70 deletions(-) create mode 100644 .semgrepignore diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..7ab54da --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,9 @@ +# Keep ordinary Rust and Python tests in production scans. A repository-owned +# file replaces Semgrep's built-in exclusions for test/, tests/, and testsuite/. +# Git-ignored environments, caches, and build outputs remain excluded by Git. +.git/ +node_modules/ +vendor/ + +# Deliberately invalid rule fixtures are checked separately by just semgrep-test. +/tests/semgrep/ diff --git a/REFERENCES.md b/REFERENCES.md index fedd910..f1c02e8 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -79,8 +79,10 @@ original coordinates rather than an already-rounded difference vector. `det_sign_exact()` uses a Shewchuk-style f64 error-bound filter \[[8]\] (the same bound exposed by `det_errbound()` above) backed by exact `BigInt` arithmetic. Each f64 entry is decomposed -into `mantissa × 2^exponent` and scaled to a common integer base. Dimensions 0–4 use direct -integer determinant expansions; D ≥ 5 uses integer-only Bareiss elimination \[[7]\]. Neither +into `mantissa × 2^exponent`. Each row uses its own minimum nonzero exponent `e_i` +to form an integer row; the determinant is reconstructed as `det_int × 2^(Σ e_i)`. +Zero rows use exponent zero. Dimensions 0–4 use direct integer determinant +expansions; D ≥ 5 uses integer-only Bareiss elimination \[[7]\]. Neither path constructs `BigRational` values or performs GCD normalization. See `src/exact.rs` for the full architecture description. @@ -136,8 +138,9 @@ Both the determinant and solve paths convert their finite-by-construction entrie `decompose_proven_finite_f64`, which extracts the IEEE 754 binary64 sign, unbiased exponent, and significand \[[9]\]. For nonzero `x`, it strips trailing zeros from the significand so `|x| = m · 2^e` with `m` odd; signed zeros use a separate zero -component. The integer matrix is then assembled by shifting each mantissa left by -`exp − e_min`, giving a GCD-free exact-integer starting point. Solves and D ≥ 5 determinants +component. Shifting each mantissa left by `exp − e_min` gives a GCD-free +exact-integer starting point. Determinants choose `e_min` separately for each row; +solves use the matrix and RHS scales described above. Solves and D ≥ 5 determinants then apply Bareiss elimination; D ≤ 4 determinants use direct expansions. Tests verify non-finite input rejection at the `Matrix` and `Vector` constructors. The test-only `f64_to_big_rational` helper uses `BigRational::from_f64` as an independent oracle for diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index 15ed8e1..0401b90 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -845,8 +845,10 @@ measured 304 comparative benchmarks in about 55m 34s, plus 2m 9s compilation that run provides no complete exact-suite runtime. The old 60-minute outer limit therefore could not accommodate even that smaller harness. -Inventory on 2026-09-07 found the following current workloads. The planning -estimate uses 12 seconds per benchmark, allowing analysis/report overhead +The comparative inventory on 2026-09-07 contained 533 cases. The current exact +suite contains 354 cases: 264 original cases plus 90 diagnostic cases retained +by the [row-scaling study](archive/performance/studies/exact-row-scaling.md). +The planning estimate uses 12 seconds per benchmark, allowing analysis/report overhead above the configured 3-second warmup and 5-second measurement target. This is a capacity estimate, not a measured runtime or a guaranteed upper bound; Criterion can extend measurement for expensive iterations. @@ -854,7 +856,7 @@ Criterion can extend measurement for expensive iterations. | Suite | Benchmarks | Nominal warmup + measurement | Planning estimate | Step limit | Headroom above estimate | | --- | --- | --- | --- | --- | --- | | `vs_linalg` | 533 | 71.1 min | 106.6 min | 150 min | 43.4 min (41%) | -| `exact` | 264 | 35.2 min | 52.8 min | 90 min | 37.2 min (70%) | +| `exact` | 354 | 47.2 min | 70.8 min | 90 min | 19.2 min (27%) | Both commands retain Criterion's release defaults: 100 samples, 100,000 resamples, 95% confidence, 3-second warmup, and 5-second measurement target. diff --git a/docs/MEASURING_COVERAGE.md b/docs/MEASURING_COVERAGE.md index 3328e1d..d7f1d7d 100644 --- a/docs/MEASURING_COVERAGE.md +++ b/docs/MEASURING_COVERAGE.md @@ -45,9 +45,10 @@ Nextest also writes JUnit test results to: target/nextest/coverage/test-results/junit.xml ``` -The Codecov workflow reads the `cargo-llvm-cov` and `cargo-nextest` versions -from the `justfile`, installs those exact versions plus Rust's -`llvm-tools-preview` component, and runs `just coverage-ci`. It verifies both +The Codecov workflow uses the shared tool setup to install the `cargo-llvm-cov` +and `cargo-nextest` versions declared in `pyproject.toml` under +`[tool.research-repo-tools.toolchain.cargo]`, plus the `llvm-tools-preview` +component declared in `rust-toolchain.toml`, and runs `just coverage-ci`. It verifies both reports, uploads the Cobertura and JUnit files to Codecov, and archives each report directory as a workflow artifact. Local setup via `just setup-tools` installs the same Rust component and cargo subcommands. diff --git a/docs/code_organization.md b/docs/code_organization.md index c539f3a..e1a9d02 100644 --- a/docs/code_organization.md +++ b/docs/code_organization.md @@ -211,6 +211,10 @@ dispositions, Python 3.15 review and native validation gates. `tooling/semgrep/` owns a locked Python 3.14 environment for Semgrep 1.180.0 while its wheels exclude Python 3.15. Local scans, shared fixture validation and hosted SARIF use it; ordinary support scripts use Python 3.15. +The root `.semgrepignore` keeps ordinary Rust and Python tests in those scans +while excluding deliberate `tests/semgrep/` fixtures, which `just semgrep-test` +validates separately. Git-ignored dependencies, environments, caches, and build +outputs remain outside production scans. `.github/actions/prepare-release-benchmarks/action.yml` groups tool installation, input validation, and inventory under the release workflow's shared setup timeout. diff --git a/docs/roadmap.md b/docs/roadmap.md index 256cf10..10b0e21 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -10,9 +10,10 @@ Until v1.0, API breaks are acceptable when they improve correctness, performance, or orthogonality. The `v0.4.x` line stays on stable Rust and includes focused robustness APIs, downstream ergonomics, API-contract cleanup, documentation, validation, invariant audits, benchmark coverage, and -release/tooling hardening. The current release is `v0.4.6`. -The next planned release is `v0.5.0`, consolidating the work previously targeted -at `v0.4.7`. It remains on stable Rust and makes the Rust 1.99.1 minimum an +release/tooling hardening. The current release is +[`v0.5.0`](https://github.com/acgetchell/la-stack/releases/tag/v0.5.0), published +on 2026-10-17, consolidating the work previously targeted at `v0.4.7`. +It remains on stable Rust and makes the Rust 1.99.1 minimum an explicit downstream upgrade boundary. Expanded const-generic work is tracked separately without an assigned release; it depends on the specific language capabilities required by the proposed API. @@ -237,13 +238,13 @@ storage remains available through `as_array()` and `into_array()`. Further stable-Rust maintenance can remain in `v0.4.x`; it does not depend on expanded const-generic support. -### v0.5.0 Stable Rust APIs and Performance (planned) +### v0.5.0 Stable Rust APIs and Performance (released) The [v0.5.0 milestone](https://github.com/acgetchell/la-stack/milestone/3) -replaces the planned `v0.4.7` release. It combines the completed Rust 1.99 +replaced the planned `v0.4.7` release. It combined the completed Rust 1.99 migration and stable vector-angle API with correctness-preserving performance improvements and release-tooling adoption. Expanded const-generics stabilization -does not gate this release. +did not gate this release. Completed work included in this release: @@ -279,28 +280,23 @@ API or feature is planned. The decision, executed checks, and unperformed native Linux/Windows runs; the [study](archive/performance/studies/algebraic-float.md) retains the raw evidence. -Remaining release sequence: +The release sequence is complete: -1. Review and land the triangular rational-solve optimization +1. The triangular rational-solve optimization ([#246](https://github.com/acgetchell/la-stack/issues/246), - [draft PR #278](https://github.com/acgetchell/la-stack/pull/278)), retaining the + [PR #278](https://github.com/acgetchell/la-stack/pull/278)) landed with the exact general-system fallback and independent regression evidence. -2. Adopt the published research-repo-tools v0.1.9 capabilities - ([#276](https://github.com/acgetchell/la-stack/issues/276)) after the upstream - [publication gate](https://github.com/acgetchell/research-repo-tools/issues/99) - verifies the registry release. Preserve the dimension plot, scientific - eligibility rules, and historical artifacts. -3. Complete the release validation and `v0.4.6` versus `v0.5.0` performance record - through the validated adopted workflow. Configure and verify the crates.io - publishing workflow and publisher settings - ([#259](https://github.com/acgetchell/la-stack/issues/259)) before publication. -4. Publish the reviewed `v0.5.0` release and verify crates.io availability and a - clean downstream dependency resolution. Close #259 only after actual - publication succeeds and its evidence is recorded. - -Consumers using a `0.4` Cargo requirement must explicitly adopt `0.5` after the -release is available. Delaunay's adoption issues remain scheduled for `v0.8.3` -and retain their independent correctness and performance gates. Package versions +2. The published shared-tooling capabilities were adopted + ([#276](https://github.com/acgetchell/la-stack/issues/276)), preserving the + dimension plot, scientific eligibility rules, and historical artifacts. +3. Release validation and the `v0.4.6` versus `v0.5.0` performance record completed + through the adopted workflow. The crates.io publishing workflow and publisher + settings were verified ([#259](https://github.com/acgetchell/la-stack/issues/259)). +4. The reviewed `v0.5.0` release was published and registry availability verified; + #259 closed after publication on 2026-10-17. + +Consumers using a `0.4` Cargo requirement must explicitly adopt `0.5`. +Downstream adoption retains independent correctness and performance gates. Package versions and generated release artifacts are updated during dedicated release preparation. ### v0.5.1 Follow-Up Work (planned) diff --git a/justfile b/justfile index 2accb5b..9407319 100644 --- a/justfile +++ b/justfile @@ -496,7 +496,7 @@ security-secrets: # Repository-owned Semgrep rules for project-specific diagnostics. semgrep: _ensure-uv - uv run --locked --project tooling/semgrep semgrep --metrics off --error --strict --timeout 30 --exclude tests/semgrep/src/project_rules/algebraic_float.rs --config semgrep.yaml . + uv run --locked --project tooling/semgrep semgrep --metrics off --error --strict --timeout 30 --config semgrep.yaml . # Fixture tests for repository-owned Semgrep rules. semgrep-test: python-sync diff --git a/scripts/tests/test_maintenance_integration.py b/scripts/tests/test_maintenance_integration.py index c791424..4e94094 100644 --- a/scripts/tests/test_maintenance_integration.py +++ b/scripts/tests/test_maintenance_integration.py @@ -139,3 +139,34 @@ def test_semgrep_keeps_its_current_version_outside_the_python_315_environment() assert scanner["project"]["requires-python"] == ">=3.14,<3.15" result = run_command("uv", ["run", "--locked", "--project", "tooling/semgrep", "semgrep", "--version"], cwd=REPO_ROOT) assert result.stdout.strip() == "1.180.0" + + +def test_semgrep_production_targets_include_tests_and_exclude_rule_fixtures() -> None: + """Check the pinned scanner's actual selection shared by local and SARIF scans.""" + result = run_command( + "uv", + [ + "run", + "--locked", + "--project", + "tooling/semgrep", + "semgrep", + "scan", + "--metrics", + "off", + "--disable-version-check", + "--config", + "semgrep.yaml", + "--x-ls-long", + ".", + ], + cwd=REPO_ROOT, + ) + selected = {Path(line.removeprefix("selected ")).as_posix() for line in result.stdout.splitlines() if line.startswith("selected ")} + assert { + "tests/proptest_exact.rs", + "benches/comparison/tests/vs_linalg_inputs.rs", + "scripts/tests/test_release_baseline.py", + ".github/workflows/semgrep-sarif.yml", + } <= selected + assert not any(path.startswith(("tests/semgrep/", "target/", ".venv/", "tooling/semgrep/.venv/")) for path in selected) diff --git a/scripts/tests/test_performance_workflow.py b/scripts/tests/test_performance_workflow.py index a374f51..e12b385 100644 --- a/scripts/tests/test_performance_workflow.py +++ b/scripts/tests/test_performance_workflow.py @@ -309,7 +309,8 @@ def assert_readme_presentation(readme: str, statistic: str) -> None: assert f"Statistic: {statistic}. Unit: ns." in table current, peer, ratio = ("12
[10.8, 13.2]", "20
[18, 22]", "1.66667") if statistic == "mean" else ("8
[7.2, 8.8]", "10
[9, 11]", "1.25") faer, faer_ratio = ("32
[28.8, 35.2]", "2.66667") if statistic == "mean" else ("16
[14.4, 17.6]", "2") - assert f"| 2 | {current} | {peer} | {faer} | {ratio}\N{MULTIPLICATION SIGN} | {faer_ratio}\N{MULTIPLICATION SIGN} |" in table + multiplication_sign = "\N{MULTIPLICATION SIGN}" + assert f"| 2 | {current} | {peer} | {faer} | {ratio}{multiplication_sign} | {faer_ratio}{multiplication_sign} |" in table assert "ratios are point estimates, with no ratio intervals or significance claims" in table for name in ("nalgebra", "faer"): assert f"Speedup vs {name}: ratio; numerator = {name} (" in table @@ -620,7 +621,8 @@ def test_native_command_policy_and_compatibility_adapter() -> None: def test_native_inventory_and_phase_commands_use_the_measured_checkout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, baseline_tag: str, scope: str) -> None: """Exercise the configured Justfile, stubbing only the expensive Cargo boundary.""" root = tmp_path / "checkout with spaces" - shutil.copytree(ROOT / "tooling", root / "tooling") + # Native benchmark policy needs the root configs, not the scanner environment. + shutil.copytree(ROOT / "tooling", root / "tooling", ignore=shutil.ignore_patterns("semgrep")) shutil.copyfile(ROOT / "rust-toolchain.toml", root / "rust-toolchain.toml") binary = tmp_path / "bin" binary.mkdir() diff --git a/scripts/tests/test_release_baseline.py b/scripts/tests/test_release_baseline.py index fd5d605..de7a337 100644 --- a/scripts/tests/test_release_baseline.py +++ b/scripts/tests/test_release_baseline.py @@ -351,7 +351,10 @@ def test_installed_cli_binds_trusted_target_and_leaves_draft_for_review(tmp_path assert json.loads(target)["expected_title"] == BASELINE stale = run_shell(runner + shell_step("Attach baseline for review"), fixture, {**env, "RELEASE_TARGET": target, "REMOTE_COMMIT": "b" * 40}) assert stale.returncode != 0 + assert not (fixture / "uploaded").exists() assert not (fixture / "published").exists() + stale_calls = [json.loads(line) for line in (fixture / "api-calls").read_text(encoding="utf-8").splitlines()] + assert not any("uploads.github.com" in item for call in stale_calls for item in call) result = run_shell(runner + shell_step("Attach baseline for review"), fixture, {**env, "RELEASE_TARGET": target}) assert result.returncode == 0, result.stderr assert (fixture / "uploaded").exists() diff --git a/semgrep.yaml b/semgrep.yaml index 1873485..b0d3f6f 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -101,7 +101,7 @@ rules: - "/examples/**/*.rs" - "/benches/**/*.rs" # Includes integration/property tests and the deliberate violation - # fixture. `just semgrep` excludes that fixture, while + # fixture. The root .semgrepignore excludes rule fixtures, while # `just semgrep-test` scans it directly. - "/tests/**/*.rs" pattern-either: @@ -376,6 +376,9 @@ rules: - "/benches/**/*.rs" - "/examples/**/*.rs" - "/tests/semgrep/src/project_rules/bench_example_usage.rs" + exclude: + # The comparison crate's integration tests use ordinary test assertions. + - "/benches/comparison/tests/**" pattern-either: - pattern: $VALUE.unwrap() - pattern: $VALUE.expect(...) diff --git a/src/exact.rs b/src/exact.rs index d5a1e22..bbd031d 100644 --- a/src/exact.rs +++ b/src/exact.rs @@ -500,21 +500,11 @@ impl ExactF64Conversion for RationalVector { } } -/// Convert a `BigInt × 2^exp` pair to an exactly represented finite `f64`. +/// Extract the low 64 bits of `|value| >> shift` without allocating. /// -/// This avoids allocating a [`BigRational`] when determinant and solve paths -/// already have an integer significand plus binary exponent. The optional -/// `index` is forwarded to [`LaError::Unrepresentable`] for vector-valued solve -/// components; determinant callers pass `None`. -/// -/// # Errors -/// Returns [`LaError::Unrepresentable`] with -/// [`UnrepresentableReason::RequiresRounding`] when the exact nonzero value -/// would need rounding or underflows below the smallest positive subnormal. -/// -/// Returns [`LaError::Unrepresentable`] with -/// [`UnrepresentableReason::NotFinite`] when the exact value cannot be -/// represented by any finite `f64`. +/// Discards the sign and bits outside this window; callers must establish that +/// the shifted magnitude fits when they need the whole value. Returns `None` +/// when the starting word is absent or its index does not fit in `usize`. fn shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option { let word_bits = u64::from(u64::BITS); let word_index = usize::try_from(shift / word_bits).ok()?; diff --git a/src/lib.rs b/src/lib.rs index 5137950..1d22063 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -561,7 +561,7 @@ pub mod guide { //! //! The error coefficients (`ERR_COEFF_2`, `ERR_COEFF_3`, `ERR_COEFF_4`) are //! conservative, dimension-specific constants, not caller-tunable tolerances. The - //! [mathematical basis](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/mathematical_basis.md#determinants-and-certified-sign-filtering) + //! [mathematical basis](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#determinants-and-certified-sign-filtering) //! documents the bound and states its range preconditions. The constants are explicit //! crate-root exports for advanced users who want to compose the same bound: //! `use la_stack::{ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4};`. They intentionally stay diff --git a/tests/proptest_exact.rs b/tests/proptest_exact.rs index 10120e9..83a479d 100644 --- a/tests/proptest_exact.rs +++ b/tests/proptest_exact.rs @@ -900,22 +900,39 @@ macro_rules! gen_extreme_exponent_det_filter_proptests { prop_assert_eq!(matrix.det_sign_exact(), expected_sign); - if let Ok(Some(bound)) = matrix.det_errbound() { - let direct = matrix - .det_direct() - .unwrap() - .expect("D<=4 has closed-form det_direct"); - let direct_exact = BigRational::from_f64(direct) - .expect("det_direct returned finite f64"); - let bound_exact = BigRational::from_f64(bound) - .expect("det_errbound returned finite f64"); - let error = (direct_exact - exact).abs(); - - prop_assert!( - error <= bound_exact, - "mixed-exponent determinant error exceeded bound for D={}: error={error}, bound={bound_exact}", + match matrix.det_errbound() { + Ok(Some(bound)) => { + let direct = matrix + .det_direct() + .unwrap() + .expect("D<=4 has closed-form det_direct"); + let direct_exact = BigRational::from_f64(direct) + .expect("det_direct returned finite f64"); + let bound_exact = BigRational::from_f64(bound) + .expect("det_errbound returned finite f64"); + let error = (direct_exact - exact).abs(); + + prop_assert!( + error <= bound_exact, + "mixed-exponent determinant error exceeded bound for D={}: error={error}, bound={bound_exact}", + $d, + ); + } + Ok(None) => {} + Err(LaError::NonFinite { + origin: NonFiniteOrigin::Computation { + operation: ArithmeticOperation::Determinant + | ArithmeticOperation::DeterminantErrorBound, + .. + }, + location: NonFiniteLocation::Scalar, + .. + }) => {} + Err(error) => prop_assert!( + false, + "unexpected determinant filter error for D={}: {error:?}, entries={entries:?}", $d, - ); + ), } } } diff --git a/tests/rational_allocations.rs b/tests/rational_allocations.rs index df57291..67fa60b 100644 --- a/tests/rational_allocations.rs +++ b/tests/rational_allocations.rs @@ -71,7 +71,7 @@ fn rational_allocation_evidence() { let _ = black_box(black_box(matrix).det_sign()); }); record::(kind, "solve", || { - let _ = black_box(black_box(matrix).solve(black_box(rhs)).unwrap()); + drop(black_box(black_box(matrix).solve(black_box(rhs)).unwrap())); }); for augmented in [false, true] { From 789b3317ff88465f8ddd7266be1a764bd63e1f9b Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Mon, 19 Oct 2026 22:42:05 -0700 Subject: [PATCH 2/2] fix: preserve scan exclusions and match upload hosts precisely - Preserve Semgrep default exclusions while including ordinary Rust and Python tests in local and SARIF scan selections. - Parse release-fixture URLs before matching GitHub's upload hostname. - Require uv 0.13.2 and refresh the cc and find-msvc-tools lockfile entries. --- .semgrepignore | 29 ++++++-- Cargo.lock | 8 +-- pyproject.toml | 2 +- scripts/tests/test_maintenance_integration.py | 66 +++++++++++++++---- scripts/tests/test_release_baseline.py | 6 +- 5 files changed, 87 insertions(+), 24 deletions(-) diff --git a/.semgrepignore b/.semgrepignore index 7ab54da..93e119f 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -1,9 +1,28 @@ -# Keep ordinary Rust and Python tests in production scans. A repository-owned -# file replaces Semgrep's built-in exclusions for test/, tests/, and testsuite/. -# Git-ignored environments, caches, and build outputs remain excluded by Git. -.git/ -node_modules/ +# Preserve Semgrep 1.180.0 defaults except test/, tests/, and testsuite/ so +# ordinary Rust and Python tests remain in production scans. +# https://github.com/semgrep/semgrep/blob/v1.180.0/src/targeting/default.semgrepignore +.git +.svn +.hg +_darcs +CVS + +build/ vendor/ +dist/ +*.min.js +.env/ +.tox/ + +node_modules/ +.npm/ +.yarn/ +.venv/ +_opam/ +_build/ +_cargo/ + +*_test.go # Deliberately invalid rule fixtures are checked separately by just semgrep-test. /tests/semgrep/ diff --git a/Cargo.lock b/Cargo.lock index b970439..77655e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,9 +148,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" dependencies = [ "find-msvc-tools", "shlex", @@ -449,9 +449,9 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "fnv" diff --git a/pyproject.toml b/pyproject.toml index bd46a81..d6234ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ line-length = 160 [tool.uv] package = false -required-version = "==0.13.1" +required-version = "==0.13.2" # Semgrep has its own locked Python 3.14 environment under tooling/semgrep. [dependency-groups] diff --git a/scripts/tests/test_maintenance_integration.py b/scripts/tests/test_maintenance_integration.py index 4e94094..257872b 100644 --- a/scripts/tests/test_maintenance_integration.py +++ b/scripts/tests/test_maintenance_integration.py @@ -141,32 +141,74 @@ def test_semgrep_keeps_its_current_version_outside_the_python_315_environment() assert result.stdout.strip() == "1.180.0" -def test_semgrep_production_targets_include_tests_and_exclude_rule_fixtures() -> None: - """Check the pinned scanner's actual selection shared by local and SARIF scans.""" +@pytest.mark.parametrize("output_args", [pytest.param((), id="local"), pytest.param(("--sarif",), id="sarif")]) +def test_semgrep_production_targets_include_tests_and_exclude_rule_fixtures(tmp_path: Path, output_args: tuple[str, ...]) -> None: + """Exercise both scan formats without Git ignores masking Semgrep exclusions.""" + shutil.copyfile(REPO_ROOT / ".semgrepignore", tmp_path / ".semgrepignore") + included = { + "tests/proptest_exact.rs", + "benches/comparison/tests/vs_linalg_inputs.rs", + "scripts/tests/test_release_baseline.py", + ".github/workflows/semgrep-sarif.yml", + "test/control.rs", + "testsuite/control.py", + "src/control.go", + "assets/control.js", + } + excluded = { + f"{directory}/ignored.py" + for directory in ( + ".svn", + ".hg", + "_darcs", + "CVS", + "build", + "vendor", + "dist", + ".env", + ".tox", + "node_modules", + ".npm", + ".yarn", + ".venv", + "_opam", + "_build", + "_cargo", + "tests/semgrep", + "nested/dist", + "nested/.env", + "nested/_build", + ) + } | {"assets/bundle.min.js", "src/example_test.go", "nested/example_test.go"} + for name in included | excluded: + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + comment = "#" if path.suffix in {".py", ".yml"} else "//" + path.write_text(f"{comment} Target selection fixture.\n", encoding="utf-8") result = run_command( "uv", [ "run", "--locked", "--project", - "tooling/semgrep", + str(REPO_ROOT / "tooling/semgrep"), "semgrep", "scan", "--metrics", "off", "--disable-version-check", + "--error", + "--strict", + "--timeout", + "30", "--config", - "semgrep.yaml", + str(REPO_ROOT / "semgrep.yaml"), + *output_args, "--x-ls-long", ".", ], - cwd=REPO_ROOT, + cwd=tmp_path, ) selected = {Path(line.removeprefix("selected ")).as_posix() for line in result.stdout.splitlines() if line.startswith("selected ")} - assert { - "tests/proptest_exact.rs", - "benches/comparison/tests/vs_linalg_inputs.rs", - "scripts/tests/test_release_baseline.py", - ".github/workflows/semgrep-sarif.yml", - } <= selected - assert not any(path.startswith(("tests/semgrep/", "target/", ".venv/", "tooling/semgrep/.venv/")) for path in selected) + assert included <= selected + assert not (excluded & selected) diff --git a/scripts/tests/test_release_baseline.py b/scripts/tests/test_release_baseline.py index de7a337..2a10a65 100644 --- a/scripts/tests/test_release_baseline.py +++ b/scripts/tests/test_release_baseline.py @@ -11,6 +11,7 @@ import textwrap import tomllib from pathlib import Path +from urllib.parse import urlsplit import pytest from research_repo_tools.criterion_inventory import CriterionInventory, load_discovery, serialize_inventory @@ -274,6 +275,7 @@ def test_preflight_rejects_dispatch_outside_release_tag(tmp_path: Path, ref: str GH_FIXTURE = r"""import hashlib, json, os, sys from pathlib import Path +from urllib.parse import urlsplit args = sys.argv[1:] root = Path(os.environ["RELEASE_FIXTURE"]) tag = os.environ["RELEASE_TAG"] @@ -289,7 +291,7 @@ def test_preflight_rejects_dispatch_outside_release_tag(tmp_path: Path, ref: str value = [[release]] elif args[0] == "release": value = {"databaseId": 42, "tagName": tag} -elif any("uploads.github.com" in item for item in args): +elif any(urlsplit(item).hostname == "uploads.github.com" for item in args): (root / "uploaded").touch() value = {} elif "PATCH" in args: @@ -354,7 +356,7 @@ def test_installed_cli_binds_trusted_target_and_leaves_draft_for_review(tmp_path assert not (fixture / "uploaded").exists() assert not (fixture / "published").exists() stale_calls = [json.loads(line) for line in (fixture / "api-calls").read_text(encoding="utf-8").splitlines()] - assert not any("uploads.github.com" in item for call in stale_calls for item in call) + assert not any(urlsplit(item).hostname == "uploads.github.com" for call in stale_calls for item in call) result = run_shell(runner + shell_step("Attach baseline for review"), fixture, {**env, "RELEASE_TARGET": target}) assert result.returncode == 0, result.stderr assert (fixture / "uploaded").exists()