document_test.rs
#[cfg(test)]
mod tests {
use super::*;
const DOC: &str = "\
Title\n\
=====\n\
\n\
Some prose.\n\
\n\
[testmark]:# (one/input)\n\
```text\n\
hello\n\
world\n\
```\n\
\n\
More prose, and an untagged block that must be ignored:\n\
\n\
```sh\n\
not a fixture\n\
```\n\
\n\
[testmark]:# (one/output)\n\
```\n\
```\n";
// ----- existing code-hunk tests (minimally updated for new accessor API) -----
#[test]
fn parses_hunks_and_ignores_untagged_blocks() {
let doc = Document::parse(DOC).expect("parses");
assert_eq!(doc.hunks.len(), 2);
assert_eq!(doc.hunk("one/input").unwrap().body().unwrap(), "hello\nworld\n");
assert_eq!(doc.hunk("one/input").unwrap().block_tag().unwrap(), "text");
assert_eq!(doc.hunk("one/output").unwrap().body().unwrap(), "");
}
#[test]
fn patch_roundtrip_is_identity() {
let doc = Document::parse(DOC).expect("parses");
let same = doc.patched("one/input", "hello\nworld\n").unwrap();
assert_eq!(same, DOC);
}
#[test]
fn patch_replaces_only_the_target_hunk() {
let doc = Document::parse(DOC).expect("parses");
let patched = doc.patched("one/output", "fresh content\n").unwrap();
let redoc = Document::parse(&patched).expect("reparses");
assert_eq!(redoc.hunk("one/output").unwrap().body().unwrap(), "fresh content\n");
assert_eq!(redoc.hunk("one/input").unwrap().body().unwrap(), "hello\nworld\n");
assert!(patched.contains("not a fixture"));
assert!(patched.contains("Some prose."));
}
#[test]
fn patch_can_empty_a_hunk() {
let doc = Document::parse(DOC).expect("parses");
let patched = doc.patched("one/input", "").unwrap();
let redoc = Document::parse(&patched).expect("reparses");
assert_eq!(redoc.hunk("one/input").unwrap().body().unwrap(), "");
}
#[test]
fn patch_of_unknown_hunk_is_none() {
let doc = Document::parse(DOC).expect("parses");
assert_eq!(doc.patched("no/such/hunk", "x\n"), None);
}
#[test]
fn comment_lookalikes_inside_fences_are_inert() {
// A `[testmark]:#` line inside a fenced block is content, not an annotation
// (this is what lets fixture files document the testmark format itself).
let doc = Document::parse("```\n[testmark]:# (ghost)\n```\n").expect("parses");
assert_eq!(doc.hunks.len(), 0);
}
#[test]
fn comment_trailing_content_is_reserved_and_ignored() {
// The spec reserves everything after the first whitespace inside the parens.
let doc = Document::parse("[testmark]:# (a future extension)\n```\nx\n```\n")
.expect("parses");
assert_eq!(doc.hunks.len(), 1);
assert_eq!(doc.hunks[0].name, "a");
}
#[test]
fn empty_document_parses() {
let doc = Document::parse("").expect("parses");
assert_eq!(doc.hunks.len(), 0);
}
#[test]
fn rejects_dangling_comment() {
let err = Document::parse("[testmark]:# (a)\nno fence here\n").unwrap_err();
assert_eq!(
err,
ParseError::DanglingComment {
line: 1,
}
);
}
#[test]
fn rejects_comment_at_end_of_file() {
// Dangling in the harshest way: nothing at all after the comment.
let err = Document::parse("[testmark]:# (a)").unwrap_err();
assert_eq!(
err,
ParseError::DanglingComment {
line: 1,
}
);
}
#[test]
fn rejects_malformed_comment() {
let err = Document::parse("[testmark]:# missing-parens\n```\n```\n").unwrap_err();
assert_eq!(
err,
ParseError::MalformedComment {
line: 1,
}
);
}
#[test]
fn rejects_empty_name() {
let err = Document::parse("[testmark]:# ()\n```\n```\n").unwrap_err();
assert_eq!(
err,
ParseError::MalformedComment {
line: 1,
}
);
}
#[test]
fn rejects_unclosed_fence() {
let err = Document::parse("[testmark]:# (a)\n```\nstuff\n").unwrap_err();
assert_eq!(
err,
ParseError::UnclosedFence {
line: 2,
}
);
}
#[test]
fn rejects_unclosed_untagged_fence() {
let err = Document::parse("prose\n```\nstuff\n").unwrap_err();
assert_eq!(
err,
ParseError::UnclosedFence {
line: 2,
}
);
}
#[test]
fn rejects_duplicate_names() {
let doc = "[testmark]:# (a)\n```\nx\n```\n[testmark]:# (a)\n```\ny\n```\n";
let err = Document::parse(doc).unwrap_err();
assert_eq!(
err,
ParseError::DuplicateName {
name: "a".into(),
line: 5,
}
);
}
#[test]
fn normalizes_crlf() {
let doc = Document::parse("[testmark]:# (a)\r\n```\r\nx\r\n```\r\n").expect("parses");
assert_eq!(doc.hunk("a").unwrap().body().unwrap(), "x\n");
}
#[test]
fn error_display_uses_one_based_lines() {
let err = Document::parse("prose\n[testmark]:# (a)\nno fence\n").unwrap_err();
assert_eq!(
err.to_string(),
"line 2: testmark comment not followed by a code fence"
);
}
// ----- table hunk tests -----
#[test]
fn parses_basic_table_hunk() {
let src = "[testmark]:# (arith/cases)\n\
| a | b | sum |\n\
| --- | --- | --- |\n\
| 1 | 2 | 3 |\n";
let doc = Document::parse(src).expect("parses");
assert_eq!(doc.hunks.len(), 1);
let h = doc.hunk("arith/cases").unwrap();
let HunkContent::Table(ref t) = h.content else { panic!("expected table") };
assert_eq!(t.headers, vec!["a", "b", "sum"]);
assert_eq!(t.rows.len(), 1);
assert_eq!(t.rows[0], vec!["1", "2", "3"]);
}
#[test]
fn parses_table_with_multiple_rows() {
let src = "[testmark]:# (t)\n\
| x | y |\n\
| --- | --- |\n\
| 10 | 20 |\n\
| 30 | 40 |\n\
| 50 | 60 |\n";
let doc = Document::parse(src).expect("parses");
let HunkContent::Table(ref t) = doc.hunk("t").unwrap().content else { panic!() };
assert_eq!(t.headers, vec!["x", "y"]);
assert_eq!(t.rows.len(), 3);
assert_eq!(t.rows[2], vec!["50", "60"]);
}
#[test]
fn separator_row_not_in_rows() {
let src = "[testmark]:# (t)\n\
| h |\n\
| --- |\n\
| d |\n";
let doc = Document::parse(src).expect("parses");
let HunkContent::Table(ref t) = doc.hunk("t").unwrap().content else { panic!() };
// Separator must not appear in rows.
assert_eq!(t.rows.len(), 1);
assert_eq!(t.rows[0], vec!["d"]);
}
#[test]
fn cells_are_trimmed() {
let src = "[testmark]:# (t)\n\
| spaces | around |\n\
| --- | --- |\n\
| val | ue |\n";
let doc = Document::parse(src).expect("parses");
let HunkContent::Table(ref t) = doc.hunk("t").unwrap().content else { panic!() };
assert_eq!(t.headers, vec!["spaces", "around"]);
assert_eq!(t.rows[0], vec!["val", "ue"]);
}
#[test]
fn table_and_code_hunk_coexist() {
let src = "[testmark]:# (tbl)\n\
| a | b |\n\
| --- | --- |\n\
| 1 | 2 |\n\
\n\
[testmark]:# (code)\n\
```\n\
hello\n\
```\n";
let doc = Document::parse(src).expect("parses");
assert_eq!(doc.hunks.len(), 2);
assert!(matches!(doc.hunk("tbl").unwrap().content, HunkContent::Table(_)));
assert!(matches!(doc.hunk("code").unwrap().content, HunkContent::Code { .. }));
assert_eq!(doc.hunk("code").unwrap().body().unwrap(), "hello\n");
}
#[test]
fn rejects_table_missing_separator() {
// Only a header row, no separator.
let src = "[testmark]:# (t)\n| a | b |\n";
let err = Document::parse(src).unwrap_err();
assert_eq!(err, ParseError::MissingTableSeparator { line: 2 });
}
#[test]
fn rejects_table_bad_separator() {
// Second row present but not a valid separator.
let src = "[testmark]:# (t)\n| a | b |\n| x | y |\n| 1 | 2 |\n";
let err = Document::parse(src).unwrap_err();
assert_eq!(err, ParseError::MissingTableSeparator { line: 3 });
}
#[test]
fn rejects_table_column_mismatch_in_data_row() {
let src = "[testmark]:# (t)\n\
| a | b |\n\
| --- | --- |\n\
| 1 | 2 | 3 |\n";
let err = Document::parse(src).unwrap_err();
assert_eq!(
err,
ParseError::TableColumnMismatch {
line: 4,
expected: 2,
got: 3,
}
);
}
#[test]
fn rejects_table_column_mismatch_in_separator() {
let src = "[testmark]:# (t)\n\
| a | b |\n\
| --- |\n\
| 1 | 2 |\n";
let err = Document::parse(src).unwrap_err();
assert_eq!(
err,
ParseError::TableColumnMismatch {
line: 3,
expected: 2,
got: 1,
}
);
}
#[test]
fn patched_table_roundtrip() {
let src = "prose\n\
[testmark]:# (t)\n\
| a | b |\n\
| --- | --- |\n\
| 1 | 2 |\n\
more prose\n";
let doc = Document::parse(src).expect("parses");
let new_table = ParsedTable {
headers: vec!["a".into(), "b".into()],
rows: vec![vec!["1".into(), "2".into()]],
};
let patched = doc.patched("t", NewContent::Table(&new_table)).unwrap();
let redoc = Document::parse(&patched).expect("reparses");
let HunkContent::Table(ref t) = redoc.hunk("t").unwrap().content else { panic!() };
assert_eq!(t.headers, vec!["a", "b"]);
assert_eq!(t.rows, vec![vec!["1", "2"]]);
// Prose preserved.
assert!(patched.contains("prose"));
assert!(patched.contains("more prose"));
}
#[test]
fn patch_with_explicit_code_changes_block_tag() {
let doc = Document::parse(DOC).expect("parses");
let patched = doc
.patched("one/input", NewContent::Code { block_tag: "rust", body: "let x = 1;\n" })
.unwrap();
let redoc = Document::parse(&patched).expect("reparses");
assert_eq!(redoc.hunk("one/input").unwrap().block_tag().unwrap(), "rust");
assert_eq!(redoc.hunk("one/input").unwrap().body().unwrap(), "let x = 1;\n");
}
#[test]
fn body_and_block_tag_return_none_for_table_hunk() {
let src = "[testmark]:# (t)\n| a |\n| --- |\n| 1 |\n";
let doc = Document::parse(src).expect("parses");
let h = doc.hunk("t").unwrap();
assert_eq!(h.body(), None);
assert_eq!(h.block_tag(), None);
}
}
crates/testmark/src/document_test.rs · 9.2 KiB · 71242d0778