Skip to content

Commit

Permalink
Merge pull request #485 from moonbitlang/doc_test
Browse files Browse the repository at this point in the history
feat: support doc test
  • Loading branch information
Young-Flash authored Nov 20, 2024
2 parents 81c53e4 + c23967e commit 9135ec2
Show file tree
Hide file tree
Showing 22 changed files with 390 additions and 40 deletions.
19 changes: 10 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ ariadne = { version = "0.4.1", features = ["auto-color"] }
clap_complete = { version = "4.5.4" }
schemars = "0.8"
nucleo-matcher = "0.3.1"
regex = { version = "1.10.3", default-features = false, features = ["std"] }

[profile.release]
debug = false
Expand Down
1 change: 1 addition & 0 deletions crates/moon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ tokio.workspace = true
futures.workspace = true
clap_complete.workspace = true
indexmap.workspace = true
regex.workspace = true

[target.'cfg(not(windows))'.dependencies]
openssl = { version = "0.10.66", features = ["vendored"] }
Expand Down
161 changes: 160 additions & 1 deletion crates/moon/src/cli/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@

use anyhow::Context;
use colored::Colorize;
use doc_test::DocTestExtractor;
use doc_test::PatchJSON;
use moonbuild::dry_run;
use moonbuild::entry;
use mooncake::pkg::sync::auto_sync;
use moonutil::common::backend_filter;
use moonutil::common::lower_surface_targets;
use moonutil::common::FileLock;
use moonutil::common::GeneratedTestDriver;
use moonutil::common::MooncOpt;
use moonutil::common::RunMode;
use moonutil::common::MOON_DOC_TEST_POSTFIX;
use moonutil::common::{MoonbuildOpt, TestOpt};
use moonutil::dirs::mk_arch_mode_dir;
use moonutil::dirs::PackageDirs;
Expand Down Expand Up @@ -82,8 +86,12 @@ pub struct TestSubcommand {
pub test_failure_json: bool,

/// Path to the patch file
#[clap(long)]
#[clap(long, requires("package"), conflicts_with = "update")]
pub patch_file: Option<PathBuf>,

/// Run doc test
#[clap(long = "doc", conflicts_with = "update")]
pub doc_test: bool,
}

pub fn run_test(cli: UniversalFlags, cmd: TestSubcommand) -> anyhow::Result<i32> {
Expand Down Expand Up @@ -290,6 +298,37 @@ fn run_test_internal(
continue;
}

pkg.patch_file = cmd.patch_file.clone();

if cmd.doc_test {
let mbt_files = backend_filter(
&pkg.files,
moonc_opt.build_opt.debug_flag,
moonc_opt.build_opt.target_backend,
);

let mut doc_tests = vec![];
let doc_test_extractor = DocTestExtractor::new();
for file in mbt_files {
let doc_test_in_mbt_file = doc_test_extractor.extract_from_file(&file)?;
if !doc_test_in_mbt_file.is_empty() {
doc_tests.push(doc_test_in_mbt_file);
}
}

let pj = PatchJSON::from_doc_tests(doc_tests);
let pj_path = pkg
.artifact
.with_file_name(format!("{}.json", MOON_DOC_TEST_POSTFIX));
if !pj_path.parent().unwrap().exists() {
std::fs::create_dir_all(pj_path.parent().unwrap())?;
}
std::fs::write(&pj_path, serde_json::to_string_pretty(&pj)?)
.context(format!("failed to write {}", &pj_path.display()))?;

pkg.doc_test_patch_file = Some(pj_path);
}

{
// test driver file will be generated via `moon generate-test-driver` command
let internal_generated_file = target_dir
Expand Down Expand Up @@ -405,3 +444,123 @@ fn do_run_test(
Ok(2)
}
}

mod doc_test {
use regex::Regex;
use std::fs;
use std::path::Path;

#[derive(Debug)]
pub struct DocTest {
pub content: String,
pub file_name: String,
pub line_number: usize,
pub line_count: usize,
}

pub struct DocTestExtractor {
test_pattern: Regex,
}

impl DocTestExtractor {
pub fn new() -> Self {
// \r\n for windows, \n for unix
let pattern = r#"///\s*```(?:\r?\n)((?:///.*(?:\r?\n))*?)///\s*```"#;
Self {
test_pattern: Regex::new(pattern).expect("Invalid regex pattern"),
}
}

pub fn extract_from_file(&self, file_path: &Path) -> anyhow::Result<Vec<DocTest>> {
let content = fs::read_to_string(file_path)?;

let mut tests = Vec::new();

for cap in self.test_pattern.captures_iter(&content) {
if let Some(test_match) = cap.get(0) {
let line_number = content[..test_match.start()]
.chars()
.filter(|&c| c == '\n')
.count()
+ 1;

if let Some(test_content) = cap.get(1) {
let processed_content = test_content
.as_str()
.lines()
.map(|line| {
format!(" {}", line.trim_start_matches("/// ")).to_string()
})
.collect::<Vec<_>>()
.join("\n");

let line_count = processed_content.split('\n').count();

tests.push(DocTest {
content: processed_content,
file_name: file_path.file_name().unwrap().to_str().unwrap().to_string(),
line_number,
line_count,
});
}
}
}

Ok(tests)
}
}

#[derive(Debug, serde::Serialize)]
pub struct PatchJSON {
pub drops: Vec<String>,
pub patches: Vec<PatchItem>,
}

#[derive(Debug, serde::Serialize)]
pub struct PatchItem {
pub name: String,
pub content: String,
}

impl PatchJSON {
pub fn from_doc_tests(doc_tests: Vec<Vec<DocTest>>) -> Self {
let mut patches = vec![];
for doc_tests_in_mbt_file in doc_tests.iter() {
let mut current_line = 1;
let mut content = String::new();
for doc_test in doc_tests_in_mbt_file {
let test_name = format!(
"{} {} {} {}",
"doc_test", doc_test.file_name, doc_test.line_number, doc_test.line_count
);

let start_line_number = doc_test.line_number;
let empty_lines = "\n".repeat(start_line_number - current_line);

content.push_str(&format!(
"{}test \"{}\" {{\n{}\n}}",
empty_lines, test_name, doc_test.content
));

// +1 for the }
current_line = start_line_number + doc_test.line_count + 1;
}

patches.push(PatchItem {
// xxx.mbt -> xxx_doc_test.mbt
name: format!(
"{}{}.mbt",
doc_tests_in_mbt_file[0].file_name.trim_end_matches(".mbt"),
moonutil::common::MOON_DOC_TEST_POSTFIX,
),
content,
});
}

PatchJSON {
drops: vec![],
patches,
}
}
}
}
51 changes: 51 additions & 0 deletions crates/moon/tests/test_cases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7898,3 +7898,54 @@ fn test_add_mi_if_self_not_set_in_test_imports() {
"#]],
);
}

#[test]
fn test_run_doc_test() {
let dir = TestDir::new("run_doc_test.in");

// `moon test --doc` run doc test only
check(
get_err_stdout(&dir, ["test", "--sort-input", "--doc"]),
expect![[r#"
doc_test 1 from hello.mbt
doc_test 2 from hello.mbt
doc_test 3 from hello.mbt
doc_test
doc_test 1 from greet.mbt
doc_test 2 from greet.mbt
doc_test 3 from greet.mbt
doc_test from greet.mbt
test username/hello/lib/hello.mbt::2 failed: FAILED: $ROOT/src/lib/hello.mbt:22:5-22:31 this is a failure
test username/hello/lib/greet.mbt::3 failed
expect test failed at $ROOT/src/lib/greet.mbt:33:5-33:18
Diff:
----
423
----
Total tests: 8, passed: 6, failed: 2.
"#]],
);

// --doc conflicts with --update
#[cfg(unix)]
check(
get_err_stderr(&dir, ["test", "--doc", "--update"]),
expect![[r#"
error: the argument '--doc' cannot be used with '--update'
Usage: moon test --doc
For more information, try '--help'.
"#]],
);

// `moon test` will not run doc test
check(
get_stdout(&dir, ["test", "--sort-input"]),
expect![[r#"
hello from hello_test.mbt
Total tests: 1, passed: 1, failed: 0.
"#]],
);
}
1 change: 1 addition & 0 deletions crates/moon/tests/test_cases/run_doc_test.in/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# username/hello
10 changes: 10 additions & 0 deletions crates/moon/tests/test_cases/run_doc_test.in/moon.mod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "username/hello",
"version": "0.1.0",
"readme": "README.md",
"repository": "",
"license": "Apache-2.0",
"keywords": [],
"description": "",
"source": "src"
}
38 changes: 38 additions & 0 deletions crates/moon/tests/test_cases/run_doc_test.in/src/lib/greet.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/// ```
/// println("doc_test 1 from greet.mbt")
/// ```
pub fn greet1() -> String {
"greet, world!"
}


/// ```
/// println("doc_test 2 from greet.mbt")
///
///
///
/// ```
pub fn greet2() -> String {
"greet, world!"
}

/// ```
/// println("doc_test 3 from greet.mbt")
///
///
///
/// ```
pub fn greet3() -> String {
"greet, wor1ld!"
}

/// ```
/// println("doc_test from greet.mbt")
///
///
/// inspect!(423)
///
/// ```
pub fn greet() -> String {
"greet, wor1ld!"
}
Loading

0 comments on commit 9135ec2

Please sign in to comment.