testmark-rs

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";

	#[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, "hello\nworld\n");
		assert_eq!(doc.hunk("one/input").unwrap().block_tag, "text");
		assert_eq!(doc.hunk("one/output").unwrap().body, "");
	}

	#[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, "fresh content\n");
		assert_eq!(redoc.hunk("one/input").unwrap().body, "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, "");
	}

	#[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, "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"
		);
	}
}

crates/testmark/src/document_test.rs · 4.4 KiB · b6c25bffdc