Skip to content

Commit 23cb6d3

Browse files
committed
cargo-rail: working on the changelogs; testing; DX/UX for v1
1 parent 12a33c1 commit 23cb6d3

File tree

10 files changed

+391
-89
lines changed

10 files changed

+391
-89
lines changed

src/commands/test.rs

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ use crate::workspace::{ChangeImpact, WorkspaceContext};
1010
pub struct TestConfig {
1111
/// Git ref to compare against (if None, auto-detect)
1212
pub since: Option<String>,
13-
/// Show what would be tested without running tests
14-
pub dry_run: bool,
1513
/// Explain why tests are being run
1614
pub explain: bool,
1715
/// Prefer cargo-nextest if available
@@ -42,60 +40,52 @@ pub fn run_test(ctx: &WorkspaceContext, config: TestConfig) -> RailResult<()> {
4240
return Ok(());
4341
}
4442

45-
// Dry run mode - just show what would be tested
46-
if config.dry_run {
47-
println!("Would test {} affected crate(s):", test_targets.len());
48-
for target in &test_targets {
49-
println!(" • {}", target);
43+
if config.explain {
44+
println!("Change Impact Analysis:");
45+
println!(" Total files changed: {}", impact.changed_files.len());
46+
println!(
47+
" Requires rebuild: {}",
48+
if impact.requires_rebuild { "yes" } else { "no" }
49+
);
50+
println!(
51+
" Requires retest: {}",
52+
if impact.requires_retest { "yes" } else { "no" }
53+
);
54+
55+
println!("\nFile Breakdown:");
56+
if impact.categories.has_source_changes() {
57+
println!(" - Source code: {} file(s)", impact.categories.source_files.len());
58+
}
59+
if impact.categories.has_test_changes() {
60+
println!(" - Tests: {} file(s)", impact.categories.test_files.len());
61+
}
62+
if impact.categories.has_example_changes() {
63+
println!(" - Examples: {} file(s)", impact.categories.example_files.len());
64+
}
65+
if impact.categories.has_config_changes() {
66+
println!(" - Config: {} file(s)", impact.categories.config_files.len());
67+
}
68+
if !impact.categories.build_scripts.is_empty() {
69+
println!(" - Build scripts: {} file(s)", impact.categories.build_scripts.len());
70+
}
71+
if !impact.categories.doc_files.is_empty() {
72+
println!(" - Documentation: {} file(s)", impact.categories.doc_files.len());
5073
}
5174

52-
if config.explain {
53-
println!("\nChange Impact Analysis:");
54-
println!(" Total files changed: {}", impact.changed_files.len());
55-
println!(
56-
" Requires rebuild: {}",
57-
if impact.requires_rebuild { "yes" } else { "no" }
58-
);
59-
println!(
60-
" Requires retest: {}",
61-
if impact.requires_retest { "yes" } else { "no" }
62-
);
63-
64-
println!("\nFile Breakdown:");
65-
if impact.categories.has_source_changes() {
66-
println!(" - Source code: {} file(s)", impact.categories.source_files.len());
67-
}
68-
if impact.categories.has_test_changes() {
69-
println!(" - Tests: {} file(s)", impact.categories.test_files.len());
70-
}
71-
if impact.categories.has_example_changes() {
72-
println!(" - Examples: {} file(s)", impact.categories.example_files.len());
73-
}
74-
if impact.categories.has_config_changes() {
75-
println!(" - Config: {} file(s)", impact.categories.config_files.len());
76-
}
77-
if !impact.categories.build_scripts.is_empty() {
78-
println!(" - Build scripts: {} file(s)", impact.categories.build_scripts.len());
79-
}
80-
if !impact.categories.doc_files.is_empty() {
81-
println!(" - Documentation: {} file(s)", impact.categories.doc_files.len());
82-
}
83-
84-
println!("\nAffected Crates:");
85-
println!(" - Direct: {} crate(s)", impact.direct_crates.len());
86-
if !impact.direct_crates.is_empty() {
87-
for crate_name in &impact.direct_crates {
88-
println!(" • {}", crate_name);
89-
}
75+
println!("\nAffected Crates:");
76+
println!(" - Direct: {} crate(s)", impact.direct_crates.len());
77+
if !impact.direct_crates.is_empty() {
78+
for crate_name in &impact.direct_crates {
79+
println!(" • {}", crate_name);
9080
}
91-
println!(" - Transitive: {} crate(s)", impact.transitive_crates.len());
92-
if !impact.transitive_crates.is_empty() {
93-
for crate_name in &impact.transitive_crates {
94-
println!(" • {}", crate_name);
95-
}
81+
}
82+
println!(" - Transitive: {} crate(s)", impact.transitive_crates.len());
83+
if !impact.transitive_crates.is_empty() {
84+
for crate_name in &impact.transitive_crates {
85+
println!(" • {}", crate_name);
9686
}
9787
}
98-
return Ok(());
88+
println!();
9989
}
10090

10191
// Select test runner

src/commands/watch.rs

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,22 @@ fn run_with_cargo_watch(ctx: &WorkspaceContext, config: TestConfig) -> RailResul
133133
let mut cmd = Command::new("cargo-watch");
134134
cmd.current_dir(ctx.workspace_root());
135135

136-
// cargo-watch -x "rail test --since HEAD~1"
136+
let rail_cmd = format_watch_test_command(&config);
137+
cmd.arg("-x").arg(rail_cmd);
138+
139+
let status = cmd
140+
.status()
141+
.map_err(|e| crate::error::RailError::message(format!("Failed to run cargo-watch: {}", e)))?;
142+
143+
if !status.success() {
144+
std::process::exit(status.code().unwrap_or(1));
145+
}
146+
147+
Ok(())
148+
}
149+
150+
/// Build the inner `cargo rail test ...` command string for watch integrations.
151+
fn format_watch_test_command(config: &TestConfig) -> String {
137152
let mut rail_cmd = String::from("rail test");
138153

139154
if let Some(ref since) = config.since {
@@ -149,17 +164,7 @@ fn run_with_cargo_watch(ctx: &WorkspaceContext, config: TestConfig) -> RailResul
149164
rail_cmd.push_str(&config.test_args.join(" "));
150165
}
151166

152-
cmd.arg("-x").arg(rail_cmd);
153-
154-
let status = cmd
155-
.status()
156-
.map_err(|e| crate::error::RailError::message(format!("Failed to run cargo-watch: {}", e)))?;
157-
158-
if !status.success() {
159-
std::process::exit(status.code().unwrap_or(1));
160-
}
161-
162-
Ok(())
167+
rail_cmd
163168
}
164169

165170
#[cfg(test)]
@@ -197,4 +202,20 @@ mod tests {
197202
}
198203
}
199204
}
205+
206+
#[test]
207+
fn test_format_watch_test_command_includes_flags_and_args() {
208+
let cfg = TestConfig {
209+
since: Some("main".to_string()),
210+
explain: false,
211+
prefer_nextest: true,
212+
test_args: vec!["--nocapture".into(), "some::test".into()],
213+
};
214+
215+
let cmd = format_watch_test_command(&cfg);
216+
assert!(cmd.starts_with("rail test"));
217+
assert!(cmd.contains("--since main"));
218+
assert!(cmd.contains("--nextest"));
219+
assert!(cmd.contains("-- --nocapture some::test"));
220+
}
200221
}

src/main.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,6 @@ enum Commands {
5151
/// Use cargo-nextest if available
5252
#[arg(long)]
5353
nextest: bool,
54-
/// Show what would be tested without running tests
55-
#[arg(long)]
56-
dry_run: bool,
5754
/// Explain why tests are being run
5855
#[arg(long)]
5956
explain: bool,
@@ -329,15 +326,13 @@ fn main() {
329326
Commands::Test {
330327
since,
331328
nextest,
332-
dry_run,
333329
explain,
334330
watch,
335331
watch_mode,
336332
test_args,
337333
} => {
338334
let config = commands::test::TestConfig {
339335
since,
340-
dry_run,
341336
explain,
342337
prefer_nextest: nextest,
343338
test_args,

src/release/changelog.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,11 @@ fn extract_pr_numbers(text: &str) -> Vec<u32> {
169169

170170
for word in text.split_whitespace() {
171171
if let Some(num_str) = word.strip_prefix("(#").and_then(|s| s.strip_suffix(')'))
172-
&& let Ok(num) = num_str.parse::<u32>() {
173-
prs.push(num);
174-
continue;
175-
}
172+
&& let Ok(num) = num_str.parse::<u32>()
173+
{
174+
prs.push(num);
175+
continue;
176+
}
176177

177178
if let Some(num_str) = word.strip_prefix('#') {
178179
let numeric = num_str.trim_end_matches(|c: char| !c.is_ascii_digit());

tests/integration/helpers.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,35 @@ mod tests {{
134134
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
135135
}
136136

137+
/// Set origin remote (useful for link generation)
138+
pub fn set_remote(&self, url: &str) -> Result<()> {
139+
git(&self.path, &["remote", "remove", "origin"]).ok();
140+
git(&self.path, &["remote", "add", "origin", url])?;
141+
Ok(())
142+
}
143+
144+
/// Create an annotated tag
145+
pub fn tag(&self, name: &str, message: &str) -> Result<()> {
146+
git(&self.path, &["tag", "-a", name, "-m", message])?;
147+
Ok(())
148+
}
149+
150+
/// Overwrite or create the release config block in .config/rail.toml
151+
pub fn write_release_config(&self, content: &str) -> Result<()> {
152+
let config_path = self.path.join(".config/rail.toml");
153+
let mut existing = std::fs::read_to_string(&config_path)?;
154+
155+
if let Some(idx) = existing.find("[release]") {
156+
existing.truncate(idx);
157+
}
158+
159+
existing.push_str("\n[release]\n");
160+
existing.push_str(content);
161+
162+
std::fs::write(&config_path, existing)?;
163+
Ok(())
164+
}
165+
137166
/// Modify a file in a crate
138167
pub fn modify_file(&self, crate_name: &str, file: &str, content: &str) -> Result<()> {
139168
let file_path = self.path.join("crates").join(crate_name).join(file);

tests/integration/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod test_affected;
33
mod test_classification;
44
mod test_git_notes;
55
mod test_init;
6+
mod test_release_changelog;
67
mod test_runner;
78
mod test_split;
89
mod test_sync;

0 commit comments

Comments
 (0)