Skip to content

Commit 4c3970a

Browse files
committed
cargo-rail: more cleaning; new change detection for split/sync/test runners; added test infrastructure to catch weird cross platform issues early
1 parent 03632d2 commit 4c3970a

File tree

17 files changed

+1030
-575
lines changed

17 files changed

+1030
-575
lines changed

src/cargo/manifest.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,13 @@ impl CargoTransform {
396396
// Replace dependency with workspace inheritance
397397
deps.insert(dep_name, toml_edit::Item::Value(new_dep.into()));
398398

399+
// Add trailing comment to mark this as unified
400+
if let Some(item) = deps.get_mut(dep_name)
401+
&& let Some(value) = item.as_value_mut()
402+
{
403+
value.decor_mut().set_suffix(" # unified by cargo-rail\n");
404+
}
405+
399406
// Write back to file
400407
std::fs::write(member_toml_path, doc.to_string()).context("Failed to write member Cargo.toml")?;
401408

src/cargo/metadata.rs

Lines changed: 152 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,40 @@ impl WorkspaceMetadata {
8383
self.metadata.resolve.as_ref()
8484
}
8585

86-
/// Get all features defined by a package
86+
/// Get all features defined by a package.
8787
///
88-
/// Returns map of feature_name -> Vec of required features
88+
/// Returns map of feature_name -> Vec of required features.
8989
///
90-
/// TODO: Future feature - will be used for:
91-
/// - Feature audit: `cargo rail audit features` to detect unused features
92-
/// - Feature analysis: understanding feature dependencies
90+
/// # Future Use
91+
/// Will power feature analysis and auditing:
92+
/// - `cargo rail audit features`: Detect unused or redundant features
93+
/// - Feature dependency analysis: Understand feature activation chains
94+
/// - Feature optimization: Identify minimal feature sets for build times
95+
///
96+
/// # Example
97+
/// ```ignore
98+
/// let features = metadata.package_features("serde")?;
99+
/// // Returns: {"derive": ["serde_derive"], "std": [], ...}
100+
/// ```
93101
#[allow(dead_code)]
94102
pub fn package_features(&self, name: &str) -> Option<&BTreeMap<String, Vec<String>>> {
95103
self.get_package(name).map(|pkg| &pkg.features)
96104
}
97105

98-
/// Get all targets (lib, bin, test, etc.) for a package
106+
/// Get all targets (lib, bin, test, etc.) for a package.
107+
///
108+
/// # Future Use
109+
/// Will enable smart testing and target filtering:
110+
/// - Quality engine: Only test packages that have test targets
111+
/// - `cargo rail test --bins-only`: Run tests only for binary targets
112+
/// - `cargo rail build --lib`: Build only library targets
113+
/// - Target coverage: Ensure all crates have appropriate targets
99114
///
100-
/// TODO: Future feature - will be used for:
101-
/// - Quality engine: only test packages that have test targets
102-
/// - Target filtering: `cargo rail test --bins-only`
115+
/// # Example
116+
/// ```ignore
117+
/// let targets = metadata.package_targets("cargo-rail");
118+
/// let has_tests = targets.iter().any(|t| t.is_test());
119+
/// ```
103120
#[allow(dead_code)]
104121
pub fn package_targets(&self, name: &str) -> Vec<&Target> {
105122
self
@@ -108,21 +125,38 @@ impl WorkspaceMetadata {
108125
.unwrap_or_default()
109126
}
110127

111-
/// Get package edition (2015, 2018, 2021, 2024)
128+
/// Get package edition (2015, 2018, 2021, 2024).
112129
///
113-
/// TODO: Wire into unify.rs - will be used for:
114-
/// - Edition compatibility validation before unification
115-
/// - Warning if deps have edition conflicts
130+
/// # Future Use
131+
/// **Ready to wire into unify.rs** for edition compatibility validation:
132+
/// - Pre-unification check: Warn if dependencies have edition conflicts
133+
/// - Migration planning: Identify crates stuck on old editions
134+
/// - Compatibility matrix: Track edition usage across workspace
135+
///
136+
/// # Example
137+
/// ```ignore
138+
/// if metadata.package_edition("lib-core")? == "2021" {
139+
/// // Safe to use 2021 edition features
140+
/// }
141+
/// ```
116142
#[allow(dead_code)]
117143
pub fn package_edition(&self, name: &str) -> Option<&str> {
118144
self.get_package(name).map(|pkg| pkg.edition.as_str())
119145
}
120146

121-
/// Get package MSRV (minimum supported Rust version)
147+
/// Get package MSRV (minimum supported Rust version).
148+
///
149+
/// # Future Use
150+
/// **Ready to wire into unify.rs** for MSRV validation:
151+
/// - Pre-unification check: Ensure dependencies are MSRV-compatible
152+
/// - Workspace MSRV policy: Enforce minimum Rust version across crates
153+
/// - CI matrix: Generate test matrix based on supported Rust versions
122154
///
123-
/// TODO: Wire into unify.rs - will be used for:
124-
/// - MSRV validation before unification
125-
/// - Warning if deps have MSRV conflicts
155+
/// # Example
156+
/// ```ignore
157+
/// let msrv = metadata.package_rust_version("lib-core")?;
158+
/// // Returns: Some(Version { major: 1, minor: 76, patch: 0 })
159+
/// ```
126160
#[allow(dead_code)]
127161
pub fn package_rust_version(&self, name: &str) -> Option<&Version> {
128162
self.get_package(name).and_then(|pkg| pkg.rust_version.as_ref())
@@ -132,11 +166,20 @@ impl WorkspaceMetadata {
132166
// Tier 3: Advanced Dependency Analysis
133167
// ============================================================================
134168

135-
/// Get all dependencies for a package (normal + dev + build)
169+
/// Get all dependencies for a package (normal + dev + build).
136170
///
137-
/// TODO: Future feature - will be used for:
138-
/// - Quality engine: comprehensive dependency audit
139-
/// - Dependency graph: full dep tree visualization
171+
/// # Future Use
172+
/// Will enable comprehensive dependency analysis:
173+
/// - Quality engine: Full dependency audit and vulnerability scanning
174+
/// - Dependency graph: Visualize complete dependency tree
175+
/// - License compliance: Check all dependency licenses
176+
/// - Duplicate detection: Find dependencies listed multiple times
177+
///
178+
/// # Example
179+
/// ```ignore
180+
/// let deps = metadata.package_dependencies("cargo-rail");
181+
/// println!("Total dependencies: {}", deps.len());
182+
/// ```
140183
#[allow(dead_code)]
141184
pub fn package_dependencies(&self, name: &str) -> Vec<&Dependency> {
142185
self
@@ -145,11 +188,23 @@ impl WorkspaceMetadata {
145188
.unwrap_or_default()
146189
}
147190

148-
/// Get dependencies of a specific kind (normal, dev, or build)
191+
/// Get dependencies of a specific kind (normal, dev, or build).
192+
///
193+
/// # Future Use
194+
/// Will enable targeted dependency analysis:
195+
/// - Quality engine: "Find all dev-only dependencies"
196+
/// - Audit: Detect mis-categorized dependencies (prod code using dev deps)
197+
/// - Build optimization: Separate build-time vs runtime dependencies
198+
/// - Release validation: Ensure dev deps aren't leaked into releases
149199
///
150-
/// TODO: Future feature - will be used for:
151-
/// - Quality engine: "find all dev-only deps"
152-
/// - Audit: detect mis-categorized dependencies
200+
/// # Example
201+
/// ```ignore
202+
/// use cargo_metadata::DependencyKind;
203+
/// let dev_deps = metadata.package_dependencies_by_kind(
204+
/// "cargo-rail",
205+
/// DependencyKind::Development
206+
/// );
207+
/// ```
153208
#[allow(dead_code)]
154209
pub fn package_dependencies_by_kind(&self, name: &str, kind: DependencyKind) -> Vec<&Dependency> {
155210
self
@@ -159,11 +214,20 @@ impl WorkspaceMetadata {
159214
.collect()
160215
}
161216

162-
/// Get all optional dependencies for a package
217+
/// Get all optional dependencies for a package.
218+
///
219+
/// # Future Use
220+
/// Will power optional dependency management:
221+
/// - Quality engine: Validate optional deps are feature-gated
222+
/// - Feature analysis: Map features to their optional dependencies
223+
/// - Documentation: Auto-generate feature documentation
224+
/// - Build variants: Create minimal builds by excluding optional deps
163225
///
164-
/// TODO: Future feature - will be used for:
165-
/// - Quality engine: optional dependency management
166-
/// - Feature analysis: map features to optional deps
226+
/// # Example
227+
/// ```ignore
228+
/// let optional = metadata.package_optional_dependencies("serde");
229+
/// // Returns deps like "serde_derive" (optional feature)
230+
/// ```
167231
#[allow(dead_code)]
168232
pub fn package_optional_dependencies(&self, name: &str) -> Vec<&Dependency> {
169233
self
@@ -173,11 +237,21 @@ impl WorkspaceMetadata {
173237
.collect()
174238
}
175239

176-
/// Check if a dependency uses default features
240+
/// Check if a dependency uses default features.
177241
///
178-
/// TODO: Wire into unify.rs - will be used for:
179-
/// - Smarter default_features handling in unification
180-
/// - Better feature merging logic
242+
/// # Future Use
243+
/// **Ready to wire into unify.rs** for smarter feature handling:
244+
/// - Smarter default_features handling during unification
245+
/// - Better feature merging logic: Respect default_features = false
246+
/// - Feature conflict resolution: Detect incompatible feature sets
247+
/// - Build optimization: Minimize features when default_features = false
248+
///
249+
/// # Example
250+
/// ```ignore
251+
/// if metadata.dependency_uses_default_features("lib-core", "serde")? {
252+
/// println!("Using serde with default features");
253+
/// }
254+
/// ```
181255
#[allow(dead_code)]
182256
pub fn dependency_uses_default_features(&self, package: &str, dep_name: &str) -> Option<bool> {
183257
self
@@ -187,11 +261,22 @@ impl WorkspaceMetadata {
187261
.map(|dep| dep.uses_default_features)
188262
}
189263

190-
/// Get platform-specific dependencies (e.g., only on Windows, Unix, etc.)
264+
/// Get platform-specific dependencies (e.g., only on Windows, Unix, etc.).
265+
///
266+
/// Returns tuples of (Dependency, target_spec) for platform-conditional deps.
267+
///
268+
/// # Future Use
269+
/// Will enable cross-platform analysis:
270+
/// - Quality engine: Build platform compatibility matrix
271+
/// - Cross-platform testing: Detect platform-specific dependency issues
272+
/// - CI optimization: Only test relevant platforms for changed deps
273+
/// - Documentation: Auto-generate platform-specific setup guides
191274
///
192-
/// TODO: Future feature - will be used for:
193-
/// - Quality engine: platform compatibility matrix
194-
/// - Cross-platform testing: detect platform-specific issues
275+
/// # Example
276+
/// ```ignore
277+
/// let platform_deps = metadata.package_platform_specific_dependencies("tokio");
278+
/// // Returns: [(winapi, "cfg(windows)"), (libc, "cfg(unix)"), ...]
279+
/// ```
195280
#[allow(dead_code)]
196281
pub fn package_platform_specific_dependencies(&self, name: &str) -> Vec<(&Dependency, String)> {
197282
self
@@ -201,11 +286,21 @@ impl WorkspaceMetadata {
201286
.collect()
202287
}
203288

204-
/// Get all packages (workspace members + dependencies)
289+
/// Get all packages (workspace members + dependencies).
205290
///
206-
/// TODO: Future feature - will be used for:
207-
/// - Graph queries: include external deps in dependency graph
208-
/// - Full workspace analysis: not just workspace members
291+
/// # Future Use
292+
/// Will enable full workspace + dependency tree analysis:
293+
/// - Graph queries: Include external deps in dependency visualization
294+
/// - Full workspace analysis: Audit not just workspace but all transitive deps
295+
/// - Dependency tree: Build complete dependency graph with versions
296+
/// - Supply chain security: Analyze entire dependency chain
297+
///
298+
/// # Example
299+
/// ```ignore
300+
/// let all = metadata.all_packages();
301+
/// let external_count = all.len() - metadata.list_crates().len();
302+
/// println!("External dependencies: {}", external_count);
303+
/// ```
209304
#[allow(dead_code)]
210305
pub fn all_packages(&self) -> &[Package] {
211306
&self.metadata.packages
@@ -263,11 +358,23 @@ impl WorkspaceMetadata {
263358
&self.metadata
264359
}
265360

266-
/// Get raw JSON string for external tools
361+
/// Get raw JSON string for external tools.
362+
///
363+
/// Serializes the complete cargo metadata to JSON format.
364+
///
365+
/// # Future Use
366+
/// Will enable external tooling integration and debugging:
367+
/// - External tooling: Pass metadata to other cargo ecosystem tools
368+
/// - Debugging: Export full metadata for inspection and issue reporting
369+
/// - Custom analysis: Process metadata with external scripts/tools
370+
/// - CI integration: Export metadata for build system consumption
267371
///
268-
/// TODO: Future feature - will be used for:
269-
/// - External tooling integration: Pass metadata to other tools
270-
/// - Debugging: Export full metadata for inspection
372+
/// # Example
373+
/// ```ignore
374+
/// let json = metadata.to_json_string()?;
375+
/// std::fs::write("metadata.json", json)?;
376+
/// // Use with: cargo metadata | jq .packages
377+
/// ```
271378
#[allow(dead_code)]
272379
pub fn to_json_string(&self) -> RailResult<String> {
273380
serde_json::to_string(&self.metadata)

src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ pub mod config_sync;
2323
pub mod split;
2424
pub mod status;
2525
pub mod sync;
26+
pub mod test;
2627
pub mod unify;
2728

2829
pub use affected::run_affected;
2930
pub use config_sync::run_config_sync;
3031
pub use split::run_split;
3132
pub use status::run_status;
3233
pub use sync::run_sync;
34+
pub use test::run_test;
3335
pub use unify::{run_unify_analyze, run_unify_apply, run_unify_check};

src/commands/test.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//! Smart test runner - only test affected crates
2+
3+
use crate::error::RailResult;
4+
use crate::workspace::{ChangeImpact, WorkspaceContext};
5+
use std::process::Command;
6+
7+
/// Run tests only for crates affected by changes since a given ref
8+
pub fn run_test(ctx: &WorkspaceContext, since: String, test_args: Vec<String>) -> RailResult<()> {
9+
let analyzer = ChangeImpact::new(ctx);
10+
11+
// Analyze changes since the given ref
12+
let impact = analyzer.analyze_changes(&since, "HEAD")?;
13+
14+
// Get minimal test set
15+
let test_targets = impact.minimal_test_set();
16+
17+
if test_targets.is_empty() {
18+
println!("✓ No affected crates - all tests skipped");
19+
return Ok(());
20+
}
21+
22+
println!("Running tests for {} affected crate(s):", test_targets.len());
23+
for target in &test_targets {
24+
println!(" • {}", target);
25+
}
26+
println!();
27+
28+
// Build cargo test command with package filters
29+
let mut cmd = Command::new("cargo");
30+
cmd.arg("test");
31+
32+
// Add package filters for each affected crate
33+
for target in &test_targets {
34+
cmd.arg("-p").arg(target);
35+
}
36+
37+
// Add user-provided test arguments
38+
if !test_args.is_empty() {
39+
cmd.arg("--");
40+
cmd.args(&test_args);
41+
}
42+
43+
// Run the test command
44+
let status = cmd
45+
.status()
46+
.map_err(|e| crate::error::RailError::message(format!("Failed to run cargo test: {}", e)))?;
47+
48+
if !status.success() {
49+
std::process::exit(status.code().unwrap_or(1));
50+
}
51+
52+
println!("\n✓ All affected tests passed");
53+
54+
Ok(())
55+
}

0 commit comments

Comments
 (0)