Skip to main content

exspec_core/
rules.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
8pub enum Severity {
9    Info,
10    Warn,
11    Block,
12}
13
14impl Severity {
15    pub fn as_str(&self) -> &'static str {
16        match self {
17            Severity::Block => "BLOCK",
18            Severity::Warn => "WARN",
19            Severity::Info => "INFO",
20        }
21    }
22
23    pub fn exit_code(&self) -> i32 {
24        match self {
25            Severity::Block => 1,
26            Severity::Warn => 0,
27            Severity::Info => 0,
28        }
29    }
30}
31
32impl fmt::Display for Severity {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "{}", self.as_str())
35    }
36}
37
38impl FromStr for Severity {
39    type Err = String;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s.to_ascii_uppercase().as_str() {
43            "BLOCK" => Ok(Severity::Block),
44            "WARN" => Ok(Severity::Warn),
45            "INFO" => Ok(Severity::Info),
46            _ => Err(format!("unknown severity: {s}")),
47        }
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub struct RuleId(pub String);
53
54impl RuleId {
55    pub fn new(id: &str) -> Self {
56        Self(id.to_string())
57    }
58}
59
60impl fmt::Display for RuleId {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "{}", self.0)
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Diagnostic {
68    pub rule: RuleId,
69    pub severity: Severity,
70    pub file: String,
71    pub line: Option<usize>,
72    pub message: String,
73    pub details: Option<String>,
74}
75
76/// Canonical list of all known rule IDs.
77/// Single source of truth — used for config validation and SARIF output.
78pub const KNOWN_RULE_IDS: &[&str] = &[
79    "T001", "T002", "T003", "T004", "T005", "T006", "T007", "T008", "T101", "T102", "T103", "T105",
80    "T106", "T107", "T108", "T109", "T110",
81];
82
83pub struct Config {
84    pub mock_max: usize,
85    pub mock_class_max: usize,
86    pub test_max_lines: usize,
87    pub test_max_lines_is_explicit: bool,
88    pub parameterized_min_ratio: f64,
89    pub fixture_max: usize,
90    pub min_assertions_for_t105: usize,
91    pub min_duplicate_count: usize,
92    pub disabled_rules: Vec<RuleId>,
93    pub custom_assertion_patterns: Vec<String>,
94    pub ignore_patterns: Vec<String>,
95    pub min_severity: Severity,
96    pub severity_overrides: HashMap<String, Severity>,
97    pub max_fan_out_percent: f64,
98    pub max_reverse_fan_out: usize,
99}
100
101impl Default for Config {
102    fn default() -> Self {
103        Self {
104            mock_max: 5,
105            mock_class_max: 3,
106            test_max_lines: 50,
107            test_max_lines_is_explicit: false,
108            parameterized_min_ratio: 0.1,
109            fixture_max: 5,
110            min_assertions_for_t105: 5,
111            min_duplicate_count: 3,
112            disabled_rules: vec![RuleId::new("T106")], // 93% FP rate (Phase 8a-3)
113            custom_assertion_patterns: Vec::new(),
114            ignore_patterns: Vec::new(),
115            min_severity: Severity::Info,
116            severity_overrides: HashMap::new(),
117            max_fan_out_percent: 6.5,
118            max_reverse_fan_out: 5,
119        }
120    }
121}
122
123use crate::extractor::TestFunction;
124
125pub fn default_test_max_lines(file: &str) -> usize {
126    let ext = std::path::Path::new(file)
127        .extension()
128        .and_then(|e| e.to_str())
129        .unwrap_or("");
130    match ext {
131        "php" | "rs" => 100,
132        "ts" | "tsx" | "jsx" => 75,
133        _ => 50,
134    }
135}
136
137pub fn evaluate_rules(functions: &[TestFunction], config: &Config) -> Vec<Diagnostic> {
138    let mut diagnostics = Vec::new();
139
140    for func in functions {
141        let analysis = &func.analysis;
142
143        // T001: assertion-free
144        if !is_disabled(config, "T001")
145            && !is_suppressed(analysis, "T001")
146            && analysis.assertion_count == 0
147            && !analysis.has_skip_call
148        {
149            diagnostics.push(Diagnostic {
150                rule: RuleId::new("T001"),
151                severity: effective_severity(config, "T001", Severity::Block),
152                file: func.file.clone(),
153                line: Some(func.line),
154                message: format!("assertion-free: {} has no assertions", func.name),
155                details: None,
156            });
157        }
158
159        // T002: mock-overuse
160        if !is_disabled(config, "T002")
161            && !is_suppressed(analysis, "T002")
162            && (analysis.mock_count > config.mock_max
163                || analysis.mock_classes.len() > config.mock_class_max)
164        {
165            diagnostics.push(Diagnostic {
166                rule: RuleId::new("T002"),
167                severity: effective_severity(config, "T002", Severity::Warn),
168                file: func.file.clone(),
169                line: Some(func.line),
170                message: format!(
171                    "mock-overuse: {} mocks ({} classes), threshold: {} mocks / {} classes",
172                    analysis.mock_count,
173                    analysis.mock_classes.len(),
174                    config.mock_max,
175                    config.mock_class_max,
176                ),
177                details: None,
178            });
179        }
180
181        // T003: giant-test
182        let t003_threshold = if config.test_max_lines_is_explicit {
183            config.test_max_lines
184        } else {
185            default_test_max_lines(&func.file)
186        };
187        if !is_disabled(config, "T003")
188            && !is_suppressed(analysis, "T003")
189            && analysis.line_count > t003_threshold
190        {
191            diagnostics.push(Diagnostic {
192                rule: RuleId::new("T003"),
193                severity: effective_severity(config, "T003", Severity::Warn),
194                file: func.file.clone(),
195                line: Some(func.line),
196                message: format!(
197                    "giant-test: {} lines, threshold: {}",
198                    analysis.line_count, t003_threshold,
199                ),
200                details: None,
201            });
202        }
203
204        // T102: fixture-sprawl
205        if !is_disabled(config, "T102")
206            && !is_suppressed(analysis, "T102")
207            && analysis.fixture_count > config.fixture_max
208        {
209            diagnostics.push(Diagnostic {
210                rule: RuleId::new("T102"),
211                severity: effective_severity(config, "T102", Severity::Info),
212                file: func.file.clone(),
213                line: Some(func.line),
214                message: format!(
215                    "fixture-sprawl: {} fixtures, threshold: {}",
216                    analysis.fixture_count, config.fixture_max,
217                ),
218                details: None,
219            });
220        }
221
222        // T108: wait-and-see
223        if !is_disabled(config, "T108") && !is_suppressed(analysis, "T108") && analysis.has_wait {
224            diagnostics.push(Diagnostic {
225                rule: RuleId::new("T108"),
226                severity: effective_severity(config, "T108", Severity::Info),
227                file: func.file.clone(),
228                line: Some(func.line),
229                message: "wait-and-see: test uses sleep/delay (causes flaky tests, consider async/mock alternatives)".to_string(),
230                details: None,
231            });
232        }
233
234        // T110: skip-only-test
235        if !is_disabled(config, "T110")
236            && !is_suppressed(analysis, "T110")
237            && analysis.has_skip_call
238            && analysis.assertion_count == 0
239        {
240            diagnostics.push(Diagnostic {
241                rule: RuleId::new("T110"),
242                severity: effective_severity(config, "T110", Severity::Info),
243                file: func.file.clone(),
244                line: Some(func.line),
245                message: format!(
246                    "skip-only-test: {} contains skip/incomplete without assertions",
247                    func.name
248                ),
249                details: None,
250            });
251        }
252
253        // T106: duplicate-literal-assertion
254        if !is_disabled(config, "T106")
255            && !is_suppressed(analysis, "T106")
256            && analysis.duplicate_literal_count >= config.min_duplicate_count
257        {
258            diagnostics.push(Diagnostic {
259                rule: RuleId::new("T106"),
260                severity: effective_severity(config, "T106", Severity::Info),
261                file: func.file.clone(),
262                line: Some(func.line),
263                message: format!(
264                    "duplicate-literal-assertion: literal appears {} times in assertions (consider extracting to constant or parameter)",
265                    analysis.duplicate_literal_count,
266                ),
267                details: None,
268            });
269        }
270
271        // T107: assertion-roulette
272        if !is_disabled(config, "T107")
273            && !is_suppressed(analysis, "T107")
274            && analysis.assertion_count >= 2
275            && analysis.assertion_message_count == 0
276        {
277            diagnostics.push(Diagnostic {
278                rule: RuleId::new("T107"),
279                severity: effective_severity(config, "T107", Severity::Info),
280                file: func.file.clone(),
281                line: Some(func.line),
282                message: format!(
283                    "assertion-roulette: {} assertions without messages (add failure messages for readability)",
284                    analysis.assertion_count,
285                ),
286                details: None,
287            });
288        }
289
290        // T109: undescriptive-test-name
291        if !is_disabled(config, "T109")
292            && !is_suppressed(analysis, "T109")
293            && is_undescriptive_test_name(&func.name)
294        {
295            diagnostics.push(Diagnostic {
296                rule: RuleId::new("T109"),
297                severity: effective_severity(config, "T109", Severity::Info),
298                file: func.file.clone(),
299                line: Some(func.line),
300                message: format!(
301                    "undescriptive-test-name: \"{}\" does not describe behavior (use descriptive names like \"test_user_creation_returns_valid_id\")",
302                    func.name,
303                ),
304                details: None,
305            });
306        }
307
308        // T101: how-not-what
309        if !is_disabled(config, "T101")
310            && !is_suppressed(analysis, "T101")
311            && analysis.how_not_what_count > 0
312        {
313            diagnostics.push(Diagnostic {
314                rule: RuleId::new("T101"),
315                severity: effective_severity(config, "T101", Severity::Info),
316                file: func.file.clone(),
317                line: Some(func.line),
318                message: format!(
319                    "how-not-what: {} implementation-testing pattern(s) detected",
320                    analysis.how_not_what_count,
321                ),
322                details: None,
323            });
324        }
325    }
326
327    diagnostics
328}
329
330use crate::extractor::FileAnalysis;
331
332pub fn evaluate_file_rules(analyses: &[FileAnalysis], config: &Config) -> Vec<Diagnostic> {
333    let mut diagnostics = Vec::new();
334
335    for analysis in analyses {
336        if analysis.functions.is_empty() {
337            continue;
338        }
339
340        // T006: low-assertion-density
341        // Total assertions / total functions < 1.0 → WARN
342        // Skip if ALL functions are assertion-free (T001 handles those entirely)
343        if !is_disabled(config, "T006") {
344            let has_any_asserting = analysis
345                .functions
346                .iter()
347                .any(|f| f.analysis.assertion_count > 0);
348
349            if has_any_asserting {
350                let total_assertions: usize = analysis
351                    .functions
352                    .iter()
353                    .map(|f| f.analysis.assertion_count)
354                    .sum();
355                let density = total_assertions as f64 / analysis.functions.len() as f64;
356
357                if density < 1.0 {
358                    diagnostics.push(Diagnostic {
359                        rule: RuleId::new("T006"),
360                        severity: effective_severity(config, "T006", Severity::Warn),
361                        file: analysis.file.clone(),
362                        line: None,
363                        message: format!(
364                            "low-assertion-density: {density:.2} assertions/test (threshold: 1.0)",
365                        ),
366                        details: None,
367                    });
368                }
369            }
370        }
371
372        // T004: no-parameterized
373        if !is_disabled(config, "T004") {
374            let total = analysis.functions.len();
375            let ratio = analysis.parameterized_count as f64 / total as f64;
376            if ratio < config.parameterized_min_ratio {
377                diagnostics.push(Diagnostic {
378                    rule: RuleId::new("T004"),
379                    severity: effective_severity(config, "T004", Severity::Info),
380                    file: analysis.file.clone(),
381                    line: None,
382                    message: format!(
383                        "no-parameterized: {}/{} ({:.0}%) parameterized, threshold: {:.0}%",
384                        analysis.parameterized_count,
385                        total,
386                        ratio * 100.0,
387                        config.parameterized_min_ratio * 100.0,
388                    ),
389                    details: None,
390                });
391            }
392        }
393
394        // T005: pbt-missing
395        if !is_disabled(config, "T005") && !analysis.has_pbt_import {
396            diagnostics.push(Diagnostic {
397                rule: RuleId::new("T005"),
398                severity: effective_severity(config, "T005", Severity::Info),
399                file: analysis.file.clone(),
400                line: None,
401                message: "pbt-missing: no property-based testing library imported".to_string(),
402                details: None,
403            });
404        }
405
406        // T008: no-contract
407        if !is_disabled(config, "T008") && !analysis.has_contract_import {
408            diagnostics.push(Diagnostic {
409                rule: RuleId::new("T008"),
410                severity: effective_severity(config, "T008", Severity::Info),
411                file: analysis.file.clone(),
412                line: None,
413                message: "no-contract: no contract/schema library imported".to_string(),
414                details: None,
415            });
416        }
417
418        // T105: deterministic-no-metamorphic
419        if !is_disabled(config, "T105") {
420            let total_assertions: usize = analysis
421                .functions
422                .iter()
423                .map(|f| f.analysis.assertion_count)
424                .sum();
425            if total_assertions >= config.min_assertions_for_t105
426                && !analysis.has_relational_assertion
427            {
428                diagnostics.push(Diagnostic {
429                    rule: RuleId::new("T105"),
430                    severity: effective_severity(config, "T105", Severity::Info),
431                    file: analysis.file.clone(),
432                    line: None,
433                    message: format!(
434                        "deterministic-no-metamorphic: {total_assertions} assertions, all exact equality",
435                    ),
436                    details: None,
437                });
438            }
439        }
440
441        // T103: missing-error-test
442        if !is_disabled(config, "T103") && !analysis.has_error_test {
443            diagnostics.push(Diagnostic {
444                rule: RuleId::new("T103"),
445                severity: effective_severity(config, "T103", Severity::Info),
446                file: analysis.file.clone(),
447                line: None,
448                message: "missing-error-test: no error/exception test found in file".to_string(),
449                details: None,
450            });
451        }
452    }
453
454    diagnostics
455}
456
457pub fn evaluate_project_rules(
458    test_file_count: usize,
459    source_file_count: usize,
460    config: &Config,
461) -> Vec<Diagnostic> {
462    let mut diagnostics = Vec::new();
463
464    // T007: test-source-ratio
465    if !is_disabled(config, "T007") && source_file_count > 0 {
466        let ratio = test_file_count as f64 / source_file_count as f64;
467        diagnostics.push(Diagnostic {
468            rule: RuleId::new("T007"),
469            severity: effective_severity(config, "T007", Severity::Info),
470            file: "<project>".to_string(),
471            line: None,
472            message: format!(
473                "test-source-ratio: {test_file_count}/{source_file_count} ({ratio:.2})",
474            ),
475            details: None,
476        });
477    }
478
479    diagnostics
480}
481
482/// Blacklist of generic test name suffixes (after stripping test_ prefix).
483const GENERIC_TEST_NAMES: &[&str] = &[
484    "case", "example", "sample", "basic", "data", "check", "func", "method",
485];
486
487/// Check if a test name is undescriptive.
488///
489/// Violation patterns:
490/// - `test_` + digits only: `test_1`, `test_123`
491/// - `test` + digits only (camelCase): `test1`, `testCase1`
492/// - `test_` + single short word (4 chars or less): `test_it`, `test_foo`
493/// - Generic blacklist: `test_case`, `test_example`, etc.
494/// - Short string names (TypeScript): `"test 1"`, `"works"`, `"it"`
495pub fn is_undescriptive_test_name(name: &str) -> bool {
496    // Handle TypeScript string-style test names (may contain spaces)
497    let trimmed = name.trim_matches(|c: char| c == '"' || c == '\'');
498    let normalized = if trimmed != name {
499        // String-style name: normalize spaces to underscores for uniform checks
500        trimmed.to_lowercase().replace(' ', "_")
501    } else {
502        name.to_lowercase()
503    };
504
505    // Strip test_ or test prefix
506    let suffix = if let Some(s) = normalized.strip_prefix("test_") {
507        s
508    } else if let Some(s) = normalized.strip_prefix("test") {
509        if s.is_empty() {
510            return true; // just "test"
511        }
512        s
513    } else {
514        // No test prefix - check if the whole name is very short/generic
515        // e.g. TypeScript `it('works', ...)` -> name="works"
516        // Non-ASCII names (CJK, Arabic, Devanagari, etc.) skip single-word heuristic:
517        // CJK languages don't use spaces/underscores between words.
518        let has_non_ascii = !normalized.is_ascii();
519        let is_single_word =
520            !has_non_ascii && !normalized.contains('_') && !normalized.contains(' ');
521        return is_single_word || GENERIC_TEST_NAMES.contains(&normalized.as_str());
522    };
523
524    // Digits only after prefix
525    if suffix.chars().all(|c| c.is_ascii_digit() || c == '_')
526        && suffix.chars().any(|c| c.is_ascii_digit())
527    {
528        return true;
529    }
530
531    // Single short word (4 chars or less, no underscores = single word)
532    // Non-ASCII suffixes skip this check (CJK chars are multi-byte, and even
533    // a single CJK character carries semantic meaning).
534    let has_non_ascii_suffix = !suffix.is_ascii();
535    if !has_non_ascii_suffix && !suffix.contains('_') && suffix.chars().count() <= 4 {
536        return true;
537    }
538
539    // Generic blacklist
540    GENERIC_TEST_NAMES.contains(&suffix)
541}
542
543fn effective_severity(config: &Config, rule_id: &str, default: Severity) -> Severity {
544    config
545        .severity_overrides
546        .get(rule_id)
547        .copied()
548        .unwrap_or(default)
549}
550
551fn is_disabled(config: &Config, rule_id: &str) -> bool {
552    config.disabled_rules.iter().any(|r| r.0 == rule_id)
553}
554
555fn is_suppressed(analysis: &crate::extractor::TestAnalysis, rule_id: &str) -> bool {
556    analysis.suppressed_rules.iter().any(|r| r.0 == rule_id)
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562    use crate::extractor::{TestAnalysis, TestFunction};
563
564    fn make_func(name: &str, analysis: TestAnalysis) -> TestFunction {
565        TestFunction {
566            name: name.to_string(),
567            file: "test.py".to_string(),
568            line: 1,
569            end_line: 10,
570            analysis,
571        }
572    }
573
574    fn make_func_with_file(name: &str, file: &str, analysis: TestAnalysis) -> TestFunction {
575        TestFunction {
576            name: name.to_string(),
577            file: file.to_string(),
578            line: 1,
579            end_line: 10,
580            analysis,
581        }
582    }
583
584    // --- Severity tests (from Phase 1) ---
585
586    #[test]
587    fn severity_ordering() {
588        assert!(Severity::Block > Severity::Warn);
589        assert!(Severity::Warn > Severity::Info);
590    }
591
592    #[test]
593    fn severity_as_str_roundtrip() {
594        for severity in [Severity::Block, Severity::Warn, Severity::Info] {
595            let s = severity.as_str();
596            let parsed = Severity::from_str(s).unwrap();
597            assert_eq!(parsed, severity);
598        }
599    }
600
601    #[test]
602    fn severity_to_exit_code() {
603        assert_eq!(Severity::Block.exit_code(), 1);
604        assert_eq!(Severity::Warn.exit_code(), 0);
605        assert_eq!(Severity::Info.exit_code(), 0);
606    }
607
608    #[test]
609    fn severity_from_str_invalid() {
610        assert!(Severity::from_str("UNKNOWN").is_err());
611    }
612
613    // --- #59: case-insensitive FromStr ---
614
615    #[test]
616    fn severity_from_str_lowercase_block() {
617        assert_eq!(Severity::from_str("block").unwrap(), Severity::Block);
618    }
619
620    #[test]
621    fn severity_from_str_mixed_case_block() {
622        assert_eq!(Severity::from_str("Block").unwrap(), Severity::Block);
623    }
624
625    #[test]
626    fn severity_from_str_lowercase_warn() {
627        assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
628    }
629
630    #[test]
631    fn severity_from_str_lowercase_info() {
632        assert_eq!(Severity::from_str("info").unwrap(), Severity::Info);
633    }
634
635    #[test]
636    fn severity_from_str_invalid_word() {
637        assert!(Severity::from_str("invalid").is_err());
638    }
639
640    #[test]
641    fn rule_id_display() {
642        let id = RuleId::new("T001");
643        assert_eq!(id.to_string(), "T001");
644    }
645
646    // --- T001: assertion-free ---
647
648    #[test]
649    fn t001_assertion_count_zero_produces_block() {
650        let funcs = vec![make_func(
651            "test_no_assert",
652            TestAnalysis {
653                assertion_count: 0,
654                ..Default::default()
655            },
656        )];
657        let diags = evaluate_rules(&funcs, &Config::default());
658        assert_eq!(diags.len(), 1);
659        assert_eq!(diags[0].rule, RuleId::new("T001"));
660        assert_eq!(diags[0].severity, Severity::Block);
661    }
662
663    #[test]
664    fn t001_assertion_count_positive_no_diagnostic() {
665        let funcs = vec![make_func(
666            "test_with_assert",
667            TestAnalysis {
668                assertion_count: 1,
669                ..Default::default()
670            },
671        )];
672        let diags = evaluate_rules(&funcs, &Config::default());
673        assert!(diags.is_empty());
674    }
675
676    #[test]
677    fn t001_has_skip_call_suppresses_t001() {
678        let funcs = vec![make_func(
679            "test_skipped",
680            TestAnalysis {
681                assertion_count: 0,
682                has_skip_call: true,
683                ..Default::default()
684            },
685        )];
686        let diags = evaluate_rules(&funcs, &Config::default());
687        let t001_diags: Vec<_> = diags.iter().filter(|d| d.rule.0 == "T001").collect();
688        assert!(
689            t001_diags.is_empty(),
690            "T001 should not fire when has_skip_call=true"
691        );
692    }
693
694    #[test]
695    fn t110_skip_only_produces_info() {
696        let funcs = vec![make_func(
697            "test_skipped",
698            TestAnalysis {
699                assertion_count: 0,
700                has_skip_call: true,
701                ..Default::default()
702            },
703        )];
704        let diags = evaluate_rules(&funcs, &Config::default());
705        let t110: Vec<_> = diags
706            .iter()
707            .filter(|d| d.rule == RuleId::new("T110"))
708            .collect();
709        assert_eq!(t110.len(), 1);
710        assert_eq!(t110[0].severity, Severity::Info);
711    }
712
713    #[test]
714    fn t110_skip_with_assertion_no_diagnostic() {
715        let funcs = vec![make_func(
716            "test_skipped_after_assert",
717            TestAnalysis {
718                assertion_count: 1,
719                has_skip_call: true,
720                ..Default::default()
721            },
722        )];
723        let diags = evaluate_rules(&funcs, &Config::default());
724        assert!(!diags.iter().any(|d| d.rule == RuleId::new("T110")));
725    }
726
727    #[test]
728    fn t110_assertion_free_non_skip_keeps_t001_exclusive() {
729        let funcs = vec![make_func(
730            "test_no_assert",
731            TestAnalysis {
732                assertion_count: 0,
733                has_skip_call: false,
734                ..Default::default()
735            },
736        )];
737        let diags = evaluate_rules(&funcs, &Config::default());
738        assert!(diags.iter().any(|d| d.rule == RuleId::new("T001")));
739        assert!(!diags.iter().any(|d| d.rule == RuleId::new("T110")));
740    }
741
742    #[test]
743    fn t110_disabled_no_diagnostic() {
744        let funcs = vec![make_func(
745            "test_skipped",
746            TestAnalysis {
747                assertion_count: 0,
748                has_skip_call: true,
749                ..Default::default()
750            },
751        )];
752        let config = Config {
753            disabled_rules: vec![RuleId::new("T110")],
754            ..Config::default()
755        };
756        let diags = evaluate_rules(&funcs, &config);
757        assert!(!diags.iter().any(|d| d.rule == RuleId::new("T110")));
758    }
759
760    #[test]
761    fn t110_suppressed_no_diagnostic() {
762        let funcs = vec![make_func(
763            "test_skipped",
764            TestAnalysis {
765                assertion_count: 0,
766                has_skip_call: true,
767                suppressed_rules: vec![RuleId::new("T110")],
768                ..Default::default()
769            },
770        )];
771        let diags = evaluate_rules(&funcs, &Config::default());
772        assert!(!diags.iter().any(|d| d.rule == RuleId::new("T110")));
773    }
774
775    #[test]
776    fn t110_skip_only_participates_in_t006_density() {
777        let funcs = vec![
778            make_func(
779                "test_skipped",
780                TestAnalysis {
781                    assertion_count: 0,
782                    has_skip_call: true,
783                    ..Default::default()
784                },
785            ),
786            make_func(
787                "test_with_one_assert",
788                TestAnalysis {
789                    assertion_count: 1,
790                    ..Default::default()
791                },
792            ),
793        ];
794        let function_diags = evaluate_rules(&funcs, &Config::default());
795        let file_diags = evaluate_file_rules(
796            &[make_file_analysis("test.py", funcs, false, false, 0)],
797            &Config::default(),
798        );
799        assert!(function_diags.iter().any(|d| d.rule == RuleId::new("T110")));
800        assert!(file_diags.iter().any(|d| d.rule == RuleId::new("T006")));
801    }
802
803    // --- T002: mock-overuse ---
804
805    #[test]
806    fn t002_mock_count_exceeds_threshold_produces_warn() {
807        let funcs = vec![make_func(
808            "test_many_mocks",
809            TestAnalysis {
810                assertion_count: 1,
811                mock_count: 6,
812                mock_classes: vec![
813                    "a".into(),
814                    "b".into(),
815                    "c".into(),
816                    "d".into(),
817                    "e".into(),
818                    "f".into(),
819                ],
820                ..Default::default()
821            },
822        )];
823        let diags = evaluate_rules(&funcs, &Config::default());
824        assert_eq!(diags.len(), 1);
825        assert_eq!(diags[0].rule, RuleId::new("T002"));
826        assert_eq!(diags[0].severity, Severity::Warn);
827    }
828
829    #[test]
830    fn t002_mock_count_within_threshold_no_diagnostic() {
831        let funcs = vec![make_func(
832            "test_few_mocks",
833            TestAnalysis {
834                assertion_count: 1,
835                mock_count: 2,
836                mock_classes: vec!["db".into()],
837                ..Default::default()
838            },
839        )];
840        let diags = evaluate_rules(&funcs, &Config::default());
841        assert!(diags.is_empty());
842    }
843
844    #[test]
845    fn t002_mock_class_count_exceeds_threshold_alone_produces_warn() {
846        let funcs = vec![make_func(
847            "test_many_classes",
848            TestAnalysis {
849                assertion_count: 1,
850                mock_count: 4, // within mock_max=5
851                mock_classes: vec!["a".into(), "b".into(), "c".into(), "d".into()], // > mock_class_max=3
852                ..Default::default()
853            },
854        )];
855        let diags = evaluate_rules(&funcs, &Config::default());
856        assert_eq!(diags.len(), 1);
857        assert_eq!(diags[0].rule, RuleId::new("T002"));
858    }
859
860    // --- T003: giant-test ---
861
862    #[test]
863    fn t003_line_count_exceeds_threshold_produces_warn() {
864        let funcs = vec![make_func(
865            "test_giant",
866            TestAnalysis {
867                assertion_count: 1,
868                line_count: 73,
869                ..Default::default()
870            },
871        )];
872        let diags = evaluate_rules(&funcs, &Config::default());
873        assert_eq!(diags.len(), 1);
874        assert_eq!(diags[0].rule, RuleId::new("T003"));
875        assert_eq!(diags[0].severity, Severity::Warn);
876    }
877
878    #[test]
879    fn t003_line_count_at_threshold_no_diagnostic() {
880        let funcs = vec![make_func(
881            "test_boundary",
882            TestAnalysis {
883                assertion_count: 1,
884                line_count: 50, // exactly at threshold, strict >
885                ..Default::default()
886            },
887        )];
888        let diags = evaluate_rules(&funcs, &Config::default());
889        assert!(diags.is_empty());
890    }
891
892    // --- T003: language-specific default thresholds ---
893
894    #[test]
895    fn tc01_default_test_max_lines_php_returns_100() {
896        // Given: .php file with test function
897        // When: default_test_max_lines called
898        // Then: returns 100
899        assert_eq!(default_test_max_lines("tests/ExampleTest.php"), 100);
900    }
901
902    #[test]
903    fn tc02_default_test_max_lines_rs_returns_100() {
904        // Given: .rs file with test function
905        // When: default_test_max_lines called
906        // Then: returns 100
907        assert_eq!(default_test_max_lines("crates/core/src/rules.rs"), 100);
908    }
909
910    #[test]
911    fn tc03_default_test_max_lines_ts_returns_75() {
912        // Given: .ts file with test function
913        // When: default_test_max_lines called
914        // Then: returns 75
915        assert_eq!(default_test_max_lines("src/auth.test.ts"), 75);
916    }
917
918    #[test]
919    fn tc03_default_test_max_lines_tsx_returns_75() {
920        // Given: .tsx file with test function
921        // When: default_test_max_lines called
922        // Then: returns 75
923        assert_eq!(default_test_max_lines("src/components/Button.test.tsx"), 75);
924    }
925
926    #[test]
927    fn tc04_default_test_max_lines_py_returns_50() {
928        // Given: .py file with test function
929        // When: default_test_max_lines called
930        // Then: returns 50
931        assert_eq!(default_test_max_lines("tests/test_auth.py"), 50);
932    }
933
934    #[test]
935    fn tc05_explicit_config_overrides_language_default_for_php() {
936        // Given: .exspec.toml with explicit test_max_lines = 60, test_max_lines_is_explicit = true
937        // When: T003 evaluated for PHP file with 80-line test
938        // Then: WARN triggered (80 > 60 = explicit config)
939        let config = Config {
940            test_max_lines: 60,
941            test_max_lines_is_explicit: true,
942            ..Config::default()
943        };
944        let funcs = vec![make_func_with_file(
945            "test_feature_create_user",
946            "tests/Feature/UserTest.php",
947            TestAnalysis {
948                assertion_count: 1,
949                line_count: 80,
950                ..Default::default()
951            },
952        )];
953        let diags = evaluate_rules(&funcs, &config);
954        assert_eq!(diags.len(), 1);
955        assert_eq!(diags[0].rule, RuleId::new("T003"));
956        assert_eq!(diags[0].severity, Severity::Warn);
957    }
958
959    #[test]
960    fn tc06_no_explicit_config_php_80_lines_no_warn() {
961        // Given: no explicit test_max_lines in config (test_max_lines_is_explicit = false)
962        // When: T003 evaluated for PHP file with 80-line test
963        // Then: no WARN (80 < 100 = PHP default)
964        let config = Config {
965            test_max_lines_is_explicit: false,
966            ..Config::default()
967        };
968        let funcs = vec![make_func_with_file(
969            "test_feature_create_user",
970            "tests/Feature/UserTest.php",
971            TestAnalysis {
972                assertion_count: 1,
973                line_count: 80,
974                ..Default::default()
975            },
976        )];
977        let diags = evaluate_rules(&funcs, &config);
978        let t003_diags: Vec<_> = diags
979            .iter()
980            .filter(|d| d.rule == RuleId::new("T003"))
981            .collect();
982        assert!(
983            t003_diags.is_empty(),
984            "expected no T003 WARN for 80-line PHP test with default config"
985        );
986    }
987
988    // --- Config disabled ---
989
990    #[test]
991    fn disabled_rule_not_reported() {
992        let funcs = vec![make_func(
993            "test_no_assert",
994            TestAnalysis {
995                assertion_count: 0,
996                ..Default::default()
997            },
998        )];
999        let config = Config {
1000            disabled_rules: vec![RuleId::new("T001")],
1001            ..Config::default()
1002        };
1003        let diags = evaluate_rules(&funcs, &config);
1004        assert!(diags.is_empty());
1005    }
1006
1007    // --- Suppression ---
1008
1009    #[test]
1010    fn suppressed_rule_not_reported() {
1011        let funcs = vec![make_func(
1012            "test_many_mocks",
1013            TestAnalysis {
1014                assertion_count: 1,
1015                mock_count: 6,
1016                mock_classes: vec![
1017                    "a".into(),
1018                    "b".into(),
1019                    "c".into(),
1020                    "d".into(),
1021                    "e".into(),
1022                    "f".into(),
1023                ],
1024                suppressed_rules: vec![RuleId::new("T002")],
1025                ..Default::default()
1026            },
1027        )];
1028        let diags = evaluate_rules(&funcs, &Config::default());
1029        assert!(diags.is_empty());
1030    }
1031
1032    // --- T101: how-not-what ---
1033
1034    #[test]
1035    fn t101_how_not_what_produces_info() {
1036        let funcs = vec![make_func(
1037            "test_calls_repo",
1038            TestAnalysis {
1039                assertion_count: 1,
1040                how_not_what_count: 2,
1041                ..Default::default()
1042            },
1043        )];
1044        let diags = evaluate_rules(&funcs, &Config::default());
1045        assert_eq!(diags.len(), 1);
1046        assert_eq!(diags[0].rule, RuleId::new("T101"));
1047        assert_eq!(diags[0].severity, Severity::Info);
1048        assert!(diags[0]
1049            .message
1050            .contains("2 implementation-testing pattern(s)"));
1051    }
1052
1053    #[test]
1054    fn t101_zero_how_not_what_no_diagnostic() {
1055        let funcs = vec![make_func(
1056            "test_behavior",
1057            TestAnalysis {
1058                assertion_count: 1,
1059                how_not_what_count: 0,
1060                ..Default::default()
1061            },
1062        )];
1063        let diags = evaluate_rules(&funcs, &Config::default());
1064        assert!(diags.is_empty());
1065    }
1066
1067    #[test]
1068    fn t101_disabled_no_diagnostic() {
1069        let funcs = vec![make_func(
1070            "test_calls_repo",
1071            TestAnalysis {
1072                assertion_count: 1,
1073                how_not_what_count: 2,
1074                ..Default::default()
1075            },
1076        )];
1077        let config = Config {
1078            disabled_rules: vec![RuleId::new("T101")],
1079            ..Config::default()
1080        };
1081        let diags = evaluate_rules(&funcs, &config);
1082        assert!(diags.is_empty());
1083    }
1084
1085    #[test]
1086    fn t101_suppressed_no_diagnostic() {
1087        let funcs = vec![make_func(
1088            "test_calls_repo",
1089            TestAnalysis {
1090                assertion_count: 1,
1091                how_not_what_count: 2,
1092                suppressed_rules: vec![RuleId::new("T101")],
1093                ..Default::default()
1094            },
1095        )];
1096        let diags = evaluate_rules(&funcs, &Config::default());
1097        assert!(diags.is_empty());
1098    }
1099
1100    // --- T102: fixture-sprawl ---
1101
1102    #[test]
1103    fn t102_fixture_count_exceeds_threshold_produces_info() {
1104        let funcs = vec![make_func(
1105            "test_sprawl",
1106            TestAnalysis {
1107                assertion_count: 1,
1108                fixture_count: 7,
1109                ..Default::default()
1110            },
1111        )];
1112        let diags = evaluate_rules(&funcs, &Config::default());
1113        assert_eq!(diags.len(), 1);
1114        assert_eq!(diags[0].rule, RuleId::new("T102"));
1115        assert_eq!(diags[0].severity, Severity::Info);
1116        assert!(diags[0].message.contains("7 fixtures"));
1117    }
1118
1119    #[test]
1120    fn t102_fixture_count_at_threshold_no_diagnostic() {
1121        let funcs = vec![make_func(
1122            "test_fixtures_at_threshold",
1123            TestAnalysis {
1124                assertion_count: 1,
1125                fixture_count: 5, // exactly at threshold, strict >
1126                ..Default::default()
1127            },
1128        )];
1129        let diags = evaluate_rules(&funcs, &Config::default());
1130        assert!(diags.is_empty());
1131    }
1132
1133    #[test]
1134    fn t102_zero_fixtures_no_diagnostic() {
1135        let funcs = vec![make_func(
1136            "test_no_fixtures",
1137            TestAnalysis {
1138                assertion_count: 1,
1139                fixture_count: 0,
1140                ..Default::default()
1141            },
1142        )];
1143        let diags = evaluate_rules(&funcs, &Config::default());
1144        assert!(diags.is_empty());
1145    }
1146
1147    #[test]
1148    fn t102_disabled_no_diagnostic() {
1149        let funcs = vec![make_func(
1150            "test_sprawl",
1151            TestAnalysis {
1152                assertion_count: 1,
1153                fixture_count: 7,
1154                ..Default::default()
1155            },
1156        )];
1157        let config = Config {
1158            disabled_rules: vec![RuleId::new("T102")],
1159            ..Config::default()
1160        };
1161        let diags = evaluate_rules(&funcs, &config);
1162        assert!(diags.is_empty());
1163    }
1164
1165    #[test]
1166    fn t102_suppressed_no_diagnostic() {
1167        let funcs = vec![make_func(
1168            "test_sprawl",
1169            TestAnalysis {
1170                assertion_count: 1,
1171                fixture_count: 7,
1172                suppressed_rules: vec![RuleId::new("T102")],
1173                ..Default::default()
1174            },
1175        )];
1176        let diags = evaluate_rules(&funcs, &Config::default());
1177        assert!(diags.is_empty());
1178    }
1179
1180    #[test]
1181    fn t102_custom_threshold() {
1182        let funcs = vec![make_func(
1183            "test_sprawl",
1184            TestAnalysis {
1185                assertion_count: 1,
1186                fixture_count: 4,
1187                ..Default::default()
1188            },
1189        )];
1190        let config = Config {
1191            fixture_max: 3,
1192            ..Config::default()
1193        };
1194        let diags = evaluate_rules(&funcs, &config);
1195        assert_eq!(diags.len(), 1);
1196        assert_eq!(diags[0].rule, RuleId::new("T102"));
1197    }
1198
1199    // --- Multiple violations ---
1200
1201    #[test]
1202    fn multiple_violations_reported() {
1203        let funcs = vec![make_func(
1204            "test_assertion_free_and_giant",
1205            TestAnalysis {
1206                assertion_count: 0,
1207                line_count: 73,
1208                ..Default::default()
1209            },
1210        )];
1211        let diags = evaluate_rules(&funcs, &Config::default());
1212        assert_eq!(diags.len(), 2);
1213        let rule_ids: Vec<&str> = diags.iter().map(|d| d.rule.0.as_str()).collect();
1214        assert!(rule_ids.contains(&"T001"));
1215        assert!(rule_ids.contains(&"T003"));
1216    }
1217
1218    // === File-level rules ===
1219
1220    fn make_file_analysis(
1221        file: &str,
1222        functions: Vec<TestFunction>,
1223        has_pbt_import: bool,
1224        has_contract_import: bool,
1225        parameterized_count: usize,
1226    ) -> FileAnalysis {
1227        make_file_analysis_full(
1228            file,
1229            functions,
1230            has_pbt_import,
1231            has_contract_import,
1232            false,
1233            parameterized_count,
1234        )
1235    }
1236
1237    fn make_file_analysis_full(
1238        file: &str,
1239        functions: Vec<TestFunction>,
1240        has_pbt_import: bool,
1241        has_contract_import: bool,
1242        has_error_test: bool,
1243        parameterized_count: usize,
1244    ) -> FileAnalysis {
1245        FileAnalysis {
1246            file: file.to_string(),
1247            functions,
1248            has_pbt_import,
1249            has_contract_import,
1250            has_error_test,
1251            has_relational_assertion: false,
1252            parameterized_count,
1253        }
1254    }
1255
1256    // --- T006: low-assertion-density ---
1257
1258    #[test]
1259    fn t006_low_density_produces_warn() {
1260        // density = total_assertions / total_functions (all functions, including assertion-free).
1261        // Fires when density < 1.0 and at least one function has assertions.
1262        // When ALL functions are assertion-free, T006 does not fire (T001 handles those).
1263        let funcs = vec![
1264            make_func(
1265                "test_a",
1266                TestAnalysis {
1267                    assertion_count: 1,
1268                    ..Default::default()
1269                },
1270            ),
1271            make_func(
1272                "test_b",
1273                TestAnalysis {
1274                    assertion_count: 0,
1275                    ..Default::default()
1276                },
1277            ),
1278            make_func(
1279                "test_c",
1280                TestAnalysis {
1281                    assertion_count: 0,
1282                    ..Default::default()
1283                },
1284            ),
1285        ];
1286        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1287        let diags = evaluate_file_rules(&analyses, &Config::default());
1288        assert!(diags.iter().any(|d| d.rule.0 == "T006"));
1289    }
1290
1291    #[test]
1292    fn t006_high_density_no_diagnostic() {
1293        let funcs = vec![
1294            make_func(
1295                "test_a",
1296                TestAnalysis {
1297                    assertion_count: 2,
1298                    ..Default::default()
1299                },
1300            ),
1301            make_func(
1302                "test_b",
1303                TestAnalysis {
1304                    assertion_count: 1,
1305                    ..Default::default()
1306                },
1307            ),
1308        ];
1309        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1310        let diags = evaluate_file_rules(&analyses, &Config::default());
1311        assert!(!diags.iter().any(|d| d.rule.0 == "T006"));
1312    }
1313
1314    #[test]
1315    fn t006_all_assertion_free_no_diagnostic() {
1316        let funcs = vec![
1317            make_func(
1318                "test_a",
1319                TestAnalysis {
1320                    assertion_count: 0,
1321                    ..Default::default()
1322                },
1323            ),
1324            make_func(
1325                "test_b",
1326                TestAnalysis {
1327                    assertion_count: 0,
1328                    ..Default::default()
1329                },
1330            ),
1331        ];
1332        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1333        let diags = evaluate_file_rules(&analyses, &Config::default());
1334        assert!(
1335            !diags.iter().any(|d| d.rule.0 == "T006"),
1336            "T006 should not fire when all functions are assertion-free (T001 handles)"
1337        );
1338    }
1339
1340    #[test]
1341    fn t006_empty_file_no_diagnostic() {
1342        let analyses = vec![make_file_analysis("test.py", vec![], false, false, 0)];
1343        let diags = evaluate_file_rules(&analyses, &Config::default());
1344        assert!(!diags.iter().any(|d| d.rule.0 == "T006"));
1345    }
1346
1347    #[test]
1348    fn t006_disabled_no_diagnostic() {
1349        let funcs = vec![
1350            make_func(
1351                "test_a",
1352                TestAnalysis {
1353                    assertion_count: 1,
1354                    ..Default::default()
1355                },
1356            ),
1357            make_func(
1358                "test_b",
1359                TestAnalysis {
1360                    assertion_count: 0,
1361                    ..Default::default()
1362                },
1363            ),
1364        ];
1365        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1366        let config = Config {
1367            disabled_rules: vec![RuleId::new("T006")],
1368            ..Config::default()
1369        };
1370        let diags = evaluate_file_rules(&analyses, &config);
1371        assert!(!diags.iter().any(|d| d.rule.0 == "T006"));
1372    }
1373
1374    // --- T004: no-parameterized ---
1375
1376    #[test]
1377    fn t004_no_parameterized_produces_info() {
1378        let funcs = vec![make_func(
1379            "test_a",
1380            TestAnalysis {
1381                assertion_count: 1,
1382                ..Default::default()
1383            },
1384        )];
1385        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1386        let diags = evaluate_file_rules(&analyses, &Config::default());
1387        assert!(diags.iter().any(|d| d.rule.0 == "T004"));
1388        let t004 = diags.iter().find(|d| d.rule.0 == "T004").unwrap();
1389        assert_eq!(t004.severity, Severity::Info);
1390    }
1391
1392    #[test]
1393    fn t004_sufficient_parameterized_no_diagnostic() {
1394        let funcs = vec![
1395            make_func(
1396                "test_a",
1397                TestAnalysis {
1398                    assertion_count: 1,
1399                    ..Default::default()
1400                },
1401            ),
1402            make_func(
1403                "test_b",
1404                TestAnalysis {
1405                    assertion_count: 1,
1406                    ..Default::default()
1407                },
1408            ),
1409        ];
1410        // parameterized_count=1 out of 2 → ratio 0.5 >= 0.1
1411        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 1)];
1412        let diags = evaluate_file_rules(&analyses, &Config::default());
1413        assert!(!diags.iter().any(|d| d.rule.0 == "T004"));
1414    }
1415
1416    #[test]
1417    fn t004_custom_threshold() {
1418        let funcs = vec![
1419            make_func(
1420                "test_a",
1421                TestAnalysis {
1422                    assertion_count: 1,
1423                    ..Default::default()
1424                },
1425            ),
1426            make_func(
1427                "test_b",
1428                TestAnalysis {
1429                    assertion_count: 1,
1430                    ..Default::default()
1431                },
1432            ),
1433        ];
1434        // 1/2 = 0.5, threshold 0.6 → should fire
1435        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 1)];
1436        let config = Config {
1437            parameterized_min_ratio: 0.6,
1438            ..Config::default()
1439        };
1440        let diags = evaluate_file_rules(&analyses, &config);
1441        assert!(diags.iter().any(|d| d.rule.0 == "T004"));
1442    }
1443
1444    // --- T005: pbt-missing ---
1445
1446    #[test]
1447    fn t005_no_pbt_import_produces_info() {
1448        let funcs = vec![make_func(
1449            "test_a",
1450            TestAnalysis {
1451                assertion_count: 1,
1452                ..Default::default()
1453            },
1454        )];
1455        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1456        let diags = evaluate_file_rules(&analyses, &Config::default());
1457        assert!(diags.iter().any(|d| d.rule.0 == "T005"));
1458    }
1459
1460    #[test]
1461    fn t005_has_pbt_import_no_diagnostic() {
1462        let funcs = vec![make_func(
1463            "test_a",
1464            TestAnalysis {
1465                assertion_count: 1,
1466                ..Default::default()
1467            },
1468        )];
1469        let analyses = vec![make_file_analysis("test.py", funcs, true, false, 0)];
1470        let diags = evaluate_file_rules(&analyses, &Config::default());
1471        assert!(!diags.iter().any(|d| d.rule.0 == "T005"));
1472    }
1473
1474    #[test]
1475    fn t005_empty_file_no_diagnostic() {
1476        let analyses = vec![make_file_analysis("test.py", vec![], false, false, 0)];
1477        let diags = evaluate_file_rules(&analyses, &Config::default());
1478        assert!(!diags.iter().any(|d| d.rule.0 == "T005"));
1479    }
1480
1481    // --- T008: no-contract ---
1482
1483    #[test]
1484    fn t008_no_contract_import_produces_info() {
1485        let funcs = vec![make_func(
1486            "test_a",
1487            TestAnalysis {
1488                assertion_count: 1,
1489                ..Default::default()
1490            },
1491        )];
1492        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1493        let diags = evaluate_file_rules(&analyses, &Config::default());
1494        assert!(diags.iter().any(|d| d.rule.0 == "T008"));
1495    }
1496
1497    #[test]
1498    fn t008_has_contract_import_no_diagnostic() {
1499        let funcs = vec![make_func(
1500            "test_a",
1501            TestAnalysis {
1502                assertion_count: 1,
1503                ..Default::default()
1504            },
1505        )];
1506        let analyses = vec![make_file_analysis("test.py", funcs, false, true, 0)];
1507        let diags = evaluate_file_rules(&analyses, &Config::default());
1508        assert!(!diags.iter().any(|d| d.rule.0 == "T008"));
1509    }
1510
1511    #[test]
1512    fn t008_empty_file_no_diagnostic() {
1513        let analyses = vec![make_file_analysis("test.py", vec![], false, false, 0)];
1514        let diags = evaluate_file_rules(&analyses, &Config::default());
1515        assert!(!diags.iter().any(|d| d.rule.0 == "T008"));
1516    }
1517
1518    // --- T103: missing-error-test ---
1519
1520    #[test]
1521    fn t103_no_error_test_produces_info() {
1522        let funcs = vec![make_func(
1523            "test_a",
1524            TestAnalysis {
1525                assertion_count: 1,
1526                ..Default::default()
1527            },
1528        )];
1529        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1530        let diags = evaluate_file_rules(&analyses, &Config::default());
1531        assert!(diags.iter().any(|d| d.rule.0 == "T103"));
1532        let t103 = diags.iter().find(|d| d.rule.0 == "T103").unwrap();
1533        assert_eq!(t103.severity, Severity::Info);
1534    }
1535
1536    #[test]
1537    fn t103_has_error_test_no_diagnostic() {
1538        let funcs = vec![make_func(
1539            "test_a",
1540            TestAnalysis {
1541                assertion_count: 1,
1542                ..Default::default()
1543            },
1544        )];
1545        let analyses = vec![make_file_analysis_full(
1546            "test.py", funcs, false, false, true, 0,
1547        )];
1548        let diags = evaluate_file_rules(&analyses, &Config::default());
1549        assert!(!diags.iter().any(|d| d.rule.0 == "T103"));
1550    }
1551
1552    #[test]
1553    fn t103_empty_file_no_diagnostic() {
1554        let analyses = vec![make_file_analysis("test.py", vec![], false, false, 0)];
1555        let diags = evaluate_file_rules(&analyses, &Config::default());
1556        assert!(!diags.iter().any(|d| d.rule.0 == "T103"));
1557    }
1558
1559    // --- T105: deterministic-no-metamorphic ---
1560
1561    #[test]
1562    fn t105_all_equality_above_threshold_produces_info() {
1563        let funcs: Vec<TestFunction> = (0..5)
1564            .map(|i| {
1565                make_func(
1566                    &format!("test_{i}"),
1567                    TestAnalysis {
1568                        assertion_count: 1,
1569                        ..Default::default()
1570                    },
1571                )
1572            })
1573            .collect();
1574        let analyses = vec![make_file_analysis_full(
1575            "test.py", funcs, false, false, false, 0,
1576        )];
1577        let diags = evaluate_file_rules(&analyses, &Config::default());
1578        assert!(diags.iter().any(|d| d.rule.0 == "T105"));
1579        let t105 = diags.iter().find(|d| d.rule.0 == "T105").unwrap();
1580        assert_eq!(t105.severity, Severity::Info);
1581        assert!(t105.message.contains("5 assertions"));
1582    }
1583
1584    #[test]
1585    fn t105_has_relational_no_diagnostic() {
1586        let funcs: Vec<TestFunction> = (0..5)
1587            .map(|i| {
1588                make_func(
1589                    &format!("test_{i}"),
1590                    TestAnalysis {
1591                        assertion_count: 1,
1592                        ..Default::default()
1593                    },
1594                )
1595            })
1596            .collect();
1597        let mut analysis = make_file_analysis_full("test.py", funcs, false, false, false, 0);
1598        analysis.has_relational_assertion = true;
1599        let diags = evaluate_file_rules(&[analysis], &Config::default());
1600        assert!(!diags.iter().any(|d| d.rule.0 == "T105"));
1601    }
1602
1603    #[test]
1604    fn t105_below_threshold_no_diagnostic() {
1605        let funcs: Vec<TestFunction> = (0..2)
1606            .map(|i| {
1607                make_func(
1608                    &format!("test_{i}"),
1609                    TestAnalysis {
1610                        assertion_count: 1,
1611                        ..Default::default()
1612                    },
1613                )
1614            })
1615            .collect();
1616        let analyses = vec![make_file_analysis_full(
1617            "test.py", funcs, false, false, false, 0,
1618        )];
1619        let diags = evaluate_file_rules(&analyses, &Config::default());
1620        assert!(!diags.iter().any(|d| d.rule.0 == "T105"));
1621    }
1622
1623    #[test]
1624    fn t105_disabled_no_diagnostic() {
1625        let funcs: Vec<TestFunction> = (0..5)
1626            .map(|i| {
1627                make_func(
1628                    &format!("test_{i}"),
1629                    TestAnalysis {
1630                        assertion_count: 1,
1631                        ..Default::default()
1632                    },
1633                )
1634            })
1635            .collect();
1636        let analyses = vec![make_file_analysis_full(
1637            "test.py", funcs, false, false, false, 0,
1638        )];
1639        let config = Config {
1640            disabled_rules: vec![RuleId::new("T105")],
1641            ..Config::default()
1642        };
1643        let diags = evaluate_file_rules(&analyses, &config);
1644        assert!(!diags.iter().any(|d| d.rule.0 == "T105"));
1645    }
1646
1647    #[test]
1648    fn t105_custom_threshold() {
1649        let funcs: Vec<TestFunction> = (0..3)
1650            .map(|i| {
1651                make_func(
1652                    &format!("test_{i}"),
1653                    TestAnalysis {
1654                        assertion_count: 1,
1655                        ..Default::default()
1656                    },
1657                )
1658            })
1659            .collect();
1660        let analyses = vec![make_file_analysis_full(
1661            "test.py", funcs, false, false, false, 0,
1662        )];
1663        let config = Config {
1664            min_assertions_for_t105: 3,
1665            ..Config::default()
1666        };
1667        let diags = evaluate_file_rules(&analyses, &config);
1668        assert!(diags.iter().any(|d| d.rule.0 == "T105"));
1669    }
1670
1671    #[test]
1672    fn t103_disabled_no_diagnostic() {
1673        let funcs = vec![make_func(
1674            "test_a",
1675            TestAnalysis {
1676                assertion_count: 1,
1677                ..Default::default()
1678            },
1679        )];
1680        let analyses = vec![make_file_analysis("test.py", funcs, false, false, 0)];
1681        let config = Config {
1682            disabled_rules: vec![RuleId::new("T103")],
1683            ..Config::default()
1684        };
1685        let diags = evaluate_file_rules(&analyses, &config);
1686        assert!(!diags.iter().any(|d| d.rule.0 == "T103"));
1687    }
1688
1689    // === Project-level rules ===
1690
1691    // --- T007: test-source-ratio ---
1692
1693    #[test]
1694    fn t007_produces_info_with_ratio() {
1695        let diags = evaluate_project_rules(5, 10, &Config::default());
1696        assert_eq!(diags.len(), 1);
1697        assert_eq!(diags[0].rule, RuleId::new("T007"));
1698        assert_eq!(diags[0].severity, Severity::Info);
1699        assert!(diags[0].message.contains("5/10"));
1700    }
1701
1702    #[test]
1703    fn t007_zero_source_files_no_diagnostic() {
1704        let diags = evaluate_project_rules(5, 0, &Config::default());
1705        assert!(diags.is_empty());
1706    }
1707
1708    #[test]
1709    fn t007_disabled_no_diagnostic() {
1710        let config = Config {
1711            disabled_rules: vec![RuleId::new("T007")],
1712            ..Config::default()
1713        };
1714        let diags = evaluate_project_rules(5, 10, &config);
1715        assert!(diags.is_empty());
1716    }
1717
1718    // --- T108: wait-and-see ---
1719
1720    #[test]
1721    fn t108_has_wait_produces_info() {
1722        let funcs = vec![make_func(
1723            "test_sleepy",
1724            TestAnalysis {
1725                assertion_count: 1,
1726                has_wait: true,
1727                ..Default::default()
1728            },
1729        )];
1730        let diags = evaluate_rules(&funcs, &Config::default());
1731        let t108: Vec<_> = diags
1732            .iter()
1733            .filter(|d| d.rule == RuleId::new("T108"))
1734            .collect();
1735        assert_eq!(t108.len(), 1);
1736        assert_eq!(t108[0].severity, Severity::Info);
1737        assert!(t108[0].message.contains("wait-and-see"));
1738    }
1739
1740    #[test]
1741    fn t108_no_wait_no_diagnostic() {
1742        let funcs = vec![make_func(
1743            "test_fast",
1744            TestAnalysis {
1745                assertion_count: 1,
1746                has_wait: false,
1747                ..Default::default()
1748            },
1749        )];
1750        let diags = evaluate_rules(&funcs, &Config::default());
1751        let t108: Vec<_> = diags
1752            .iter()
1753            .filter(|d| d.rule == RuleId::new("T108"))
1754            .collect();
1755        assert!(t108.is_empty());
1756    }
1757
1758    #[test]
1759    fn t108_disabled_no_diagnostic() {
1760        let funcs = vec![make_func(
1761            "test_sleepy",
1762            TestAnalysis {
1763                assertion_count: 1,
1764                has_wait: true,
1765                ..Default::default()
1766            },
1767        )];
1768        let config = Config {
1769            disabled_rules: vec![RuleId::new("T108")],
1770            ..Config::default()
1771        };
1772        let diags = evaluate_rules(&funcs, &config);
1773        let t108: Vec<_> = diags
1774            .iter()
1775            .filter(|d| d.rule == RuleId::new("T108"))
1776            .collect();
1777        assert!(t108.is_empty());
1778    }
1779
1780    #[test]
1781    fn t108_suppressed_no_diagnostic() {
1782        let funcs = vec![make_func(
1783            "test_sleepy",
1784            TestAnalysis {
1785                assertion_count: 1,
1786                has_wait: true,
1787                suppressed_rules: vec![RuleId::new("T108")],
1788                ..Default::default()
1789            },
1790        )];
1791        let diags = evaluate_rules(&funcs, &Config::default());
1792        let t108: Vec<_> = diags
1793            .iter()
1794            .filter(|d| d.rule == RuleId::new("T108"))
1795            .collect();
1796        assert!(t108.is_empty());
1797    }
1798
1799    // --- T109: undescriptive-test-name ---
1800
1801    #[test]
1802    fn t109_is_undescriptive_digits_only() {
1803        assert!(is_undescriptive_test_name("test_1"));
1804        assert!(is_undescriptive_test_name("test_123"));
1805        assert!(is_undescriptive_test_name("test1"));
1806    }
1807
1808    #[test]
1809    fn t109_is_undescriptive_short_word() {
1810        assert!(is_undescriptive_test_name("test_it"));
1811        assert!(is_undescriptive_test_name("test_foo"));
1812        assert!(is_undescriptive_test_name("test_run"));
1813        // 4-char suffix = boundary (chars().count() <= 4)
1814        assert!(is_undescriptive_test_name("test_main"));
1815        // 5-char suffix = descriptive
1816        assert!(!is_undescriptive_test_name("test_login"));
1817    }
1818
1819    #[test]
1820    fn t109_is_undescriptive_blacklist() {
1821        assert!(is_undescriptive_test_name("test_case"));
1822        assert!(is_undescriptive_test_name("test_example"));
1823        assert!(is_undescriptive_test_name("test_sample"));
1824        assert!(is_undescriptive_test_name("test_basic"));
1825        assert!(is_undescriptive_test_name("test_data"));
1826        assert!(is_undescriptive_test_name("test_check"));
1827        assert!(is_undescriptive_test_name("test_func"));
1828        assert!(is_undescriptive_test_name("test_method"));
1829    }
1830
1831    #[test]
1832    fn t109_is_undescriptive_just_test() {
1833        assert!(is_undescriptive_test_name("test"));
1834    }
1835
1836    #[test]
1837    fn t109_is_descriptive_pass() {
1838        assert!(!is_undescriptive_test_name(
1839            "test_user_creation_returns_valid_id"
1840        ));
1841        assert!(!is_undescriptive_test_name("test_empty_input_raises_error"));
1842        assert!(!is_undescriptive_test_name("test_calculate_total_price"));
1843        assert!(!is_undescriptive_test_name("test_login"));
1844    }
1845
1846    // TC-01: CJK Japanese test name without prefix → descriptive
1847    #[test]
1848    fn t109_cjk_japanese_no_prefix_is_descriptive() {
1849        assert!(!is_undescriptive_test_name(
1850            "ローディング中にスケルトンが表示される"
1851        ));
1852    }
1853
1854    // TC-02: CJK Japanese test name with prefix → descriptive
1855    #[test]
1856    fn t109_cjk_japanese_with_prefix_is_descriptive() {
1857        assert!(!is_undescriptive_test_name(
1858            "test_ユーザー作成が有効なIDを返す"
1859        ));
1860    }
1861
1862    // TC-03: CJK single character with prefix → descriptive (FP削減トレードオフ)
1863    #[test]
1864    fn t109_cjk_single_char_with_prefix_is_descriptive() {
1865        assert!(!is_undescriptive_test_name("test_中"));
1866    }
1867
1868    // TC-04: CJK Chinese test name without prefix → descriptive
1869    #[test]
1870    fn t109_cjk_chinese_no_prefix_is_descriptive() {
1871        assert!(!is_undescriptive_test_name("测试用户创建返回有效ID"));
1872    }
1873
1874    // TC-05: CJK Korean test name with underscores → descriptive
1875    #[test]
1876    fn t109_cjk_korean_with_underscores_is_descriptive() {
1877        assert!(!is_undescriptive_test_name(
1878            "사용자_생성이_유효한_ID를_반환한다"
1879        ));
1880    }
1881
1882    // TC-08: TypeScript string-style CJK name → descriptive
1883    #[test]
1884    fn t109_cjk_typescript_string_style_is_descriptive() {
1885        assert!(!is_undescriptive_test_name(
1886            "'ローディング中にスケルトンが表示される'"
1887        ));
1888    }
1889
1890    #[test]
1891    fn t109_typescript_string_names() {
1892        // Undescriptive
1893        assert!(is_undescriptive_test_name("works"));
1894        assert!(is_undescriptive_test_name("test"));
1895        assert!(is_undescriptive_test_name("it"));
1896        // Descriptive
1897        assert!(!is_undescriptive_test_name(
1898            "should calculate total price correctly"
1899        ));
1900        assert!(!is_undescriptive_test_name(
1901            "returns valid user when given valid credentials"
1902        ));
1903    }
1904
1905    #[test]
1906    fn t109_produces_info_diagnostic() {
1907        let funcs = vec![make_func(
1908            "test_1",
1909            TestAnalysis {
1910                assertion_count: 1,
1911                ..Default::default()
1912            },
1913        )];
1914        let diags = evaluate_rules(&funcs, &Config::default());
1915        let t109: Vec<_> = diags
1916            .iter()
1917            .filter(|d| d.rule == RuleId::new("T109"))
1918            .collect();
1919        assert_eq!(t109.len(), 1);
1920        assert_eq!(t109[0].severity, Severity::Info);
1921        assert!(t109[0].message.contains("undescriptive-test-name"));
1922    }
1923
1924    #[test]
1925    fn t109_descriptive_name_no_diagnostic() {
1926        let funcs = vec![make_func(
1927            "test_user_creation_returns_valid_id",
1928            TestAnalysis {
1929                assertion_count: 1,
1930                ..Default::default()
1931            },
1932        )];
1933        let diags = evaluate_rules(&funcs, &Config::default());
1934        let t109: Vec<_> = diags
1935            .iter()
1936            .filter(|d| d.rule == RuleId::new("T109"))
1937            .collect();
1938        assert!(t109.is_empty());
1939    }
1940
1941    #[test]
1942    fn t109_disabled_no_diagnostic() {
1943        let funcs = vec![make_func(
1944            "test_1",
1945            TestAnalysis {
1946                assertion_count: 1,
1947                ..Default::default()
1948            },
1949        )];
1950        let config = Config {
1951            disabled_rules: vec![RuleId::new("T109")],
1952            ..Config::default()
1953        };
1954        let diags = evaluate_rules(&funcs, &config);
1955        let t109: Vec<_> = diags
1956            .iter()
1957            .filter(|d| d.rule == RuleId::new("T109"))
1958            .collect();
1959        assert!(t109.is_empty());
1960    }
1961
1962    // --- T107: assertion-roulette ---
1963
1964    #[test]
1965    fn t107_multiple_assertions_no_messages_produces_info() {
1966        let funcs = vec![make_func(
1967            "test_multiple_asserts_no_messages",
1968            TestAnalysis {
1969                assertion_count: 3,
1970                assertion_message_count: 0,
1971                ..Default::default()
1972            },
1973        )];
1974        let diags = evaluate_rules(&funcs, &Config::default());
1975        let t107: Vec<_> = diags
1976            .iter()
1977            .filter(|d| d.rule == RuleId::new("T107"))
1978            .collect();
1979        assert_eq!(t107.len(), 1);
1980        assert_eq!(t107[0].severity, Severity::Info);
1981        assert!(t107[0].message.contains("assertion-roulette"));
1982    }
1983
1984    #[test]
1985    fn t107_single_assertion_no_diagnostic() {
1986        let funcs = vec![make_func(
1987            "test_single_assert_passes",
1988            TestAnalysis {
1989                assertion_count: 1,
1990                assertion_message_count: 0,
1991                ..Default::default()
1992            },
1993        )];
1994        let diags = evaluate_rules(&funcs, &Config::default());
1995        let t107: Vec<_> = diags
1996            .iter()
1997            .filter(|d| d.rule == RuleId::new("T107"))
1998            .collect();
1999        assert!(t107.is_empty());
2000    }
2001
2002    #[test]
2003    fn t107_assertions_with_messages_no_diagnostic() {
2004        let funcs = vec![make_func(
2005            "test_asserts_with_messages_pass",
2006            TestAnalysis {
2007                assertion_count: 3,
2008                assertion_message_count: 3,
2009                ..Default::default()
2010            },
2011        )];
2012        let diags = evaluate_rules(&funcs, &Config::default());
2013        let t107: Vec<_> = diags
2014            .iter()
2015            .filter(|d| d.rule == RuleId::new("T107"))
2016            .collect();
2017        assert!(t107.is_empty());
2018    }
2019
2020    #[test]
2021    fn t107_partial_messages_no_diagnostic() {
2022        let funcs = vec![make_func(
2023            "test_partial_messages_still_pass",
2024            TestAnalysis {
2025                assertion_count: 3,
2026                assertion_message_count: 1, // at least some have messages
2027                ..Default::default()
2028            },
2029        )];
2030        let diags = evaluate_rules(&funcs, &Config::default());
2031        let t107: Vec<_> = diags
2032            .iter()
2033            .filter(|d| d.rule == RuleId::new("T107"))
2034            .collect();
2035        assert!(t107.is_empty(), "partial messages should not trigger T107");
2036    }
2037
2038    #[test]
2039    fn t107_disabled_no_diagnostic() {
2040        let funcs = vec![make_func(
2041            "test_multiple_asserts_disabled",
2042            TestAnalysis {
2043                assertion_count: 3,
2044                assertion_message_count: 0,
2045                ..Default::default()
2046            },
2047        )];
2048        let config = Config {
2049            disabled_rules: vec![RuleId::new("T107")],
2050            ..Config::default()
2051        };
2052        let diags = evaluate_rules(&funcs, &config);
2053        let t107: Vec<_> = diags
2054            .iter()
2055            .filter(|d| d.rule == RuleId::new("T107"))
2056            .collect();
2057        assert!(t107.is_empty());
2058    }
2059
2060    // --- T106: duplicate-literal-assertion ---
2061
2062    #[test]
2063    fn t106_default_is_disabled() {
2064        let funcs = vec![make_func(
2065            "test_duplicate_literals",
2066            TestAnalysis {
2067                assertion_count: 4,
2068                duplicate_literal_count: 4,
2069                ..Default::default()
2070            },
2071        )];
2072        let config = Config::default(); // min_duplicate_count = 3
2073        let diags = evaluate_rules(&funcs, &config);
2074        let t106: Vec<_> = diags
2075            .iter()
2076            .filter(|d| d.rule == RuleId::new("T106"))
2077            .collect();
2078        assert!(t106.is_empty());
2079    }
2080
2081    #[test]
2082    fn t106_duplicate_literal_can_be_reenabled_via_severity_override() {
2083        let funcs = vec![make_func(
2084            "test_duplicate_literals",
2085            TestAnalysis {
2086                assertion_count: 4,
2087                duplicate_literal_count: 4,
2088                ..Default::default()
2089            },
2090        )];
2091        let mut overrides = HashMap::new();
2092        overrides.insert("T106".to_string(), Severity::Info);
2093        let config = Config {
2094            disabled_rules: Vec::new(),
2095            severity_overrides: overrides,
2096            ..Default::default()
2097        };
2098        let diags = evaluate_rules(&funcs, &config);
2099        let t106: Vec<_> = diags
2100            .iter()
2101            .filter(|d| d.rule == RuleId::new("T106"))
2102            .collect();
2103        assert_eq!(t106.len(), 1);
2104        assert_eq!(t106[0].severity, Severity::Info);
2105        assert!(t106[0].message.contains("duplicate-literal-assertion"));
2106    }
2107
2108    #[test]
2109    fn t106_below_threshold_no_diagnostic() {
2110        let funcs = vec![make_func(
2111            "test_few_duplicates",
2112            TestAnalysis {
2113                assertion_count: 3,
2114                duplicate_literal_count: 2,
2115                ..Default::default()
2116            },
2117        )];
2118        let config = Config::default();
2119        let diags = evaluate_rules(&funcs, &config);
2120        let t106: Vec<_> = diags
2121            .iter()
2122            .filter(|d| d.rule == RuleId::new("T106"))
2123            .collect();
2124        assert!(t106.is_empty());
2125    }
2126
2127    #[test]
2128    fn t106_at_threshold_produces_diagnostic() {
2129        let funcs = vec![make_func(
2130            "test_at_threshold",
2131            TestAnalysis {
2132                assertion_count: 3,
2133                duplicate_literal_count: 3,
2134                ..Default::default()
2135            },
2136        )];
2137        let mut overrides = HashMap::new();
2138        overrides.insert("T106".to_string(), Severity::Info);
2139        let config = Config {
2140            disabled_rules: Vec::new(),
2141            severity_overrides: overrides,
2142            ..Default::default()
2143        };
2144        let diags = evaluate_rules(&funcs, &config);
2145        let t106: Vec<_> = diags
2146            .iter()
2147            .filter(|d| d.rule == RuleId::new("T106"))
2148            .collect();
2149        assert_eq!(t106.len(), 1);
2150    }
2151
2152    #[test]
2153    fn t106_disabled_no_diagnostic() {
2154        let funcs = vec![make_func(
2155            "test_duplicate_literals",
2156            TestAnalysis {
2157                assertion_count: 4,
2158                duplicate_literal_count: 4,
2159                ..Default::default()
2160            },
2161        )];
2162        let config = Config {
2163            disabled_rules: vec![RuleId::new("T106")],
2164            ..Default::default()
2165        };
2166        let diags = evaluate_rules(&funcs, &config);
2167        let t106: Vec<_> = diags
2168            .iter()
2169            .filter(|d| d.rule == RuleId::new("T106"))
2170            .collect();
2171        assert!(t106.is_empty());
2172    }
2173
2174    #[test]
2175    fn t106_custom_threshold() {
2176        let funcs = vec![make_func(
2177            "test_duplicate_literals",
2178            TestAnalysis {
2179                assertion_count: 4,
2180                duplicate_literal_count: 4,
2181                ..Default::default()
2182            },
2183        )];
2184        let config = Config {
2185            min_duplicate_count: 5,
2186            ..Default::default()
2187        };
2188        let diags = evaluate_rules(&funcs, &config);
2189        let t106: Vec<_> = diags
2190            .iter()
2191            .filter(|d| d.rule == RuleId::new("T106"))
2192            .collect();
2193        assert!(t106.is_empty(), "count=4 with threshold=5 should not fire");
2194    }
2195
2196    // --- #60: effective_severity ---
2197
2198    #[test]
2199    fn effective_severity_returns_override_when_present() {
2200        let mut overrides = HashMap::new();
2201        overrides.insert("T001".to_string(), Severity::Info);
2202        let config = Config {
2203            severity_overrides: overrides,
2204            ..Default::default()
2205        };
2206        assert_eq!(
2207            effective_severity(&config, "T001", Severity::Block),
2208            Severity::Info
2209        );
2210    }
2211
2212    #[test]
2213    fn effective_severity_returns_default_when_no_override() {
2214        let config = Config::default();
2215        assert_eq!(
2216            effective_severity(&config, "T001", Severity::Block),
2217            Severity::Block
2218        );
2219    }
2220
2221    #[test]
2222    fn integration_t107_severity_overridden_to_warn() {
2223        let funcs = vec![make_func(
2224            "test_something",
2225            TestAnalysis {
2226                assertion_count: 3,
2227                assertion_message_count: 0,
2228                ..Default::default()
2229            },
2230        )];
2231        let mut overrides = HashMap::new();
2232        overrides.insert("T107".to_string(), Severity::Warn);
2233        let config = Config {
2234            severity_overrides: overrides,
2235            ..Default::default()
2236        };
2237        let diags = evaluate_rules(&funcs, &config);
2238        let t107: Vec<_> = diags
2239            .iter()
2240            .filter(|d| d.rule == RuleId::new("T107"))
2241            .collect();
2242        assert_eq!(t107.len(), 1);
2243        assert_eq!(
2244            t107[0].severity,
2245            Severity::Warn,
2246            "T107 should be overridden to Warn"
2247        );
2248    }
2249
2250    #[test]
2251    fn integration_t001_severity_overridden_to_warn() {
2252        let funcs = vec![make_func(
2253            "test_no_assertions",
2254            TestAnalysis {
2255                assertion_count: 0,
2256                ..Default::default()
2257            },
2258        )];
2259        let mut overrides = HashMap::new();
2260        overrides.insert("T001".to_string(), Severity::Warn);
2261        let config = Config {
2262            severity_overrides: overrides,
2263            ..Default::default()
2264        };
2265        let diags = evaluate_rules(&funcs, &config);
2266        let t001: Vec<_> = diags
2267            .iter()
2268            .filter(|d| d.rule == RuleId::new("T001"))
2269            .collect();
2270        assert_eq!(t001.len(), 1);
2271        assert_eq!(
2272            t001[0].severity,
2273            Severity::Warn,
2274            "T001 should be overridden to Warn"
2275        );
2276    }
2277}