From b1f0298048662016857a735235f359a234b33dc4 Mon Sep 17 00:00:00 2001 From: protosphinx <133899485+protosphinx@users.noreply.github.com> Date: Sat, 2 May 2026 17:20:21 -0700 Subject: [PATCH] ci: add cargo fmt + clippy + test workflow maya has 42 inline unit tests but no CI to gate them. Every PR has relied on the author running `cargo test` locally; a regression would only be caught at the next manual run. Adds .github/workflows/ci.yml with three jobs: - cargo fmt --all -- --check - cargo clippy --all-targets -- -D warnings - cargo test --all-targets clippy and test both `needs: fmt` so a fmt drift fails one cheap job rather than four expensive ones. Drive-by hygiene fixes so the first CI run is green: - cargo fmt applied (whitespace only). - cargo clippy --fix where applicable (behaviour-equivalent, mostly assign-op-pattern and similar idiomatic-modernisation lints). Verified locally: 42/42 tests pass, fmt clean, clippy clean under -D warnings. --- .github/workflows/ci.yml | 49 ++++++++++++++++++++++++++++++++++++++++ src/dynamics.rs | 30 ++++++++++++++++-------- src/lib.rs | 2 +- src/tensor.rs | 14 ++---------- 4 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..077b1db --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +jobs: + fmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + name: cargo clippy + runs-on: ubuntu-latest + needs: fmt + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --all-targets -- -D warnings + + test: + name: cargo test + runs-on: ubuntu-latest + needs: fmt + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --all-targets diff --git a/src/dynamics.rs b/src/dynamics.rs index fc34878..ba906d5 100644 --- a/src/dynamics.rs +++ b/src/dynamics.rs @@ -262,7 +262,11 @@ mod tests { let dt = 0.1; let (np, nv) = euler_step(&p, &v, &a, dt); // Explicit Euler: position uses old velocity (0), so position unchanged. - approx(np.value.data[1], 0.0, "explicit Euler position(y) at t=0+dt"); + approx( + np.value.data[1], + 0.0, + "explicit Euler position(y) at t=0+dt", + ); // Velocity gains -9.81 * dt = -0.981. approx(nv.value.data[1], -0.981, "vy"); } @@ -538,9 +542,8 @@ mod tests { let force = t.var(Tensor::from_data(vec![0.0, -g], vec![2])); let torque = t.var(Tensor::from_data(vec![0.0], vec![])); let dt = 0.1; - let (np, nv, _na, _no) = rigid_body_step_2d( - &pos, &vel, &angle, &omega, 1.0, 1.0, &force, &torque, dt, - ); + let (np, nv, _na, _no) = + rigid_body_step_2d(&pos, &vel, &angle, &omega, 1.0, 1.0, &force, &torque, dt); approx(nv.value.data[1], -g * dt, "vy_new"); approx(np.value.data[1], -g * dt * dt, "y_new"); } @@ -557,7 +560,15 @@ mod tests { let inv_inertia = 0.5; // I = 2.0 let dt = 0.1; let (_np, _nv, na, no) = rigid_body_step_2d( - &pos, &vel, &angle, &omega, 1.0, inv_inertia, &force, &torque, dt, + &pos, + &vel, + &angle, + &omega, + 1.0, + inv_inertia, + &force, + &torque, + dt, ); // omega_new = 0 + tau * (1/I) * dt = 5 * 0.5 * 0.1 = 0.25 approx(no.value.data[0], 0.25, "omega_new"); @@ -591,9 +602,9 @@ mod tests { for &x in &grads[vel.idx()].data { approx(x, dt, "dL/dvel"); } - let expected_dF = dt * dt * inv_mass; + let expected_d_f = dt * dt * inv_mass; for &x in &grads[force.idx()].data { - approx(x, expected_dF, "dL/dF"); + approx(x, expected_d_f, "dL/dF"); } } @@ -610,9 +621,8 @@ mod tests { let torque = t.var(Tensor::from_data(vec![0.0], vec![])); let dt = 0.1; for _ in 0..5 { - let (np, nv, na, no) = rigid_body_step_2d( - &pos, &vel, &angle, &omega, 1.0, 1.0, &force, &torque, dt, - ); + let (np, nv, na, no) = + rigid_body_step_2d(&pos, &vel, &angle, &omega, 1.0, 1.0, &force, &torque, dt); pos = np; vel = nv; angle = na; diff --git a/src/lib.rs b/src/lib.rs index 3b46762..7f70161 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,4 +26,4 @@ pub use dynamics::{ semi_implicit_euler_step, }; pub use tape::{Tape, Var}; -pub use tensor::{Tensor, TensorTape, TVar}; +pub use tensor::{TVar, Tensor, TensorTape}; diff --git a/src/tensor.rs b/src/tensor.rs index df3fd0c..3396ee2 100644 --- a/src/tensor.rs +++ b/src/tensor.rs @@ -427,10 +427,7 @@ impl TensorTape { grads[lhs].add_in_place(&dlhs); grads[rhs].add_in_place(&drhs); } - Op::Sum { - input, - input_shape, - } => { + Op::Sum { input, input_shape } => { // g is scalar; broadcast to input_shape. let gs = g.data[0]; let dinput = Tensor::full(input_shape, gs); @@ -638,14 +635,7 @@ mod tests { fn approx_tensor(a: &Tensor, b: &Tensor, eps: f64, ctx: &str) { assert_eq!(a.shape, b.shape, "{}: shape mismatch", ctx); for (i, (x, y)) in a.data.iter().zip(b.data.iter()).enumerate() { - assert!( - (x - y).abs() < eps, - "{}: data[{}] = {} vs {}", - ctx, - i, - x, - y - ); + assert!((x - y).abs() < eps, "{}: data[{}] = {} vs {}", ctx, i, x, y); } }