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