Skip to main content

quanttide_devops/stage/
test.rs

1//! 测试阶段:测试覆盖率、函数覆盖率和错误变体覆盖率的审计。
2
3/// 测试结果汇总。
4#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
5pub struct TestSummary {
6    pub total: u32,
7    pub passed: u32,
8    pub failed: u32,
9    pub skipped: u32,
10}
11
12/// 覆盖率数据。
13#[derive(Debug, Default, Clone, PartialEq)]
14pub struct Coverage {
15    pub percentage: f64,
16    pub threshold: f64,
17}
18
19impl Coverage {
20    pub fn met(&self) -> bool {
21        self.percentage >= self.threshold
22    }
23}
24
25/// 质量审计结果。
26#[derive(Debug, Default, Clone, PartialEq)]
27pub struct AuditReport {
28    pub total_tests: usize,
29    pub total_pub_fns: usize,
30    pub pure_pub_fns: usize,
31    pub tested_pub_fns: usize,
32    pub error_variants: usize,
33    pub tested_variants: usize,
34    pub uncovered_fns: Vec<(String, String)>,
35    pub uncovered_variants: Vec<String>,
36    pub coverage_pct: f64,
37    pub coverage_threshold: f64,
38    pub gates_met: bool,
39}
40
41/// I/O 函数模式:这些名称的函数不要求单元测试(集成测试覆盖即可)。
42///
43/// 注意:纯函数(parse_/determine_/build_/fmt_/map_/normalize_/apply_/extract_/is_)
44/// 不应在此列表。
45const IO_FN_PATTERNS: &[&str] = &[
46    "status", "status_to", "run", "run_direct", "run_scoped",
47    "sync", "sync_all", "sync_to_parent", "sync_all_to_parent",
48    "publish", "scan", "scan_offline",
49    "push_submodule", "push_parent", "fetch_submodule", "rebase_submodule",
50    "check_command", "check_syntax", "check_ci", "check_deps",
51    "clear_cache", "save_test_summary",
52    "ensure_", "delete_", "create_",
53    "git_output", "llm_decide", "llm_changelog", "edit_llm",
54    "detect_version", "detect_single_scope", "detect_project_type",
55    "resolve_roadmap_path",
56    "print_status", "print_status_to", "print_scope_audit",
57    "collect_git_log", "collect_tags_with_scope", "collect_test_summary_from_run",
58    "load_contract_scopes", "load_scopes_map",
59    "apply_rule_fixes",
60    "collect_rs_files", "collect_test_fns", "collect_pub_fns", "collect_error_variants",
61];
62
63/// 判断函数名是否为 I/O 型(无需单元测试,集成测试覆盖即可)。
64pub fn is_io_fn(name: &str) -> bool {
65    IO_FN_PATTERNS.iter().any(|p| {
66        if p.ends_with('_') {
67            name.starts_with(p)
68        } else {
69            name == *p
70        }
71    })
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    // ── TestSummary ──────────────────────────────────────────────
79
80    #[test]
81    fn test_test_summary_default() {
82        let s = TestSummary::default();
83        assert_eq!(s.total, 0);
84    }
85
86    #[test]
87    fn test_test_summary_serde_roundtrip() {
88        let s = TestSummary { total: 10, passed: 8, failed: 1, skipped: 1 };
89        let json = serde_json::to_string(&s).unwrap();
90        let back: TestSummary = serde_json::from_str(&json).unwrap();
91        assert_eq!(back, s);
92    }
93
94    #[test]
95    fn test_test_summary_serde_all_zero() {
96        let s = TestSummary { total: 0, passed: 0, failed: 0, skipped: 0 };
97        let json = serde_json::to_string(&s).unwrap();
98        let back: TestSummary = serde_json::from_str(&json).unwrap();
99        assert_eq!(back, s);
100    }
101
102    // ── Coverage ─────────────────────────────────────────────────
103
104    #[test]
105    fn test_coverage_met() {
106        let c = Coverage { percentage: 90.0, threshold: 80.0 };
107        assert!(c.met());
108    }
109
110    #[test]
111    fn test_coverage_not_met() {
112        let c = Coverage { percentage: 70.0, threshold: 80.0 };
113        assert!(!c.met());
114    }
115
116    #[test]
117    fn test_coverage_met_exact() {
118        let c = Coverage { percentage: 80.0, threshold: 80.0 };
119        assert!(c.met());
120    }
121
122    #[test]
123    fn test_coverage_met_zero_threshold() {
124        let c = Coverage { percentage: 0.0, threshold: 0.0 };
125        assert!(c.met());
126    }
127
128    #[test]
129    fn test_coverage_default() {
130        let c = Coverage::default();
131        assert_eq!(c.percentage, 0.0);
132        assert_eq!(c.threshold, 0.0);
133    }
134
135    // ── AuditReport ──────────────────────────────────────────────
136
137    #[test]
138    fn test_audit_report_default() {
139        let r = AuditReport::default();
140        assert_eq!(r.total_tests, 0);
141        assert!(!r.gates_met);
142    }
143
144    #[test]
145    fn test_audit_report_gates_met() {
146        let r = AuditReport {
147            total_tests: 10,
148            total_pub_fns: 5,
149            pure_pub_fns: 5,
150            tested_pub_fns: 3,
151            error_variants: 2,
152            tested_variants: 2,
153            uncovered_fns: vec![],
154            uncovered_variants: vec![],
155            coverage_pct: 100.0,
156            coverage_threshold: 80.0,
157            gates_met: true,
158        };
159        assert!(r.gates_met);
160    }
161
162    // ── is_io_fn ──────────────────────────────────────────────────
163
164    #[test]
165    fn test_is_io_fn_status() {
166        assert!(is_io_fn("status"));
167        assert!(is_io_fn("publish"));
168        assert!(is_io_fn("clear_cache"));
169    }
170
171    #[test]
172    fn test_is_io_fn_prefix() {
173        assert!(is_io_fn("ensure_changelog"));
174        assert!(is_io_fn("delete_tag"));
175        assert!(is_io_fn("create_release"));
176    }
177
178    #[test]
179    fn test_is_io_fn_pure() {
180        assert!(!is_io_fn("parse_version"));
181        assert!(!is_io_fn("normalize_version"));
182        assert!(!is_io_fn("build_version"));
183    }
184}