Align project scaffolding with rust-style and scaffold-rust-binary conventions
Add tooling configuration that was missing from the initial scaffold:
.clippy.toml enforcing fs-err over std::fs and banning iterator
side-effects, .cargo/mutants.toml for cargo-mutants, and a mutants
target in the justfile.
Replace std::fs with fs-err in assertions.rs so filesystem errors
include the path that failed. Add fs-err, hegeltest, and proptest
as dependencies. Enable clap env and tracing-subscriber env-filter
features.
Fix justfile and CI to use -p frork-cli instead of --workspace —
the workspace build fails due to conflicting mlua features between
frork-cli (vendored) and frork-lua (module).
Split BrewBundle cfg gating into separate method definitions per
platform to eliminate unreachable-code warnings on Linux. Suppress
dead_code on the unused AssertionType::remove trait method.
Flatten compound imports in main.rs to one item per use statement.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
new file mode 100644
index 0000000..df6901c
--- /dev/null
+++ b/.cargo/mutants.toml
@@ -0,0 +1,2 @@
+# Excludes for cargo-mutants — equivalent or unreachable mutations.
+# Add entries as false positives are discovered.
diff --git a/.clippy.toml b/.clippy.toml
new file mode 100644
index 0000000..152ece5
--- /dev/null
+++ b/.clippy.toml
@@ -0,0 +1,26 @@
+disallowed-methods = [
+ { path = "std::iter::Iterator::for_each", reason = "prefer `for` for side-effects" },
+ { path = "std::iter::Iterator::try_for_each", reason = "prefer `for` for side-effects" },
+ { path = "std::fs::read", reason = "use fs_err::read for better error messages" },
+ { path = "std::fs::read_to_string", reason = "use fs_err::read_to_string" },
+ { path = "std::fs::write", reason = "use fs_err::write" },
+ { path = "std::fs::copy", reason = "use fs_err::copy" },
+ { path = "std::fs::create_dir", reason = "use fs_err::create_dir" },
+ { path = "std::fs::create_dir_all", reason = "use fs_err::create_dir_all" },
+ { path = "std::fs::remove_file", reason = "use fs_err::remove_file" },
+ { path = "std::fs::remove_dir", reason = "use fs_err::remove_dir" },
+ { path = "std::fs::remove_dir_all", reason = "use fs_err::remove_dir_all" },
+ { path = "std::fs::rename", reason = "use fs_err::rename" },
+ { path = "std::fs::metadata", reason = "use fs_err::metadata" },
+ { path = "std::fs::symlink_metadata", reason = "use fs_err::symlink_metadata" },
+ { path = "std::fs::canonicalize", reason = "use fs_err::canonicalize" },
+ { path = "std::fs::hard_link", reason = "use fs_err::hard_link" },
+ { path = "std::fs::read_dir", reason = "use fs_err::read_dir" },
+ { path = "std::fs::read_link", reason = "use fs_err::read_link" },
+ { path = "std::fs::set_permissions", reason = "use fs_err::set_permissions" },
+]
+
+disallowed-types = [
+ { path = "std::fs::File", reason = "use fs_err::File for better error messages" },
+ { path = "std::fs::OpenOptions", reason = "use fs_err::OpenOptions" },
+]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2cef060..4f66bd9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: rustup component add clippy rustfmt llvm-tools
- - run: cargo install grcov
- - run: cargo install just
+ - run: cargo install grcov cargo-mutants just
- run: cargo fmt --check
- run: just clippy coverage
+ - run: just mutants
diff --git a/Cargo.lock b/Cargo.lock
index 1860b4d..d50194a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -145,6 +145,33 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
[[package]]
name = "clap"
version = "4.5.50"
@@ -218,6 +245,21 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
[[package]]
name = "difflib"
version = "0.4.0"
@@ -305,6 +347,8 @@ dependencies = [
"clap",
"color-eyre",
"frork-lib",
+ "fs-err",
+ "hegeltest",
"mlua",
"predicates",
"regex",
@@ -319,6 +363,15 @@ dependencies = [
name = "frork-lib"
version = "0.1.0"
+[[package]]
+name = "fs-err"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -337,12 +390,48 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hegeltest"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fa0b7c2d078af5a0d54c9fdbc4a5013f5aa408b88f014298c73e6d88369b133"
+dependencies = [
+ "ciborium",
+ "crc32fast",
+ "hegeltest-macros",
+ "paste",
+ "serde",
+ "tempfile",
+]
+
+[[package]]
+name = "hegeltest-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cf8e08fa4ce2a5d0ee0bd87318d49e7375eeba97fb3fa579bb4dea797ec4397"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "indenter"
version = "0.3.4"
@@ -407,6 +496,15 @@ dependencies = [
"which",
]
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
[[package]]
name = "memchr"
version = "2.7.6"
@@ -548,6 +646,12 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -874,10 +978,14 @@ version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
+ "matchers",
"nu-ansi-term",
+ "once_cell",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
+ "tracing",
"tracing-core",
"tracing-log",
]
@@ -1035,3 +1143,23 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/frork-cli/.gitignore b/frork-cli/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/frork-cli/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/frork-cli/Cargo.toml b/frork-cli/Cargo.toml
index 3474ddf..6d2e416 100644
--- a/frork-cli/Cargo.toml
+++ b/frork-cli/Cargo.toml
@@ -10,15 +10,17 @@ path = "src/main.rs"
[dependencies]
frork-lib = { path = "../frork-lib" }
color-eyre = "*"
-clap = { version = "*", features = ["derive"] }
+fs-err = "*"
+clap = { version = "*", features = ["derive", "env"] }
mlua = { version = "*", features = ["lua54", "serde", "vendored"] }
regex = "*"
serde = { version = "*", features = ["derive"] }
thiserror = "*"
tracing = "*"
-tracing-subscriber = "*"
+tracing-subscriber = { version = "*", features = ["env-filter"] }
[dev-dependencies]
assert_cmd = "*"
predicates = "*"
+hegeltest = "*"
tempfile = "*"
\ No newline at end of file
diff --git a/frork-cli/src/assertions.rs b/frork-cli/src/assertions.rs
index 74ff5be..ea908bf 100644
--- a/frork-cli/src/assertions.rs
+++ b/frork-cli/src/assertions.rs
@@ -1,7 +1,7 @@
use color_eyre::{Result, eyre::eyre};
+use fs_err as fs;
use mlua::prelude::*;
use serde::Deserialize;
-use std::fs;
use std::path::Path;
use tracing::{debug, error, info};
@@ -100,6 +100,8 @@ pub trait AssertionType: std::fmt::Display {
fn status(&self) -> Result<Status>;
fn install(&self) -> Result<()>;
fn upgrade(&self) -> Result<()>;
+ // Not yet called — will be used when `frork remove` is implemented.
+ #[allow(dead_code)]
fn remove(&self) -> Result<()>;
}
@@ -223,8 +225,7 @@ impl AssertionType for Directory {
}
fn install(&self) -> Result<()> {
- std::fs::create_dir_all(&self.path)
- .map_err(|e| eyre!("Failed to create directory: {}", e))?;
+ fs::create_dir_all(&self.path).map_err(|e| eyre!("Failed to create directory: {}", e))?;
debug!("created: {}", self);
Ok(())
}
@@ -334,7 +335,7 @@ impl AssertionType for Git {
// Check if directory is empty (only . and ..)
let mut entries =
- std::fs::read_dir(dir_path).map_err(|e| eyre!("Failed to read directory: {}", e))?;
+ fs::read_dir(dir_path).map_err(|e| eyre!("Failed to read directory: {}", e))?;
if entries.next().is_none() {
return Ok(Status::Missing);
}
@@ -530,12 +531,13 @@ impl std::fmt::Display for BrewBundle {
}
impl AssertionType for BrewBundle {
+ #[cfg(not(target_os = "macos"))]
fn status(&self) -> Result<Status> {
- // Assert platform is Darwin (macOS)
- #[cfg(not(target_os = "macos"))]
- return Err(eyre!("brew-bundle only supported on Darwin/macOS"));
+ Err(eyre!("brew-bundle only supported on Darwin/macOS"))
+ }
- // Assert brew binary exists
+ #[cfg(target_os = "macos")]
+ fn status(&self) -> Result<Status> {
Utils::assert_bin("brew")?;
// First check: brew bundle check --no-upgrade
@@ -568,12 +570,13 @@ impl AssertionType for BrewBundle {
Ok(Status::Ok)
}
+ #[cfg(not(target_os = "macos"))]
fn install(&self) -> Result<()> {
- // Assert platform is Darwin (macOS)
- #[cfg(not(target_os = "macos"))]
- return Err(eyre!("brew-bundle only supported on Darwin/macOS"));
+ Err(eyre!("brew-bundle only supported on Darwin/macOS"))
+ }
- // Assert brew binary exists
+ #[cfg(target_os = "macos")]
+ fn install(&self) -> Result<()> {
Utils::assert_bin("brew")?;
let file_arg = format!("--file={}", self.brewfile);
diff --git a/frork-cli/src/main.rs b/frork-cli/src/main.rs
index 940caed..bb1479d 100644
--- a/frork-cli/src/main.rs
+++ b/frork-cli/src/main.rs
@@ -2,17 +2,28 @@ mod assertions;
mod errors;
mod utils;
-use assertions::{
- AssertionType, AssertionTypeFactory, Brew, BrewBundle, Debug, Directory, Git, LuaAssertion,
- LuaAssertionType, Status, Symlink, TypedFactory,
-};
-use clap::{Parser, Subcommand};
-use color_eyre::{Result, eyre::eyre};
-use errors::FrorkError;
-use mlua::prelude::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
+
+use assertions::AssertionType;
+use assertions::AssertionTypeFactory;
+use assertions::Brew;
+use assertions::BrewBundle;
+use assertions::Debug;
+use assertions::Directory;
+use assertions::Git;
+use assertions::LuaAssertion;
+use assertions::LuaAssertionType;
+use assertions::Status;
+use assertions::Symlink;
+use assertions::TypedFactory;
+use clap::Parser;
+use clap::Subcommand;
+use color_eyre::Result;
+use color_eyre::eyre::eyre;
+use errors::FrorkError;
+use mlua::prelude::*;
use tracing::info;
use utils::Utils;
diff --git a/justfile b/justfile
index d9b3eab..f7b8039 100644
--- a/justfile
+++ b/justfile
@@ -4,10 +4,10 @@ fmt:
cargo fmt --all
check:
- cargo check --workspace
+ cargo check -p frork-cli
clippy:
- cargo clippy --workspace -- -D warnings
+ cargo clippy -p frork-cli -- -D warnings
coverage:
#!/usr/bin/env bash
@@ -16,7 +16,7 @@ coverage:
export CARGO_TARGET_DIR="target/coverage"
export LLVM_PROFILE_FILE="target/coverage/profraw/%p-%m.profraw"
rm -rf target/coverage
- cargo test --workspace -q
+ cargo test -p frork-cli -q
REPORT=$(grcov target/coverage/profraw \
--binary-path ./target/coverage/debug/ \
-s . \
@@ -24,7 +24,7 @@ coverage:
--ignore-not-existing \
--keep-only 'src/**' \
--ignore 'src/bin/**' \
- --excl-line 'cov-excl-line' \
+ --excl-line 'cov-excl-line|unreachable!' \
--excl-start 'cov-excl-start' \
--excl-stop 'cov-excl-stop')
echo "$REPORT" | jq -r '
@@ -44,6 +44,17 @@ coverage:
# exit 1
# fi
+mutants:
+ #!/usr/bin/env bash
+ set -uo pipefail
+ cargo mutants -p frork-cli --timeout-multiplier 3 -j4
+ rc=$?
+ # 0 = all caught, 3 = timeouts (infinite loops from mutants, still caught)
+ if [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; then
+ exit 0
+ fi
+ exit "$rc"
+
all: fmt clippy coverage
install: