use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use insta_cmd::assert_cmd_snapshot;
use tempfile::TempDir;
const FORWARD_REF_DOC: &str = r"<p>See $\eqref{eq:a}$.</p>
<p>$$\begin{align} x = 1 \label{eq:a}\end{align}$$</p>
";
const NUMBERED_DOC: &str = r"<p>$$\begin{align} a = 1 \label{eq:x}\end{align}$$ and $\eqref{eq:x}$</p>
";
const UNDEFINED_REF_DOC: &str = r"<p>See $\eqref{nope}$ and
later $\eqref{alsonope}$.</p>
";
fn mathcore(dir: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mathcore"));
cmd.current_dir(dir);
cmd
}
fn empty_dir() -> TempDir {
TempDir::new().expect("failed to create temporary directory")
}
fn settings() -> insta::Settings {
let mut settings = insta::Settings::clone_current();
settings.set_strip_ansi_escape_codes(true);
settings
}
fn write(dir: &Path, name: &str, content: &str) -> PathBuf {
let path = dir.join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("failed to create directory");
}
fs::write(&path, content).expect("failed to write file");
path
}
fn run_with_stdin(mut cmd: Command, input: &str) -> Output {
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("failed to run mathcore");
child
.stdin
.take()
.expect("stdin was not piped")
.write_all(input.as_bytes())
.expect("failed to write to stdin");
child.wait_with_output().expect("failed to run mathcore")
}
fn read(path: &Path) -> String {
fs::read_to_string(path).expect("failed to read file")
}
#[test]
fn formula_block() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(mathcore(dir.path()).args(["--formula", "x^2", "--block"]));
});
}
#[test]
fn formula_from_stdin() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(mathcore(dir.path()).pass_stdin("a + b"));
});
}
#[test]
fn formula_error_report() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(mathcore(dir.path()).args(["--formula", r"\frac"]));
});
}
#[test]
fn html_from_stdin() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(
mathcore(dir.path())
.arg("-")
.pass_stdin("inline $a+b$ and block $$c$$\n")
);
});
}
#[test]
fn forward_reference_resolves() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(mathcore(dir.path()).arg("-").pass_stdin(FORWARD_REF_DOC));
});
}
#[test]
fn html_entities_are_decoded() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(
mathcore(dir.path())
.arg("-")
.pass_stdin("$a < b$ and $x > y$\n")
);
});
}
#[test]
fn custom_delimiters() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(
mathcore(dir.path())
.args(["--inline-open", r"\(", "--inline-close", r"\)", "-"])
.pass_stdin(r"let \(a=1\) and $not math$")
);
});
}
#[test]
fn latex_error_aborts_with_report() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(
mathcore(dir.path())
.arg("-")
.pass_stdin("good $x$ then bad $\\frac$\n")
);
});
}
#[test]
fn continue_on_error_inlines_the_error() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(
mathcore(dir.path())
.args(["-", "--continue-on-error"])
.pass_stdin("good $x$ then bad $\\frac$\n")
);
});
}
#[test]
fn unclosed_delimiter() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(
mathcore(dir.path())
.arg("-")
.pass_stdin("unclosed $delim\n")
);
});
}
#[test]
fn mismatched_delimiters() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(
mathcore(dir.path())
.arg("-")
.pass_stdin("mismatch $$ and $ signs\n")
);
});
}
#[test]
fn color_follows_the_environment() {
fn stderr_of(html: bool, env: &[(&str, &str)]) -> Vec<u8> {
let dir = empty_dir();
let mut cmd = mathcore(dir.path());
if html {
write(dir.path(), "doc.html", "good $x$ then bad $\\frac$\n");
cmd.arg("doc.html");
} else {
cmd.args(["--formula", r"\frac"]);
}
cmd.env_remove("NO_COLOR").env_remove("CLICOLOR_FORCE");
for (name, value) in env {
cmd.env(name, value);
}
let out = cmd.output().expect("failed to run mathcore");
assert_eq!(out.status.code(), Some(2), "expected a conversion error");
out.stderr
}
fn colorized(stderr: &[u8]) -> bool {
stderr.contains(&0x1b)
}
struct Case {
env: &'static [(&'static str, &'static str)],
colorized: bool,
reason: &'static str,
}
let cases = [
Case {
env: &[],
colorized: false,
reason: "stderr is not a terminal, so no colors",
},
Case {
env: &[("CLICOLOR_FORCE", "1")],
colorized: true,
reason: "CLICOLOR_FORCE forces colors",
},
Case {
env: &[("CLICOLOR_FORCE", "0")],
colorized: true,
reason: "CLICOLOR_FORCE counts as set for any non-empty value, even \"0\"",
},
Case {
env: &[("CLICOLOR_FORCE", "")],
colorized: false,
reason: "an empty CLICOLOR_FORCE is not set",
},
Case {
env: &[("CLICOLOR_FORCE", "1"), ("NO_COLOR", "1")],
colorized: false,
reason: "NO_COLOR wins over CLICOLOR_FORCE",
},
Case {
env: &[("CLICOLOR_FORCE", "1"), ("NO_COLOR", "0")],
colorized: false,
reason: "NO_COLOR counts as set for any non-empty value, even \"0\"",
},
Case {
env: &[("CLICOLOR_FORCE", "1"), ("NO_COLOR", "")],
colorized: true,
reason: "an empty NO_COLOR is not set",
},
];
for html in [false, true] {
for case in &cases {
assert_eq!(
colorized(&stderr_of(html, case.env)),
case.colorized,
"html={html}, env={:?}: {}",
case.env,
case.reason
);
}
}
}
#[test]
fn missing_config_file() {
let dir = empty_dir();
settings().bind(|| {
assert_cmd_snapshot!(mathcore(dir.path()).args([
"--config-file",
"nope.toml",
"--formula",
"x"
]));
});
}
#[test]
fn without_write_the_file_is_left_alone() {
let dir = empty_dir();
let file = write(dir.path(), "doc.html", NUMBERED_DOC);
let out = mathcore(dir.path())
.arg("doc.html")
.output()
.expect("failed to run mathcore");
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("<math"));
assert_eq!(read(&file), NUMBERED_DOC, "the input file was modified");
}
#[test]
fn write_converts_in_place() {
let dir = empty_dir();
let file = write(dir.path(), "doc.html", NUMBERED_DOC);
let out = mathcore(dir.path())
.args(["--write", "doc.html"])
.output()
.expect("failed to run mathcore");
assert!(out.status.success());
assert!(out.stdout.is_empty(), "--write should not print the result");
assert!(read(&file).contains("<math"));
}
#[test]
fn write_is_idempotent() {
let dir = empty_dir();
let file = write(dir.path(), "doc.html", NUMBERED_DOC);
for _ in 0..2 {
assert!(
mathcore(dir.path())
.args(["--write", "doc.html"])
.status()
.expect("failed to run mathcore")
.success()
);
}
let once = read(&file);
let fresh = empty_dir();
let fresh_file = write(fresh.path(), "doc.html", NUMBERED_DOC);
assert!(
mathcore(fresh.path())
.args(["--write", "doc.html"])
.status()
.expect("failed to run mathcore")
.success()
);
assert_eq!(once, read(&fresh_file), "second run changed the document");
}
#[test]
fn dry_run_writes_nothing() {
let dir = empty_dir();
let file = write(dir.path(), "doc.html", NUMBERED_DOC);
let out = mathcore(dir.path())
.args(["--dry-run", "--write", "doc.html"])
.output()
.expect("failed to run mathcore");
assert!(out.status.success());
assert!(out.stdout.is_empty());
assert_eq!(read(&file), NUMBERED_DOC, "--dry-run modified the file");
}
#[test]
fn warnings_of_a_file_go_to_stderr() {
let dir = empty_dir();
write(dir.path(), "doc.html", UNDEFINED_REF_DOC);
settings().bind(|| {
assert_cmd_snapshot!(mathcore(dir.path()).arg("doc.html"));
});
}
#[test]
fn warnings_are_printed_when_writing_in_place() {
let dir = empty_dir();
write(dir.path(), "doc.html", UNDEFINED_REF_DOC);
let out = mathcore(dir.path())
.args(["--write", "doc.html"])
.output()
.expect("failed to run mathcore");
assert!(out.status.success());
assert!(out.stdout.is_empty(), "--write should not print the result");
assert_eq!(
String::from_utf8_lossy(&out.stderr).lines().count(),
2,
"expected one warning per undefined reference"
);
}
#[test]
fn warnings_of_stdin_are_printed_without_a_file_name() {
let dir = empty_dir();
let mut cmd = mathcore(dir.path());
cmd.arg("-");
let out = run_with_stdin(cmd, UNDEFINED_REF_DOC);
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("<math"));
assert_eq!(
String::from_utf8_lossy(&out.stderr),
"Warning: undefined reference in the formula on line 1, column 9.\n\
Warning: undefined reference in the formula on line 2, column 8.\n"
);
}
#[test]
fn a_failing_file_is_left_untouched() {
let dir = empty_dir();
let file = write(dir.path(), "doc.html", "before $x$ and $\\frac$ after\n");
let out = mathcore(dir.path())
.args(["--write", "doc.html"])
.output()
.expect("failed to run mathcore");
assert_eq!(out.status.code(), Some(2));
assert!(String::from_utf8_lossy(&out.stderr).contains("Conversion error in 'doc.html'"));
assert_eq!(
read(&file),
"before $x$ and $\\frac$ after\n",
"the file was modified despite the error"
);
}
#[test]
fn recursive_numbers_each_file_from_one() {
let dir = empty_dir();
let first = write(dir.path(), "one.html", NUMBERED_DOC);
let second = write(dir.path(), "sub/two.html", NUMBERED_DOC);
assert!(
mathcore(dir.path())
.args(["--recursive", "."])
.status()
.expect("failed to run mathcore")
.success()
);
for file in [&first, &second] {
let converted = read(file);
assert!(converted.contains("(1)"), "not converted: {converted}");
assert!(
!converted.contains("(2)"),
"numbering continued across files: {converted}"
);
}
}
#[test]
fn recursive_ignores_non_html_files() {
let dir = empty_dir();
let htm = write(dir.path(), "old.htm", NUMBERED_DOC);
let txt = write(dir.path(), "notes.txt", NUMBERED_DOC);
assert!(
mathcore(dir.path())
.args(["--recursive", "."])
.status()
.expect("failed to run mathcore")
.success()
);
assert_eq!(read(&htm), NUMBERED_DOC, ".htm files must be ignored");
assert_eq!(read(&txt), NUMBERED_DOC, ".txt files must be ignored");
}