0.1.9
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { parseXml, LineMap, stripBom } from "../out/language/xmlParser.js";
|
||||
import { extractIndexRecords } from "../out/indexer/records.js";
|
||||
import { buildSearchPaths } from "../out/indexer/includeResolver.js";
|
||||
import {
|
||||
buildDocumentScope,
|
||||
withLocalOverlay,
|
||||
} from "../out/indexer/localScope.js";
|
||||
import {
|
||||
findContainingGameObject,
|
||||
findLocalId,
|
||||
collectLocalIds,
|
||||
} from "../out/indexer/logicalTree.js";
|
||||
import { resolveReferenceTargetsForType } from "../out/indexer/refs.js";
|
||||
import { resolveElementType } from "../out/language/typeContext.js";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const project = join(root, "test", "fixtures", "minimod");
|
||||
const sdk = join(root, "test", "fixtures", "fakesdk");
|
||||
const standalonePath = join(project, "Data", "Standalone.xml");
|
||||
|
||||
async function readParsed(path) {
|
||||
const text = stripBom(await readFile(path, "utf8"));
|
||||
const lineMap = new LineMap(text);
|
||||
const parse = parseXml(text);
|
||||
return {
|
||||
file: { path, stat: null },
|
||||
parse,
|
||||
records: extractIndexRecords(parse, lineMap),
|
||||
lineMap,
|
||||
};
|
||||
}
|
||||
|
||||
async function makeScope() {
|
||||
const searchPaths = buildSearchPaths(sdk, project);
|
||||
const text = await readFile(standalonePath, "utf8");
|
||||
return buildDocumentScope(standalonePath, text, 1, {
|
||||
projectDir: project,
|
||||
sdkDir: sdk,
|
||||
searchPaths,
|
||||
readRecords: readParsed,
|
||||
readDom: readParsed,
|
||||
});
|
||||
}
|
||||
|
||||
test("local overlay resolves refs for a file outside every global stream", async () => {
|
||||
const scope = await makeScope();
|
||||
const merged = withLocalOverlay(null, scope.overlay, project, sdk);
|
||||
|
||||
assert.ok(merged.local.assetsById.has("standalonetank"));
|
||||
assert.ok(merged.local.assetsById.has("standalonebase"));
|
||||
assert.ok(merged.local.defines.has("standalone_health"));
|
||||
|
||||
const targets = resolveReferenceTargetsForType(
|
||||
merged,
|
||||
"GameObject",
|
||||
"inheritFrom",
|
||||
"StandaloneBase",
|
||||
);
|
||||
assert.equal(targets.length, 1);
|
||||
assert.match(targets[0].def.file, /StandaloneBase\.xml$/);
|
||||
assert.equal(targets[0].def.origin, "project");
|
||||
});
|
||||
|
||||
test("logical xi:include expansion gives included modules their Draws context", async () => {
|
||||
const scope = await makeScope();
|
||||
const included = scope.expanded.elements.find(
|
||||
(e) =>
|
||||
e.name === "TruckDraw" &&
|
||||
e.attrs.some((a) => a.name === "id" && a.value === "ModuleTag_Headlight"),
|
||||
);
|
||||
assert.ok(included, "xi:include target spliced into the logical tree");
|
||||
assert.match(included.sourceFile, /HeadlightModules\.xml$/i);
|
||||
assert.equal(
|
||||
resolveElementType(included),
|
||||
"W3DTruckDrawModuleData",
|
||||
"included module resolves through the logical Draws parent",
|
||||
);
|
||||
|
||||
const update = scope.expanded.elements.find(
|
||||
(e) => e.name === "ReconstituteStateSpecialAbility",
|
||||
);
|
||||
assert.ok(update);
|
||||
const gameObject = findContainingGameObject(update);
|
||||
assert.equal(gameObject?.name, "GameObject");
|
||||
assert.equal(
|
||||
findLocalId(gameObject, "ModuleTag_Headlight"),
|
||||
included,
|
||||
"Poid reference can reach a module spliced in through xi:include",
|
||||
);
|
||||
|
||||
const localIds = collectLocalIds(gameObject).map((i) => i.id);
|
||||
assert.ok(localIds.includes("ModuleTag_Draw"));
|
||||
assert.ok(localIds.includes("ModuleTag_Headlight"));
|
||||
});
|
||||
|
||||
test("local overlay wins over a global definition with the same id", async () => {
|
||||
const scope = await makeScope();
|
||||
const global = {
|
||||
assets: new Map(),
|
||||
assetsById: new Map([
|
||||
[
|
||||
"standalonebase",
|
||||
[
|
||||
{
|
||||
type: "GameObject",
|
||||
id: "StandaloneBase",
|
||||
file: join(sdk, "SageXml", "VanillaBase.xml"),
|
||||
line: 1,
|
||||
origin: "sdk",
|
||||
},
|
||||
],
|
||||
],
|
||||
]),
|
||||
defines: new Map(),
|
||||
};
|
||||
const merged = withLocalOverlay(global, scope.overlay, project, sdk);
|
||||
const targets = resolveReferenceTargetsForType(
|
||||
merged,
|
||||
"GameObject",
|
||||
"inheritFrom",
|
||||
"StandaloneBase",
|
||||
);
|
||||
assert.equal(targets.length, 2);
|
||||
assert.match(targets[0].def.file, /StandaloneBase\.xml$/);
|
||||
assert.equal(targets[0].def.origin, "project");
|
||||
assert.match(targets[1].def.file, /VanillaBase\.xml$/);
|
||||
});
|
||||
|
||||
test("logical expansion terminates on xi:include cycles", async (t) => {
|
||||
const tmp = await mkdtemp(join(tmpdir(), "ra3-local-cycle-"));
|
||||
t.after(() => rm(tmp, { recursive: true, force: true }));
|
||||
const dataDir = join(tmp, "Data");
|
||||
const includesDir = join(dataDir, "Includes");
|
||||
await mkdir(includesDir, { recursive: true });
|
||||
const aPath = join(dataDir, "A.xml");
|
||||
const bPath = join(includesDir, "B.xml");
|
||||
await writeFile(
|
||||
aPath,
|
||||
`<AssetDeclaration><GameObject id="A"><xi:include href="Includes/B.xml"/></GameObject></AssetDeclaration>`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
bPath,
|
||||
`<AssetDeclaration><GameObject id="B"><xi:include href="../A.xml"/></GameObject></AssetDeclaration>`,
|
||||
"utf8",
|
||||
);
|
||||
const searchPaths = buildSearchPaths(sdk, tmp);
|
||||
const text = await readFile(aPath, "utf8");
|
||||
const scope = await buildDocumentScope(aPath, text, 1, {
|
||||
projectDir: tmp,
|
||||
sdkDir: sdk,
|
||||
searchPaths,
|
||||
readRecords: readParsed,
|
||||
readDom: readParsed,
|
||||
});
|
||||
assert.ok(
|
||||
scope.expanded.elements.some(
|
||||
(e) =>
|
||||
e.name === "GameObject" &&
|
||||
e.attrs.some((a) => a.name === "id" && a.value === "A"),
|
||||
),
|
||||
"entry document survives a cycle",
|
||||
);
|
||||
});
|
||||
|
||||
test("the same xi:include target can expand under multiple parents", async (t) => {
|
||||
const tmp = await mkdtemp(join(tmpdir(), "ra3-local-shared-"));
|
||||
t.after(() => rm(tmp, { recursive: true, force: true }));
|
||||
const dataDir = join(tmp, "Data");
|
||||
const includesDir = join(dataDir, "Includes");
|
||||
await mkdir(includesDir, { recursive: true });
|
||||
const mainPath = join(dataDir, "Main.xml");
|
||||
const fragmentPath = join(includesDir, "Fragment.xml");
|
||||
await writeFile(
|
||||
fragmentPath,
|
||||
`<AssetDeclaration><HeadlightDraw2><TruckDraw id="ModuleTag_Shared"/></HeadlightDraw2></AssetDeclaration>`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
mainPath,
|
||||
`<AssetDeclaration>` +
|
||||
`<GameObject id="A"><Draws><xi:include href="DATA:Includes/Fragment.xml" ` +
|
||||
`xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:HeadlightDraw2/child::*)"/></Draws></GameObject>` +
|
||||
`<GameObject id="B"><Draws><xi:include href="DATA:Includes/Fragment.xml" ` +
|
||||
`xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:HeadlightDraw2/child::*)"/></Draws></GameObject>` +
|
||||
`</AssetDeclaration>`,
|
||||
"utf8",
|
||||
);
|
||||
const searchPaths = buildSearchPaths(sdk, tmp);
|
||||
const text = await readFile(mainPath, "utf8");
|
||||
const scope = await buildDocumentScope(mainPath, text, 1, {
|
||||
projectDir: tmp,
|
||||
sdkDir: sdk,
|
||||
searchPaths,
|
||||
readRecords: readParsed,
|
||||
readDom: readParsed,
|
||||
});
|
||||
const shared = scope.expanded.elements.filter(
|
||||
(e) =>
|
||||
e.name === "TruckDraw" &&
|
||||
e.attrs.some((a) => a.name === "id" && a.value === "ModuleTag_Shared"),
|
||||
);
|
||||
assert.equal(shared.length, 2, "same fragment expands once per parent");
|
||||
});
|
||||
Reference in New Issue
Block a user