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