diff options
Diffstat (limited to '.run/src/bin/ci.rs')
| -rw-r--r-- | .run/src/bin/ci.rs | 158 |
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/`"); |
