use crate::error::Result;
use crate::{Document, Violation, rule::Rule};
use std::path::PathBuf;
pub fn create_test_document(content: &str, filename: &str) -> Document {
Document::new(content.to_string(), PathBuf::from(filename))
.expect("Failed to create test document")
}
pub fn create_document(content: &str) -> Document {
create_test_document(content, "test.md")
}
pub fn check_rule<T: Rule>(rule: T, content: &str) -> Result<Vec<Violation>> {
let document = create_document(content);
rule.check(&document)
}
pub fn assert_no_violations<T: Rule>(rule: T, content: &str) {
let violations = check_rule(rule, content).expect("Rule check failed");
assert_eq!(
violations.len(),
0,
"Expected no violations but found: {violations:#?}"
);
}
pub fn assert_single_violation<T: Rule>(rule: T, content: &str) -> Violation {
let violations = check_rule(rule, content).expect("Rule check failed");
assert_eq!(
violations.len(),
1,
"Expected exactly one violation but found: {violations:#?}"
);
violations.into_iter().next().unwrap()
}
pub fn assert_violation_count<T: Rule>(
rule: T,
content: &str,
expected_count: usize,
) -> Vec<Violation> {
let violations = check_rule(rule, content).expect("Rule check failed");
assert_eq!(
violations.len(),
expected_count,
"Expected {} violations but found {}: {:#?}",
expected_count,
violations.len(),
violations
);
violations
}
pub fn assert_violation_contains_message(violations: &[Violation], message: &str) {
let found = violations.iter().any(|v| v.message.contains(message));
assert!(
found,
"Expected to find violation containing '{message}' but found: {violations:#?}"
);
}
pub fn assert_violation_at_line(violations: &[Violation], line: usize) {
let found = violations.iter().any(|v| v.line == line);
assert!(
found,
"Expected to find violation at line {} but found violations at lines: {:?}",
line,
violations.iter().map(|v| v.line).collect::<Vec<_>>()
);
}
pub fn assert_violation_rule_id(violations: &[Violation], rule_id: &str) {
let found = violations.iter().any(|v| v.rule_id == rule_id);
assert!(
found,
"Expected to find violation with rule ID '{}' but found rule IDs: {:?}",
rule_id,
violations.iter().map(|v| &v.rule_id).collect::<Vec<_>>()
);
}
pub fn assert_violation_severity(violations: &[Violation], severity: crate::violation::Severity) {
let found = violations.iter().any(|v| v.severity == severity);
assert!(
found,
"Expected to find violation with severity {:?} but found severities: {:?}",
severity,
violations.iter().map(|v| v.severity).collect::<Vec<_>>()
);
}
pub struct MarkdownBuilder {
content: Vec<String>,
}
impl MarkdownBuilder {
pub fn new() -> Self {
Self {
content: Vec::new(),
}
}
pub fn heading(mut self, level: usize, text: &str) -> Self {
let prefix = "#".repeat(level);
self.content.push(format!("{prefix} {text}"));
self
}
pub fn paragraph(mut self, text: &str) -> Self {
self.content.push(text.to_string());
self
}
pub fn blank_line(mut self) -> Self {
self.content.push(String::new());
self
}
pub fn code_block(mut self, language: &str, code: &str) -> Self {
self.content.push(format!("```{language}"));
for line in code.lines() {
self.content.push(line.to_string());
}
self.content.push("```".to_string());
self
}
pub fn unordered_list(mut self, items: &[&str]) -> Self {
for item in items {
self.content.push(format!("- {item}"));
}
self
}
pub fn ordered_list(mut self, items: &[&str]) -> Self {
for (i, item) in items.iter().enumerate() {
self.content.push(format!("{}. {}", i + 1, item));
}
self
}
pub fn line(mut self, text: &str) -> Self {
self.content.push(text.to_string());
self
}
pub fn blockquote(mut self, text: &str) -> Self {
for line in text.lines() {
self.content.push(format!("> {line}"));
}
self
}
pub fn table(mut self, headers: &[&str], rows: &[Vec<&str>]) -> Self {
let header_line = format!("| {} |", headers.join(" | "));
self.content.push(header_line);
let separator = format!(
"|{}|",
headers.iter().map(|_| "---").collect::<Vec<_>>().join("|")
);
self.content.push(separator);
for row in rows {
let row_line = format!("| {} |", row.join(" | "));
self.content.push(row_line);
}
self
}
pub fn link(mut self, text: &str, url: &str) -> Self {
self.content.push(format!("[{text}]({url})"));
self
}
pub fn image(mut self, alt_text: &str, url: &str) -> Self {
self.content.push(format!(""));
self
}
pub fn horizontal_rule(mut self) -> Self {
self.content.push("---".to_string());
self
}
pub fn inline_code(mut self, text: &str, code: &str) -> Self {
self.content.push(format!("{text} `{code}`"));
self
}
pub fn emphasis(mut self, text: &str) -> Self {
self.content.push(format!("*{text}*"));
self
}
pub fn strong(mut self, text: &str) -> Self {
self.content.push(format!("**{text}**"));
self
}
pub fn strikethrough(mut self, text: &str) -> Self {
self.content.push(format!("~~{text}~~"));
self
}
pub fn footnote_definition(mut self, label: &str, content: &str) -> Self {
self.content.push(format!("[^{label}]: {content}"));
self
}
pub fn footnote_reference(mut self, text: &str, label: &str) -> Self {
self.content.push(format!("{text}[^{label}]"));
self
}
pub fn task_list(mut self, items: &[(&str, bool)]) -> Self {
for (item, checked) in items {
let checkbox = if *checked { "[x]" } else { "[ ]" };
self.content.push(format!("- {checkbox} {item}"));
}
self
}
pub fn nested_list(mut self, items: &[(&str, Option<Vec<&str>>)]) -> Self {
for (item, sub_items) in items {
self.content.push(format!("- {item}"));
if let Some(sub_list) = sub_items {
for sub_item in sub_list {
self.content.push(format!(" - {sub_item}"));
}
}
}
self
}
pub fn definition_list(mut self, definitions: &[(&str, &str)]) -> Self {
for (term, definition) in definitions {
self.content.push(term.to_string());
self.content.push(format!(": {definition}"));
}
self
}
pub fn math_block(mut self, formula: &str) -> Self {
self.content.push("$$".to_string());
self.content.push(formula.to_string());
self.content.push("$$".to_string());
self
}
pub fn inline_math(mut self, text: &str, formula: &str) -> Self {
self.content.push(format!("{text} ${formula}$"));
self
}
pub fn build(self) -> String {
self.content.join("\n")
}
}
impl Default for MarkdownBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
rule::{Rule, RuleCategory, RuleMetadata},
violation::Severity,
};
struct TestRule;
impl Rule for TestRule {
fn id(&self) -> &'static str {
"TEST001"
}
fn name(&self) -> &'static str {
"test-rule"
}
fn description(&self) -> &'static str {
"A test rule for testing helpers"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Structure)
}
fn check_with_ast<'a>(
&self,
_document: &Document,
_ast: Option<&'a comrak::nodes::AstNode<'a>>,
) -> Result<Vec<Violation>> {
Ok(vec![self.create_violation(
"Test violation".to_string(),
1,
1,
Severity::Warning,
)])
}
}
#[test]
fn test_create_document() {
let doc = create_document("# Test");
assert_eq!(doc.content, "# Test");
assert_eq!(doc.path, PathBuf::from("test.md"));
}
#[test]
fn test_check_rule() {
let rule = TestRule;
let violations = check_rule(rule, "# Test").unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].message, "Test violation");
}
#[test]
fn test_assert_single_violation() {
let rule = TestRule;
let violation = assert_single_violation(rule, "# Test");
assert_eq!(violation.rule_id, "TEST001");
assert_eq!(violation.message, "Test violation");
}
#[test]
fn test_assert_violation_contains_message() {
let violations = vec![Violation {
rule_id: "TEST001".to_string(),
rule_name: "test-rule".to_string(),
message: "This is a test violation".to_string(),
line: 1,
column: 1,
severity: Severity::Warning,
}];
assert_violation_contains_message(&violations, "test violation");
}
#[test]
fn test_markdown_builder() {
let content = MarkdownBuilder::new()
.heading(1, "Title")
.blank_line()
.paragraph("Some text")
.blank_line()
.code_block("rust", "fn main() {}")
.blank_line()
.unordered_list(&["Item 1", "Item 2"])
.build();
let expected = "# Title\n\nSome text\n\n```rust\nfn main() {}\n```\n\n- Item 1\n- Item 2";
assert_eq!(content, expected);
}
#[test]
fn test_ordered_list_builder() {
let content = MarkdownBuilder::new()
.ordered_list(&["First", "Second", "Third"])
.build();
let expected = "1. First\n2. Second\n3. Third";
assert_eq!(content, expected);
}
#[test]
fn test_table_builder() {
let content = MarkdownBuilder::new()
.table(
&["Name", "Age", "City"],
&[
vec!["Alice", "30", "New York"],
vec!["Bob", "25", "San Francisco"],
],
)
.build();
let expected = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | New York |\n| Bob | 25 | San Francisco |";
assert_eq!(content, expected);
}
#[test]
fn test_complex_markdown_builder() {
let content = MarkdownBuilder::new()
.heading(1, "Test Document")
.blank_line()
.paragraph("This is a test document with various elements.")
.blank_line()
.blockquote("This is an important quote that spans\nmultiple lines.")
.blank_line()
.task_list(&[("Complete tests", true), ("Write docs", false)])
.blank_line()
.link("Visit our site", "https://example.com")
.blank_line()
.horizontal_rule()
.build();
assert!(content.contains("# Test Document"));
assert!(content.contains("> This is an important quote"));
assert!(content.contains("- [x] Complete tests"));
assert!(content.contains("- [ ] Write docs"));
assert!(content.contains("[Visit our site](https://example.com)"));
assert!(content.contains("---"));
}
#[test]
fn test_nested_list_builder() {
let content = MarkdownBuilder::new()
.nested_list(&[
("Item 1", Some(vec!["Sub-item A", "Sub-item B"])),
("Item 2", None),
("Item 3", Some(vec!["Sub-item C"])),
])
.build();
let expected =
"- Item 1\n - Sub-item A\n - Sub-item B\n- Item 2\n- Item 3\n - Sub-item C";
assert_eq!(content, expected);
}
#[test]
fn test_assert_violation_count() {
let rule = TestRule;
let violations = assert_violation_count(rule, "# Test", 1);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_id, "TEST001");
}
#[test]
fn test_assert_violation_at_line() {
let violations = vec![
Violation {
rule_id: "TEST001".to_string(),
rule_name: "test-rule".to_string(),
message: "Test violation".to_string(),
line: 5,
column: 1,
severity: Severity::Warning,
},
Violation {
rule_id: "TEST002".to_string(),
rule_name: "test-rule-2".to_string(),
message: "Another test violation".to_string(),
line: 10,
column: 1,
severity: Severity::Error,
},
];
assert_violation_at_line(&violations, 5);
assert_violation_at_line(&violations, 10);
}
#[test]
fn test_assert_violation_rule_id() {
let violations = vec![
Violation {
rule_id: "MD001".to_string(),
rule_name: "heading-increment".to_string(),
message: "Test violation".to_string(),
line: 1,
column: 1,
severity: Severity::Warning,
},
Violation {
rule_id: "MD013".to_string(),
rule_name: "line-length".to_string(),
message: "Line too long".to_string(),
line: 2,
column: 1,
severity: Severity::Error,
},
];
assert_violation_rule_id(&violations, "MD001");
assert_violation_rule_id(&violations, "MD013");
}
#[test]
fn test_assert_violation_severity() {
let violations = vec![
Violation {
rule_id: "TEST001".to_string(),
rule_name: "test-rule".to_string(),
message: "Warning violation".to_string(),
line: 1,
column: 1,
severity: Severity::Warning,
},
Violation {
rule_id: "TEST002".to_string(),
rule_name: "test-rule-2".to_string(),
message: "Error violation".to_string(),
line: 2,
column: 1,
severity: Severity::Error,
},
];
assert_violation_severity(&violations, Severity::Warning);
assert_violation_severity(&violations, Severity::Error);
}
#[test]
fn test_markdown_builder_all_methods() {
let content = MarkdownBuilder::new()
.heading(1, "Main Title")
.blank_line()
.paragraph("Introduction paragraph")
.blank_line()
.heading(2, "Section")
.code_block("rust", "fn main() {\n println!(\"Hello\");\n}")
.blank_line()
.unordered_list(&["First item", "Second item", "Third item"])
.blank_line()
.ordered_list(&["Step 1", "Step 2", "Step 3"])
.blank_line()
.line("Custom line of text")
.blockquote("Important quote\nSpanning multiple lines")
.blank_line()
.link("Example", "https://example.com")
.blank_line()
.image("Alt text", "image.png")
.blank_line()
.horizontal_rule()
.blank_line()
.inline_code("Here is", "some_code")
.blank_line()
.emphasis("emphasized text")
.blank_line()
.strong("strong text")
.blank_line()
.strikethrough("crossed out")
.blank_line()
.footnote_definition("note1", "This is a footnote")
.footnote_reference("Text with footnote", "note1")
.blank_line()
.task_list(&[("Completed task", true), ("Pending task", false)])
.blank_line()
.definition_list(&[("Term 1", "Definition 1"), ("Term 2", "Definition 2")])
.blank_line()
.math_block("x = y + z")
.blank_line()
.inline_math("The equation", "E = mc^2")
.build();
assert!(content.contains("# Main Title"));
assert!(content.contains("Introduction paragraph"));
assert!(content.contains("```rust"));
assert!(content.contains("- First item"));
assert!(content.contains("1. Step 1"));
assert!(content.contains("Custom line of text"));
assert!(content.contains("> Important quote"));
assert!(content.contains("[Example](https://example.com)"));
assert!(content.contains(""));
assert!(content.contains("---"));
assert!(content.contains("Here is `some_code`"));
assert!(content.contains("*emphasized text*"));
assert!(content.contains("**strong text**"));
assert!(content.contains("~~crossed out~~"));
assert!(content.contains("[^note1]: This is a footnote"));
assert!(content.contains("Text with footnote[^note1]"));
assert!(content.contains("- [x] Completed task"));
assert!(content.contains("- [ ] Pending task"));
assert!(content.contains("Term 1"));
assert!(content.contains(": Definition 1"));
assert!(content.contains("$$"));
assert!(content.contains("$E = mc^2$"));
}
#[test]
fn test_markdown_builder_default() {
let builder = MarkdownBuilder::default();
let content = builder.heading(1, "Test").build();
assert_eq!(content, "# Test");
}
#[test]
fn test_create_test_document_with_filename() {
let doc = create_test_document("# Content", "custom.md");
assert_eq!(doc.content, "# Content");
assert_eq!(doc.path, PathBuf::from("custom.md"));
}
#[test]
fn test_all_markdown_builder_edge_cases() {
let content = MarkdownBuilder::new()
.unordered_list(&[])
.ordered_list(&[])
.build();
assert_eq!(content, "");
let content = MarkdownBuilder::new()
.unordered_list(&["Single"])
.blank_line()
.ordered_list(&["One"])
.build();
assert_eq!(content, "- Single\n\n1. One");
let content = MarkdownBuilder::new()
.nested_list(&[("Item", None)])
.build();
assert_eq!(content, "- Item");
let content = MarkdownBuilder::new().table(&["Header"], &[]).build();
assert_eq!(content, "| Header |\n|---|");
let content = MarkdownBuilder::new().definition_list(&[]).build();
assert_eq!(content, "");
}
struct NoViolationRule;
impl Rule for NoViolationRule {
fn id(&self) -> &'static str {
"NO_VIO"
}
fn name(&self) -> &'static str {
"no-violation"
}
fn description(&self) -> &'static str {
"Never produces violations"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Structure)
}
fn check_with_ast<'a>(
&self,
_document: &Document,
_ast: Option<&'a comrak::nodes::AstNode<'a>>,
) -> Result<Vec<Violation>> {
Ok(vec![])
}
}
struct MultiViolationRule;
impl Rule for MultiViolationRule {
fn id(&self) -> &'static str {
"MULTI"
}
fn name(&self) -> &'static str {
"multi-violation"
}
fn description(&self) -> &'static str {
"Produces multiple violations"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Structure)
}
fn check_with_ast<'a>(
&self,
_document: &Document,
_ast: Option<&'a comrak::nodes::AstNode<'a>>,
) -> Result<Vec<Violation>> {
Ok(vec![
self.create_violation("First violation".to_string(), 1, 1, Severity::Warning),
self.create_violation("Second violation".to_string(), 2, 1, Severity::Error),
])
}
}
#[test]
#[should_panic(expected = "Expected no violations but found")]
fn test_assert_no_violations_error_path() {
assert_no_violations(TestRule, "# Test content");
}
#[test]
#[should_panic(expected = "Expected exactly one violation but found")]
fn test_assert_single_violation_multiple_violations_error() {
assert_single_violation(MultiViolationRule, "# Test content");
}
#[test]
#[should_panic(expected = "Expected exactly one violation but found")]
fn test_assert_single_violation_no_violations_error() {
assert_single_violation(NoViolationRule, "# Test content");
}
#[test]
#[should_panic(expected = "Expected 3 violations but found")]
fn test_assert_violation_count_wrong_count_error() {
assert_violation_count(TestRule, "# Test content", 3);
}
#[test]
#[should_panic(expected = "Expected to find violation containing 'nonexistent message'")]
fn test_assert_violation_contains_message_not_found() {
let violations = vec![Violation {
rule_id: "TEST".to_string(),
rule_name: "test".to_string(),
message: "Test violation".to_string(),
line: 1,
column: 1,
severity: Severity::Warning,
}];
assert_violation_contains_message(&violations, "nonexistent message");
}
#[test]
#[should_panic(expected = "Expected to find violation at line 999")]
fn test_assert_violation_at_line_not_found() {
let violations = vec![Violation {
rule_id: "TEST".to_string(),
rule_name: "test".to_string(),
message: "Test violation".to_string(),
line: 1,
column: 1,
severity: Severity::Warning,
}];
assert_violation_at_line(&violations, 999);
}
#[test]
#[should_panic(expected = "Expected to find violation with rule ID 'NONEXISTENT'")]
fn test_assert_violation_rule_id_not_found() {
let violations = vec![Violation {
rule_id: "TEST".to_string(),
rule_name: "test".to_string(),
message: "Test violation".to_string(),
line: 1,
column: 1,
severity: Severity::Warning,
}];
assert_violation_rule_id(&violations, "NONEXISTENT");
}
#[test]
#[should_panic(expected = "Expected to find violation with severity")]
fn test_assert_violation_severity_not_found() {
let violations = vec![Violation {
rule_id: "TEST".to_string(),
rule_name: "test".to_string(),
message: "Test violation".to_string(),
line: 1,
column: 1,
severity: Severity::Warning,
}];
assert_violation_severity(&violations, Severity::Error);
}
#[test]
fn test_successful_helper_paths() {
assert_no_violations(NoViolationRule, "# Test content");
let violation = assert_single_violation(TestRule, "# Test content");
assert_eq!(violation.message, "Test violation");
let violations = assert_violation_count(MultiViolationRule, "# Test content", 2);
assert_eq!(violations.len(), 2);
let test_violations = vec![Violation {
rule_id: "TEST123".to_string(),
rule_name: "test".to_string(),
message: "Contains specific text".to_string(),
line: 42,
column: 1,
severity: Severity::Error,
}];
assert_violation_contains_message(&test_violations, "specific text");
assert_violation_at_line(&test_violations, 42);
assert_violation_rule_id(&test_violations, "TEST123");
assert_violation_severity(&test_violations, Severity::Error);
}
}