Tests read the stock chain: added patch archives are left out

The tests that read the install assert the stock 1.12.1 data, so a
player's texture pack (Project Reforged's patch-A, -G, -T...) or an
interface mod (patch-F) in the same Data folder failed them. With
$BENILLA_STOCK_CHAIN set, Chain::open mounts a Data directory's base
set, patch.MPQ, patch-2.MPQ and speech2.MPQ, and leaves out every other
patch-?.MPQ; the language pack's folder is not filtered.
scripts/unit-tests.sh, the gates' test runner, exports it.

Verified: a chain test (stock mode keeps patch and patch-2, drops
patch-A.mpq and patch-F.MPQ; without it all mount); benilla-formats'
547 tests pass on the install with Reforged's A and G under
BENILLA_STOCK_CHAIN=1, where three character tests failed before. The
other crates' install tests were not run here. Built on Windows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-27 11:51:55 +04:00
parent 616cbf3bd0
commit ebaab7fa41
2 changed files with 78 additions and 4 deletions
+75 -4
View File
@@ -59,6 +59,19 @@ fn is_patch_glob_match(name: &str) -> bool {
/// [`VANILLA_BASE_ORDER`] archives, `patch.MPQ`, every `patch-?.MPQ` by case-folded name (the
/// reference sorts descending with `strnicmp` and walks backwards), then `speech2.MPQ`. Names
/// match case-insensitively and come back as found on disk.
/// `$BENILLA_STOCK_CHAIN` set and not empty: a `Data` directory mounts only the stock 1.12.1
/// archives, its base set, `patch.MPQ`, `patch-2.MPQ` and `speech2.MPQ`, and leaves out every
/// other `patch-?.MPQ` (texture packs, interface mods), so the tests that read the install see the
/// data they assert. The language pack's folder is not filtered. `scripts/unit-tests.sh` sets it.
fn stock_chain_only() -> bool {
std::env::var_os("BENILLA_STOCK_CHAIN").is_some_and(|v| !v.is_empty())
}
/// Whether `name` is a stock 1.12.1 patch archive.
fn is_stock_patch(name: &str) -> bool {
name.eq_ignore_ascii_case("patch-2.MPQ")
}
fn mount_order(dir_names: &[String]) -> Vec<String> {
let find = |want: &str| {
dir_names
@@ -86,11 +99,17 @@ impl Chain {
/// `"Failed to open archive"` and goes on, because a skipped corrupt archive surfaces only as
/// missing files far downstream.
pub fn open(path: &Path) -> Result<Self> {
Self::open_without(path, &[])
Self::open_dir(path, stock_chain_only(), &[])
}
/// [`Chain::open`], `Data`'s archives named in `skip` left out.
fn open_without(path: &Path, skip: &[String]) -> Result<Self> {
Self::open_dir(path, stock_chain_only(), skip)
}
/// [`Chain::open`]; `stock` keeps the `Data` directory to its stock archives
/// ([`stock_chain_only`]).
fn open_dir(path: &Path, stock: bool, skip: &[String]) -> Result<Self> {
let mut archives = Vec::new();
if path.is_dir() {
let mut names: Vec<String> = std::fs::read_dir(path)
@@ -105,7 +124,9 @@ impl Chain {
// read_dir order is arbitrary; sort so case-variant ties resolve deterministically.
names.sort();
for name in mount_order(&names) {
if skip.iter().any(|s| s.eq_ignore_ascii_case(&name)) {
if skip.iter().any(|s| s.eq_ignore_ascii_case(&name))
|| (stock && is_patch_glob_match(&name) && !is_stock_patch(&name))
{
continue;
}
let mpq = path.join(&name);
@@ -161,7 +182,9 @@ impl Chain {
let folder = pack
.pack_folder(path)
.context("ruRU language pack is not installed")?;
chain.archives.extend(Self::open(&folder)?.archives);
chain
.archives
.extend(Self::open_dir(&folder, false, &[])?.archives);
}
chain.locale = Locale::for_language_pack(pack);
Ok(chain)
@@ -473,6 +496,54 @@ mod tests {
);
}
/// The stock chain keeps the base set, `patch.MPQ` and `patch-2.MPQ`, and drops the other
/// `patch-?.MPQ` in any case; without it every patch mounts.
#[test]
fn the_stock_chain_mounts_only_the_stock_patches() {
let dir = std::env::temp_dir().join(format!(
"benilla-chain-stock-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for archive in [
"interface.MPQ",
"patch.MPQ",
"patch-2.MPQ",
"patch-A.mpq",
"patch-F.MPQ",
] {
let line = format!("X\\{archive}\r\n");
std::fs::write(
dir.join(archive),
fixture_mpq(&[("(listfile)", line.as_bytes())]),
)
.unwrap();
}
let stock = Chain::open_dir(&dir, true, &[])
.expect("open")
.enumerate_archive_files("x\\");
let every = Chain::open_dir(&dir, false, &[])
.expect("open")
.enumerate_archive_files("x\\");
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(
stock,
["X\\patch.MPQ", "X\\patch-2.MPQ", "X\\interface.MPQ"]
);
assert_eq!(
every,
[
"X\\patch.MPQ",
"X\\patch-2.MPQ",
"X\\patch-A.mpq",
"X\\patch-F.MPQ",
"X\\interface.MPQ"
]
);
}
/// An archive left out by name, in any case, does not mount; the others keep their order.
#[test]
fn a_left_out_archive_does_not_mount() {
@@ -491,7 +562,7 @@ mod tests {
)
.unwrap();
}
let chain = Chain::open_without(&dir, &["PATCH-3.mpq".to_owned()]).expect("open");
let chain = Chain::open_dir(&dir, false, &["PATCH-3.mpq".to_owned()]).expect("open");
let walked = chain.enumerate_archive_files("x\\");
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(
+3
View File
@@ -7,6 +7,9 @@
# scripts/unit-tests.sh --workspace
# scripts/unit-tests.sh -p benilla-app --no-default-features --lib
set -euo pipefail
# The tests that read the install assert the stock 1.12.1 data: the chain leaves out any other
# patch archive a player added (texture packs, interface mods) (`Chain::open`).
export BENILLA_STOCK_CHAIN=1
if cargo nextest --version >/dev/null 2>&1; then
exec cargo nextest run --no-fail-fast --no-tests=pass "$@"
fi