use std::{
fs,
path::{Path, PathBuf},
process::Command,
time::{SystemTime, UNIX_EPOCH},
};
const DOCS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../web/docs");
const WIDTH: usize = 640;
const HEIGHT: usize = 360;
const TEST_FRAMES: usize = 6;
const INPUT: &str = "in.yuv";
struct Scratch(PathBuf);
impl Scratch {
fn new(page: &str) -> Self {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path =
std::env::temp_dir().join(format!("nc-cli-docs-{page}-{}-{stamp}", std::process::id()));
fs::create_dir(&path).unwrap();
Self(path)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
enum Block {
Run(String),
File(String, String),
}
fn unescape(text: &str) -> String {
text.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("&", "&")
}
fn blocks(html: &str) -> Vec<Block> {
const RUN: &str = "<pre class=\"run\"><code>";
const FILE: &str = "<pre class=\"file\" data-name=\"";
const END: &str = "</code></pre>";
let mut found = Vec::new();
let mut rest = html;
loop {
let run = rest.find(RUN);
let file = rest.find(FILE);
let Some(start) = [run, file].into_iter().flatten().min() else {
return found;
};
let tail = &rest[start..];
let end = tail.find(END).expect("an unterminated <pre>");
if Some(start) == run {
found.push(Block::Run(unescape(&tail[RUN.len()..end])));
} else {
let after = &tail[FILE.len()..end];
let (name, body) = after
.split_once("\"><code>")
.expect("data-name then <code>");
found.push(Block::File(name.to_owned(), unescape(body)));
}
rest = &tail[end..];
}
}
fn commands(lines: &str) -> Vec<String> {
lines
.replace("\\\n", " ")
.lines()
.map(str::to_owned)
.collect()
}
fn write_input(dir: &Path) {
let mut clip = Vec::with_capacity(WIDTH * HEIGHT * 3 / 2 * TEST_FRAMES);
for frame in 0..TEST_FRAMES {
for row in 0..HEIGHT {
clip.extend((0..WIDTH).map(|col| (16 + (col + row + 3 * frame) % 220) as u8));
}
clip.extend((0..WIDTH * HEIGHT / 2).map(|i| (96 + (i + frame) % 64) as u8));
}
fs::write(dir.join(INPUT), clip).unwrap();
}
fn run(dir: &Path, page: &str, line: &str) {
let words: Vec<&str> = line.split_whitespace().collect();
match words.as_slice() {
["nanocodec", args @ ..] => {
let mut args: Vec<String> = args.iter().map(|a| (*a).to_owned()).collect();
if let Some(i) = args.iter().position(|a| a == "--frames") {
args[i + 1] = TEST_FRAMES.to_string();
}
let out = Command::new(env!("CARGO_BIN_EXE_nanocodec"))
.args(&args)
.current_dir(dir)
.output()
.unwrap();
assert!(
out.status.success(),
"{page}: `{line}` failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
["cmp", a, b] => assert!(
fs::read(dir.join(a)).unwrap() == fs::read(dir.join(b)).unwrap(),
"{page}: `{line}`: the files differ"
),
_ => panic!("{page}: a run block line the test cannot run: `{line}`"),
}
}
#[test]
fn every_documented_command_runs() {
let mut ran = 0;
for entry in fs::read_dir(DOCS).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "html") {
continue;
}
let page = path.file_stem().unwrap().to_str().unwrap().to_owned();
let scratch = Scratch::new(&page);
write_input(&scratch.0);
for block in blocks(&fs::read_to_string(&path).unwrap()) {
match block {
Block::File(name, body) => fs::write(scratch.0.join(name), body).unwrap(),
Block::Run(lines) => {
for line in commands(&lines) {
run(&scratch.0, &page, &line);
ran += 1;
}
}
}
}
}
assert!(ran > 0, "no runnable examples found in {DOCS}");
}