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