a fresh clone gets the whole project, and the maintainer's tooling moves out
What a clone lacked: the gates went green with a thousand skipped tests and said nothing, every login script assumed a worktree-pool slot and its probe account, the client defaulted to the maintainer's account, a docker path and a home directory were in the code, the temp files broke on GNU coreutils, and two generators could only run against a sibling repo. Now the test gates print how many data-gated tests skipped and why; a scripted run logs in as the account its checkout declares in `.probe-identity` or as the three variables name, never a default, and the client refuses a scripted login on any other account; the event and message tables regenerate from vendored inputs, byte for byte; the docs state the real prerequisites. Out of the tree, into the maintainer's own tooling: the worktree pool, its hooks, the perf leg runner, the cross-platform compile and the Vulkan container leg — a way of working, and setups not every machine has. One hook stays: no writes into the install. Two tests that could never run are gone with their subject: the quest-timer geometry diff compared a copy of the window that no longer exists (and joined a chain path onto a folder, so it always skipped), and the FrameXML flag gate now reads the stock files off the player's own patch chain instead of an extracted folder only one machine had. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,7 @@ fn read_source_tree(req: &str) -> Option<String> {
|
||||
///
|
||||
/// It exists because a number taken from the corpus harness is meaningless without it. In a dev
|
||||
/// build [`read`] prefers the SOURCE TREE, so editing an `assets/ui` file changes what a survey
|
||||
/// loads **with no rebuild** — and this repo's worktrees are routinely shared by several agents at
|
||||
/// loads **with no rebuild** — and a tree is routinely edited by several agents at
|
||||
/// once. Three separate measurements this arc were taken across a moving tree and one of them
|
||||
/// landed a wrong attribution in a decision record: 87/218 was credited to a single table when a
|
||||
/// controlled A/B later put it at 75, with the other twelve belonging to a neighbour's uncommitted
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! **The reference's frame FLAGS are the test oracle** — the flag half of decision 0675's "the
|
||||
//! reference file is the test oracle", and the gate for the class decision 1739 closed.
|
||||
//!
|
||||
//! `framexml_diff` guards the *numbers* a transcribed window carries. Nothing guarded its *flags*,
|
||||
//! A window's numbers are the stock file's own now. Nothing guarded its *flags*,
|
||||
//! and they went quietly wrong at scale: on 2026-08-30 a sweep found **47 frames** the reference
|
||||
//! marks `toplevel` and ours did not (so no window ever came to the front), **23** the reference
|
||||
//! makes mouse-interactive and ours did not (so a click on a window's own background fell through
|
||||
@@ -71,11 +71,10 @@
|
||||
//! can hide a divergence, never invent one — so nothing it misses turns into a false failure. It
|
||||
//! would be a `KNOWN` entry's reason if any frame still needed one.
|
||||
//!
|
||||
//! The whole module skips cleanly with no install — `_extracted_framexml/` is a gitignored
|
||||
//! Blizzard asset, like every other client-data test here.
|
||||
//! The whole module skips cleanly with no install — the reference corpus is read off the
|
||||
//! player's own patch chain, like every other client-data test here.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use benilla_ui::framexml::{self, Element, TopLevel};
|
||||
|
||||
@@ -232,34 +231,54 @@ const KNOWN: &[Known] = &[
|
||||
// window is the reference's own now, scroll children and all.
|
||||
];
|
||||
|
||||
/// The extracted reference FrameXML directory, or `None` when the install isn't there.
|
||||
fn reference_dir() -> Option<PathBuf> {
|
||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../WoW/_extracted_framexml");
|
||||
dir.is_dir().then_some(dir)
|
||||
/// Every reference FrameXML document, read off the player's own patch chain in the client's
|
||||
/// load order: the files `FrameXML.toc` lists, and what each `<Include>`s, each once. `None`
|
||||
/// without an install. Off the chain, never an extracted folder, so the gate is armed on any
|
||||
/// machine with the install rather than on the one that happened to extract it.
|
||||
fn reference_documents() -> Option<Vec<framexml::ParsedDocument>> {
|
||||
let data = benilla_formats::wow_data()?;
|
||||
let chain = benilla_formats::open_chain(&data).ok()?;
|
||||
let toc = chain.read("Interface\\FrameXML\\FrameXML.toc").ok()?;
|
||||
let toc = benilla_ui::toc::Toc::parse(&benilla_ui::source::decode(&toc));
|
||||
let mut queue: Vec<String> = toc
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| f.to_ascii_lowercase().ends_with(".xml"))
|
||||
.map(|f| format!("Interface\\FrameXML\\{f}"))
|
||||
.collect();
|
||||
let mut seen = HashSet::new();
|
||||
let mut docs = Vec::new();
|
||||
while !queue.is_empty() {
|
||||
let path = queue.remove(0);
|
||||
if !seen.insert(path.to_ascii_lowercase()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(bytes) = chain.read(&path) else {
|
||||
continue;
|
||||
};
|
||||
// Blizzard ships a UTF-8 BOM on some of these and stray high bytes in comments; the parse
|
||||
// is what matters, so read lossily rather than refusing the file.
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else {
|
||||
continue;
|
||||
};
|
||||
let dir = path.rsplit_once('\\').map(|(d, _)| d).unwrap_or("");
|
||||
for item in &doc.items {
|
||||
if let TopLevel::Include(file) = item {
|
||||
queue.push(format!("{dir}\\{file}"));
|
||||
}
|
||||
}
|
||||
docs.push(doc);
|
||||
}
|
||||
Some(docs)
|
||||
}
|
||||
|
||||
/// Every named *frame* element in the reference corpus, keyed by name — templates and instances
|
||||
/// alike, nested `<Frames>` included, `$parent`-relative names excluded (they repeat across
|
||||
/// templates and name nothing on their own).
|
||||
fn reference_frames() -> Option<HashMap<String, Element>> {
|
||||
let dir = reference_dir()?;
|
||||
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.ok()?
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("xml")))
|
||||
.collect();
|
||||
paths.sort();
|
||||
|
||||
let mut out: HashMap<String, Element> = HashMap::new();
|
||||
for path in paths {
|
||||
// Blizzard ships a UTF-8 BOM on some of these and stray high bytes in comments; the parse
|
||||
// is what matters, so read lossily rather than refusing the file.
|
||||
let bytes = std::fs::read(&path).ok()?;
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else {
|
||||
continue;
|
||||
};
|
||||
for doc in reference_documents()? {
|
||||
for item in &doc.items {
|
||||
if let TopLevel::Template(el) | TopLevel::Instance(el) = item {
|
||||
collect_named(el, &mut out);
|
||||
@@ -315,24 +334,8 @@ fn collect_nesting(
|
||||
|
||||
/// The reference's nesting table, built over the same corpus [`reference_frames`] reads.
|
||||
fn reference_nesting() -> Option<HashMap<String, Option<String>>> {
|
||||
let dir = reference_dir()?;
|
||||
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.ok()?
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("xml")))
|
||||
.collect();
|
||||
paths.sort();
|
||||
|
||||
let mut out: HashMap<String, Option<String>> = HashMap::new();
|
||||
for path in paths {
|
||||
let Ok(bytes) = std::fs::read(&path) else {
|
||||
continue;
|
||||
};
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else {
|
||||
continue;
|
||||
};
|
||||
for doc in reference_documents()? {
|
||||
for item in &doc.items {
|
||||
if let TopLevel::Template(el) | TopLevel::Instance(el) = item {
|
||||
collect_nesting(el, None, &mut out);
|
||||
@@ -436,14 +439,8 @@ fn describe(frame: &str, flag: Flag, ours: &str, theirs: &str) -> String {
|
||||
/// `parent="UIParent"` back on `BlackoutWorld` and it names that.
|
||||
#[test]
|
||||
fn the_shipped_frames_carry_the_references_flags() {
|
||||
// Two gates, because the corpus and the manifest are two assets: the extracted reference
|
||||
// dir below, and the chain `load_default_ui` reads — under `WOW_DATA=` (1451) the first is
|
||||
// still there and the second is not.
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let Some(reference) = reference_frames() else {
|
||||
eprintln!("skipping: no extracted reference FrameXML (WoW/_extracted_framexml)");
|
||||
return;
|
||||
};
|
||||
let reference = reference_frames().expect("the reference FrameXML off the player's chain");
|
||||
let nesting = reference_nesting().expect("the same corpus the frames came from");
|
||||
assert!(
|
||||
reference.len() > 500,
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
//! **The reference file is the test oracle** (decision 0675) — the shared half of it.
|
||||
//!
|
||||
//! A transcribed window's geometry errors are invisible to every behavioural test: the frames
|
||||
//! load, the buttons click, the lists populate, and only the *look* is wrong. 0675 built the
|
||||
//! detector for `FriendsFrame` after two rounds of the director's eye caught what a one-number
|
||||
//! diff would have caught on its first run, and closed with "for the next transcribed window,
|
||||
//! write the diff test first". This is that scraper, lifted out of `friends_tests` so the next
|
||||
//! window costs a call rather than a copy (`QuestTimerFrame` is the first to pay the lower price).
|
||||
//!
|
||||
//! Deliberately narrow: it does not try to understand the XML, only to notice that a number moved.
|
||||
//! Two properties make it a guard rather than a comfort, and both are the caller's to keep —
|
||||
//! **known-benign differences are an explicit list, never a pattern**, so a new difference cannot
|
||||
//! hide inside a tolerance; and **it must be verified to fail** (perturb one number by 1 px and
|
||||
//! watch it name the element).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The extracted reference FrameXML file of that name, or `None` when the install isn't there
|
||||
/// (these tests skip cleanly without it — it is a gitignored Blizzard asset).
|
||||
pub(super) fn reference(file: &str) -> Option<PathBuf> {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../WoW/_extracted_framexml")
|
||||
.join(file);
|
||||
path.is_file().then_some(path)
|
||||
}
|
||||
|
||||
/// Named element → the `<AbsDimension>` pairs inside it, up to the next named element.
|
||||
///
|
||||
/// Hand-rolled rather than a regex: this crate has no regex dependency, and adding one for a scan
|
||||
/// of two files is a worse trade than twenty lines of `find`.
|
||||
///
|
||||
/// `$parent*` names repeat across templates (every row template has a `$parentText`), so a bare
|
||||
/// name would compare one template's column against another's. Each is qualified by the template
|
||||
/// it lives in — the nearest preceding real name.
|
||||
fn scrape(text: &str) -> Vec<(String, Vec<(f32, f32)>)> {
|
||||
const TAGS: &[&str] = &[
|
||||
"<Button name=\"",
|
||||
"<Frame name=\"",
|
||||
"<EditBox name=\"",
|
||||
"<ScrollFrame name=\"",
|
||||
"<FontString name=\"",
|
||||
"<Texture name=\"",
|
||||
"<CheckButton name=\"",
|
||||
];
|
||||
// Every (offset, name) where a named element starts, in document order.
|
||||
let mut marks: Vec<(usize, String)> = Vec::new();
|
||||
for tag in TAGS {
|
||||
let mut from = 0;
|
||||
while let Some(hit) = text[from..].find(tag) {
|
||||
let at = from + hit;
|
||||
let name_start = at + tag.len();
|
||||
let Some(len) = text[name_start..].find('"') else {
|
||||
break;
|
||||
};
|
||||
marks.push((at, text[name_start..name_start + len].to_string()));
|
||||
from = name_start + len;
|
||||
}
|
||||
}
|
||||
marks.sort_by_key(|(at, _)| *at);
|
||||
|
||||
let mut owner = String::new();
|
||||
marks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (at, name))| {
|
||||
let end = marks.get(i + 1).map_or(text.len(), |(next, _)| *next);
|
||||
let key = if let Some(child) = name.strip_prefix("$parent") {
|
||||
format!("{owner}/{child}")
|
||||
} else {
|
||||
owner = name.clone();
|
||||
name.clone()
|
||||
};
|
||||
(key, dimensions(&text[*at..end]))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Every `<AbsDimension x=".." y=".."/>` in `chunk`, in order.
|
||||
fn dimensions(chunk: &str) -> Vec<(f32, f32)> {
|
||||
const OPEN: &str = "<AbsDimension x=\"";
|
||||
let mut out = Vec::new();
|
||||
let mut rest = chunk;
|
||||
while let Some(hit) = rest.find(OPEN) {
|
||||
rest = &rest[hit + OPEN.len()..];
|
||||
let Some((x, tail)) = rest.split_once('"') else {
|
||||
break;
|
||||
};
|
||||
let Some(y_at) = tail.find("y=\"") else { break };
|
||||
let Some((y, tail)) = tail[y_at + 3..].split_once('"') else {
|
||||
break;
|
||||
};
|
||||
if let (Ok(x), Ok(y)) = (x.parse(), y.parse()) {
|
||||
out.push((x, y));
|
||||
}
|
||||
rest = tail;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Assert every element benilla's `ours` file shares with the reference `reference` file carries
|
||||
/// the reference's `<AbsDimension>` numbers.
|
||||
///
|
||||
/// - `ours` is a path under `assets/ui/`; names are matched to the reference's after stripping the
|
||||
/// house `Benilla` prefix (a window whose frames keep the bare reference names — decision 0591
|
||||
/// §3 — matches unchanged).
|
||||
/// - `expected` lists the *deliberate* deviations by their REFERENCE name. Every entry earns a
|
||||
/// comment at the call site naming its reason; that is what keeps this a guard.
|
||||
/// - `min_compared` fails the test if fewer elements paired up than that, so a scrape or a naming
|
||||
/// change that quietly stops comparing anything is caught rather than reported as a pass.
|
||||
///
|
||||
/// Elements ours has and the reference does not (our own template faces) are skipped, not failed.
|
||||
pub(super) fn assert_geometry_matches(
|
||||
ours: &str,
|
||||
reference: &Path,
|
||||
expected: &[&str],
|
||||
min_compared: usize,
|
||||
) {
|
||||
assert_geometry_matches_text(
|
||||
ours,
|
||||
&std::fs::read_to_string(reference).unwrap(),
|
||||
expected,
|
||||
min_compared,
|
||||
);
|
||||
}
|
||||
|
||||
/// [`assert_geometry_matches`] against reference TEXT rather than a file on disk.
|
||||
///
|
||||
/// The split exists because not every reference file is *on* disk: `_extracted_framexml/` holds
|
||||
/// FrameXML only, and a window whose specification is partly a `Blizzard_*` **addon** — the raid
|
||||
/// grid is the first, decision 1549 — has to read its half straight out of the patch chain
|
||||
/// (`benilla_formats::open_chain`, the same door `mpqx` uses). Reading it rather than requiring
|
||||
/// someone to have extracted it first is what keeps that half of the diff ARMED instead of
|
||||
/// skipped, and it writes nothing into the install.
|
||||
pub(super) fn assert_geometry_matches_text(
|
||||
ours: &str,
|
||||
reference_text: &str,
|
||||
expected: &[&str],
|
||||
min_compared: usize,
|
||||
) {
|
||||
let ours_text = std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(ours),
|
||||
)
|
||||
.unwrap();
|
||||
let theirs: HashMap<String, Vec<(f32, f32)>> = scrape(reference_text).into_iter().collect();
|
||||
|
||||
let mut compared = 0;
|
||||
let mut drifted = Vec::new();
|
||||
for (name, dims) in scrape(&ours_text) {
|
||||
let key = name.replace("Benilla", "");
|
||||
let Some(ref_dims) = theirs.get(&key) else {
|
||||
continue; // ours alone (the local template faces) — nothing to compare against
|
||||
};
|
||||
compared += 1;
|
||||
if dims != *ref_dims && !expected.contains(&key.as_str()) {
|
||||
drifted.push(format!("{key}: ours {dims:?} != ref {ref_dims:?}"));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
compared >= min_compared,
|
||||
"only {compared} elements of {ours} matched by name (expected >= {min_compared}) — the \
|
||||
scrape or the naming broke"
|
||||
);
|
||||
assert!(
|
||||
drifted.is_empty(),
|
||||
"{ours} geometry differs from the reference FrameXML:\n {}",
|
||||
drifted.join("\n ")
|
||||
);
|
||||
}
|
||||
@@ -1179,10 +1179,6 @@ mod duel_tests;
|
||||
#[cfg(test)]
|
||||
mod enchant_confirm_tests;
|
||||
|
||||
/// The shared reference-geometry diff (decision 0675) every transcribed window's test calls.
|
||||
#[cfg(test)]
|
||||
mod framexml_diff;
|
||||
|
||||
/// Its FLAG twin (decision 1739): the whole-tree sweep for `toplevel`/mouse/`id` against the
|
||||
/// reference, read off the loaded engine rather than off our XML.
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -181,30 +181,3 @@ fn a_world_entry_paints_a_quest_already_running() {
|
||||
// gap reads 89 through the `−1`.
|
||||
assert_eq!(row_text(&s, 1), "1 Min 29 Secs ");
|
||||
}
|
||||
|
||||
/// **The reference file is the test oracle** (decision 0675). Scrapes both this window and the
|
||||
/// extracted reference `QuestTimerFrame.xml` for `<AbsDimension>` pairs per named element and
|
||||
/// asserts every shared element's numbers match — the guard that catches a transcription whose
|
||||
/// frames all load, click and populate while every number is wrong.
|
||||
///
|
||||
/// Verified to fail: perturbing `QuestTimer1`'s TOP offset from -30 to -29 reports
|
||||
/// `QuestTimer1: ours [(140.0, 16.0), (0.0, -29.0)] != ref [(140.0, 16.0), (0.0, -30.0)]`.
|
||||
#[test]
|
||||
fn the_window_geometry_matches_the_reference_framexml() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let Some(reference) =
|
||||
super::framexml_diff::reference("Interface\\FrameXML\\QuestTimerFrame.xml")
|
||||
else {
|
||||
eprintln!("skipping: no extracted FrameXML");
|
||||
return;
|
||||
};
|
||||
// No exemptions: this window's frames keep the reference's bare names (decision 0591 §3 — the
|
||||
// manage pass and the ref's own row lookup both resolve by literal name), and every number in
|
||||
// it is the reference's. If an entry ever needs to go here, it names its reason.
|
||||
super::framexml_diff::assert_geometry_matches(
|
||||
"Interface\\FrameXML\\QuestTimerFrame.xml",
|
||||
&reference,
|
||||
&[],
|
||||
22,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user