pub struct Rng {
state: u64,
}
impl Rng {
#[must_use]
pub fn new(seed: u64) -> Self {
Self {
state: if seed == 0 {
0x9E37_79B9_7F4A_7C15
} else {
seed
},
}
}
pub fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
pub fn below(&mut self, bound: u64) -> u64 {
self.next_u64() % bound
}
}
pub const FRAGMENTS: &[&str] = &[
"ordinary prose that wraps",
"a second prose line",
"prose\u{1c}with\u{1d}four\u{1e}C0\u{1f}separators",
"prose ending in a separator\u{1c}",
"prose ending in a unit separator\u{1f}",
"",
" ",
"```",
"````",
"~~~",
"~~~~~",
"```rust",
" ```",
" ```",
"``` ```",
"> quoted prose",
">no space after the marker",
"> > deeper prose",
">>> deepest",
">",
" > indented marker",
"> indented code inside a quote",
"> ```",
"> <!-- quoted comment",
"> <div>",
"> | a | b |",
"> Alex: a quoted utterance",
"> Jordan: another quoted utterance",
"- bullet item",
"* star item",
"+ plus item",
"1. ordered item",
"12) ordered item",
"\u{663}. item",
"\u{967}\u{968}) item",
" continuation at the content column",
" deeper continuation",
"a. lettered subitem",
"b) lettered subitem",
"**Label:** value",
"**Label**: value",
"**Whole line bold**",
"Alex: an utterance",
"Alex Jordan Morgan Casey: four words",
"Alex Jordan Morgan Casey Drew: five words",
"[a stage direction]",
"[a stage direction].",
"| a | b |",
"| - | - |",
"a `x | y` span",
"a ``x | y`` span",
"a ```x | y``` span",
"an `unterminated run",
"a `a``` closing run",
"a hard break ",
"a hard break\\",
"<div>",
"</div>",
"<div>closed on its own line</div>",
"<br/>",
"<pre>",
"</pre>",
"<!-- an open comment",
"-->",
"<!-- a closed comment -->",
"<?php",
"?>",
"<![CDATA[",
"]]>",
"<!DOCTYPE html",
"<!DOCTYPE html>",
"---",
"\u{feff}---",
"...",
"title: a value",
"# a heading",
"===",
"- - -",
"***",
"[label]: https://example.com",
"[!NOTE]",
"[][ref]",
"[another](https://example.com)",
":: an admonition",
"!!! note",
"{% raw %}",
"{{ template }}",
concat!(
"A",
"eeeeeeeeee",
"eeeeeeeeee",
"eeeeeeeeee",
"eeeeeeeee",
":"
),
concat!(
"A",
"eeeeeeeeee",
"eeeeeeeeee",
"eeeeeeeeee",
"eeeeeeeeee",
":"
),
"MC 0:15",
"MC \u{660}:\u{661}\u{665}",
"JR 12:34",
];
#[must_use]
pub fn document(seed: u64) -> String {
let mut rng = Rng::new(seed);
let line_count = 1 + rng.below(24);
let mut out = String::new();
for _ in 0..line_count {
let index = rng.below(FRAGMENTS.len() as u64) as usize;
out.push_str(FRAGMENTS[index]);
match rng.below(16) {
0 => out.push_str("\r\n"),
1 => out.push('\r'),
_ => out.push('\n'),
}
}
if rng.below(8) == 0 {
while out.ends_with(['\n', '\r']) {
out.pop();
}
}
out
}
pub struct Scenario {
pub files: Vec<(String, String)>,
pub argv: Vec<String>,
}
const NAMES: [&str; 4] = ["note.md", "keep.md", "sub/nested.md", "docs/deep.md"];
const PATTERNS: [&str; 10] = [
"*.md",
"!keep.md",
"keep.md",
"sub/",
"docs/deep.md",
"/note.md",
"**/nested.md",
"# a comment",
"no-such-file.md",
"*.m?",
];
#[must_use]
pub fn scenario(seed: u64) -> Scenario {
let mut rng = Rng::new(seed ^ 0x5DEE_CE66_D000_0001);
let file_count = 1 + rng.below(NAMES.len() as u64 - 1) as usize;
let mut files: Vec<(String, String)> = NAMES[..file_count]
.iter()
.map(|name| ((*name).to_owned(), document(rng.next_u64())))
.collect();
let pattern_count = rng.below(3) as usize;
let patterns: Vec<&str> = (0..pattern_count)
.map(|_| PATTERNS[rng.below(PATTERNS.len() as u64) as usize])
.collect();
if !patterns.is_empty() {
files.push((
".unwrapignore".to_owned(),
format!("{}\n", patterns.join("\n")),
));
}
let mut argv: Vec<String> = Vec::new();
if rng.below(2) == 0 {
argv.push("--write".to_owned());
}
if rng.below(2) == 0 {
argv.push("--json".to_owned());
}
if rng.below(3) == 0 {
argv.push("--fail-on-change".to_owned());
}
let exclude = rng.below(4);
if exclude < PATTERNS.len() as u64 {
argv.push("--exclude".to_owned());
argv.push(PATTERNS[exclude as usize].to_owned());
}
let names: Vec<String> = files
.iter()
.map(|(name, _)| name.clone())
.filter(|name| name != ".unwrapignore")
.collect();
match rng.below(3) {
0 => argv.extend(names),
1 => {
files.push(("list.txt".to_owned(), format!("{}\n", names.join("\n"))));
argv.push("--files-from".to_owned());
argv.push("list.txt".to_owned());
}
_ => {
files.push(("list.txt".to_owned(), format!("{}\n", names.join("\n"))));
argv.push("--files-from".to_owned());
argv.push("list.txt".to_owned());
argv.extend(names);
}
}
Scenario { files, argv }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::label::is_speaker_prefix;
use crate::scan::{is_list_line, match_list_marker};
#[test]
fn the_bank_reaches_its_hazards() {
let has = |predicate: fn(&str) -> bool| FRAGMENTS.iter().any(|f| predicate(f));
assert!(has(|f| f.contains('\u{1c}')), "no C0 separator");
assert!(has(|f| f.contains('\u{feff}')), "no byte order mark");
assert!(has(|f| f.ends_with(" ")), "no two-space hard break");
assert!(has(|f| f.ends_with('\\')), "no backslash hard break");
assert!(has(|f| f.contains("```")), "no backtick fence");
assert!(has(|f| f.contains("~~~")), "no tilde fence");
assert!(has(|f| f.contains("<![CDATA[")), "no CDATA");
assert!(has(|f| f.contains("<?")), "no processing instruction");
assert!(has(|f| f.starts_with("<!DOCTYPE")), "no declaration");
assert!(has(|f| f.contains('|')), "no table pipe");
assert!(has(|f| f.starts_with("> ")), "no blockquote");
assert!(has(is_speaker_prefix), "no speaker prefix");
assert!(has(|f| match_list_marker(f).is_some()), "no list marker");
}
#[test]
fn the_bank_carries_a_non_ascii_digit_that_is_not_a_marker() {
let digits: Vec<&&str> = FRAGMENTS
.iter()
.filter(|f| f.chars().any(|c| c.is_numeric() && !c.is_ascii_digit()))
.collect();
assert!(digits.len() >= 3, "found {digits:?}");
assert!(!is_list_line("\u{663}. item"));
assert!(match_list_marker("\u{967}\u{968}) item").is_none());
}
#[test]
fn the_speaker_boundary_pair_really_straddles_the_boundary() {
let short = FRAGMENTS
.iter()
.find(|f| f.starts_with("Ae") && f.len() == 41)
.expect("no 39-character heading");
let long = FRAGMENTS
.iter()
.find(|f| f.starts_with("Ae") && f.len() == 42)
.expect("no 40-character heading");
assert_eq!(short.matches('e').count(), 39);
assert_eq!(long.matches('e').count(), 40);
}
#[test]
fn the_generator_is_deterministic_and_never_empty() {
for seed in 1..200 {
assert_eq!(document(seed), document(seed));
}
assert_eq!(
Rng::new(0).next_u64(),
Rng::new(0x9E37_79B9_7F4A_7C15).next_u64()
);
let mut zero = Rng::new(0);
assert_ne!(zero.next_u64(), 0);
}
#[test]
fn the_generator_reaches_every_fragment() {
let mut seen = vec![false; FRAGMENTS.len()];
for seed in 1..4000 {
let doc = document(seed);
for (index, fragment) in FRAGMENTS.iter().enumerate() {
if !fragment.is_empty() && doc.contains(fragment) {
seen[index] = true;
}
}
}
let missed: Vec<&&str> = FRAGMENTS
.iter()
.zip(&seen)
.filter(|(fragment, hit)| !**hit && !fragment.is_empty())
.map(|(fragment, _)| fragment)
.collect();
assert!(missed.is_empty(), "never generated: {missed:?}");
}
}