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