Skip to content

Commit 11f31d0

Browse files
committed
cargo-rail: better 'init' testing; end to end testing; improved error messages. fixed 'audit/deny' in justfile
1 parent 001c265 commit 11f31d0

File tree

9 files changed

+492
-25
lines changed

9 files changed

+492
-25
lines changed

justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ check:
99
cargo fmt --all
1010
cargo check --workspace --all-targets --all-features
1111
cargo clippy --workspace --all-targets --all-features --fix --allow-dirty -- -D warnings
12-
# cargo deny check all
12+
cargo deny check all
1313
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features
14-
# cargo audit
14+
cargo audit
1515
@echo "✅ All checks passed!"
1616

1717
ci-check:

src/commands/init.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,109 @@ fn write_config_file(config_path: &Path, content: &str) -> RailResult<()> {
616616
Ok(())
617617
}
618618

619+
/// Standalone init that doesn't require WorkspaceContext
620+
///
621+
/// This is used when init is called on a directory that may not have
622+
/// a valid Cargo workspace yet (e.g., empty workspace or invalid state).
623+
pub fn run_init_standalone(
624+
workspace_root: &Path,
625+
output_path: &str,
626+
force: bool,
627+
_non_interactive: bool,
628+
dry_run: bool,
629+
) -> RailResult<()> {
630+
let config_path = workspace_root.join(output_path);
631+
632+
// 1. Check for existing config
633+
if let Some(existing) = check_existing_config(workspace_root) {
634+
if !force {
635+
return Err(RailError::with_help(
636+
format!(
637+
"Configuration already exists at: {}\nUse --force to overwrite or --output to specify a different location",
638+
existing.display()
639+
),
640+
"Example: cargo rail init --force",
641+
));
642+
}
643+
if !dry_run {
644+
println!("⚠️ Overwriting existing config at: {}", existing.display());
645+
}
646+
}
647+
648+
// 2. Detection phase
649+
println!("🔍 Detecting workspace configuration...\n");
650+
651+
let toolchain_config = detect_toolchain_config(workspace_root)?;
652+
let policy_config = detect_policy_config(workspace_root)?;
653+
654+
// Display detected settings
655+
println!(" Toolchain: {} ({})", toolchain_config.channel, toolchain_config.profile);
656+
if let Some(ref resolver) = policy_config.resolver {
657+
println!(" Resolver: {}", resolver);
658+
}
659+
if let Some(ref edition) = policy_config.edition {
660+
println!(" Edition: {}", edition);
661+
}
662+
if let Some(ref msrv) = policy_config.msrv {
663+
println!(" MSRV: {}", msrv);
664+
}
665+
println!();
666+
667+
// 3. Build config
668+
let config = RailConfig {
669+
workspace: WorkspaceConfig {
670+
root: PathBuf::from("."),
671+
},
672+
toolchain: toolchain_config,
673+
policy: policy_config,
674+
unify: UnifyConfig {
675+
use_all_features: true,
676+
sync_on_unify: true,
677+
validate_targets: vec![],
678+
max_parallel_jobs: 0,
679+
pin_transitives: false,
680+
pin_hosts: vec![],
681+
},
682+
security: SecurityConfig {
683+
ssh_key_path: None,
684+
signing_key_path: None,
685+
require_signed_commits: false,
686+
pr_branch_pattern: "rail/sync/{crate}/{timestamp}".to_string(),
687+
protected_branches: vec!["main".to_string(), "master".to_string()],
688+
},
689+
splits: vec![],
690+
};
691+
692+
// 4. Serialize with rich comments
693+
let config_toml = serialize_config_with_comments(&config)?;
694+
695+
// 5. Output
696+
if dry_run {
697+
println!("--- {} ---", output_path);
698+
println!("{}", config_toml);
699+
println!("\n✅ Dry-run complete (no files written)");
700+
} else {
701+
// Create parent directory if needed
702+
if let Some(parent) = config_path.parent() {
703+
fs::create_dir_all(parent).map_err(|e| {
704+
RailError::with_help(
705+
format!("Failed to create directory {}: {}", parent.display(), e),
706+
"Check file permissions",
707+
)
708+
})?;
709+
}
710+
711+
write_config_file(&config_path, &config_toml)?;
712+
println!("✅ Created {}", config_path.display());
713+
println!("\nNext steps:");
714+
println!(" 1. Review and customize {}", output_path);
715+
println!(" 2. Run `cargo rail unify` to normalize dependencies");
716+
println!(" 3. Run `cargo rail test` for change-based testing");
717+
}
718+
719+
Ok(())
720+
}
721+
619722
#[cfg(test)]
620723
mod tests {
621724
use super::*;

src/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub mod watch;
3232

3333
pub use affected::run_affected;
3434
pub use config_sync::run_config_sync;
35-
pub use init::run_init;
35+
pub use init::{run_init, run_init_standalone};
3636
pub use split::run_split;
3737
pub use status::run_status;
3838
pub use sync::run_sync;

src/commands/unify.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ pub fn run_unify_check(
511511
cfg.toolchain.targets.clone()
512512
} else {
513513
println!("\n⚠️ --validate-targets flag set but no rail.toml found");
514-
println!("Create .config/rail.toml with [toolchain.targets] to enable validation.");
514+
println!("Run 'cargo rail init' to create a configuration file with [toolchain.targets].");
515515
return Ok(());
516516
};
517517

src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ impl fmt::Display for ConfigError {
233233
ConfigError::NotFound { workspace_root } => {
234234
write!(
235235
f,
236-
"No cargo-rail configuration found.\nExpected file: {}/.rail/config.toml",
236+
"No cargo-rail configuration found in: {}\nSearched: rail.toml, .rail.toml, .cargo/rail.toml, .config/rail.toml\nRun 'cargo rail init' to create one.",
237237
workspace_root.display()
238238
)
239239
}

src/main.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ fn get_styles() -> clap::builder::Styles {
231231
fn main() {
232232
let CargoCli::Rail(cli) = CargoCli::parse();
233233

234-
// Build workspace context once (single-load pattern)
234+
// Get workspace root
235235
let workspace_root = match std::env::current_dir() {
236236
Ok(dir) => dir,
237237
Err(e) => {
@@ -240,6 +240,22 @@ fn main() {
240240
}
241241
};
242242

243+
// Handle init command specially - it doesn't require a valid workspace
244+
if let Commands::Init {
245+
output,
246+
force,
247+
non_interactive,
248+
dry_run,
249+
} = cli.command
250+
{
251+
let result = commands::run_init_standalone(&workspace_root, &output, force, non_interactive, dry_run);
252+
if let Err(e) = result {
253+
handle_error(e);
254+
}
255+
return;
256+
}
257+
258+
// Build workspace context once (single-load pattern) for all other commands
243259
let ctx = match workspace::WorkspaceContext::build(&workspace_root) {
244260
Ok(ctx) => ctx,
245261
Err(e) => {
@@ -286,13 +302,8 @@ fn main() {
286302
}
287303
}
288304

289-
// Configuration Management
290-
Commands::Init {
291-
output,
292-
force,
293-
non_interactive,
294-
dry_run,
295-
} => commands::run_init(&ctx, &output, force, non_interactive, dry_run),
305+
// Configuration Management (Init is handled above before building WorkspaceContext)
306+
Commands::Init { .. } => unreachable!("Init command should be handled earlier"),
296307

297308
// Dependency Unification
298309
Commands::Unify(unify_cmd) => match unify_cmd {

tests/integration/helpers.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ pub struct TestWorkspace {
1414
impl TestWorkspace {
1515
/// Create a new test workspace with basic structure
1616
pub fn new() -> Result<Self> {
17-
let root = TempDir::new()?;
17+
Self::new_named("test-workspace")
18+
}
19+
20+
/// Create a new test workspace with specific name
21+
pub fn new_named(name: &str) -> Result<Self> {
22+
let root = TempDir::new_in(std::env::temp_dir())
23+
.with_context(|| format!("Failed to create temp dir for test workspace '{}'", name))?;
1824
let path = root.path().to_path_buf();
1925

2026
// Initialize git repo with main as default branch
@@ -161,6 +167,15 @@ mod tests {{
161167
pub fn read_file(&self, path: &str) -> Result<String> {
162168
Ok(std::fs::read_to_string(self.path.join(path))?)
163169
}
170+
171+
/// Remove the rail.toml config file (useful for init tests)
172+
pub fn remove_config(&self) -> Result<()> {
173+
let config_path = self.path.join(".config/rail.toml");
174+
if config_path.exists() {
175+
std::fs::remove_file(config_path)?;
176+
}
177+
Ok(())
178+
}
164179
}
165180

166181
/// Run git command in a directory
@@ -189,16 +204,11 @@ pub fn run_cargo_rail(cwd: &Path, args: &[&str]) -> Result<Output> {
189204
.output()
190205
.context("Failed to run cargo-rail")?;
191206

192-
if !output.status.success() {
193-
let stderr = String::from_utf8_lossy(&output.stderr);
194-
let stdout = String::from_utf8_lossy(&output.stdout);
195-
anyhow::bail!(
196-
"cargo-rail command failed: cargo rail {}\nstdout: {}\nstderr: {}",
197-
args.join(" "),
198-
stdout,
199-
stderr
200-
);
201-
}
202-
203207
Ok(output)
204208
}
209+
210+
/// Load RailConfig from a workspace
211+
pub fn load_rail_config(workspace_root: &Path) -> Result<cargo_rail::config::RailConfig> {
212+
cargo_rail::config::RailConfig::load(workspace_root)
213+
.context("Failed to load rail.toml configuration")
214+
}

tests/integration/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod helpers;
22
mod test_affected;
33
mod test_classification;
44
mod test_git_notes;
5+
mod test_init;
56
mod test_runner;
67
mod test_split;
78
mod test_sync;

0 commit comments

Comments
 (0)