1use crate::error::Result;
6use crate::{Document, Violation, rule::Rule};
7use std::path::PathBuf;
8
9pub 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
15pub fn create_document(content: &str) -> Document {
17 create_test_document(content, "test.md")
18}
19
20pub fn check_rule<T: Rule>(rule: T, content: &str) -> Result<Vec<Violation>> {
22 let document = create_document(content);
23 rule.check(&document)
24}
25
26pub 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
36pub 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
47pub 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
65pub 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
74pub 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
85pub 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
96pub 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
107pub 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 let header_line = format!("| {} |", headers.join(" | "));
173 self.content.push(header_line);
174
175 let separator = format!(
177 "|{}|",
178 headers.iter().map(|_| "---").collect::<Vec<_>>().join("|")
179 );
180 self.content.push(separator);
181
182 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!(""));
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 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 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(""));
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 let content = MarkdownBuilder::new()
622 .unordered_list(&[])
623 .ordered_list(&[])
624 .build();
625 assert_eq!(content, "");
626
627 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 let content = MarkdownBuilder::new()
637 .nested_list(&[("Item", None)])
638 .build();
639 assert_eq!(content, "- Item");
640
641 let content = MarkdownBuilder::new().table(&["Header"], &[]).build();
643 assert_eq!(content, "| Header |\n|---|");
644
645 let content = MarkdownBuilder::new().definition_list(&[]).build();
647 assert_eq!(content, "");
648 }
649
650 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 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 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 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 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 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 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 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}