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