aboutsummaryrefslogtreecommitdiff
path: root/.run
diff options
context:
space:
mode:
Diffstat (limited to '.run')
-rw-r--r--.run/src/bin/check-docs-structure.rs352
-rw-r--r--.run/src/bin/ci.rs42
-rw-r--r--.run/src/bin/cov-test.rs69
-rw-r--r--.run/src/bin/doc-nightly.ps12
-rwxr-xr-x.run/src/bin/doc-nightly.sh2
-rw-r--r--.run/src/bin/doc.ps12
-rwxr-xr-x.run/src/bin/doc.sh2
-rw-r--r--.run/src/bin/test-examples.rs113
8 files changed, 510 insertions, 74 deletions
diff --git a/.run/src/bin/check-docs-structure.rs b/.run/src/bin/check-docs-structure.rs
new file mode 100644
index 0000000..ac13da2
--- /dev/null
+++ b/.run/src/bin/check-docs-structure.rs
@@ -0,0 +1,352 @@
+//! Checks that every translated docs directory mirrors the structure of the
+//! reference (English) docs directory exactly.
+//!
+//! The language directories are declared in `.config/docs-lang.txt`, one path
+//! per line (relative to `./docs/`). The first line is the reference
+//! directory; every other line is a translation that must match it.
+//!
+//! For each file pair the tool compares a *structural signature*: one token per
+//! line, classifying headings (both Markdown `#` and HTML `<hN>`), fenced code
+//! blocks (including their language tag), `@@@` hidden-compilation lines, blank
+//! lines, blockquotes, lists and plain text. Translated text is allowed to
+//! differ; the structure is not.
+
+use std::collections::BTreeSet;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use colored::Colorize;
+use tools::println_cargo_style;
+
+const DOCS_DIR: &str = "./docs";
+const LANG_CONFIG: &str = ".config/docs-lang.txt";
+
+fn main() {
+ println_cargo_style!("Checking: docs structure consistency across languages ...");
+
+ let repo_root = find_git_repo().expect("Cannot find git repo root");
+ let docs_dir = repo_root.join(DOCS_DIR);
+
+ let lang_lines = read_lang_config(&repo_root);
+ if lang_lines.is_empty() {
+ println!("No language directories declared in {LANG_CONFIG}, nothing to check.");
+ return;
+ }
+
+ let reference = docs_dir.join(&lang_lines[0]);
+ if !reference.is_dir() {
+ eprintln!(
+ "Reference docs directory `{}` does not exist.",
+ reference.display()
+ );
+ std::process::exit(1);
+ }
+
+ let mut failed = false;
+
+ for lang in &lang_lines[1..] {
+ let lang_dir = docs_dir.join(lang);
+ println!("\nChecking `{lang}` against `{}` ...", lang_lines[0]);
+ if !lang_dir.is_dir() {
+ eprintln!(" ERROR: `{}` does not exist.", lang_dir.display());
+ failed = true;
+ continue;
+ }
+ if check_lang_dir(&reference, &lang_dir).is_err() {
+ failed = true;
+ }
+ }
+
+ if failed {
+ println!();
+ eprintln!(
+ "{} Fix the differences above.",
+ "Docs structure check FAILED.".red().bold()
+ );
+ std::process::exit(1);
+ }
+
+ println_cargo_style!("Done: docs structure is consistent across all languages!");
+}
+
+fn read_lang_config(repo_root: &Path) -> Vec<String> {
+ let path = repo_root.join(LANG_CONFIG);
+ let Ok(content) = fs::read_to_string(&path) else {
+ return Vec::new();
+ };
+ content
+ .lines()
+ .map(str::trim)
+ .filter(|l| !l.is_empty() && !l.starts_with('#'))
+ .map(|l| l.trim_start_matches("./").to_string())
+ .collect()
+}
+
+/// Returns `Err(())` when the translated directory does not mirror the reference.
+fn check_lang_dir(reference: &Path, lang: &Path) -> Result<(), ()> {
+ let mut failed = false;
+
+ let ref_files = collect_md_files(reference);
+ let lang_files = collect_md_files(lang);
+
+ let ref_set: BTreeSet<PathBuf> = ref_files.clone().into_iter().collect();
+ let lang_set: BTreeSet<PathBuf> = lang_files.clone().into_iter().collect();
+
+ let missing: Vec<PathBuf> = ref_set.difference(&lang_set).cloned().collect();
+ let extra: Vec<PathBuf> = lang_set.difference(&ref_set).cloned().collect();
+
+ if !missing.is_empty() {
+ failed = true;
+ println!(" ERROR: files missing in translation:");
+ for f in &missing {
+ println!(" - {}", f.display());
+ }
+ }
+ if !extra.is_empty() {
+ failed = true;
+ println!(" ERROR: extra files in translation:");
+ for f in &extra {
+ println!(" - {}", f.display());
+ }
+ }
+
+ // Compare the structural signature of every file present in both sides.
+ for file in &ref_files {
+ if !lang_set.contains(file) {
+ continue;
+ }
+ let ref_path = reference.join(file);
+ let lang_path = lang.join(file);
+ match compare_signature(&ref_path, &lang_path) {
+ Ok(()) => {}
+ Err(diff) => {
+ failed = true;
+ eprintln!(
+ " {}: structure mismatch in `{}`",
+ "ERROR".red().bold(),
+ file.display().to_string().cyan()
+ );
+ for line in diff {
+ println!(" {line}");
+ }
+ }
+ }
+ }
+
+ if failed { Err(()) } else { Ok(()) }
+}
+
+fn collect_md_files(dir: &Path) -> Vec<PathBuf> {
+ let mut out = Vec::new();
+ let mut stack = vec![dir.to_path_buf()];
+ while let Some(current) = stack.pop() {
+ let Ok(entries) = fs::read_dir(&current) else {
+ continue;
+ };
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ stack.push(path);
+ } else if path.extension().is_some_and(|e| e == "md") {
+ out.push(path.strip_prefix(dir).unwrap_or(&path).to_path_buf());
+ }
+ }
+ }
+ out.sort();
+ out
+}
+
+/// Compare the structural signatures of two markdown files.
+///
+/// Returns a list of human-readable diff lines on the first structural
+/// difference found (all differences up to a small window are reported).
+fn compare_signature(ref_path: &Path, lang_path: &Path) -> Result<(), Vec<String>> {
+ let ref_content = fs::read_to_string(ref_path).unwrap_or_default();
+ let lang_content = fs::read_to_string(lang_path).unwrap_or_default();
+
+ let ref_sig = signature_of(&ref_content);
+ let lang_sig = signature_of(&lang_content);
+
+ if ref_sig == lang_sig {
+ return Ok(());
+ }
+
+ let ref_lines: Vec<&str> = ref_content.lines().collect();
+ let lang_lines: Vec<&str> = lang_content.lines().collect();
+
+ let mut diffs = Vec::new();
+ let mut window = 0;
+ let max = ref_sig.len().max(lang_sig.len());
+ for i in 0..max {
+ let ref_tok = ref_sig.get(i);
+ let lang_tok = lang_sig.get(i);
+ if ref_tok == lang_tok {
+ continue;
+ }
+ if window >= 5 {
+ diffs.push(format!("... ({}-line window truncated)", max - i));
+ break;
+ }
+ window += 1;
+ let ref_line = ref_lines.get(i).copied().unwrap_or("<missing>");
+ let lang_line = lang_lines.get(i).copied().unwrap_or("<missing>");
+ diffs.push(format!(
+ " {}: {}",
+ "line".yellow().bold(),
+ (i + 1).to_string().yellow()
+ ));
+ diffs.push(format!(
+ " {} : {} {}",
+ "expect".green().bold(),
+ format!("`{}`", token_label(ref_tok.map_or("<eof>", String::as_str))).green(),
+ display_line(ref_line).cyan()
+ ));
+ diffs.push(format!(
+ " {} : {} {}",
+ "found".red().bold(),
+ format!(
+ "`{}`",
+ token_label(lang_tok.map_or("<eof>", String::as_str))
+ )
+ .red(),
+ display_line(lang_line).cyan()
+ ));
+ if ref_sig.len() != lang_sig.len() && window >= 5 {
+ diffs.push(format!(
+ " note: reference has {} lines, translation has {} lines",
+ ref_sig.len(),
+ lang_sig.len()
+ ));
+ break;
+ }
+ }
+ if diffs.is_empty() {
+ diffs.push("signatures differ in length (see line count note)".to_string());
+ }
+ Err(diffs)
+}
+
+/// Human-readable label for a structural token.
+fn token_label(token: &str) -> String {
+ match token {
+ "B" => "blank".to_string(),
+ "A" => "@@@".to_string(),
+ "Q" => "quote".to_string(),
+ "L" => "list".to_string(),
+ "P" => "text".to_string(),
+ t if t.starts_with("H") => format!("heading-{}", &t[1..]),
+ t if t.starts_with("F:") => {
+ let lang = &t[2..];
+ if lang.is_empty() {
+ "fence".to_string()
+ } else {
+ format!("fence:{lang}")
+ }
+ }
+ _ => token.to_string(),
+ }
+}
+
+/// Render a source line for display: blank lines become `<blank>`.
+fn display_line(line: &str) -> String {
+ if line.trim().is_empty() {
+ "<blank>".to_string()
+ } else {
+ truncate(line)
+ }
+}
+
+/// Build the structural signature of a markdown file.
+fn signature_of(content: &str) -> Vec<String> {
+ let mut sig = Vec::new();
+ let mut in_fence = false;
+ let mut fence_lang = String::new();
+
+ for raw_line in content.lines() {
+ let line = raw_line.trim();
+
+ if in_fence {
+ if line.starts_with("```") {
+ in_fence = false;
+ sig.push(format!("F:{}", fence_lang));
+ } else if line.starts_with("@@@") {
+ sig.push("A".to_string());
+ } else if line.is_empty() {
+ sig.push("B".to_string());
+ } else {
+ sig.push("P".to_string());
+ }
+ continue;
+ }
+
+ if line.starts_with("```") {
+ in_fence = true;
+ fence_lang = line.trim_start_matches("```").trim().to_string();
+ sig.push(format!("F:{fence_lang}"));
+ } else if line.starts_with('#') {
+ let level = line.chars().take_while(|c| *c == '#').count();
+ sig.push(format!("H{level}"));
+ } else if line.starts_with("<h") || line.starts_with("</h") {
+ // HTML headings (e.g. `<h1 align="center">` / `</h1>`)
+ let level = line
+ .trim_start_matches(['<', '/'])
+ .chars()
+ .next()
+ .and_then(|c| c.to_digit(10))
+ .unwrap_or(1);
+ sig.push(format!("H{level}"));
+ } else if line.starts_with("@@@") {
+ sig.push("A".to_string());
+ } else if line.is_empty() {
+ sig.push("B".to_string());
+ } else if line.starts_with('>') {
+ sig.push("Q".to_string());
+ } else if is_list_line(line) {
+ sig.push("L".to_string());
+ } else {
+ sig.push("P".to_string());
+ }
+ }
+
+ // An unclosed fence is still a fence line; the signature already recorded it.
+ sig
+}
+
+fn is_list_line(line: &str) -> bool {
+ let trimmed = line.trim_start();
+ trimmed.starts_with("- ")
+ || trimmed.starts_with("* ")
+ || trimmed.starts_with("+ ")
+ || is_numbered_list(trimmed)
+}
+
+/// A numbered list item: `1. text`, `1) text`, `10. text`, ...
+fn is_numbered_list(line: &str) -> bool {
+ let digit_count = line.chars().take_while(|c| c.is_ascii_digit()).count();
+ if digit_count == 0 {
+ return false;
+ }
+ let rest = &line[digit_count..];
+ (rest.starts_with(". ") || rest.starts_with(") "))
+ && rest.chars().nth(1).is_some_and(|c| c == ' ' || c == '\t')
+}
+
+fn truncate(line: &str) -> String {
+ const MAX: usize = 60;
+ if line.chars().count() <= MAX {
+ line.to_string()
+ } else {
+ let cut: String = line.chars().take(MAX).collect();
+ format!("{cut}...")
+ }
+}
+
+fn find_git_repo() -> Option<PathBuf> {
+ let mut current = std::env::current_dir().ok()?;
+ loop {
+ if current.join(".git").is_dir() {
+ return Some(current);
+ }
+ current = current.parent()?.to_path_buf();
+ }
+}
diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs
index c090eaa..b6d92b8 100644
--- a/.run/src/bin/ci.rs
+++ b/.run/src/bin/ci.rs
@@ -20,6 +20,7 @@ struct Checks {
markdown_code: bool,
examples: bool,
docs_refresh: bool,
+ docs_structure: bool,
api_docs: bool,
}
@@ -32,6 +33,7 @@ impl Checks {
|| self.markdown_code
|| self.examples
|| self.docs_refresh
+ || self.docs_structure
|| self.api_docs
}
}
@@ -41,17 +43,18 @@ fn print_help() {
r"
Usage: ci [options]
Options:
- -h, --help Print this help message
- -y Auto-confirm temporary commits
- --dirty Run CI on dirty workspace (skip temp commit & clean check)
- --check-build Build all crates
- --check-clippy Run clippy on all crates (-D warnings)
- --check-test Run unit tests for all crates
- --check-arg-picker Test the arg-picker crate
- --check-markdown-code Verify all *.md code blocks compile
- --check-examples Test all examples
- --check-docs-refresh Refresh docs and fail if the tree is contaminated
- --check-api-docs Build API docs with docs.rs features
+ -h, --help Print this help message
+ -y Auto-confirm temporary commits
+ --dirty Run CI on dirty workspace (skip temp commit & clean check)
+ --check-build Build all crates
+ --check-clippy Run clippy on all crates (-D warnings)
+ --check-test Run unit tests for all crates
+ --check-arg-picker Test the arg-picker crate
+ --check-markdown-code Verify all *.md code blocks compile
+ --check-examples Test all examples
+ --check-docs-refresh Refresh docs and fail if the tree is contaminated
+ --check-docs-structure Verify translated docs mirror the English structure
+ --check-api-docs Build API docs with docs.rs features
If no specific options are given, all checks are run.
"
@@ -73,6 +76,7 @@ fn main() {
check_markdown_code,
check_examples,
check_docs_refresh,
+ check_docs_structure,
check_api_docs,
help,
) = Picker::from_args()
@@ -85,6 +89,7 @@ fn main() {
.pick_or_default(&arg![check_markdown_code: bool])
.pick_or_default(&arg![check_examples: bool])
.pick_or_default(&arg![check_docs_refresh: bool])
+ .pick_or_default(&arg![check_docs_structure: bool])
.pick_or_default(&arg![check_api_docs: bool])
.pick_or_default(&arg![help: bool, 'h'])
.unwrap();
@@ -102,6 +107,7 @@ fn main() {
markdown_code: check_markdown_code,
examples: check_examples,
docs_refresh: check_docs_refresh,
+ docs_structure: check_docs_structure,
api_docs: check_api_docs,
};
let run_all = !checks.any();
@@ -242,6 +248,14 @@ fn ci(checks: &Checks, run_all: bool) -> Result<(), i32> {
run_all,
)?;
}
+ if run_all || checks.docs_structure {
+ run_step(
+ &mut exit_code,
+ "Phase: Check translated docs structure consistency",
+ docs_structure,
+ run_all,
+ )?;
+ }
if run_all || checks.api_docs {
run_step(
&mut exit_code,
@@ -442,3 +456,9 @@ fn docs_refresh() -> Result<(), i32> {
Ok(())
}
+
+fn docs_structure() -> Result<(), i32> {
+ println_cargo_style!("Check: docs structure consistency across languages");
+
+ run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin check-docs-structure")
+}
diff --git a/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs
index 1b2342e..f62ff01 100644
--- a/.run/src/bin/cov-test.rs
+++ b/.run/src/bin/cov-test.rs
@@ -9,8 +9,12 @@
//!
//! - `--object <PATH>`: include arbitrary binaries in the report
//! (upstream issue taiki-e/cargo-llvm-cov#367)
-//! - `--include-tests-examples-benches`: stop filtering those source dirs
-//! (upstream issue taiki-e/cargo-llvm-cov#503)
+//! - `--include-examples`: stop filtering source files under the
+//! `examples` directory (upstream issue taiki-e/cargo-llvm-cov#503)
+//!
+//! The script itself does not use `--include-examples`; it passes
+//! `--no-default-ignore-filename-regex` and supplies its own filter so that
+//! `tests`/`benches` directories stay in the report too.
//!
//! Install it with:
//!
@@ -18,7 +22,6 @@
//! cargo install --git https://github.com/Weicao-CatilGrass/cargo-llvm-cov cargo-llvm-cov
//! ```
-use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
@@ -34,15 +37,16 @@ const OUTPUT_DIR: &str = "docs/cov-test";
/// in one place so the final `report` can merge everything.
const COV_TARGET_DIR: &str = ".temp/cov-llvm";
-/// Parsed `examples/test-examples.toml` (`test.<example> = [ { command, expect } ]`).
+/// An example's `test.toml` (`[[runs]]` entries).
#[derive(Deserialize)]
struct TestConfig {
- test: HashMap<String, Vec<TestCase>>,
+ runs: Vec<TestCase>,
}
+/// One `[[runs]]` entry of an example's `test.toml`.
#[derive(Deserialize)]
struct TestCase {
- command: String,
+ input: Vec<String>,
}
fn main() {
@@ -108,7 +112,7 @@ fn main() {
}
// 3. Examples: build each example with explicit RUSTFLAGS, then execute
- // every command from test-examples.toml directly.
+ // every command declared in the example's test.toml directly.
//
// NOTE: `cargo llvm-cov run` cannot be used here. Its rustc wrapper
// only instruments the crates of the *current* cargo project (with
@@ -127,7 +131,7 @@ fn main() {
}
let examples = load_example_commands(&repo_root);
let mut built = std::collections::HashSet::new();
- for (example, command) in &examples {
+ for (example, input) in &examples {
if built.insert(example.clone()) {
println_cargo_style!("Building: {}", example);
run_cmd!(format!(
@@ -150,7 +154,7 @@ fn main() {
example
);
match std::process::Command::new(&binary)
- .args(command.split_whitespace())
+ .args(input)
.env("LLVM_PROFILE_FILE", &profraw)
.status()
{
@@ -279,22 +283,43 @@ fn find_test_crate_manifests(repo_root: &Path) -> Vec<PathBuf> {
manifests
}
-/// Parse `examples/test-examples.toml` into `(example_name, command)` pairs.
-fn load_example_commands(repo_root: &Path) -> Vec<(String, String)> {
- let content =
- fs::read_to_string(repo_root.join("examples/test-examples.toml")).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to read examples/test-examples.toml: {}", e);
+/// Parse every `examples/<name>/test.toml` into `(example_name, input)` pairs.
+fn load_example_commands(repo_root: &Path) -> Vec<(String, Vec<String>)> {
+ let examples_dir = repo_root.join("examples");
+ let mut entries: Vec<_> = std::fs::read_dir(&examples_dir)
+ .unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read {}: {}", examples_dir.display(), e);
std::process::exit(1);
- });
- let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to parse examples/test-examples.toml: {}", e);
- std::process::exit(1);
- });
+ })
+ .flatten()
+ .collect();
+ entries.sort_by_key(|e| e.file_name());
let mut pairs = Vec::new();
- for (example, cases) in &config.test {
- for case in cases {
- pairs.push((example.clone(), case.command.clone()));
+ for entry in entries {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+ let test_toml = path.join("test.toml");
+ if !test_toml.is_file() {
+ continue;
+ }
+ let name = path
+ .file_name()
+ .and_then(|n| n.to_str())
+ .unwrap_or_default()
+ .to_string();
+ let content = fs::read_to_string(&test_toml).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e);
+ std::process::exit(1);
+ });
+ let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e);
+ std::process::exit(1);
+ });
+ for case in config.runs {
+ pairs.push((name.clone(), case.input));
}
}
pairs
diff --git a/.run/src/bin/doc-nightly.ps1 b/.run/src/bin/doc-nightly.ps1
index 30d6aaf..58d7af4 100644
--- a/.run/src/bin/doc-nightly.ps1
+++ b/.run/src/bin/doc-nightly.ps1
@@ -1,6 +1,6 @@
cargo +nightly rustdoc `
--manifest-path mingling/Cargo.toml `
- --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros `
+ --features docs_rs,core,macros,builds,structural_renderer,repl,comp,picker,clap,extra_macros `
--open `
-- `
--cfg docsrs
diff --git a/.run/src/bin/doc-nightly.sh b/.run/src/bin/doc-nightly.sh
index 944f4b3..d16b6fc 100755
--- a/.run/src/bin/doc-nightly.sh
+++ b/.run/src/bin/doc-nightly.sh
@@ -2,7 +2,7 @@
cargo rustdoc \
--manifest-path mingling/Cargo.toml \
- --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros \
+ --features docs_rs,core,macros,builds,structural_renderer,repl,comp,picker,clap,extra_macros \
--open \
-- \
--cfg docsrs
diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1
index 731168c..d400f76 100644
--- a/.run/src/bin/doc.ps1
+++ b/.run/src/bin/doc.ps1
@@ -1,5 +1,5 @@
$env:RUSTDOCFLAGS="--html-in-header mingling/arborium-header.html"; cargo doc `
--manifest-path mingling/Cargo.toml `
--no-deps `
- --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros,pathf `
+ --features docs_rs,core,macros,builds,structural_renderer,repl,comp,picker,clap,extra_macros,pathf `
--open
diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh
index d6181d3..4229853 100755
--- a/.run/src/bin/doc.sh
+++ b/.run/src/bin/doc.sh
@@ -3,5 +3,5 @@
RUSTDOCFLAGS="--html-in-header mingling/arborium-header.html" cargo doc \
--manifest-path mingling/Cargo.toml \
--no-deps \
- --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros,pathf \
+ --features docs_rs,core,macros,builds,structural_renderer,repl,comp,picker,clap,extra_macros,pathf \
--open
diff --git a/.run/src/bin/test-examples.rs b/.run/src/bin/test-examples.rs
index 539459e..617a745 100644
--- a/.run/src/bin/test-examples.rs
+++ b/.run/src/bin/test-examples.rs
@@ -1,18 +1,20 @@
-use std::collections::HashMap;
+use std::path::Path;
use colored::Colorize;
use indicatif::ProgressBar;
use serde::Deserialize;
-use tools::{eprintln_cargo_style, println_cargo_style};
+use tools::{eprintln_cargo_style, println_cargo_style, run_parallel};
+/// An example's `test.toml` (`[[runs]]` entries).
#[derive(Deserialize)]
struct TestConfig {
- test: HashMap<String, Vec<TestCase>>,
+ runs: Vec<TestCase>,
}
+/// A single `[[runs]]` entry of an example's `test.toml`.
#[derive(Deserialize)]
struct TestCase {
- command: String,
+ input: Vec<String>,
expect: Expect,
}
@@ -27,10 +29,16 @@ fn main() {
#[cfg(windows)]
let _ = colored::control::set_virtual_terminal(true);
- let config = load_config();
+ let configs = load_all_test_configs();
- // Count total test cases upfront
- let total: usize = config.test.values().map(|cases| cases.len()).sum();
+ // Phase 1: build all examples in parallel.
+ if let Err(code) = build_all_examples(&configs) {
+ // `run_parallel` already printed every failed build above.
+ std::process::exit(code);
+ }
+
+ // Phase 2: run the tests serially against the pre-built binaries.
+ let total: usize = configs.iter().map(|(_, cases)| cases.len()).sum();
let bar = ProgressBar::new(total as u64);
bar.set_style(
indicatif::ProgressStyle::default_bar()
@@ -43,7 +51,7 @@ fn main() {
);
bar.set_message("examples");
- let passed = run_all_tests(&config, &bar);
+ let passed = run_all_tests(&configs, &bar);
bar.finish_and_clear();
@@ -55,31 +63,71 @@ fn main() {
}
}
-/// Parse test config from TOML file
-fn load_config() -> TestConfig {
- let content = std::fs::read_to_string("examples/test-examples.toml").unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to read TOML config file: {}", e);
+/// Load `examples/<name>/test.toml` for every example that has one, in
+/// alphabetical order of the example directory name.
+fn load_all_test_configs() -> Vec<(String, Vec<TestCase>)> {
+ let examples_dir = Path::new("examples");
+ let mut configs = Vec::new();
+
+ let entries = std::fs::read_dir(examples_dir).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read examples dir: {}", e);
std::process::exit(1);
});
- toml::from_str(&content).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to parse TOML config: {}", e);
- std::process::exit(1);
- })
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+ let test_toml = path.join("test.toml");
+ if !test_toml.is_file() {
+ continue;
+ }
+ let name = path
+ .file_name()
+ .and_then(|n| n.to_str())
+ .unwrap_or_default()
+ .to_string();
+ let content = std::fs::read_to_string(&test_toml).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e);
+ std::process::exit(1);
+ });
+ let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e);
+ std::process::exit(1);
+ });
+ configs.push((name, config.runs));
+ }
+
+ configs.sort_by(|a, b| a.0.cmp(&b.0));
+ configs
}
-/// Run all example test groups, return number passed
-fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize {
+/// Phase 1: build every example that has a `test.toml` in parallel.
+///
+/// Build tasks are spawned in parallel (like `ci.rs`'s `build_all`); on any
+/// build failure the whole run aborts with the first failure's exit code.
+fn build_all_examples(configs: &[(String, Vec<TestCase>)]) -> Result<(), i32> {
+ let tasks: Vec<(String, String, String)> = configs
+ .iter()
+ .map(|(name, _)| {
+ (
+ format!("Build: {name}"),
+ name.clone(),
+ format!("cargo build --manifest-path examples/{name}/Cargo.toml --color always"),
+ )
+ })
+ .collect();
+ run_parallel("Building", tasks)
+}
+
+/// Phase 2: run all example test groups serially, return number passed
+fn run_all_tests(configs: &[(String, Vec<TestCase>)], bar: &ProgressBar) -> usize {
let mut passed = 0;
- for (example_name, test_cases) in &config.test {
+ for (example_name, test_cases) in configs {
bar.set_message(example_name.clone());
- if !build_example(example_name) {
- bar.inc(test_cases.len() as u64);
- continue;
- }
-
for test_case in test_cases {
if run_single_test(example_name, test_case, bar) {
passed += 1;
@@ -91,27 +139,18 @@ fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize {
passed
}
-/// Build the example binary, return true on success
-fn build_example(example_name: &str) -> bool {
- let manifest = format!("examples/{example_name}/Cargo.toml");
- tools::run_cmd_capture(format!(
- "cargo build --manifest-path {manifest} --color always",
- ))
- .is_ok()
-}
-
/// Run a single test case, return true on pass
fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) -> bool {
let binary_path = format!(".temp/target/debug/{}", get_binary_name(example_name));
- let args: Vec<&str> = test_case.command.split_whitespace().collect();
+ let command = test_case.input.join(" ");
let output = match std::process::Command::new(&binary_path)
- .args(&args)
+ .args(&test_case.input)
.output()
{
Ok(o) => o,
Err(e) => {
- bar.println(format!("'{}' - failed to run: {}", test_case.command, e));
+ bar.println(format!("'{command}' - failed to run: {e}"));
return false;
}
};
@@ -127,7 +166,7 @@ fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar)
if exit_ok && result_ok {
true
} else {
- bar.println(format!("failed: '{}'", test_case.command));
+ bar.println(format!("failed: '{command}'"));
if !exit_ok {
bar.println(format!(
" Expected exit code: {}, actual: {}",