retex-kpath 0.9.0

kpathsea-compatible file lookup for retex
Documentation
//! Differential tests against the installed `kpsewhich`.
//!
//! Skipped (with a note) when `kpsewhich` is not installed.

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

/// The process environment with `TEXINPUTS` replaced.
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()
    }
}

#[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();
    // l3build sets TEXINPUTS like this: its own directories, then the
    // default path.
    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);
}