aboutsummaryrefslogtreecommitdiff
path: root/.run/src/bin/ci.rs
diff options
context:
space:
mode:
Diffstat (limited to '.run/src/bin/ci.rs')
-rw-r--r--.run/src/bin/ci.rs158
1 files changed, 137 insertions, 21 deletions
diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs
index 2527ece..39d55eb 100644
--- a/.run/src/bin/ci.rs
+++ b/.run/src/bin/ci.rs
@@ -1,16 +1,14 @@
use std::io::Write as _;
+use std::path::{Path, PathBuf};
use std::process::exit;
+use arg_picker::{Picker, macros::arg};
use tools::{
cargo_tomls, crate_name_from, eprintln_cargo_style, println_cargo_style, run_cmd, run_parallel,
};
fn get_ignore_dirs() -> Vec<String> {
- vec![
- ".temp".to_string(),
- "mling/res".to_string(),
- "mling\\res".to_string(),
- ]
+ vec![".temp".to_string()]
}
fn print_help() {
@@ -35,19 +33,20 @@ fn main() {
let _ = colored::control::set_virtual_terminal(true);
println!("{}", include_str!("../../../docs/res/ci_banner.txt"));
- let args: Vec<String> = std::env::args().collect();
+ let (auto_yes, dirty, test_docs, refresh_docs, test_codes, help) = Picker::from_args()
+ .pick_or_default(&arg![yes: bool, 'y'])
+ .pick_or_default(&arg![dirty: bool])
+ .pick_or_default(&arg![test_docs: bool])
+ .pick_or_default(&arg![refresh_docs: bool])
+ .pick_or_default(&arg![test_codes: bool])
+ .pick_or_default(&arg![help: bool, 'h'])
+ .unwrap();
- if args.iter().any(|a| a == "-h" || a == "--help") {
+ if help {
print_help();
return;
}
- let auto_yes = args.iter().any(|a| a == "-y");
- let dirty = args.iter().any(|a| a == "--dirty");
-
- let test_docs = args.iter().any(|a| a == "--test-docs");
- let refresh_docs = args.iter().any(|a| a == "--refresh-docs");
- let test_codes = args.iter().any(|a| a == "--test-codes");
let any_specified = test_docs || refresh_docs || test_codes;
let run_all = !any_specified;
@@ -120,17 +119,37 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> {
println_cargo_style!("Phase: Test all crates");
test_all()?;
+
+ println_cargo_style!("Phase: Test arg picker");
+ test_arg_picker()?;
}
if run_all || test_docs {
- println_cargo_style!("Phase: Test all examples");
- test_examples()?;
+ let mut exit_code = 0;
println_cargo_style!("Phase: Verify all *.md document code blocks are compilable");
- test_docs_code_blocks()?;
+ if let Err(code) = test_docs_code_blocks() {
+ exit_code = exit_code.max(code);
+ }
+
+ println_cargo_style!("Phase: Test all examples");
+ if let Err(code) = test_examples() {
+ exit_code = exit_code.max(code);
+ }
println_cargo_style!("Phase: Check all documentation is up to date");
- docs_refresh()?;
+ if let Err(code) = docs_refresh() {
+ exit_code = exit_code.max(code);
+ }
+
+ println_cargo_style!("Phase: Try Build API docs");
+ if let Err(code) = deploy_api_docs() {
+ exit_code = exit_code.max(code);
+ }
+
+ if exit_code != 0 {
+ return Err(exit_code);
+ }
}
run_cmd!("git add --renormalize .")?;
@@ -148,16 +167,56 @@ fn test_docs_code_blocks() -> Result<(), i32> {
)
}
+/// Returns the manifest paths of all workspace members (via `cargo metadata --no-deps`).
+///
+/// These crates are tested/built/clipped together with `--workspace` so that
+/// feature-gated code is covered, instead of relying on each crate's default features.
+fn workspace_manifests() -> Vec<PathBuf> {
+ let Ok(output) = tools::run_cmd_capture("cargo metadata --no-deps --format-version 1") else {
+ return Vec::new();
+ };
+ let Ok(json) = serde_json::from_str::<serde_json::Value>(&output) else {
+ return Vec::new();
+ };
+ json["packages"]
+ .as_array()
+ .into_iter()
+ .flatten()
+ .filter_map(|p| p["manifest_path"].as_str().map(PathBuf::from))
+ .collect()
+}
+
+fn same_path(a: &Path, b: &Path) -> bool {
+ let norm = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
+ norm(a) == norm(b)
+}
+
fn build_all() -> Result<(), i32> {
let ignore_dirs = get_ignore_dirs();
let cargo_tomls = cargo_tomls();
+ let workspace_manifests = workspace_manifests();
let mut tasks = Vec::new();
+
+ // Workspace members: build with all documented features (same set used by cov-test)
+ let features_arg = doc_features_arg();
+ tasks.push((
+ "Build: workspace".to_string(),
+ "workspace".to_string(),
+ format!("cargo build --workspace{features_arg} --color always"),
+ ));
+
for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
+ let path = cargo_toml.parent().unwrap_or(Path::new(""));
let path_str = path.to_string_lossy();
if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
continue;
}
+ if workspace_manifests
+ .iter()
+ .any(|m| same_path(m, &cargo_toml))
+ {
+ continue;
+ }
let label = format!("Build: {}", cargo_toml.to_string_lossy());
let crate_name = crate_name_from(&cargo_toml);
let cmd = format!(
@@ -172,13 +231,29 @@ fn build_all() -> Result<(), i32> {
fn clippy_all() -> Result<(), i32> {
let ignore_dirs = get_ignore_dirs();
let cargo_tomls = cargo_tomls();
+ let workspace_manifests = workspace_manifests();
let mut tasks = Vec::new();
+
+ // Workspace members: clippy with all documented features
+ let features_arg = doc_features_arg();
+ tasks.push((
+ "Clippy: workspace".to_string(),
+ "workspace".to_string(),
+ format!("cargo clippy --workspace{features_arg} --color always -- -D warnings"),
+ ));
+
for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
+ let path = cargo_toml.parent().unwrap_or(Path::new(""));
let path_str = path.to_string_lossy();
if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
continue;
}
+ if workspace_manifests
+ .iter()
+ .any(|m| same_path(m, &cargo_toml))
+ {
+ continue;
+ }
let label = format!("Clippy: {}", cargo_toml.to_string_lossy());
let crate_name = crate_name_from(&cargo_toml);
let cmd = format!(
@@ -190,17 +265,43 @@ fn clippy_all() -> Result<(), i32> {
run_parallel("Clippy", tasks)
}
+/// ` --features "<docs.rs features>"` (empty string when unavailable)
+fn doc_features_arg() -> String {
+ match tools::read_features() {
+ Ok(features) if !features.is_empty() => format!(" --features \"{}\"", features.join(",")),
+ _ => String::new(),
+ }
+}
+
fn test_all() -> Result<(), i32> {
let ignore_dirs = get_ignore_dirs();
let cargo_tomls = cargo_tomls();
+ let workspace_manifests = workspace_manifests();
let mut tasks = Vec::new();
+
+ // Workspace members: test with all documented features so that feature-gated
+ // tests (comp/repl/picker/structural_renderer/...) are actually executed.
+ // `arg-picker` is excluded here and tested separately via [`test_arg_picker`].
+ let features_arg = doc_features_arg();
+ tasks.push((
+ "Test: workspace".to_string(),
+ "workspace".to_string(),
+ format!("cargo test --workspace{features_arg} --exclude arg-picker --color always"),
+ ));
+
for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
+ let path = cargo_toml.parent().unwrap_or(Path::new(""));
let path_str = path.to_string_lossy();
if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
continue;
}
- let label = format!("Testing: {}", cargo_toml.to_string_lossy());
+ if workspace_manifests
+ .iter()
+ .any(|m| same_path(m, &cargo_toml))
+ {
+ continue;
+ }
+ let label = format!("Test: {}", cargo_toml.to_string_lossy());
let crate_name = crate_name_from(&cargo_toml);
let cmd = format!(
"cargo test --manifest-path {} --color always",
@@ -211,6 +312,21 @@ fn test_all() -> Result<(), i32> {
run_parallel("Testing", tasks)
}
+/// `arg-picker` is excluded from the workspace test command: when built with
+/// `mingling_support` (enabled via `mingling/picker`), its README doctests
+/// expand `arg!` to `::mingling::picker::PickerArg`, which is not available
+/// inside the arg-picker crate itself. Test it separately with its default
+/// features instead.
+fn test_arg_picker() -> Result<(), i32> {
+ run_cmd!("cargo test -p arg-picker --color always")
+}
+
+fn deploy_api_docs() -> Result<(), i32> {
+ run_cmd!(
+ "cargo run --manifest-path .run/Cargo.toml --color always --bin deploy-api-docs -- --docsrs"
+ )
+}
+
fn docs_refresh() -> Result<(), i32> {
println_cargo_style!("Refresh: document at `./docs/`");