Skip to content

Commit f88809f

Browse files
aevyriefslabs-bot[bot]
authored andcommitted
Sort test execution order by change status and transitive dependency count for better test prioritization.
1 parent 5a5cc8a commit f88809f

3 files changed

Lines changed: 108 additions & 7 deletions

File tree

src/commands/check_workspace/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,12 +1246,12 @@ impl<'a, C: CrateChecker> WorkspaceChecker<'a, C> {
12461246
// Check changed from a git pov
12471247
let changed_package_paths =
12481248
crates.changed_packages(&repo, base_commit_id, head_commit_id, &diff_strategy)?;
1249-
tracing::info!("Changed packages: {changed_package_paths:#?}");
1249+
tracing::debug!("Changed packages: {changed_package_paths:#?}");
12501250
// Any packages that transitively depend on changed packages are also considered "changed".
12511251
let changed_closure = crates
12521252
.default_dependency_graph()
12531253
.reverse_closure(changed_package_paths.iter().map(AsRef::as_ref));
1254-
tracing::info!("Changed closure: {changed_closure:#?}");
1254+
tracing::debug!("Changed closure: {changed_closure:#?}");
12551255

12561256
for package_key in package_keys.clone() {
12571257
if let Some(ref pb) = pb {

src/commands/tests/mod.rs

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,110 @@ pub async fn tests(
287287
let semaphore = Arc::new(Semaphore::new(common_options.job_limit));
288288
let mut handles = vec![];
289289

290-
for (_, member) in results.members.into_iter().filter(|(_, member)| {
291-
!member.test_detail.skip.unwrap_or_default() && (member.perform_test || options.run_all)
292-
}) {
290+
// Precompute transitive dependency counts for sorting and display.
291+
let dep_graph = results.crate_graph.dependency_graph();
292+
let transitive_dep_counts: HashMap<String, usize> = results
293+
.members
294+
.values()
295+
.filter_map(|m| {
296+
m.package_id.as_ref().map(|id| {
297+
(
298+
m.package.clone(),
299+
dep_graph.get_transitive_dependencies(id.clone()).len(),
300+
)
301+
})
302+
})
303+
.collect();
304+
305+
// Sort members so directly changed crates run first (fail fast), then
306+
// dependency-changed, then unchanged (only present with --run-all).
307+
// Within each group, crates with more transitive dependencies run first
308+
// (higher in the tree, more likely to surface issues), then alphabetically.
309+
let mut members: Vec<_> = results
310+
.members
311+
.into_values()
312+
.filter(|member| {
313+
!member.test_detail.skip.unwrap_or_default() && (member.perform_test || options.run_all)
314+
})
315+
.collect();
316+
members.sort_by(|a, b| {
317+
let a_deps = transitive_dep_counts.get(&a.package).copied().unwrap_or(0);
318+
let b_deps = transitive_dep_counts.get(&b.package).copied().unwrap_or(0);
319+
b.changed
320+
.cmp(&a.changed)
321+
.then(b.dependencies_changed.cmp(&a.dependencies_changed))
322+
.then(b_deps.cmp(&a_deps))
323+
.then(a.package.cmp(&b.package))
324+
});
325+
326+
// Print execution order grouped by change status.
327+
{
328+
let name_w = members
329+
.iter()
330+
.map(|m| m.package.len())
331+
.max()
332+
.unwrap_or(7)
333+
.max(7);
334+
let deps_w = 4; // "Deps" header
335+
#[expect(clippy::type_complexity)]
336+
let groups: &[(&str, Box<dyn Fn(&super::check_workspace::Result) -> bool>)] = &[
337+
("Directly changed", Box::new(|m| m.changed)),
338+
(
339+
"Dependency changed",
340+
Box::new(|m| !m.changed && m.dependencies_changed),
341+
),
342+
(
343+
"Unchanged",
344+
Box::new(|m| !m.changed && !m.dependencies_changed),
345+
),
346+
];
347+
let active_groups: Vec<_> = groups
348+
.iter()
349+
.filter_map(|(label, pred)| {
350+
let rows: Vec<_> = members.iter().filter(|m| pred(m)).collect();
351+
if rows.is_empty() {
352+
None
353+
} else {
354+
Some((*label, rows))
355+
}
356+
})
357+
.collect();
358+
if !active_groups.is_empty() {
359+
// Column widths including padding: " content "
360+
let nc = name_w + 2; // name column cell width
361+
let dc = deps_w + 2; // deps column cell width
362+
// Total width between outer box chars: nc + 1(│) + dc
363+
let total = nc + 1 + dc;
364+
let nb = "─".repeat(nc);
365+
let db = "─".repeat(dc);
366+
let fb = "─".repeat(total);
367+
let mut table = String::new();
368+
table.push_str(&format!("╭{fb}╮\n"));
369+
for (i, (label, rows)) in active_groups.iter().enumerate() {
370+
if i > 0 {
371+
table.push_str(&format!("├{nb}┴{db}┤\n"));
372+
}
373+
table.push_str(&format!("│ {:<w$}│\n", label, w = total - 1));
374+
table.push_str(&format!("├{nb}┬{db}┤\n"));
375+
table.push_str(&format!(
376+
"│ {:<name_w$} │ {:<deps_w$} │\n",
377+
"Package", "Deps"
378+
));
379+
table.push_str(&format!("├{nb}┼{db}┤\n"));
380+
for m in rows {
381+
let dep_count = transitive_dep_counts.get(&m.package).copied().unwrap_or(0);
382+
table.push_str(&format!(
383+
"│ {:<name_w$} │ {:<deps_w$} │\n",
384+
m.package, dep_count,
385+
));
386+
}
387+
}
388+
table.push_str(&format!("╰{nb}┴{db}╯"));
389+
tracing::info!("Test execution order:\n{table}");
390+
}
391+
}
392+
393+
for member in members {
293394
let common_opts = Arc::new(common_options.clone());
294395
let base_revspec = options
295396
.diff

src/crate_graph.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,11 +487,11 @@ impl DependencyGraph {
487487
deps.push(dep);
488488
reverse_deps.push(node.id.clone());
489489
} else if is_accepted_dep && !me.id_to_path.contains_key(dep_id) {
490-
tracing::warn!(
490+
tracing::trace!(
491491
source_id = %node.id,
492492
dep_id = %dep_id,
493493
dep_name = %node_dep.name,
494-
"dropping dependency edge: dep_id not found in registered workspace packages"
494+
"skipping external dependency edge"
495495
);
496496
}
497497
}

0 commit comments

Comments
 (0)