mdbook_lint_core/
test_helpers.rs

1//! Test helper utilities for rule testing
2//!
3//! This module provides common utilities to reduce boilerplate in rule tests.
4
5use crate::error::Result;
6use crate::{Document, Violation, rule::Rule};
7use std::path::PathBuf;
8
9/// Helper to create a test document from content
10pub fn create_test_document(content: &str, filename: &str) -> Document {
11    Document::new(content.to_string(), PathBuf::from(filename))
12        .expect("Failed to create test document")
13}
14
15/// Helper to create a test document with default filename
16pub fn create_document(content: &str) -> Document {
17    create_test_document(content, "test.md")
18}
19
20/// Helper to run a rule on content and return violations
21pub fn check_rule<T: Rule>(rule: T, content: &str) -> Result<Vec<Violation>> {
22    let document = create_document(content);
23    rule.check(&document)
24}
25
26/// Helper to assert that a rule has no violations
27pub fn assert_no_violations<T: Rule>(rule: T, content: &str) {
28    let violations = check_rule(rule, content).expect("Rule check failed");
29    assert_eq!(
30        violations.len(),
31        0,
32        "Expected no violations but found: {violations:#?}"
33    );
34}
35
36/// Helper to assert that a rule has exactly one violation
37pub fn assert_single_violation<T: Rule>(rule: T, content: &str) -> Violation {
38    let violations = check_rule(rule, content).expect("Rule check failed");
39    assert_eq!(
40        violations.len(),
41        1,
42        "Expected exactly one violation but found: {violations:#?}"
43    );
44    violations.into_iter().next().unwrap()
45}
46
47/// Helper to assert that a rule has a specific number of violations
48pub fn assert_violation_count<T: Rule>(
49    rule: T,
50    content: &str,
51    expected_count: usize,
52) -> Vec<Violation> {
53    let violations = check_rule(rule, content).expect("Rule check failed");
54    assert_eq!(
55        violations.len(),
56        expected_count,
57        "Expected {} violations but found {}: {:#?}",
58        expected_count,
59        violations.len(),
60        violations
61    );
62    violations
63}
64
65/// Helper to check if violations contain a specific message
66pub fn assert_violation_contains_message(violations: &[Violation], message: &str) {
67    let found = violations.iter().any(|v| v.message.contains(message));
68    assert!(
69        found,
70        "Expected to find violation containing '{message}' but found: {violations:#?}"
71    );
72}
73
74/// Helper to assert a violation at a specific line
75pub fn assert_violation_at_line(violations: &[Violation], line: usize) {
76    let found = violations.iter().any(|v| v.line == line);
77    assert!(
78        found,
79        "Expected to find violation at line {} but found violations at lines: {:?}",
80        line,
81        violations.iter().map(|v| v.line).collect::<Vec<_>>()
82    );
83}
84
85/// Helper to assert a violation with specific rule ID
86pub fn assert_violation_rule_id(violations: &[Violation], rule_id: &str) {
87    let found = violations.iter().any(|v| v.rule_id == rule_id);
88    assert!(
89        found,
90        "Expected to find violation with rule ID '{}' but found rule IDs: {:?}",
91        rule_id,
92        violations.iter().map(|v| &v.rule_id).collect::<Vec<_>>()
93    );
94}
95
96/// Helper to assert a violation with specific severity
97pub fn assert_violation_severity(violations: &[Violation], severity: crate::violation::Severity) {
98    let found = violations.iter().any(|v| v.severity == severity);
99    assert!(
100        found,
101        "Expected to find violation with severity {:?} but found severities: {:?}",
102        severity,
103        violations.iter().map(|v| v.severity).collect::<Vec<_>>()
104    );
105}
106
107/// Builder pattern for creating test content with common markdown patterns
108pub struct MarkdownBuilder {
109    content: Vec<String>,
110}
111
112impl MarkdownBuilder {
113    pub fn new() -> Self {
114        Self {
115            content: Vec::new(),
116        }
117    }
118
119    pub fn heading(mut self, level: usize, text: &str) -> Self {
120        let prefix = "#".repeat(level);
121        self.content.push(format!("{prefix} {text}"));
122        self
123    }
124
125    pub fn paragraph(mut self, text: &str) -> Self {
126        self.content.push(text.to_string());
127        self
128    }
129
130    pub fn blank_line(mut self) -> Self {
131        self.content.push(String::new());
132        self
133    }
134
135    pub fn code_block(mut self, language: &str, code: &str) -> Self {
136        self.content.push(format!("```{language}"));
137        for line in code.lines() {
138            self.content.push(line.to_string());
139        }
140        self.content.push("```".to_string());
141        self
142    }
143
144    pub fn unordered_list(mut self, items: &[&str]) -> Self {
145        for item in items {
146            self.content.push(format!("- {item}"));
147        }
148        self
149    }
150
151    pub fn ordered_list(mut self, items: &[&str]) -> Self {
152        for (i, item) in items.iter().enumerate() {
153            self.content.push(format!("{}. {}", i + 1, item));
154        }
155        self
156    }
157
158    pub fn line(mut self, text: &str) -> Self {
159        self.content.push(text.to_string());
160        self
161    }
162
163    pub fn blockquote(mut self, text: &str) -> Self {
164        for line in text.lines() {
165            self.content.push(format!("> {line}"));
166        }
167        self
168    }
169
170    pub fn table(mut self, headers: &[&str], rows: &[Vec<&str>]) -> Self {
171        // Header row
172        let header_line = format!("| {} |", headers.join(" | "));
173        self.content.push(header_line);
174
175        // Separator row
176        let separator = format!(
177            "|{}|",
178            headers.iter().map(|_| "---").collect::<Vec<_>>().join("|")
179        );
180        self.content.push(separator);
181
182        // Data rows
183        for row in rows {
184            let row_line = format!("| {} |", row.join(" | "));
185            self.content.push(row_line);
186        }
187        self
188    }
189
190    pub fn link(mut self, text: &str, url: &str) -> Self {
191        self.content.push(format!("[{text}]({url})"));
192        self
193    }
194
195    pub fn image(mut self, alt_text: &str, url: &str) -> Self {
196        self.content.push(format!("![{alt_text}]({url})"));
197        self
198    }
199
200    pub fn horizontal_rule(mut self) -> Self {
201        self.content.push("---".to_string());
202        self
203    }
204
205    pub fn inline_code(mut self, text: &str, code: &str) -> Self {
206        self.content.push(format!("{text} `{code}`"));
207        self
208    }
209
210    pub fn emphasis(mut self, text: &str) -> Self {
211        self.content.push(format!("*{text}*"));
212        self
213    }
214
215    pub fn strong(mut self, text: &str) -> Self {
216        self.content.push(format!("**{text}**"));
217        self
218    }
219
220    pub fn strikethrough(mut self, text: &str) -> Self {
221        self.content.push(format!("~~{text}~~"));
222        self
223    }
224
225    pub fn footnote_definition(mut self, label: &str, content: &str) -> Self {
226        self.content.push(format!("[^{label}]: {content}"));
227        self
228    }
229
230    pub fn footnote_reference(mut self, text: &str, label: &str) -> Self {
231        self.content.push(format!("{text}[^{label}]"));
232        self
233    }
234
235    pub fn task_list(mut self, items: &[(&str, bool)]) -> Self {
236        for (item, checked) in items {
237            let checkbox = if *checked { "[x]" } else { "[ ]" };
238            self.content.push(format!("- {checkbox} {item}"));
239        }
240        self
241    }
242
243    pub fn nested_list(mut self, items: &[(&str, Option<Vec<&str>>)]) -> Self {
244        for (item, sub_items) in items {
245            self.content.push(format!("- {item}"));
246            if let Some(sub_list) = sub_items {
247                for sub_item in sub_list {
248                    self.content.push(format!("  - {sub_item}"));
249                }
250            }
251        }
252        self
253    }
254
255    pub fn definition_list(mut self, definitions: &[(&str, &str)]) -> Self {
256        for (term, definition) in definitions {
257            self.content.push(term.to_string());
258            self.content.push(format!(": {definition}"));
259        }
260        self
261    }
262
263    pub fn math_block(mut self, formula: &str) -> Self {
264        self.content.push("$$".to_string());
265        self.content.push(formula.to_string());
266        self.content.push("$$".to_string());
267        self
268    }
269
270    pub fn inline_math(mut self, text: &str, formula: &str) -> Self {
271        self.content.push(format!("{text} ${formula}$"));
272        self
273    }
274
275    pub fn build(self) -> String {
276        self.content.join("\n")
277    }
278}
279
280impl Default for MarkdownBuilder {
281    fn default() -> Self {
282        Self::new()
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::{
290        rule::{Rule, RuleCategory, RuleMetadata},
291        violation::Severity,
292    };
293
294    // Mock rule for testing
295    struct TestRule;
296
297    impl Rule for TestRule {
298        fn id(&self) -> &'static str {
299            "TEST001"
300        }
301
302        fn name(&self) -> &'static str {
303            "test-rule"
304        }
305
306        fn description(&self) -> &'static str {
307            "A test rule for testing helpers"
308        }
309
310        fn metadata(&self) -> RuleMetadata {
311            RuleMetadata::stable(RuleCategory::Structure)
312        }
313
314        fn check_with_ast<'a>(
315            &self,
316            _document: &Document,
317            _ast: Option<&'a comrak::nodes::AstNode<'a>>,
318        ) -> Result<Vec<Violation>> {
319            Ok(vec![self.create_violation(
320                "Test violation".to_string(),
321                1,
322                1,
323                Severity::Warning,
324            )])
325        }
326    }
327
328    #[test]
329    fn test_create_document() {
330        let doc = create_document("# Test");
331        assert_eq!(doc.content, "# Test");
332        assert_eq!(doc.path, PathBuf::from("test.md"));
333    }
334
335    #[test]
336    fn test_check_rule() {
337        let rule = TestRule;
338        let violations = check_rule(rule, "# Test").unwrap();
339        assert_eq!(violations.len(), 1);
340        assert_eq!(violations[0].message, "Test violation");
341    }
342
343    #[test]
344    fn test_assert_single_violation() {
345        let rule = TestRule;
346        let violation = assert_single_violation(rule, "# Test");
347        assert_eq!(violation.rule_id, "TEST001");
348        assert_eq!(violation.message, "Test violation");
349    }
350
351    #[test]
352    fn test_assert_violation_contains_message() {
353        let violations = vec![Violation {
354            rule_id: "TEST001".to_string(),
355            rule_name: "test-rule".to_string(),
356            message: "This is a test violation".to_string(),
357            line: 1,
358            column: 1,
359            severity: Severity::Warning,
360            fix: None,
361        }];
362
363        assert_violation_contains_message(&violations, "test violation");
364    }
365
366    #[test]
367    fn test_markdown_builder() {
368        let content = MarkdownBuilder::new()
369            .heading(1, "Title")
370            .blank_line()
371            .paragraph("Some text")
372            .blank_line()
373            .code_block("rust", "fn main() {}")
374            .blank_line()
375            .unordered_list(&["Item 1", "Item 2"])
376            .build();
377
378        let expected = "# Title\n\nSome text\n\n```rust\nfn main() {}\n```\n\n- Item 1\n- Item 2";
379        assert_eq!(content, expected);
380    }
381
382    #[test]
383    fn test_ordered_list_builder() {
384        let content = MarkdownBuilder::new()
385            .ordered_list(&["First", "Second", "Third"])
386            .build();
387
388        let expected = "1. First\n2. Second\n3. Third";
389        assert_eq!(content, expected);
390    }
391
392    #[test]
393    fn test_table_builder() {
394        let content = MarkdownBuilder::new()
395            .table(
396                &["Name", "Age", "City"],
397                &[
398                    vec!["Alice", "30", "New York"],
399                    vec!["Bob", "25", "San Francisco"],
400                ],
401            )
402            .build();
403
404        let expected = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | New York |\n| Bob | 25 | San Francisco |";
405        assert_eq!(content, expected);
406    }
407
408    #[test]
409    fn test_complex_markdown_builder() {
410        let content = MarkdownBuilder::new()
411            .heading(1, "Test Document")
412            .blank_line()
413            .paragraph("This is a test document with various elements.")
414            .blank_line()
415            .blockquote("This is an important quote that spans\nmultiple lines.")
416            .blank_line()
417            .task_list(&[("Complete tests", true), ("Write docs", false)])
418            .blank_line()
419            .link("Visit our site", "https://example.com")
420            .blank_line()
421            .horizontal_rule()
422            .build();
423
424        assert!(content.contains("# Test Document"));
425        assert!(content.contains("> This is an important quote"));
426        assert!(content.contains("- [x] Complete tests"));
427        assert!(content.contains("- [ ] Write docs"));
428        assert!(content.contains("[Visit our site](https://example.com)"));
429        assert!(content.contains("---"));
430    }
431
432    #[test]
433    fn test_nested_list_builder() {
434        let content = MarkdownBuilder::new()
435            .nested_list(&[
436                ("Item 1", Some(vec!["Sub-item A", "Sub-item B"])),
437                ("Item 2", None),
438                ("Item 3", Some(vec!["Sub-item C"])),
439            ])
440            .build();
441
442        let expected =
443            "- Item 1\n  - Sub-item A\n  - Sub-item B\n- Item 2\n- Item 3\n  - Sub-item C";
444        assert_eq!(content, expected);
445    }
446
447    #[test]
448    fn test_assert_violation_count() {
449        let rule = TestRule;
450        let violations = assert_violation_count(rule, "# Test", 1);
451        assert_eq!(violations.len(), 1);
452        assert_eq!(violations[0].rule_id, "TEST001");
453    }
454
455    #[test]
456    fn test_assert_violation_at_line() {
457        let violations = vec![
458            Violation {
459                rule_id: "TEST001".to_string(),
460                rule_name: "test-rule".to_string(),
461                message: "Test violation".to_string(),
462                line: 5,
463                column: 1,
464                severity: Severity::Warning,
465                fix: None,
466            },
467            Violation {
468                rule_id: "TEST002".to_string(),
469                rule_name: "test-rule-2".to_string(),
470                message: "Another test violation".to_string(),
471                line: 10,
472                column: 1,
473                severity: Severity::Error,
474                fix: None,
475            },
476        ];
477
478        assert_violation_at_line(&violations, 5);
479        assert_violation_at_line(&violations, 10);
480    }
481
482    #[test]
483    fn test_assert_violation_rule_id() {
484        let violations = vec![
485            Violation {
486                rule_id: "MD001".to_string(),
487                rule_name: "heading-increment".to_string(),
488                message: "Test violation".to_string(),
489                line: 1,
490                column: 1,
491                severity: Severity::Warning,
492                fix: None,
493            },
494            Violation {
495                rule_id: "MD013".to_string(),
496                rule_name: "line-length".to_string(),
497                message: "Line too long".to_string(),
498                line: 2,
499                column: 1,
500                severity: Severity::Error,
501                fix: None,
502            },
503        ];
504
505        assert_violation_rule_id(&violations, "MD001");
506        assert_violation_rule_id(&violations, "MD013");
507    }
508
509    #[test]
510    fn test_assert_violation_severity() {
511        let violations = vec![
512            Violation {
513                rule_id: "TEST001".to_string(),
514                rule_name: "test-rule".to_string(),
515                message: "Warning violation".to_string(),
516                line: 1,
517                column: 1,
518                severity: Severity::Warning,
519                fix: None,
520            },
521            Violation {
522                rule_id: "TEST002".to_string(),
523                rule_name: "test-rule-2".to_string(),
524                message: "Error violation".to_string(),
525                line: 2,
526                column: 1,
527                severity: Severity::Error,
528                fix: None,
529            },
530        ];
531
532        assert_violation_severity(&violations, Severity::Warning);
533        assert_violation_severity(&violations, Severity::Error);
534    }
535
536    #[test]
537    fn test_markdown_builder_all_methods() {
538        let content = MarkdownBuilder::new()
539            .heading(1, "Main Title")
540            .blank_line()
541            .paragraph("Introduction paragraph")
542            .blank_line()
543            .heading(2, "Section")
544            .code_block("rust", "fn main() {\n    println!(\"Hello\");\n}")
545            .blank_line()
546            .unordered_list(&["First item", "Second item", "Third item"])
547            .blank_line()
548            .ordered_list(&["Step 1", "Step 2", "Step 3"])
549            .blank_line()
550            .line("Custom line of text")
551            .blockquote("Important quote\nSpanning multiple lines")
552            .blank_line()
553            .link("Example", "https://example.com")
554            .blank_line()
555            .image("Alt text", "image.png")
556            .blank_line()
557            .horizontal_rule()
558            .blank_line()
559            .inline_code("Here is", "some_code")
560            .blank_line()
561            .emphasis("emphasized text")
562            .blank_line()
563            .strong("strong text")
564            .blank_line()
565            .strikethrough("crossed out")
566            .blank_line()
567            .footnote_definition("note1", "This is a footnote")
568            .footnote_reference("Text with footnote", "note1")
569            .blank_line()
570            .task_list(&[("Completed task", true), ("Pending task", false)])
571            .blank_line()
572            .definition_list(&[("Term 1", "Definition 1"), ("Term 2", "Definition 2")])
573            .blank_line()
574            .math_block("x = y + z")
575            .blank_line()
576            .inline_math("The equation", "E = mc^2")
577            .build();
578
579        // Verify various components are present
580        assert!(content.contains("# Main Title"));
581        assert!(content.contains("Introduction paragraph"));
582        assert!(content.contains("```rust"));
583        assert!(content.contains("- First item"));
584        assert!(content.contains("1. Step 1"));
585        assert!(content.contains("Custom line of text"));
586        assert!(content.contains("> Important quote"));
587        assert!(content.contains("[Example](https://example.com)"));
588        assert!(content.contains("![Alt text](image.png)"));
589        assert!(content.contains("---"));
590        assert!(content.contains("Here is `some_code`"));
591        assert!(content.contains("*emphasized text*"));
592        assert!(content.contains("**strong text**"));
593        assert!(content.contains("~~crossed out~~"));
594        assert!(content.contains("[^note1]: This is a footnote"));
595        assert!(content.contains("Text with footnote[^note1]"));
596        assert!(content.contains("- [x] Completed task"));
597        assert!(content.contains("- [ ] Pending task"));
598        assert!(content.contains("Term 1"));
599        assert!(content.contains(": Definition 1"));
600        assert!(content.contains("$$"));
601        assert!(content.contains("$E = mc^2$"));
602    }
603
604    #[test]
605    fn test_markdown_builder_default() {
606        let builder = MarkdownBuilder::default();
607        let content = builder.heading(1, "Test").build();
608        assert_eq!(content, "# Test");
609    }
610
611    #[test]
612    fn test_create_test_document_with_filename() {
613        let doc = create_test_document("# Content", "custom.md");
614        assert_eq!(doc.content, "# Content");
615        assert_eq!(doc.path, PathBuf::from("custom.md"));
616    }
617
618    #[test]
619    fn test_all_markdown_builder_edge_cases() {
620        // Test empty lists
621        let content = MarkdownBuilder::new()
622            .unordered_list(&[])
623            .ordered_list(&[])
624            .build();
625        assert_eq!(content, "");
626
627        // Test single items
628        let content = MarkdownBuilder::new()
629            .unordered_list(&["Single"])
630            .blank_line()
631            .ordered_list(&["One"])
632            .build();
633        assert_eq!(content, "- Single\n\n1. One");
634
635        // Test nested list with empty sub-items
636        let content = MarkdownBuilder::new()
637            .nested_list(&[("Item", None)])
638            .build();
639        assert_eq!(content, "- Item");
640
641        // Test table with empty rows
642        let content = MarkdownBuilder::new().table(&["Header"], &[]).build();
643        assert_eq!(content, "| Header |\n|---|");
644
645        // Test definition list edge cases
646        let content = MarkdownBuilder::new().definition_list(&[]).build();
647        assert_eq!(content, "");
648    }
649
650    // Error path testing for test helpers - targeting uncovered assertion failures
651
652    // Mock rule that produces no violations for testing assert_no_violations error path
653    struct NoViolationRule;
654    impl Rule for NoViolationRule {
655        fn id(&self) -> &'static str {
656            "NO_VIO"
657        }
658        fn name(&self) -> &'static str {
659            "no-violation"
660        }
661        fn description(&self) -> &'static str {
662            "Never produces violations"
663        }
664        fn metadata(&self) -> RuleMetadata {
665            RuleMetadata::stable(RuleCategory::Structure)
666        }
667        fn check_with_ast<'a>(
668            &self,
669            _document: &Document,
670            _ast: Option<&'a comrak::nodes::AstNode<'a>>,
671        ) -> Result<Vec<Violation>> {
672            Ok(vec![])
673        }
674    }
675
676    // Mock rule that produces multiple violations for testing single violation error path
677    struct MultiViolationRule;
678    impl Rule for MultiViolationRule {
679        fn id(&self) -> &'static str {
680            "MULTI"
681        }
682        fn name(&self) -> &'static str {
683            "multi-violation"
684        }
685        fn description(&self) -> &'static str {
686            "Produces multiple violations"
687        }
688        fn metadata(&self) -> RuleMetadata {
689            RuleMetadata::stable(RuleCategory::Structure)
690        }
691        fn check_with_ast<'a>(
692            &self,
693            _document: &Document,
694            _ast: Option<&'a comrak::nodes::AstNode<'a>>,
695        ) -> Result<Vec<Violation>> {
696            Ok(vec![
697                self.create_violation("First violation".to_string(), 1, 1, Severity::Warning),
698                self.create_violation("Second violation".to_string(), 2, 1, Severity::Error),
699            ])
700        }
701    }
702
703    #[test]
704    #[should_panic(expected = "Expected no violations but found")]
705    fn test_assert_no_violations_error_path() {
706        // This should panic because TestRule produces violations
707        assert_no_violations(TestRule, "# Test content");
708    }
709
710    #[test]
711    #[should_panic(expected = "Expected exactly one violation but found")]
712    fn test_assert_single_violation_multiple_violations_error() {
713        // This should panic because MultiViolationRule produces 2 violations
714        assert_single_violation(MultiViolationRule, "# Test content");
715    }
716
717    #[test]
718    #[should_panic(expected = "Expected exactly one violation but found")]
719    fn test_assert_single_violation_no_violations_error() {
720        // This should panic because NoViolationRule produces 0 violations
721        assert_single_violation(NoViolationRule, "# Test content");
722    }
723
724    #[test]
725    #[should_panic(expected = "Expected 3 violations but found")]
726    fn test_assert_violation_count_wrong_count_error() {
727        // This should panic because TestRule only produces 1 violation, not 3
728        assert_violation_count(TestRule, "# Test content", 3);
729    }
730
731    #[test]
732    #[should_panic(expected = "Expected to find violation containing 'nonexistent message'")]
733    fn test_assert_violation_contains_message_not_found() {
734        let violations = vec![Violation {
735            rule_id: "TEST".to_string(),
736            rule_name: "test".to_string(),
737            message: "Test violation".to_string(),
738            line: 1,
739            column: 1,
740            severity: Severity::Warning,
741            fix: None,
742        }];
743        assert_violation_contains_message(&violations, "nonexistent message");
744    }
745
746    #[test]
747    #[should_panic(expected = "Expected to find violation at line 999")]
748    fn test_assert_violation_at_line_not_found() {
749        let violations = vec![Violation {
750            rule_id: "TEST".to_string(),
751            rule_name: "test".to_string(),
752            message: "Test violation".to_string(),
753            line: 1,
754            column: 1,
755            severity: Severity::Warning,
756            fix: None,
757        }];
758        assert_violation_at_line(&violations, 999);
759    }
760
761    #[test]
762    #[should_panic(expected = "Expected to find violation with rule ID 'NONEXISTENT'")]
763    fn test_assert_violation_rule_id_not_found() {
764        let violations = vec![Violation {
765            rule_id: "TEST".to_string(),
766            rule_name: "test".to_string(),
767            message: "Test violation".to_string(),
768            line: 1,
769            column: 1,
770            severity: Severity::Warning,
771            fix: None,
772        }];
773        assert_violation_rule_id(&violations, "NONEXISTENT");
774    }
775
776    #[test]
777    #[should_panic(expected = "Expected to find violation with severity")]
778    fn test_assert_violation_severity_not_found() {
779        let violations = vec![Violation {
780            rule_id: "TEST".to_string(),
781            rule_name: "test".to_string(),
782            message: "Test violation".to_string(),
783            line: 1,
784            column: 1,
785            severity: Severity::Warning,
786            fix: None,
787        }];
788        assert_violation_severity(&violations, Severity::Error);
789    }
790
791    #[test]
792    fn test_successful_helper_paths() {
793        // Test successful paths to ensure they work correctly
794        assert_no_violations(NoViolationRule, "# Test content");
795
796        let violation = assert_single_violation(TestRule, "# Test content");
797        assert_eq!(violation.message, "Test violation");
798
799        let violations = assert_violation_count(MultiViolationRule, "# Test content", 2);
800        assert_eq!(violations.len(), 2);
801
802        // Test successful assertion helpers
803        let test_violations = vec![Violation {
804            rule_id: "TEST123".to_string(),
805            rule_name: "test".to_string(),
806            message: "Contains specific text".to_string(),
807            line: 42,
808            column: 1,
809            severity: Severity::Error,
810            fix: None,
811        }];
812
813        assert_violation_contains_message(&test_violations, "specific text");
814        assert_violation_at_line(&test_violations, 42);
815        assert_violation_rule_id(&test_violations, "TEST123");
816        assert_violation_severity(&test_violations, Severity::Error);
817    }
818}