use std::path::PathBuf;
use std::process::Command;
use retex_kpath::{FileFormat, Kpathsea, Options};
fn kpsewhich(args: &[&str]) -> Option<String> {
let out = Command::new("kpsewhich")
.args(["-progname=lualatex", "-engine=luahbtex"])
.args(args)
.output()
.ok()?;
Some(String::from_utf8_lossy(&out.stdout).trim().to_owned())
}
fn available() -> bool {
let ok = Command::new("kpsewhich").arg("--version").output().is_ok();
if !ok {
eprintln!("kpsewhich not installed; skipping differential test");
}
ok
}
#[test]
fn finds_same_files_as_kpsewhich() {
if !available() {
return;
}
let mut kpse = Kpathsea::new(Options::lualatex()).expect("kpathsea init");
let cases: &[(&str, FileFormat, &str)] = &[
("lualatex.ini", FileFormat::Tex, "tex"),
("latex.ltx", FileFormat::Tex, "tex"),
("luatexconfig.tex", FileFormat::Tex, "tex"),
("article.cls", FileFormat::Tex, "tex"),
("size10.clo", FileFormat::Tex, "tex"),
("tulmr.fd", FileFormat::Tex, "tex"),
("hyphen.cfg", FileFormat::Tex, "tex"),
("language.dat", FileFormat::Tex, "tex"),
("language.dat.lua", FileFormat::Tex, "tex"),
("hyph-en-us.pat.txt", FileFormat::Tex, "tex"),
("tikz.sty", FileFormat::Tex, "tex"),
("pgfcore.code.tex", FileFormat::Tex, "tex"),
("ltluatex", FileFormat::Lua, "lua"),
("ltluatex.lua", FileFormat::Lua, "lua"),
("lualatexquotejobname.lua", FileFormat::Lua, "lua"),
("luaotfload-main", FileFormat::Lua, "lua"),
(
"lmroman10-regular.otf",
FileFormat::OpenType,
"opentype fonts",
),
("lmroman10-regular", FileFormat::OpenType, "opentype fonts"),
(
"latinmodern-math.otf",
FileFormat::OpenType,
"opentype fonts",
),
("cmr10", FileFormat::Tfm, "tfm"),
("pdftex.map", FileFormat::Map, "map"),
("lualatex", FileFormat::Fmt, "fmt"),
("does-not-exist-xyz", FileFormat::Tex, "tex"),
];
for (name, fmt, kfmt) in cases {
let expected = kpsewhich(&[&format!("-format={kfmt}"), name]).unwrap();
let got = kpse
.find_file(name, *fmt)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
assert_eq!(
PathBuf::from(&got),
PathBuf::from(&expected),
"lookup of {name} ({kfmt})"
);
}
}
#[test]
fn same_variable_values_as_kpsewhich() {
if !available() {
return;
}
let kpse = Kpathsea::new(Options::lualatex()).expect("kpathsea init");
for var in [
"TEXMF",
"TEXINPUTS",
"LUAINPUTS",
"TEXMFVAR",
"TEXMFDBS",
"max_print_line",
] {
let expected = kpsewhich(&["-var-value", var]).unwrap();
assert_eq!(
kpse.var_value(var).unwrap_or_default(),
expected,
"variable {var}"
);
}
}
struct TexInputs(String);
impl retex_kpath::Env for TexInputs {
fn var(&self, name: &str) -> Option<String> {
if name.starts_with("TEXINPUTS") {
return (name == "TEXINPUTS").then(|| self.0.clone());
}
std::env::var(name).ok()
}
}
struct With(Vec<(&'static str, String)>);
impl retex_kpath::Env for With {
fn var(&self, name: &str) -> Option<String> {
match self.0.iter().find(|(k, _)| *k == name) {
Some((_, v)) => Some(v.clone()),
None => std::env::var(name).ok(),
}
}
}
fn kpsewhich_with(env: &[(&'static str, String)], args: &[&str]) -> String {
let out = Command::new("kpsewhich")
.args(["-progname=lualatex", "-engine=luahbtex"])
.args(args)
.envs(env.iter().map(|(k, v)| (k, v)))
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).trim().to_owned()
}
#[test]
fn selfauto_variables_ignore_the_environment() {
if !available() {
return;
}
let env = vec![
("SELFAUTOLOC", "/nonexistent/bin".to_owned()),
("SELFAUTODIR", "/nonexistent".to_owned()),
("SELFAUTOPARENT", "/".to_owned()),
];
let kpse = Kpathsea::with_env(Options::lualatex(), Box::new(With(env.clone()))).unwrap();
for var in ["SELFAUTOLOC", "SELFAUTODIR", "SELFAUTOPARENT"] {
assert_eq!(
kpse.var_value(var).unwrap_or_default(),
kpsewhich_with(&env, &["-var-value", var]),
"variable {var}"
);
}
}
#[test]
fn empty_environment_values_count_as_unset() {
if !available() {
return;
}
let env = vec![
("TEXMFHOME", String::new()),
("openin_any", String::new()),
("TEXINPUTS", String::new()),
];
let mut kpse = Kpathsea::with_env(Options::lualatex(), Box::new(With(env.clone()))).unwrap();
for var in ["TEXMFHOME", "openin_any"] {
assert_eq!(
kpse.var_value(var).unwrap_or_default(),
kpsewhich_with(&env, &["-var-value", var]),
"variable {var}"
);
}
assert_eq!(
kpse.find_file("article.cls", FileFormat::Tex),
Some(PathBuf::from(kpsewhich_with(&env, &["article.cls"])))
);
}
fn kpsewhich_accepts(env: &[(&'static str, String)], option: &str, name: &str) -> bool {
Command::new("kpsewhich")
.args(["-progname=lualatex", "-engine=luahbtex"])
.arg(format!("-{option}={name}"))
.envs(env.iter().map(|(k, v)| (k, v)))
.output()
.unwrap()
.status
.success()
}
#[test]
#[ignore = "slow (30 s): cargo xtask ci --full"]
fn safe_names_as_kpsewhich() {
use retex_kpath::Access;
if !available() {
return;
}
let base = Kpathsea::new(Options::lualatex()).unwrap();
let var = base.var_value("TEXMFVAR").unwrap_or_default();
let sysvar = base.var_value("TEXMFSYSVAR").unwrap_or_default();
let names = [
"x".to_owned(),
"./x".into(),
".x".into(),
"a/.x".into(),
"..x".into(),
"../x".into(),
"a/../x".into(),
"a/..".into(),
"/tmp".into(),
"/tmp/x".into(),
"/tmp/.x".into(),
"/tmp/a/..".into(),
"/tmp/a/../x".into(),
"/tmpx/y".into(),
format!("{var}/x"),
format!("{var}/.x"),
format!("{var}/../x"),
format!("{var}x"),
format!("{sysvar}/x"),
];
let settings: Vec<Vec<(&'static str, String)>> = vec![
vec![],
vec![("openout_any", "a".into()), ("openin_any", "r".into())],
vec![("openout_any", "n".into()), ("openin_any", "p".into())],
vec![("openout_any", "z".into()), ("openin_any", "".into())],
vec![("TEXMFOUTPUT", "/tmp".into()), ("openin_any", "p".into())],
vec![("TEXMFOUTPUT", "/tmp/".into())],
vec![("TEXMF_OUTPUT_DIRECTORY", "/tmp".into())],
];
for env in &settings {
let kpse = Kpathsea::with_env(Options::lualatex(), Box::new(With(env.clone()))).unwrap();
for name in &names {
for (option, access, extended) in [
("safe-out-name", Access::Write, false),
("safe-extended-out-name", Access::Write, true),
("safe-in-name", Access::Read, false),
("safe-extended-in-name", Access::Read, true),
] {
assert_eq!(
kpse.name_ok(name, access, extended).is_ok(),
kpsewhich_accepts(env, option, name),
"-{option}={name} with {env:?}"
);
}
}
}
}
#[test]
fn casefolding_search_as_kpsewhich() {
if !available() {
return;
}
let dir = std::env::temp_dir().join(format!("retex-kpath-case-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
for d in ["a/deep/er", "b", "sub"] {
std::fs::create_dir_all(dir.join(d)).unwrap();
}
for f in [
"a/deep/er/Bar.TEX",
"b/bar.tex",
"b/BOTH.tex",
"a/both.tex",
"b/Foo",
"b/foo.tex",
"b/sub/Deep.tex",
"b/ÉTÉ.tex",
] {
let p = dir.join(f);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, "").unwrap();
}
let path = format!("{0}/a//:{0}/b", dir.display());
let abs_name = format!("{}/b/FOO.TEX", dir.display());
let names = [
"BAR.tex",
"bar.tex",
"Both.tex",
"both.TEX",
"Foo",
"FOO",
"sub/deep.tex",
"SUB/Deep.tex",
"été.tex",
"ARTICLE.CLS",
&abs_name,
];
for setting in ["1", "0"] {
let env = vec![
("TEXINPUTS", path.clone()),
("texmf_casefold_search", setting.to_owned()),
];
let mut kpse =
Kpathsea::with_env(Options::lualatex(), Box::new(With(env.clone()))).unwrap();
for name in names {
let got = kpse
.find_file(name, FileFormat::Tex)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
assert_eq!(
got,
kpsewhich_with(&env, &[name]),
"{name}, texmf_casefold_search={setting}"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn more_variables_of_formats() {
if !available() {
return;
}
let dir = std::env::temp_dir().join(format!("retex-kpath-vars-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("cnf")).unwrap();
std::fs::create_dir_all(dir.join("files")).unwrap();
std::fs::create_dir_all(dir.join("texmf/lualatex/x")).unwrap();
std::fs::write(
dir.join("cnf/texmf.cnf"),
format!("TEXMF = {}/texmf\n", dir.display()),
)
.unwrap();
for f in [
"files/b.bib",
"files/z.fmt",
"files/t.txt",
"texmf/lualatex/x/u.txt",
] {
std::fs::write(dir.join(f), "").unwrap();
}
let files = dir.join("files").display().to_string();
let base = vec![("TEXMFCNF", dir.join("cnf").display().to_string())];
let cases: &[(&'static str, &str, FileFormat, &str)] = &[
("TEXBIB", "b.bib", FileFormat::Bib, "bib"),
("TEXMFINI", "z", FileFormat::Fmt, "fmt"),
(
"LUALATEXINPUTS",
"t.txt",
FileFormat::Text,
"other text files",
),
(
"LUALATEXINPUTS_UNSET",
"u.txt",
FileFormat::Text,
"other text files",
),
];
for (var, name, fmt, kfmt) in cases {
let mut env = base.clone();
env.push((var, files.clone()));
let mut kpse =
Kpathsea::with_env(Options::lualatex(), Box::new(With(env.clone()))).unwrap();
let got = kpse
.find_file(name, *fmt)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let expected = kpsewhich_with(&env, &[&format!("-format={kfmt}"), name]);
assert!(!expected.is_empty(), "{var}: {name} not found by kpsewhich");
assert_eq!(got, expected, "{var}: {name}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn extra_colon_in_environment_adds_cnf_path() {
if !available() {
return;
}
let dir = std::env::temp_dir().join(format!("retex-kpath-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("retex-extra-colon.tex"), "").unwrap();
let env = TexInputs(format!("{}:", dir.display()));
let mut kpse = Kpathsea::with_env(Options::lualatex(), Box::new(env)).unwrap();
let mut found = |n| kpse.find_file(n, FileFormat::Tex);
assert_eq!(
found("retex-extra-colon.tex"),
Some(dir.join("retex-extra-colon.tex"))
);
let expected = kpsewhich(&["-format=tex", "article.cls"]).unwrap();
assert_eq!(found("article.cls"), Some(PathBuf::from(expected)));
let _ = std::fs::remove_dir_all(&dir);
}
struct TexFonts(String);
impl retex_kpath::Env for TexFonts {
fn var(&self, name: &str) -> Option<String> {
if name.starts_with("TEXFONTS") {
return (name == "TEXFONTS").then(|| self.0.clone());
}
std::env::var(name).ok()
}
}
#[test]
fn texfonts_in_environment_applies_to_all_font_formats() {
if !available() {
return;
}
let dir = std::env::temp_dir().join(format!("retex-kpath-fonts-{}", std::process::id()));
std::fs::create_dir_all(dir.join("sub")).unwrap();
std::fs::write(dir.join("sub/retex-texfonts.ttf"), "").unwrap();
let env = TexFonts(format!("{}//:", dir.display()));
let mut kpse = Kpathsea::with_env(Options::lualatex(), Box::new(env)).unwrap();
assert_eq!(
kpse.find_file("retex-texfonts.ttf", FileFormat::TrueType),
Some(dir.join("sub/retex-texfonts.ttf"))
);
let expected = kpsewhich(&["-format=tfm", "cmr10.tfm"]).unwrap();
assert_eq!(
kpse.find_file("cmr10.tfm", FileFormat::Tfm),
Some(PathBuf::from(expected))
);
let _ = std::fs::remove_dir_all(&dir);
}
struct Inputs(String);
impl retex_kpath::Env for Inputs {
fn var(&self, name: &str) -> Option<String> {
if name.starts_with("TEXINPUTS") || name.starts_with("LUAINPUTS") {
return (name == "TEXINPUTS" || name == "LUAINPUTS").then(|| self.0.clone());
}
std::env::var(name).ok()
}
}
#[test]
fn suffixes_as_kpsewhich_adds_them() {
if !available() {
return;
}
let dir = std::env::temp_dir().join(format!("retex-kpath-suffix-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
for f in ["x.cls", "x.cls.tex", "y.foo", "y.foo.tex", "q"] {
std::fs::write(dir.join(f), "").unwrap();
}
let mut kpse = Kpathsea::with_env(
Options::lualatex(),
Box::new(Inputs(dir.display().to_string())),
)
.unwrap();
assert_eq!(
kpse.find_file("x.cls", FileFormat::Tex),
Some(dir.join("x.cls"))
);
assert_eq!(
kpse.find_file("y.foo", FileFormat::Tex),
Some(dir.join("y.foo.tex"))
);
assert_eq!(kpse.find_file("q", FileFormat::Lua), None);
let _ = std::fs::remove_dir_all(&dir);
}