use std::path::Path;
use super::tool_param_types::QualityWorkspaceArgs;
use super::{cargo_tools, ci_tools, container_tools, frontend_tools, jvm_tools, python_tools};
mod skip_tail;
#[derive(Clone, Copy)]
struct QualityFlags {
run_cargo_fmt_check: bool,
run_cargo_check: bool,
run_cargo_clippy: bool,
run_cargo_test: bool,
run_frontend_lint: bool,
run_frontend_build: bool,
run_frontend_prettier_check: bool,
run_ruff_check: bool,
run_pytest: bool,
run_mypy: bool,
run_maven_compile: bool,
run_maven_test: bool,
run_gradle_compile: bool,
run_gradle_test: bool,
run_docker_compose_ps: bool,
run_podman_images: bool,
fail_fast: bool,
summary_only: bool,
}
impl QualityFlags {
fn any_step_enabled(self) -> bool {
QualityStep::ORDER
.into_iter()
.any(|step| step.enabled(self))
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum QualityStep {
CargoFmtCheck,
CargoCheck,
CargoClippy,
CargoTest,
FrontendLint,
FrontendBuild,
FrontendPrettierCheck,
RuffCheck,
Pytest,
Mypy,
MavenCompile,
MavenTest,
GradleCompile,
GradleTest,
DockerComposePs,
PodmanImages,
}
impl QualityStep {
const ORDER: [Self; 16] = [
Self::CargoFmtCheck,
Self::CargoCheck,
Self::CargoClippy,
Self::CargoTest,
Self::FrontendLint,
Self::FrontendBuild,
Self::FrontendPrettierCheck,
Self::RuffCheck,
Self::Pytest,
Self::Mypy,
Self::MavenCompile,
Self::MavenTest,
Self::GradleCompile,
Self::GradleTest,
Self::DockerComposePs,
Self::PodmanImages,
];
fn label(self) -> &'static str {
match self {
Self::CargoFmtCheck => "cargo fmt --check",
Self::CargoCheck => "cargo check",
Self::CargoClippy => "cargo clippy",
Self::CargoTest => "cargo test",
Self::FrontendLint => "frontend lint",
Self::FrontendBuild => "frontend build",
Self::FrontendPrettierCheck => "frontend prettier --check",
Self::RuffCheck => "ruff check",
Self::Pytest => "pytest",
Self::Mypy => "mypy",
Self::MavenCompile => "maven compile",
Self::MavenTest => "maven test",
Self::GradleCompile => "gradle compile",
Self::GradleTest => "gradle test",
Self::DockerComposePs => "docker compose ps",
Self::PodmanImages => "podman images",
}
}
fn enabled(self, f: QualityFlags) -> bool {
match self {
Self::CargoFmtCheck => f.run_cargo_fmt_check,
Self::CargoCheck => f.run_cargo_check,
Self::CargoClippy => f.run_cargo_clippy,
Self::CargoTest => f.run_cargo_test,
Self::FrontendLint => f.run_frontend_lint,
Self::FrontendBuild => f.run_frontend_build,
Self::FrontendPrettierCheck => f.run_frontend_prettier_check,
Self::RuffCheck => f.run_ruff_check,
Self::Pytest => f.run_pytest,
Self::Mypy => f.run_mypy,
Self::MavenCompile => f.run_maven_compile,
Self::MavenTest => f.run_maven_test,
Self::GradleCompile => f.run_gradle_compile,
Self::GradleTest => f.run_gradle_test,
Self::DockerComposePs => f.run_docker_compose_ps,
Self::PodmanImages => f.run_podman_images,
}
}
fn ignore_skip_marker(self) -> bool {
matches!(
self,
Self::FrontendLint
| Self::FrontendBuild
| Self::FrontendPrettierCheck
| Self::RuffCheck
| Self::Pytest
| Self::Mypy
| Self::MavenCompile
| Self::MavenTest
| Self::GradleCompile
| Self::GradleTest
| Self::DockerComposePs
| Self::PodmanImages
)
}
fn skip_tail_after_failure(
self,
f: QualityFlags,
summary: &mut Vec<(&'static str, &'static str)>,
) {
skip_tail::skip_tail_after_failure(self, f, summary);
}
}
fn run_quality_step(step: QualityStep, workspace_root: &Path, max_output_len: usize) -> String {
let has_cargo = workspace_root.join("Cargo.toml").is_file();
match step {
QualityStep::CargoFmtCheck => {
ci_tools::cargo_fmt_check_tool("{}", workspace_root, max_output_len)
}
QualityStep::CargoCheck => {
if has_cargo {
cargo_tools::cargo_check(r#"{"all_targets":true}"#, workspace_root, max_output_len)
} else {
"cargo check: 跳过(未找到 Cargo.toml)".to_string()
}
}
QualityStep::CargoClippy => {
if has_cargo {
cargo_tools::cargo_clippy(r#"{"all_targets":true}"#, workspace_root, max_output_len)
} else {
"cargo clippy: 跳过(未找到 Cargo.toml)".to_string()
}
}
QualityStep::CargoTest => {
if has_cargo {
cargo_tools::cargo_test("{}", workspace_root, max_output_len, None)
} else {
"cargo test: 跳过(未找到 Cargo.toml)".to_string()
}
}
QualityStep::FrontendLint => {
frontend_tools::frontend_lint(r#"{"script":"lint"}"#, workspace_root, max_output_len)
}
QualityStep::FrontendBuild => {
frontend_tools::frontend_build(r#"{"script":"build"}"#, workspace_root, max_output_len)
}
QualityStep::FrontendPrettierCheck => {
frontend_tools::frontend_prettier_check("{}", workspace_root, max_output_len)
}
QualityStep::RuffCheck => python_tools::ruff_check("{}", workspace_root, max_output_len),
QualityStep::Pytest => python_tools::pytest_run("{}", workspace_root, max_output_len),
QualityStep::Mypy => python_tools::mypy_check("{}", workspace_root, max_output_len),
QualityStep::MavenCompile => jvm_tools::maven_compile("{}", workspace_root, max_output_len),
QualityStep::MavenTest => jvm_tools::maven_test("{}", workspace_root, max_output_len),
QualityStep::GradleCompile => {
jvm_tools::gradle_compile("{}", workspace_root, max_output_len)
}
QualityStep::GradleTest => jvm_tools::gradle_test("{}", workspace_root, max_output_len),
QualityStep::DockerComposePs => {
container_tools::docker_compose_ps("{}", workspace_root, max_output_len)
}
QualityStep::PodmanImages => {
container_tools::podman_images("{}", workspace_root, max_output_len)
}
}
}
fn step_failed(step: QualityStep, output: &str) -> bool {
if step.ignore_skip_marker() && output.contains("跳过(") {
return false;
}
section_failed(output)
}
pub fn quality_workspace(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
let v = match crate::cm_tools::tools::parse_args_json(args_json) {
Ok(v) => v,
Err(e) => return e,
};
let a: QualityWorkspaceArgs = match serde_json::from_value(v) {
Ok(x) => x,
Err(e) => return format!("参数 JSON 与 quality_workspace 形状不一致: {e}"),
};
let flags = QualityFlags {
run_cargo_fmt_check: a.run_cargo_fmt_check,
run_cargo_check: a.run_cargo_check,
run_cargo_clippy: a.run_cargo_clippy,
run_cargo_test: a.run_cargo_test,
run_frontend_lint: a.run_frontend_lint,
run_frontend_build: a.run_frontend_build,
run_frontend_prettier_check: a.run_frontend_prettier_check,
run_ruff_check: a.run_ruff_check,
run_pytest: a.run_pytest,
run_mypy: a.run_mypy,
run_maven_compile: a.run_maven_compile,
run_maven_test: a.run_maven_test,
run_gradle_compile: a.run_gradle_compile,
run_gradle_test: a.run_gradle_test,
run_docker_compose_ps: a.run_docker_compose_ps,
run_podman_images: a.run_podman_images,
fail_fast: a.fail_fast,
summary_only: a.summary_only,
};
if !flags.any_step_enabled() {
return "错误:至少启用一项检查(含 run_cargo_* / run_frontend_* / run_python_* / run_maven_* / run_gradle_* / run_docker_compose_ps / run_podman_images)".to_string();
}
let mut sections: Vec<String> = Vec::new();
let mut summary: Vec<(&'static str, &'static str)> = Vec::new();
for step in QualityStep::ORDER {
if !step.enabled(flags) {
summary.push((step.label(), "skipped"));
continue;
}
let r = run_quality_step(step, workspace_root, max_output_len);
let failed = step_failed(step, &r);
summary.push((step.label(), if failed { "failed" } else { "passed" }));
sections.push(r);
if flags.fail_fast && failed {
step.skip_tail_after_failure(flags, &mut summary);
return build_output(&summary, §ions, flags.summary_only);
}
}
build_output(&summary, §ions, flags.summary_only)
}
fn section_failed(text: &str) -> bool {
section_failed_execution_markers(text)
|| section_failed_first_line_exit_patterns(text)
|| text.contains("error: could not compile")
|| text.contains("error[E")
}
fn section_failed_execution_markers(text: &str) -> bool {
text.contains("执行失败(") || text.contains(": 执行失败(")
}
fn section_failed_first_line_exit_patterns(text: &str) -> bool {
let Some(line) = text.lines().next() else {
return false;
};
section_failed_cargo_fmt_exit(line)
|| section_failed_cargo_or_rustc_exit(line)
|| section_failed_npm_run_exit(line)
|| section_failed_npx_prettier_exit(line)
|| section_failed_python_linters_exit(line)
|| section_failed_jvm_docker_exit(line)
}
fn exit_suffix_nonzero(line: &str) -> bool {
line.find("(exit=")
.is_some_and(|idx| parse_exit_nonzero(&line[idx..]))
}
fn section_failed_cargo_fmt_exit(line: &str) -> bool {
line.strip_prefix("cargo fmt --check (exit=")
.is_some_and(parse_exit_nonzero)
}
fn section_failed_cargo_or_rustc_exit(line: &str) -> bool {
(line.starts_with("cargo ") || line.starts_with("rustc ")) && exit_suffix_nonzero(line)
}
fn section_failed_npm_run_exit(line: &str) -> bool {
line.contains("npm run") && exit_suffix_nonzero(line)
}
fn section_failed_npx_prettier_exit(line: &str) -> bool {
line.contains("npx prettier") && exit_suffix_nonzero(line)
}
fn section_failed_python_linters_exit(line: &str) -> bool {
(line.starts_with("ruff check")
|| line.starts_with("python3 -m pytest")
|| line.starts_with("mypy"))
&& exit_suffix_nonzero(line)
}
fn section_failed_jvm_docker_exit(line: &str) -> bool {
(line.starts_with("mvn ")
|| line.starts_with("gradle ")
|| line.contains("docker compose ps")
|| line.starts_with("podman images"))
&& exit_suffix_nonzero(line)
}
fn parse_exit_nonzero(s: &str) -> bool {
let s = s.trim();
let rest = s.strip_prefix("(exit=").unwrap_or(s);
let code = rest
.split(':')
.next()
.and_then(|x| x.trim_end_matches(')').parse::<i32>().ok())
.unwrap_or(-1);
code != 0
}
fn build_output(
summary: &[(&'static str, &'static str)],
sections: &[String],
summary_only: bool,
) -> String {
let line = summary
.iter()
.map(|(n, s)| format!("{}={}", n, s))
.collect::<Vec<_>>()
.join(", ");
let head = format!("quality_workspace 汇总: {}\n", line);
if summary_only {
return head.trim_end().to_string();
}
let body = sections.join("\n\n====================\n\n");
format!("{}\n{}", head, body.trim())
}