Skip to content

Commit 988f4c9

Browse files
feat: add moon test --outline
1 parent 1ec9ce9 commit 988f4c9

File tree

10 files changed

+236
-1
lines changed

10 files changed

+236
-1
lines changed

crates/moon/src/cli/test.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ use crate::rr_build::preconfig_compile;
6060
use crate::rr_build::{BuildConfig, CalcUserIntentOutput};
6161
use crate::run::TestFilter;
6262
use crate::run::TestIndex;
63+
use crate::run::TestOutlineEntry;
64+
use crate::run::collect_test_outline;
6365
use crate::run::perform_promotion;
6466

6567
use super::BenchSubcommand;
@@ -93,6 +95,32 @@ fn print_test_summary(total: usize, passed: usize, quiet: bool, backend_hint: Op
9395
}
9496
}
9597

98+
fn print_test_outline(entries: &[TestOutlineEntry]) {
99+
if entries.is_empty() {
100+
eprintln!("{}: no test entry found.", "Warning".yellow().bold());
101+
return;
102+
}
103+
104+
for (i, entry) in entries.iter().enumerate() {
105+
let line = entry
106+
.line_number
107+
.map(|v| v.to_string())
108+
.unwrap_or_else(|| "?".to_string());
109+
let mut line_out = format!(
110+
"{:>4}. {} {}:{} index={}",
111+
i + 1,
112+
entry.package,
113+
entry.file,
114+
line,
115+
entry.index
116+
);
117+
if let Some(name) = &entry.name {
118+
line_out.push_str(&format!(" name={name:?}"));
119+
}
120+
println!("{line_out}");
121+
}
122+
}
123+
96124
/// Test the current package
97125
#[derive(Debug, clap::Parser, Clone)]
98126
pub struct TestSubcommand {
@@ -136,6 +164,10 @@ pub struct TestSubcommand {
136164
#[clap(long)]
137165
pub no_parallelize: bool,
138166

167+
/// Print the outline of tests to be executed and exit
168+
#[clap(long, conflicts_with_all = ["build_only", "update", "test_failure_json"])]
169+
pub outline: bool,
170+
139171
/// Print failure message in JSON format
140172
#[clap(long)]
141173
pub test_failure_json: bool,
@@ -266,6 +298,12 @@ fn run_test_internal(
266298

267299
#[instrument(level = Level::DEBUG, skip_all)]
268300
fn run_test_in_single_file(cli: &UniversalFlags, cmd: &TestSubcommand) -> anyhow::Result<i32> {
301+
if cmd.outline && cli.dry_run {
302+
anyhow::bail!("`--outline` cannot be used with `--dry-run`");
303+
}
304+
if cmd.outline && !cli.unstable_feature.rupes_recta {
305+
anyhow::bail!("`--outline` is only supported with Rupes Recta (-Z rupes_recta)");
306+
}
269307
if cli.unstable_feature.rupes_recta {
270308
return run_test_in_single_file_rr(cli, cmd);
271309
}
@@ -645,6 +683,7 @@ pub(crate) struct TestLikeSubcommand<'a> {
645683
pub auto_sync_flags: &'a AutoSyncFlags,
646684
pub build_only: bool,
647685
pub no_parallelize: bool,
686+
pub outline: bool,
648687
pub test_failure_json: bool,
649688
pub patch_file: &'a Option<PathBuf>,
650689
pub include_skipped: bool,
@@ -667,6 +706,7 @@ impl<'a> From<&'a TestSubcommand> for TestLikeSubcommand<'a> {
667706
auto_sync_flags: &cmd.auto_sync_flags,
668707
build_only: cmd.build_only,
669708
no_parallelize: cmd.no_parallelize,
709+
outline: cmd.outline,
670710
test_failure_json: cmd.test_failure_json,
671711
patch_file: &cmd.patch_file,
672712
include_skipped: cmd.include_skipped,
@@ -689,6 +729,7 @@ impl<'a> From<&'a BenchSubcommand> for TestLikeSubcommand<'a> {
689729
auto_sync_flags: &cmd.auto_sync_flags,
690730
build_only: cmd.build_only,
691731
no_parallelize: cmd.no_parallelize,
732+
outline: false,
692733
test_failure_json: false,
693734
patch_file: &None,
694735
include_skipped: false,
@@ -736,6 +777,12 @@ pub(crate) fn run_test_or_bench_internal(
736777
if cmd.explicit_file_filter.is_some() && (cmd.package.is_some() || cmd.file.is_some()) {
737778
anyhow::bail!("cannot filter package or files when testing a single file in a project");
738779
}
780+
if cmd.outline && cli.dry_run {
781+
anyhow::bail!("`--outline` cannot be used with `--dry-run`");
782+
}
783+
if cmd.outline && !cli.unstable_feature.rupes_recta {
784+
anyhow::bail!("`--outline` is only supported with Rupes Recta (-Z rupes_recta)");
785+
}
739786

740787
debug!(
741788
rupes_recta = cli.unstable_feature.rupes_recta,
@@ -1503,6 +1550,17 @@ fn rr_test_from_plan(
15031550
return Ok(result.return_code_for_success());
15041551
}
15051552

1553+
if cmd.outline {
1554+
let entries = collect_test_outline(
1555+
build_meta,
1556+
&filter,
1557+
cmd.include_skipped,
1558+
cmd.run_mode == RunMode::Bench,
1559+
)?;
1560+
print_test_outline(&entries);
1561+
return Ok(0);
1562+
}
1563+
15061564
if cmd.build_only {
15071565
// Match legacy behavior: create JS wrappers and print test artifacts as JSON
15081566
let test_artifacts = collect_test_artifacts_for_build_only(build_meta, target_dir)?;

crates/moon/src/run/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mod runtest;
2323
mod runtime;
2424

2525
pub use child::run;
26-
pub use runtest::{TestFilter, TestIndex, perform_promotion, run_tests};
26+
pub use runtest::{TestFilter, TestIndex, TestOutlineEntry, collect_test_outline, perform_promotion, run_tests};
2727
pub use runtime::{CommandGuard, command_for};
2828

2929
use std::sync::OnceLock;

crates/moon/src/run/runtest.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ use crate::{rr_build::BuildMeta, run::default_rt};
8787
pub use filter::TestFilter;
8888
pub use promotion::perform_promotion;
8989

90+
#[derive(Debug, Clone)]
91+
pub struct TestOutlineEntry {
92+
pub package: String,
93+
pub file: String,
94+
pub index: u32,
95+
pub name: Option<String>,
96+
pub line_number: Option<usize>,
97+
}
98+
9099
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91100
enum TestResultKind {
92101
Passed,
@@ -382,6 +391,111 @@ impl TargetTestResult {
382391
}
383392
}
384393

394+
fn collect_tests_by_file(
395+
meta: &MooncGenTestInfo,
396+
bench: bool,
397+
) -> IndexMap<String, IndexMap<u32, MbtTestInfo>> {
398+
let mut out: IndexMap<String, IndexMap<u32, MbtTestInfo>> = IndexMap::new();
399+
let lists: Vec<&IndexMap<String, Vec<MbtTestInfo>>> = if bench {
400+
vec![&meta.with_bench_args_tests]
401+
} else {
402+
vec![
403+
&meta.no_args_tests,
404+
&meta.with_args_tests,
405+
&meta.async_tests,
406+
&meta.async_tests_with_args,
407+
]
408+
};
409+
410+
for test_list in lists {
411+
for (file, tests) in test_list {
412+
let entry = out.entry(file.clone()).or_default();
413+
for test in tests {
414+
entry.entry(test.index).or_insert_with(|| test.clone());
415+
}
416+
}
417+
}
418+
419+
out
420+
}
421+
422+
#[instrument(level = "debug", skip(build_meta, filter))]
423+
pub fn collect_test_outline(
424+
build_meta: &BuildMeta,
425+
filter: &TestFilter,
426+
include_skipped: bool,
427+
bench: bool,
428+
) -> anyhow::Result<Vec<TestOutlineEntry>> {
429+
let executables = gather_tests(build_meta);
430+
debug!(count = executables.len(), "collecting test outline entries");
431+
let mut entries = Vec::new();
432+
433+
for test in executables {
434+
let (included, file_filt) = filter.check_package(test.target);
435+
if !included {
436+
continue;
437+
}
438+
439+
let meta_bytes = std::fs::read(test.meta)
440+
.with_context(|| format!("Failed to read test metadata at {}", test.meta.display()))?;
441+
let meta: MooncGenTestInfo = serde_json_lenient::from_slice(&meta_bytes)
442+
.with_context(|| format!("Failed to parse test metadata at {}", test.meta.display()))?;
443+
444+
let mut file_ranges = vec![];
445+
filter::apply_filter(
446+
file_filt,
447+
&meta,
448+
&mut file_ranges,
449+
include_skipped,
450+
bench,
451+
filter.name_filter.as_deref(),
452+
);
453+
454+
let mut allowed_indices: IndexMap<String, std::collections::HashSet<u32>> =
455+
IndexMap::new();
456+
for (file, ranges) in file_ranges {
457+
let entry = allowed_indices.entry(file).or_default();
458+
for range in ranges {
459+
entry.extend(range);
460+
}
461+
}
462+
463+
if allowed_indices.values().all(|v| v.is_empty()) {
464+
continue;
465+
}
466+
467+
let tests_by_file = collect_tests_by_file(&meta, bench);
468+
let pkgname = build_meta
469+
.resolve_output
470+
.pkg_dirs
471+
.get_package(test.target.package)
472+
.fqn
473+
.to_string();
474+
for (file, tests) in tests_by_file {
475+
let Some(allowed) = allowed_indices.get(&file) else {
476+
continue;
477+
};
478+
if allowed.is_empty() {
479+
continue;
480+
}
481+
for (index, test) in tests {
482+
if !allowed.contains(&index) {
483+
continue;
484+
}
485+
entries.push(TestOutlineEntry {
486+
package: pkgname.clone(),
487+
file: file.clone(),
488+
index,
489+
name: test.name.clone(),
490+
line_number: test.line_number,
491+
});
492+
}
493+
}
494+
}
495+
496+
Ok(entries)
497+
}
498+
385499
/// Gather tests executables from the build metadata.
386500
#[instrument(level = "trace", skip(build_meta))]
387501
fn gather_tests(build_meta: &BuildMeta) -> Vec<TestExecutableToRun<'_>> {

crates/moon/tests/test_cases/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ mod test_include_001;
100100
mod test_include_002;
101101
mod test_include_003;
102102
mod test_moon_info;
103+
mod test_outline;
103104
mod test_moonbitlang_x;
104105
mod test_release;
105106
mod third_party;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "username/outline",
3+
"version": "0.1.0",
4+
"readme": "README.md",
5+
"repository": "",
6+
"license": "",
7+
"keywords": [],
8+
"description": "",
9+
"source": "src"
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
test "alpha" {
2+
let _ = 1
3+
}
4+
5+
test {
6+
let _ = 2
7+
}
8+
9+
test "beta" {
10+
let _ = 3
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main {
2+
()
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"is-main": true
3+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use crate::{TestDir, get_stdout, util::check};
2+
use expect_test::expect;
3+
4+
fn normalize_outline(output: String) -> String {
5+
let mut out = output
6+
.lines()
7+
.map(|line| line.trim_start())
8+
.collect::<Vec<_>>()
9+
.join("\n");
10+
out.push('\n');
11+
out
12+
}
13+
14+
#[test]
15+
fn test_outline() {
16+
let dir = TestDir::new("test_outline.in");
17+
let output = normalize_outline(get_stdout(&dir, ["test", "--outline", "-q"]));
18+
check(
19+
output,
20+
expect![[r#"
21+
1. username/outline/lib hello.mbt:1 index=0 name="alpha"
22+
2. username/outline/lib hello.mbt:5 index=1
23+
3. username/outline/lib hello.mbt:9 index=2 name="beta"
24+
"#]],
25+
);
26+
27+
let output = normalize_outline(get_stdout(&dir, ["test", "--outline", "-q", "-F", "b*"]));
28+
check(
29+
output,
30+
expect![[r#"
31+
1. username/outline/lib hello.mbt:9 index=2 name="beta"
32+
"#]],
33+
);
34+
}

0 commit comments

Comments
 (0)