@thigasdevelopment/luam 0.19.2 → 0.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/luam.mjs +733 -18
- package/package.json +1 -1
package/luam.mjs
CHANGED
|
@@ -3253,7 +3253,7 @@ var EXIT_USAGE = 2;
|
|
|
3253
3253
|
// src/config/manifest-context.ts
|
|
3254
3254
|
var DEVELOPMENT_MODE = "development";
|
|
3255
3255
|
var PRODUCTION_MODE = "production";
|
|
3256
|
-
var DEVELOPMENT_COMMANDS = ["dev", "ensure", "server"];
|
|
3256
|
+
var DEVELOPMENT_COMMANDS = ["dev", "ensure", "server", "test"];
|
|
3257
3257
|
var PRODUCTION_COMMANDS = ["build"];
|
|
3258
3258
|
function manifestMode(command) {
|
|
3259
3259
|
if (DEVELOPMENT_COMMANDS.includes(command)) {
|
|
@@ -6762,7 +6762,7 @@ import { tmpdir } from "node:os";
|
|
|
6762
6762
|
import { join as join2 } from "node:path";
|
|
6763
6763
|
|
|
6764
6764
|
// src/cli/version.ts
|
|
6765
|
-
var VERSION = true ? "0.19.
|
|
6765
|
+
var VERSION = true ? "0.19.3" : "0.0.0-dev";
|
|
6766
6766
|
var PROGRAM_NAME = "luam";
|
|
6767
6767
|
var PROGRAM_DESCRIPTION = "luam \u2014 the Luam compiler for Multi Theft Auto resources.";
|
|
6768
6768
|
|
|
@@ -8313,7 +8313,7 @@ import { resolve as resolve8 } from "node:path";
|
|
|
8313
8313
|
function createSourceResolver(mapping) {
|
|
8314
8314
|
const matchers = new Map(SOURCE_SIDES.map((environment) => [environment, createPatternMatcher(mapping[environment])]));
|
|
8315
8315
|
const patterns = SOURCE_SIDES.flatMap((environment) => [...matchers.get(environment)?.patterns ?? []]);
|
|
8316
|
-
function
|
|
8316
|
+
function resolve21(path) {
|
|
8317
8317
|
const normalized = normalizePattern(path);
|
|
8318
8318
|
const matches = [];
|
|
8319
8319
|
for (const environment of SOURCE_SIDES) {
|
|
@@ -8327,8 +8327,8 @@ function createSourceResolver(mapping) {
|
|
|
8327
8327
|
return {
|
|
8328
8328
|
patterns,
|
|
8329
8329
|
roots: watchRoots(patterns),
|
|
8330
|
-
resolve:
|
|
8331
|
-
side: (path) =>
|
|
8330
|
+
resolve: resolve21,
|
|
8331
|
+
side: (path) => resolve21(path).environment
|
|
8332
8332
|
};
|
|
8333
8333
|
}
|
|
8334
8334
|
function describeMatches(matches) {
|
|
@@ -8338,17 +8338,28 @@ function describeMatches(matches) {
|
|
|
8338
8338
|
// ../compiler/src/project/source-kind.ts
|
|
8339
8339
|
var SOURCE_EXTENSION = ".luam";
|
|
8340
8340
|
var DECLARATION_EXTENSION = ".d.luam";
|
|
8341
|
+
var TEST_EXTENSION = ".test.luam";
|
|
8341
8342
|
function isDeclarationPath(path) {
|
|
8342
8343
|
return normalizePath(path).endsWith(DECLARATION_EXTENSION);
|
|
8343
8344
|
}
|
|
8345
|
+
function isTestPath(path) {
|
|
8346
|
+
return normalizePath(path).endsWith(TEST_EXTENSION);
|
|
8347
|
+
}
|
|
8344
8348
|
|
|
8345
8349
|
// src/build/source-discovery.ts
|
|
8346
8350
|
var MISSING_SOURCE = "config-missing-source";
|
|
8347
8351
|
var NO_SOURCES = "config-no-sources";
|
|
8348
8352
|
var SIDE_CONFLICT = "config-source-side-conflict";
|
|
8353
|
+
var TEST_SOURCE = "config-test-source";
|
|
8349
8354
|
var UNREADABLE_SOURCE = "build-source-unreadable";
|
|
8350
8355
|
function checkLiterals(root, resolver, found, diagnostics) {
|
|
8351
8356
|
for (const pattern of resolver.patterns.filter(isLiteralPattern)) {
|
|
8357
|
+
if (isTestPath(pattern)) {
|
|
8358
|
+
diagnostics.push(
|
|
8359
|
+
cliError(TEST_SOURCE, `"${pattern}" is listed in "sources" but "${TEST_EXTENSION}" files are run by "luam test" and never built into the resource.`)
|
|
8360
|
+
);
|
|
8361
|
+
continue;
|
|
8362
|
+
}
|
|
8352
8363
|
if (!pattern.endsWith(SOURCE_EXTENSION)) {
|
|
8353
8364
|
diagnostics.push(cliError(MISSING_SOURCE, `"${pattern}" is listed in "sources" but does not end in "${SOURCE_EXTENSION}".`));
|
|
8354
8365
|
continue;
|
|
@@ -8372,7 +8383,7 @@ function discoverSources(root, sources, excluded = []) {
|
|
|
8372
8383
|
const diagnostics = tree.errors.map((message) => cliError(UNREADABLE_SOURCE, message));
|
|
8373
8384
|
const files = [];
|
|
8374
8385
|
const matched = /* @__PURE__ */ new Set();
|
|
8375
|
-
for (const path of tree.files.filter((entry) => entry.endsWith(SOURCE_EXTENSION))) {
|
|
8386
|
+
for (const path of tree.files.filter((entry) => entry.endsWith(SOURCE_EXTENSION) && !isTestPath(entry))) {
|
|
8376
8387
|
const resolution = resolver.resolve(path);
|
|
8377
8388
|
if (resolution.matches.length === 0) {
|
|
8378
8389
|
continue;
|
|
@@ -8458,6 +8469,37 @@ function projectDeclarations(entries3, origin) {
|
|
|
8458
8469
|
return { globals: [envDeclaration(entries3, origin)] };
|
|
8459
8470
|
}
|
|
8460
8471
|
|
|
8472
|
+
// ../compiler/src/checker/test-declarations.ts
|
|
8473
|
+
var TEST_ORIGIN = "luam:test";
|
|
8474
|
+
var CALLBACK = fn([], VOID);
|
|
8475
|
+
var EXPECTATION_MEMBERS = [
|
|
8476
|
+
{ name: "toBe", type: fn([ANY], VOID, 1, false, ["expected"]) },
|
|
8477
|
+
{ name: "toBeFalsy", type: fn([], VOID) },
|
|
8478
|
+
{ name: "toBeNil", type: fn([], VOID) },
|
|
8479
|
+
{ name: "toBeTruthy", type: fn([], VOID) },
|
|
8480
|
+
{ name: "toContain", type: fn([ANY], VOID, 1, false, ["value"]) },
|
|
8481
|
+
{ name: "toEqual", type: fn([ANY], VOID, 1, false, ["expected"]) },
|
|
8482
|
+
{ name: "toNotBe", type: fn([ANY], VOID, 1, false, ["expected"]) },
|
|
8483
|
+
{ name: "toNotEqual", type: fn([ANY], VOID, 1, false, ["expected"]) },
|
|
8484
|
+
{ name: "toThrow", type: fn([optionalOf(STRING)], VOID, 0, false, ["message"]) }
|
|
8485
|
+
];
|
|
8486
|
+
var STUB_MEMBERS = [
|
|
8487
|
+
{ name: "calls", type: fn([STRING], arrayOf(TABLE), 1, false, ["name"]) },
|
|
8488
|
+
{ name: "reset", type: fn([], VOID) },
|
|
8489
|
+
{ name: "returns", type: fn([STRING, ANY], VOID, 2, false, ["name", "value"]) },
|
|
8490
|
+
{ name: "stub", type: fn([STRING, ANY], VOID, 2, false, ["name", "implementation"]) }
|
|
8491
|
+
];
|
|
8492
|
+
var EXPECTATION_TYPE = record("Expectation", EXPECTATION_MEMBERS, TEST_ORIGIN);
|
|
8493
|
+
var MTA_STUBS_TYPE = record("MtaStubs", STUB_MEMBERS, TEST_ORIGIN);
|
|
8494
|
+
var TEST_DECLARATIONS = [
|
|
8495
|
+
{ name: "afterEach", environment: "shared", source: "extension", type: fn([CALLBACK], VOID, 1, false, ["body"]) },
|
|
8496
|
+
{ name: "beforeEach", environment: "shared", source: "extension", type: fn([CALLBACK], VOID, 1, false, ["body"]) },
|
|
8497
|
+
{ name: "describe", environment: "shared", source: "extension", type: fn([STRING, CALLBACK], VOID, 2, false, ["name", "body"]) },
|
|
8498
|
+
{ name: "expect", environment: "shared", source: "extension", type: fn([ANY], EXPECTATION_TYPE, 1, false, ["value"]) },
|
|
8499
|
+
{ name: "mta", environment: "shared", source: "extension", type: MTA_STUBS_TYPE },
|
|
8500
|
+
{ name: "test", environment: "shared", source: "extension", type: fn([STRING, CALLBACK], VOID, 2, false, ["name", "body"]) }
|
|
8501
|
+
];
|
|
8502
|
+
|
|
8461
8503
|
// ../compiler/src/checker/ambient.ts
|
|
8462
8504
|
var EVENT_NAME_PREFIX = "event:";
|
|
8463
8505
|
var EMPTY_AMBIENT = { classes: [], interfaces: [], enums: [], globals: [], events: [] };
|
|
@@ -20517,8 +20559,8 @@ function typeOf(state, expression) {
|
|
|
20517
20559
|
function indentLine(state, text) {
|
|
20518
20560
|
return `${INDENT2.repeat(state.indent)}${text}`;
|
|
20519
20561
|
}
|
|
20520
|
-
function markSource(state,
|
|
20521
|
-
const marker = { sourceLine };
|
|
20562
|
+
function markSource(state, sourceLine2, symbol = state.symbol) {
|
|
20563
|
+
const marker = { sourceLine: sourceLine2 };
|
|
20522
20564
|
if (symbol !== void 0) {
|
|
20523
20565
|
marker.symbol = symbol;
|
|
20524
20566
|
}
|
|
@@ -22185,7 +22227,7 @@ function compileModule(file, ambient, context) {
|
|
|
22185
22227
|
...file.environment === void 0 ? {} : { environment: file.environment },
|
|
22186
22228
|
ambient,
|
|
22187
22229
|
contracts: context.contracts,
|
|
22188
|
-
project: context.project,
|
|
22230
|
+
project: isTestPath(file.path) ? context.testProject : context.project,
|
|
22189
22231
|
projectReferences: context.projectReferences,
|
|
22190
22232
|
compilerOptions: context.compilerOptions,
|
|
22191
22233
|
development: context.development
|
|
@@ -22273,6 +22315,7 @@ function createProjectCache() {
|
|
|
22273
22315
|
scope: createAmbientScope(collected, { project, references: projectReferences, options: compilerOptions, development, contracts }),
|
|
22274
22316
|
contracts,
|
|
22275
22317
|
project,
|
|
22318
|
+
testProject: { globals: [...project.globals, ...TEST_DECLARATIONS] },
|
|
22276
22319
|
projectReferences,
|
|
22277
22320
|
compilerOptions,
|
|
22278
22321
|
development
|
|
@@ -22363,6 +22406,7 @@ function runCompile(root, config, options = {}) {
|
|
|
22363
22406
|
const excluded = [config.outDir, config.contracts];
|
|
22364
22407
|
const sources = discoverSources(root, config.sources, excluded);
|
|
22365
22408
|
const inputs = readProjectInputs(root, { assets: config.assets, environment: config.environment, excluded });
|
|
22409
|
+
const files = [...sources.files, ...options.additionalFiles ?? []].sort((left, right) => left.path.localeCompare(right.path));
|
|
22366
22410
|
const contracts = readDependencyContracts(root, config);
|
|
22367
22411
|
const diagnostics = [...sources.diagnostics, ...inputs.diagnostics, ...contracts.diagnostics];
|
|
22368
22412
|
if (hasCliErrors(diagnostics)) {
|
|
@@ -22381,9 +22425,9 @@ function runCompile(root, config, options = {}) {
|
|
|
22381
22425
|
contract: null
|
|
22382
22426
|
};
|
|
22383
22427
|
}
|
|
22384
|
-
tracker.begin("compile",
|
|
22428
|
+
tracker.begin("compile", files.length);
|
|
22385
22429
|
const declarations = projectDeclarations(inputs.declared?.entries ?? null, config.environment.file);
|
|
22386
|
-
const project = cache3.compile(
|
|
22430
|
+
const project = cache3.compile(files, {
|
|
22387
22431
|
project: declarations,
|
|
22388
22432
|
contracts: contracts.contracts,
|
|
22389
22433
|
compilerOptions: config.compilerOptions,
|
|
@@ -22406,12 +22450,12 @@ function runCompile(root, config, options = {}) {
|
|
|
22406
22450
|
build,
|
|
22407
22451
|
diagnostics,
|
|
22408
22452
|
fileDiagnostics: assembly.diagnostics,
|
|
22409
|
-
fileCount:
|
|
22453
|
+
fileCount: files.length,
|
|
22410
22454
|
durationMs: performance.now() - started,
|
|
22411
22455
|
stats: project.stats,
|
|
22412
22456
|
environmentTemplate: inputs.deployed === null ? null : renderEnvironmentTemplate(inputs.deployed),
|
|
22413
22457
|
phases: tracker.durations(),
|
|
22414
|
-
sources: diagnosticSources(
|
|
22458
|
+
sources: diagnosticSources(files, assembly.diagnostics),
|
|
22415
22459
|
map: build?.map ?? null,
|
|
22416
22460
|
contract: build === null ? null : buildResourceAbi(config.name, project.modules.flatMap((module) => module.contributions))
|
|
22417
22461
|
};
|
|
@@ -23783,14 +23827,14 @@ function parseMtaLogLine(line2, activeResource, fallback = /* @__PURE__ */ new D
|
|
|
23783
23827
|
const level = severity?.[1] === void 0 ? "info" : logLevel(severity[1]) ?? "info";
|
|
23784
23828
|
const message = stamped.rest.replace(resource[0], "").replace(severity?.[0] ?? "", "").replace(/^\s*[:|-]?\s*/, "");
|
|
23785
23829
|
const sourcePath = resource[2];
|
|
23786
|
-
const
|
|
23830
|
+
const sourceLine2 = resource[3];
|
|
23787
23831
|
return {
|
|
23788
23832
|
timestamp: stamped.value,
|
|
23789
23833
|
environment: "server",
|
|
23790
23834
|
level,
|
|
23791
23835
|
message,
|
|
23792
23836
|
resource: activeResource,
|
|
23793
|
-
...sourcePath !== void 0 &&
|
|
23837
|
+
...sourcePath !== void 0 && sourceLine2 !== void 0 ? { source: { path: sourcePath, line: Number(sourceLine2) } } : {}
|
|
23794
23838
|
};
|
|
23795
23839
|
}
|
|
23796
23840
|
return { timestamp: stamped.value, environment: "server", level: "info", message: stamped.rest, resource: activeResource };
|
|
@@ -24235,9 +24279,52 @@ function registerDevCommand(program2, runtime) {
|
|
|
24235
24279
|
});
|
|
24236
24280
|
}
|
|
24237
24281
|
|
|
24282
|
+
// src/testing/lua-interpreter.ts
|
|
24283
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
24284
|
+
var REQUIRED_LUA_VERSION = "Lua 5.1";
|
|
24285
|
+
var LUA_CANDIDATES = ["lua5.1", "lua51", "lua", "luajit"];
|
|
24286
|
+
var LUA_ENV_VARIABLE = "LUAM_LUA";
|
|
24287
|
+
var INSTALL_HINT = 'Install a Lua 5.1 interpreter and put it on PATH, or point "--lua" or the LUAM_LUA variable at one. LuaJIT reports "Lua 5.1" and is accepted.';
|
|
24288
|
+
function probeInterpreter(executable) {
|
|
24289
|
+
const result = spawnSync2(executable, ["-e", "io.write(_VERSION)"], { encoding: "utf8", shell: false, windowsHide: true });
|
|
24290
|
+
if (result.error !== void 0 || result.status !== 0) {
|
|
24291
|
+
return null;
|
|
24292
|
+
}
|
|
24293
|
+
return result.stdout.trim();
|
|
24294
|
+
}
|
|
24295
|
+
function findLuaInterpreter(request = {}) {
|
|
24296
|
+
const probe = request.probe ?? probeInterpreter;
|
|
24297
|
+
const pinned = request.explicit ?? request.env ?? null;
|
|
24298
|
+
const candidates3 = pinned === null ? LUA_CANDIDATES : [pinned];
|
|
24299
|
+
for (const executable of candidates3) {
|
|
24300
|
+
const version = probe(executable);
|
|
24301
|
+
if (version === REQUIRED_LUA_VERSION) {
|
|
24302
|
+
return { executable, version };
|
|
24303
|
+
}
|
|
24304
|
+
}
|
|
24305
|
+
return null;
|
|
24306
|
+
}
|
|
24307
|
+
function describeMissingInterpreter(request = {}) {
|
|
24308
|
+
const pinned = request.explicit ?? request.env ?? null;
|
|
24309
|
+
if (pinned !== null) {
|
|
24310
|
+
return `"${pinned}" is not a ${REQUIRED_LUA_VERSION} interpreter.`;
|
|
24311
|
+
}
|
|
24312
|
+
return `No ${REQUIRED_LUA_VERSION} interpreter was found on PATH (tried ${LUA_CANDIDATES.map((name) => `"${name}"`).join(", ")}).`;
|
|
24313
|
+
}
|
|
24314
|
+
|
|
24238
24315
|
// src/commands/doctor-command.ts
|
|
24239
|
-
function
|
|
24316
|
+
function reportInterpreter(reporter, probe) {
|
|
24317
|
+
const interpreter = findLuaInterpreter(probe === void 0 ? {} : { probe });
|
|
24318
|
+
if (interpreter === null) {
|
|
24319
|
+
reporter.warn(`No ${REQUIRED_LUA_VERSION} interpreter was found on PATH, so "luam test" cannot run.`);
|
|
24320
|
+
reporter.detail(INSTALL_HINT);
|
|
24321
|
+
return;
|
|
24322
|
+
}
|
|
24323
|
+
reporter.success(`"${interpreter.executable}" reports ${interpreter.version} and can run "luam test".`);
|
|
24324
|
+
}
|
|
24325
|
+
function runDoctorCommand(reporter, editorService, probe) {
|
|
24240
24326
|
reporter.success(`Luam CLI ${VERSION} is running on Node.js ${process.versions.node}.`);
|
|
24327
|
+
reportInterpreter(reporter, probe);
|
|
24241
24328
|
const editors = editorService.detect();
|
|
24242
24329
|
if (editors.length === 0) {
|
|
24243
24330
|
reporter.warn("No supported editor command was found on PATH.");
|
|
@@ -24565,8 +24652,635 @@ function registerServerCommand(program2, runtime) {
|
|
|
24565
24652
|
});
|
|
24566
24653
|
}
|
|
24567
24654
|
|
|
24655
|
+
// src/testing/test-report.ts
|
|
24656
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
24657
|
+
import { resolve as resolve19 } from "node:path";
|
|
24658
|
+
function sourceLine(root, path, line2, cache3) {
|
|
24659
|
+
let lines = cache3.get(path);
|
|
24660
|
+
if (lines === void 0) {
|
|
24661
|
+
try {
|
|
24662
|
+
lines = readFileSync11(resolve19(root, path), "utf8").split(/\r?\n/);
|
|
24663
|
+
} catch {
|
|
24664
|
+
lines = [];
|
|
24665
|
+
}
|
|
24666
|
+
cache3.set(path, lines);
|
|
24667
|
+
}
|
|
24668
|
+
return lines[line2 - 1] ?? null;
|
|
24669
|
+
}
|
|
24670
|
+
function columnOf(text, symbol) {
|
|
24671
|
+
if (text === null) {
|
|
24672
|
+
return 1;
|
|
24673
|
+
}
|
|
24674
|
+
const symbolIndex = symbol === void 0 ? -1 : text.indexOf(symbol);
|
|
24675
|
+
if (symbolIndex >= 0) {
|
|
24676
|
+
return symbolIndex + 1;
|
|
24677
|
+
}
|
|
24678
|
+
const indent = /^\s*/.exec(text)?.[0].length ?? 0;
|
|
24679
|
+
return indent + 1;
|
|
24680
|
+
}
|
|
24681
|
+
function resolveTestPosition(root, map, result, cache3) {
|
|
24682
|
+
if (map === null || result.file === null || result.line === null) {
|
|
24683
|
+
return null;
|
|
24684
|
+
}
|
|
24685
|
+
const resolution = resolveResourcePosition(map, result.file, result.line);
|
|
24686
|
+
if (resolution.status !== "resolved") {
|
|
24687
|
+
return null;
|
|
24688
|
+
}
|
|
24689
|
+
const { file, line: line2, symbol } = resolution.position;
|
|
24690
|
+
return { path: file, line: line2, column: columnOf(sourceLine(root, file, line2, cache3), symbol) };
|
|
24691
|
+
}
|
|
24692
|
+
function formatLocation(root, map, result, cache3) {
|
|
24693
|
+
const position2 = resolveTestPosition(root, map, result, cache3);
|
|
24694
|
+
if (position2 !== null) {
|
|
24695
|
+
return `${position2.path}:${position2.line}:${position2.column}`;
|
|
24696
|
+
}
|
|
24697
|
+
return result.file === null || result.line === null ? "unknown position" : `${result.file}:${result.line}`;
|
|
24698
|
+
}
|
|
24699
|
+
function countResults(runs) {
|
|
24700
|
+
const results = runs.flatMap((run) => run.results);
|
|
24701
|
+
const passed = results.filter((result) => result.passed).length;
|
|
24702
|
+
return { passed, failed: results.length - passed };
|
|
24703
|
+
}
|
|
24704
|
+
function reportTestResults(reporter, runs, root, map, durationMs) {
|
|
24705
|
+
const cache3 = /* @__PURE__ */ new Map();
|
|
24706
|
+
const marker = (name) => reporter.style.marker(name);
|
|
24707
|
+
for (const run of runs) {
|
|
24708
|
+
for (const line2 of run.output) {
|
|
24709
|
+
reporter.detail(` ${line2}`);
|
|
24710
|
+
}
|
|
24711
|
+
for (const result of run.results) {
|
|
24712
|
+
const tone = result.passed ? "success" : "error";
|
|
24713
|
+
const label2 = `${result.environment} \xB7 ${result.name}`;
|
|
24714
|
+
reporter.raw(` ${reporter.style.paint(tone, marker(result.passed ? "success" : "failure"))} ${label2}`);
|
|
24715
|
+
if (!result.passed) {
|
|
24716
|
+
reporter.rawError(` ${formatLocation(root, map, result, cache3)} ${result.message}`);
|
|
24717
|
+
}
|
|
24718
|
+
}
|
|
24719
|
+
if (run.failure !== null) {
|
|
24720
|
+
reporter.error(`The ${run.environment} test run did not complete: ${run.failure}`);
|
|
24721
|
+
}
|
|
24722
|
+
}
|
|
24723
|
+
const totals = countResults(runs);
|
|
24724
|
+
const summary = `${pluralize(totals.passed, "test")} passed, ${totals.failed} failed in ${formatDuration(durationMs)}.`;
|
|
24725
|
+
const crashed = runs.some((run) => run.failure !== null);
|
|
24726
|
+
if (totals.failed === 0 && !crashed) {
|
|
24727
|
+
reporter.success(`Tests passed: ${summary}`);
|
|
24728
|
+
return true;
|
|
24729
|
+
}
|
|
24730
|
+
reporter.error(`Tests failed: ${summary}`);
|
|
24731
|
+
return false;
|
|
24732
|
+
}
|
|
24733
|
+
|
|
24734
|
+
// src/testing/test-discovery.ts
|
|
24735
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
24736
|
+
import { resolve as resolve20 } from "node:path";
|
|
24737
|
+
var DEFAULT_TEST_ENVIRONMENT = "shared";
|
|
24738
|
+
var SIDE_CONFLICT2 = "test-source-side-conflict";
|
|
24739
|
+
var UNREADABLE_TEST = "test-source-unreadable";
|
|
24740
|
+
function readTest(root, path, diagnostics) {
|
|
24741
|
+
try {
|
|
24742
|
+
return readFileSync12(resolve20(root, path), "utf8");
|
|
24743
|
+
} catch (error) {
|
|
24744
|
+
diagnostics.push(cliError(UNREADABLE_TEST, `The test file "${path}" could not be read: ${error instanceof Error ? error.message : String(error)}`));
|
|
24745
|
+
return null;
|
|
24746
|
+
}
|
|
24747
|
+
}
|
|
24748
|
+
function discoverTests(root, sources, excluded = []) {
|
|
24749
|
+
const resolver = createSourceResolver(sources);
|
|
24750
|
+
const tree = listProjectFiles(root, ["."], excluded);
|
|
24751
|
+
const diagnostics = tree.errors.map((message) => cliError(UNREADABLE_TEST, message));
|
|
24752
|
+
const files = [];
|
|
24753
|
+
for (const path of tree.files.filter(isTestPath)) {
|
|
24754
|
+
const resolution = resolver.resolve(path);
|
|
24755
|
+
if (resolution.matches.length > 1) {
|
|
24756
|
+
diagnostics.push(cliError(SIDE_CONFLICT2, `"${path}" is matched by more than one side: ${describeMatches(resolution.matches)}. Narrow the patterns.`));
|
|
24757
|
+
continue;
|
|
24758
|
+
}
|
|
24759
|
+
const source = readTest(root, path, diagnostics);
|
|
24760
|
+
if (source !== null) {
|
|
24761
|
+
files.push({ path, source, environment: resolution.environment ?? DEFAULT_TEST_ENVIRONMENT });
|
|
24762
|
+
}
|
|
24763
|
+
}
|
|
24764
|
+
return { files: files.sort((left, right) => left.path.localeCompare(right.path)), diagnostics };
|
|
24765
|
+
}
|
|
24766
|
+
|
|
24767
|
+
// src/testing/test-runner.ts
|
|
24768
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
24769
|
+
import { mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
24770
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
24771
|
+
import { dirname as dirname7, join as join6 } from "node:path";
|
|
24772
|
+
|
|
24773
|
+
// src/testing/harness-assertions.ts
|
|
24774
|
+
var ASSERTIONS_SOURCE = String.raw`local SENTINEL = '##luam:test'
|
|
24775
|
+
local HARNESS = debug.getinfo(1, 'S').short_src
|
|
24776
|
+
|
|
24777
|
+
local tests = {}
|
|
24778
|
+
local groups = {}
|
|
24779
|
+
local beforeHooks = {}
|
|
24780
|
+
local afterHooks = {}
|
|
24781
|
+
local callLog = {}
|
|
24782
|
+
local behaviour = {}
|
|
24783
|
+
local stubNames = rawget(_G, '__luamTestNames') or {}
|
|
24784
|
+
|
|
24785
|
+
local function escape(value)
|
|
24786
|
+
local text = tostring(value)
|
|
24787
|
+
text = text:gsub('\\', '\\\\')
|
|
24788
|
+
text = text:gsub('\t', '\\t')
|
|
24789
|
+
text = text:gsub('\r', '\\r')
|
|
24790
|
+
text = text:gsub('\n', '\\n')
|
|
24791
|
+
return text
|
|
24792
|
+
end
|
|
24793
|
+
|
|
24794
|
+
local function emit(kind, ...)
|
|
24795
|
+
local parts = { SENTINEL, kind }
|
|
24796
|
+
for index = 1, select('#', ...) do
|
|
24797
|
+
parts[#parts + 1] = escape(select(index, ...))
|
|
24798
|
+
end
|
|
24799
|
+
io.write(table.concat(parts, '\t'))
|
|
24800
|
+
io.write('\n')
|
|
24801
|
+
io.stdout:flush()
|
|
24802
|
+
end
|
|
24803
|
+
|
|
24804
|
+
local function origin()
|
|
24805
|
+
local index = 1
|
|
24806
|
+
while true do
|
|
24807
|
+
local info = debug.getinfo(index, 'Sl')
|
|
24808
|
+
if info == nil then
|
|
24809
|
+
return '', ''
|
|
24810
|
+
end
|
|
24811
|
+
if info.what ~= 'C' and info.short_src ~= HARNESS then
|
|
24812
|
+
return info.short_src, info.currentline
|
|
24813
|
+
end
|
|
24814
|
+
index = index + 1
|
|
24815
|
+
end
|
|
24816
|
+
end
|
|
24817
|
+
|
|
24818
|
+
local function failure(message)
|
|
24819
|
+
local file, line = origin()
|
|
24820
|
+
return { luamTestFailure = true, message = message, file = file, line = line }
|
|
24821
|
+
end
|
|
24822
|
+
|
|
24823
|
+
local function fail(message)
|
|
24824
|
+
error(failure(message), 0)
|
|
24825
|
+
end
|
|
24826
|
+
|
|
24827
|
+
local function handler(err)
|
|
24828
|
+
if type(err) == 'table' and err.luamTestFailure then
|
|
24829
|
+
return err
|
|
24830
|
+
end
|
|
24831
|
+
return failure(tostring(err))
|
|
24832
|
+
end
|
|
24833
|
+
|
|
24834
|
+
local function attempt(body)
|
|
24835
|
+
local ok, err = xpcall(body, handler)
|
|
24836
|
+
if ok then
|
|
24837
|
+
return nil
|
|
24838
|
+
end
|
|
24839
|
+
return err
|
|
24840
|
+
end
|
|
24841
|
+
|
|
24842
|
+
local function describeValue(value)
|
|
24843
|
+
if type(value) == 'string' then
|
|
24844
|
+
return string.format('%q', value)
|
|
24845
|
+
end
|
|
24846
|
+
return tostring(value)
|
|
24847
|
+
end
|
|
24848
|
+
|
|
24849
|
+
local function deepEqual(left, right)
|
|
24850
|
+
if left == right then
|
|
24851
|
+
return true
|
|
24852
|
+
end
|
|
24853
|
+
if type(left) ~= 'table' or type(right) ~= 'table' then
|
|
24854
|
+
return false
|
|
24855
|
+
end
|
|
24856
|
+
for key, value in pairs(left) do
|
|
24857
|
+
if not deepEqual(value, right[key]) then
|
|
24858
|
+
return false
|
|
24859
|
+
end
|
|
24860
|
+
end
|
|
24861
|
+
for key in pairs(right) do
|
|
24862
|
+
if left[key] == nil then
|
|
24863
|
+
return false
|
|
24864
|
+
end
|
|
24865
|
+
end
|
|
24866
|
+
return true
|
|
24867
|
+
end
|
|
24868
|
+
|
|
24869
|
+
local function expect(value)
|
|
24870
|
+
local matchers = {}
|
|
24871
|
+
|
|
24872
|
+
function matchers.toBe(expected)
|
|
24873
|
+
if value ~= expected then
|
|
24874
|
+
fail('expected ' .. describeValue(expected) .. ', got ' .. describeValue(value))
|
|
24875
|
+
end
|
|
24876
|
+
end
|
|
24877
|
+
|
|
24878
|
+
function matchers.toNotBe(expected)
|
|
24879
|
+
if value == expected then
|
|
24880
|
+
fail('expected a value other than ' .. describeValue(expected))
|
|
24881
|
+
end
|
|
24882
|
+
end
|
|
24883
|
+
|
|
24884
|
+
function matchers.toEqual(expected)
|
|
24885
|
+
if not deepEqual(value, expected) then
|
|
24886
|
+
fail('expected ' .. describeValue(expected) .. ', got ' .. describeValue(value))
|
|
24887
|
+
end
|
|
24888
|
+
end
|
|
24889
|
+
|
|
24890
|
+
function matchers.toNotEqual(expected)
|
|
24891
|
+
if deepEqual(value, expected) then
|
|
24892
|
+
fail('expected a value other than ' .. describeValue(expected))
|
|
24893
|
+
end
|
|
24894
|
+
end
|
|
24895
|
+
|
|
24896
|
+
function matchers.toBeNil()
|
|
24897
|
+
if value ~= nil then
|
|
24898
|
+
fail('expected nil, got ' .. describeValue(value))
|
|
24899
|
+
end
|
|
24900
|
+
end
|
|
24901
|
+
|
|
24902
|
+
function matchers.toBeTruthy()
|
|
24903
|
+
if not value then
|
|
24904
|
+
fail('expected a truthy value, got ' .. describeValue(value))
|
|
24905
|
+
end
|
|
24906
|
+
end
|
|
24907
|
+
|
|
24908
|
+
function matchers.toBeFalsy()
|
|
24909
|
+
if value then
|
|
24910
|
+
fail('expected a falsy value, got ' .. describeValue(value))
|
|
24911
|
+
end
|
|
24912
|
+
end
|
|
24913
|
+
|
|
24914
|
+
function matchers.toContain(entry)
|
|
24915
|
+
if type(value) == 'string' then
|
|
24916
|
+
if string.find(value, tostring(entry), 1, true) == nil then
|
|
24917
|
+
fail('expected ' .. describeValue(value) .. ' to contain ' .. describeValue(entry))
|
|
24918
|
+
end
|
|
24919
|
+
return
|
|
24920
|
+
end
|
|
24921
|
+
if type(value) ~= 'table' then
|
|
24922
|
+
fail('expected a string or a table, got ' .. type(value))
|
|
24923
|
+
return
|
|
24924
|
+
end
|
|
24925
|
+
for _, candidate in pairs(value) do
|
|
24926
|
+
if deepEqual(candidate, entry) then
|
|
24927
|
+
return
|
|
24928
|
+
end
|
|
24929
|
+
end
|
|
24930
|
+
fail('expected the table to contain ' .. describeValue(entry))
|
|
24931
|
+
end
|
|
24932
|
+
|
|
24933
|
+
function matchers.toThrow(message)
|
|
24934
|
+
if type(value) ~= 'function' then
|
|
24935
|
+
fail('expected a function, got ' .. type(value))
|
|
24936
|
+
return
|
|
24937
|
+
end
|
|
24938
|
+
local ok, err = pcall(value)
|
|
24939
|
+
if ok then
|
|
24940
|
+
fail('expected the function to throw')
|
|
24941
|
+
return
|
|
24942
|
+
end
|
|
24943
|
+
if message ~= nil and string.find(tostring(err), message, 1, true) == nil then
|
|
24944
|
+
fail('expected the error to contain ' .. describeValue(message) .. ', got ' .. describeValue(tostring(err)))
|
|
24945
|
+
end
|
|
24946
|
+
end
|
|
24947
|
+
|
|
24948
|
+
return matchers
|
|
24949
|
+
end
|
|
24950
|
+
`;
|
|
24951
|
+
|
|
24952
|
+
// src/testing/harness-runner.ts
|
|
24953
|
+
var RUNNER_SOURCE = String.raw`local function fullName(name)
|
|
24954
|
+
if #groups == 0 then
|
|
24955
|
+
return name
|
|
24956
|
+
end
|
|
24957
|
+
return table.concat(groups, ' > ') .. ' > ' .. name
|
|
24958
|
+
end
|
|
24959
|
+
|
|
24960
|
+
local function snapshot(list)
|
|
24961
|
+
local copy = {}
|
|
24962
|
+
for index = 1, #list do
|
|
24963
|
+
copy[index] = list[index]
|
|
24964
|
+
end
|
|
24965
|
+
return copy
|
|
24966
|
+
end
|
|
24967
|
+
|
|
24968
|
+
local function describe(name, body)
|
|
24969
|
+
groups[#groups + 1] = name
|
|
24970
|
+
local beforeDepth = #beforeHooks
|
|
24971
|
+
local afterDepth = #afterHooks
|
|
24972
|
+
body()
|
|
24973
|
+
for index = #beforeHooks, beforeDepth + 1, -1 do
|
|
24974
|
+
beforeHooks[index] = nil
|
|
24975
|
+
end
|
|
24976
|
+
for index = #afterHooks, afterDepth + 1, -1 do
|
|
24977
|
+
afterHooks[index] = nil
|
|
24978
|
+
end
|
|
24979
|
+
groups[#groups] = nil
|
|
24980
|
+
end
|
|
24981
|
+
|
|
24982
|
+
local function test(name, body)
|
|
24983
|
+
tests[#tests + 1] = { name = fullName(name), body = body, before = snapshot(beforeHooks), after = snapshot(afterHooks) }
|
|
24984
|
+
end
|
|
24985
|
+
|
|
24986
|
+
local function beforeEach(body)
|
|
24987
|
+
beforeHooks[#beforeHooks + 1] = body
|
|
24988
|
+
end
|
|
24989
|
+
|
|
24990
|
+
local function afterEach(body)
|
|
24991
|
+
afterHooks[#afterHooks + 1] = body
|
|
24992
|
+
end
|
|
24993
|
+
|
|
24994
|
+
local function resetStubs()
|
|
24995
|
+
callLog = {}
|
|
24996
|
+
behaviour = {}
|
|
24997
|
+
end
|
|
24998
|
+
|
|
24999
|
+
local function record(name, ...)
|
|
25000
|
+
local entries = callLog[name]
|
|
25001
|
+
if entries == nil then
|
|
25002
|
+
entries = {}
|
|
25003
|
+
callLog[name] = entries
|
|
25004
|
+
end
|
|
25005
|
+
entries[#entries + 1] = { n = select('#', ...), ... }
|
|
25006
|
+
end
|
|
25007
|
+
|
|
25008
|
+
local function makeStub(name)
|
|
25009
|
+
return function(...)
|
|
25010
|
+
record(name, ...)
|
|
25011
|
+
local configured = behaviour[name]
|
|
25012
|
+
if configured == nil then
|
|
25013
|
+
return nil
|
|
25014
|
+
end
|
|
25015
|
+
if configured.kind == 'function' then
|
|
25016
|
+
return configured.body(...)
|
|
25017
|
+
end
|
|
25018
|
+
return configured.value
|
|
25019
|
+
end
|
|
25020
|
+
end
|
|
25021
|
+
|
|
25022
|
+
local mta = {}
|
|
25023
|
+
|
|
25024
|
+
function mta.stub(name, implementation)
|
|
25025
|
+
behaviour[name] = { kind = 'function', body = implementation }
|
|
25026
|
+
end
|
|
25027
|
+
|
|
25028
|
+
function mta.returns(name, value)
|
|
25029
|
+
behaviour[name] = { kind = 'value', value = value }
|
|
25030
|
+
end
|
|
25031
|
+
|
|
25032
|
+
function mta.calls(name)
|
|
25033
|
+
return callLog[name] or {}
|
|
25034
|
+
end
|
|
25035
|
+
|
|
25036
|
+
function mta.reset()
|
|
25037
|
+
resetStubs()
|
|
25038
|
+
end
|
|
25039
|
+
|
|
25040
|
+
local function runAll()
|
|
25041
|
+
local passed = 0
|
|
25042
|
+
local failed = 0
|
|
25043
|
+
for _, entry in ipairs(tests) do
|
|
25044
|
+
resetStubs()
|
|
25045
|
+
local result = nil
|
|
25046
|
+
for _, hook in ipairs(entry.before) do
|
|
25047
|
+
if result == nil then
|
|
25048
|
+
result = attempt(hook)
|
|
25049
|
+
end
|
|
25050
|
+
end
|
|
25051
|
+
if result == nil then
|
|
25052
|
+
result = attempt(entry.body)
|
|
25053
|
+
end
|
|
25054
|
+
for _, hook in ipairs(entry.after) do
|
|
25055
|
+
local hookResult = attempt(hook)
|
|
25056
|
+
if result == nil then
|
|
25057
|
+
result = hookResult
|
|
25058
|
+
end
|
|
25059
|
+
end
|
|
25060
|
+
if result == nil then
|
|
25061
|
+
passed = passed + 1
|
|
25062
|
+
emit('pass', entry.name)
|
|
25063
|
+
else
|
|
25064
|
+
failed = failed + 1
|
|
25065
|
+
emit('fail', entry.name, result.file, result.line, result.message)
|
|
25066
|
+
end
|
|
25067
|
+
end
|
|
25068
|
+
emit('done', passed, failed)
|
|
25069
|
+
return failed
|
|
25070
|
+
end
|
|
25071
|
+
|
|
25072
|
+
rawset(_G, 'describe', describe)
|
|
25073
|
+
rawset(_G, 'test', test)
|
|
25074
|
+
rawset(_G, 'beforeEach', beforeEach)
|
|
25075
|
+
rawset(_G, 'afterEach', afterEach)
|
|
25076
|
+
rawset(_G, 'expect', expect)
|
|
25077
|
+
rawset(_G, 'mta', mta)
|
|
25078
|
+
|
|
25079
|
+
setmetatable(_G, {
|
|
25080
|
+
__index = function(_, key)
|
|
25081
|
+
if stubNames[key] == nil then
|
|
25082
|
+
return nil
|
|
25083
|
+
end
|
|
25084
|
+
local stub = makeStub(key)
|
|
25085
|
+
rawset(_G, key, stub)
|
|
25086
|
+
return stub
|
|
25087
|
+
end,
|
|
25088
|
+
})
|
|
25089
|
+
|
|
25090
|
+
rawset(_G, '__luamTest', {
|
|
25091
|
+
discard = function()
|
|
25092
|
+
tests = {}
|
|
25093
|
+
groups = {}
|
|
25094
|
+
beforeHooks = {}
|
|
25095
|
+
afterHooks = {}
|
|
25096
|
+
end,
|
|
25097
|
+
run = runAll,
|
|
25098
|
+
})
|
|
25099
|
+
`;
|
|
25100
|
+
|
|
25101
|
+
// src/testing/harness-source.ts
|
|
25102
|
+
var HARNESS_FILE = "harness.lua";
|
|
25103
|
+
var ENTRY_PREFIX = "main-";
|
|
25104
|
+
var SENTINEL = "##luam:test";
|
|
25105
|
+
var HARNESS_SOURCE = `${ASSERTIONS_SOURCE}
|
|
25106
|
+
${RUNNER_SOURCE}`;
|
|
25107
|
+
function entryFile(environment) {
|
|
25108
|
+
return `${ENTRY_PREFIX}${environment}.lua`;
|
|
25109
|
+
}
|
|
25110
|
+
function stubbedGlobals(environment) {
|
|
25111
|
+
return globalsFor(environment).filter((declaration) => declaration.source === "mta" && declaration.type.kind === "function").map((declaration) => declaration.name).sort();
|
|
25112
|
+
}
|
|
25113
|
+
function entrySource(environment, preload, target) {
|
|
25114
|
+
const names = stubbedGlobals(environment).map((name) => ` ['${name}'] = true,`);
|
|
25115
|
+
const discard = preload.length === 0 ? [] : ["__luamTest.discard()"];
|
|
25116
|
+
const lines = [
|
|
25117
|
+
"__luamTestNames = {",
|
|
25118
|
+
...names,
|
|
25119
|
+
"}",
|
|
25120
|
+
`dofile('${HARNESS_FILE}')`,
|
|
25121
|
+
...preload.map((path) => `dofile('${path}')`),
|
|
25122
|
+
...discard,
|
|
25123
|
+
...target.map((path) => `dofile('${path}')`),
|
|
25124
|
+
"local failed = __luamTest.run()",
|
|
25125
|
+
"os.exit(failed == 0 and 0 or 1)"
|
|
25126
|
+
];
|
|
25127
|
+
return `${lines.join("\n")}
|
|
25128
|
+
`;
|
|
25129
|
+
}
|
|
25130
|
+
|
|
25131
|
+
// src/testing/test-runner.ts
|
|
25132
|
+
function runLua(executable, args, cwd) {
|
|
25133
|
+
const result = spawnSync3(executable, [...args], { cwd, encoding: "utf8", shell: false, windowsHide: true });
|
|
25134
|
+
return {
|
|
25135
|
+
status: result.status,
|
|
25136
|
+
stdout: result.stdout ?? "",
|
|
25137
|
+
stderr: result.stderr ?? "",
|
|
25138
|
+
failure: result.error === void 0 ? null : result.error.message
|
|
25139
|
+
};
|
|
25140
|
+
}
|
|
25141
|
+
function unescape(value) {
|
|
25142
|
+
return value.replace(/\\(.)/g, (match, character) => {
|
|
25143
|
+
if (character === "n") {
|
|
25144
|
+
return "\n";
|
|
25145
|
+
}
|
|
25146
|
+
if (character === "r") {
|
|
25147
|
+
return "\r";
|
|
25148
|
+
}
|
|
25149
|
+
return character === "t" ? " " : character;
|
|
25150
|
+
});
|
|
25151
|
+
}
|
|
25152
|
+
function parseLine(environment, line2) {
|
|
25153
|
+
const parts = line2.split(" ");
|
|
25154
|
+
if (parts[0] !== SENTINEL || parts[1] !== "pass" && parts[1] !== "fail") {
|
|
25155
|
+
return null;
|
|
25156
|
+
}
|
|
25157
|
+
const passed = parts[1] === "pass";
|
|
25158
|
+
const file = unescape(parts[3] ?? "");
|
|
25159
|
+
const parsedLine = Number(parts[4] ?? "");
|
|
25160
|
+
return {
|
|
25161
|
+
environment,
|
|
25162
|
+
name: unescape(parts[2] ?? ""),
|
|
25163
|
+
passed,
|
|
25164
|
+
file: file.length === 0 ? null : file,
|
|
25165
|
+
line: Number.isSafeInteger(parsedLine) && parsedLine > 0 ? parsedLine : null,
|
|
25166
|
+
message: unescape(parts[5] ?? "")
|
|
25167
|
+
};
|
|
25168
|
+
}
|
|
25169
|
+
function parseRunOutput(environment, stdout) {
|
|
25170
|
+
const results = [];
|
|
25171
|
+
const output = [];
|
|
25172
|
+
for (const line2 of stdout.split(/\r?\n/)) {
|
|
25173
|
+
const result = parseLine(environment, line2);
|
|
25174
|
+
if (result !== null) {
|
|
25175
|
+
results.push(result);
|
|
25176
|
+
continue;
|
|
25177
|
+
}
|
|
25178
|
+
if (line2.startsWith(SENTINEL) || line2.length === 0) {
|
|
25179
|
+
continue;
|
|
25180
|
+
}
|
|
25181
|
+
output.push(line2);
|
|
25182
|
+
}
|
|
25183
|
+
return { results, output };
|
|
25184
|
+
}
|
|
25185
|
+
function writeWorkspace(directory, scripts) {
|
|
25186
|
+
writeFileSync8(join6(directory, HARNESS_FILE), HARNESS_SOURCE, "utf8");
|
|
25187
|
+
for (const script of scripts) {
|
|
25188
|
+
const target = join6(directory, script.path);
|
|
25189
|
+
mkdirSync7(dirname7(target), { recursive: true });
|
|
25190
|
+
writeFileSync8(target, script.content, "utf8");
|
|
25191
|
+
}
|
|
25192
|
+
}
|
|
25193
|
+
function loadOrder(available, environment) {
|
|
25194
|
+
const shared = bundlePath("shared");
|
|
25195
|
+
const own = bundlePath(environment);
|
|
25196
|
+
const target = available.has(own) ? [own] : [];
|
|
25197
|
+
if (environment === "shared") {
|
|
25198
|
+
return { preload: [], target };
|
|
25199
|
+
}
|
|
25200
|
+
return { preload: available.has(shared) ? [shared] : [], target };
|
|
25201
|
+
}
|
|
25202
|
+
function runTests(request) {
|
|
25203
|
+
const spawn2 = request.spawn ?? runLua;
|
|
25204
|
+
const directory = mkdtempSync2(join6(tmpdir2(), "luam-test-"));
|
|
25205
|
+
const available = new Set(request.scripts.map((script) => script.path));
|
|
25206
|
+
try {
|
|
25207
|
+
writeWorkspace(directory, request.scripts);
|
|
25208
|
+
return ALL_ENVIRONMENTS.filter((environment) => request.environments.includes(environment)).map((environment) => {
|
|
25209
|
+
const { preload, target } = loadOrder(available, environment);
|
|
25210
|
+
const entry = entryFile(environment);
|
|
25211
|
+
writeFileSync8(join6(directory, entry), entrySource(environment, preload, target), "utf8");
|
|
25212
|
+
const execution = spawn2(request.executable, [entry], directory);
|
|
25213
|
+
const parsed = parseRunOutput(environment, execution.stdout);
|
|
25214
|
+
const failure3 = execution.failure ?? (parsed.results.length === 0 && execution.stderr.length > 0 ? execution.stderr.trim() : null);
|
|
25215
|
+
return { environment, results: parsed.results, output: parsed.output, failure: failure3 };
|
|
25216
|
+
});
|
|
25217
|
+
} finally {
|
|
25218
|
+
rmSync2(directory, { recursive: true, force: true });
|
|
25219
|
+
}
|
|
25220
|
+
}
|
|
25221
|
+
|
|
25222
|
+
// src/commands/test-command.ts
|
|
25223
|
+
function testEnvironments(build, paths) {
|
|
25224
|
+
const found = build.bundles.flatMap(
|
|
25225
|
+
(bundle) => bundle.members.flatMap((member) => member.kind === "module" && paths.has(member.module.source) ? [member.module.environment] : [])
|
|
25226
|
+
);
|
|
25227
|
+
return [...new Set(found)];
|
|
25228
|
+
}
|
|
25229
|
+
function runTestCommand(context, options = {}) {
|
|
25230
|
+
const reporter = commandReporter(context);
|
|
25231
|
+
const excluded = [context.config.outDir, context.config.contracts];
|
|
25232
|
+
const discovered = discoverTests(context.root, context.config.sources, excluded);
|
|
25233
|
+
reportCliDiagnostics(reporter, discovered.diagnostics);
|
|
25234
|
+
if (hasCliErrors(discovered.diagnostics)) {
|
|
25235
|
+
return EXIT_DIAGNOSTICS;
|
|
25236
|
+
}
|
|
25237
|
+
if (discovered.files.length === 0) {
|
|
25238
|
+
reporter.warn(`No "${TEST_EXTENSION}" files were found. Write one next to the module it covers.`);
|
|
25239
|
+
return EXIT_OK;
|
|
25240
|
+
}
|
|
25241
|
+
const request = { explicit: options.lua ?? null, env: options.env ?? null, ...options.probe === void 0 ? {} : { probe: options.probe } };
|
|
25242
|
+
const interpreter = findLuaInterpreter(request);
|
|
25243
|
+
if (interpreter === null) {
|
|
25244
|
+
reporter.error(describeMissingInterpreter(request));
|
|
25245
|
+
reporter.detail(INSTALL_HINT);
|
|
25246
|
+
return EXIT_USAGE;
|
|
25247
|
+
}
|
|
25248
|
+
const started = performance.now();
|
|
25249
|
+
const outcome = runCompile(context.root, context.config, { additionalFiles: discovered.files, development: true, layout: "bundle", map: true });
|
|
25250
|
+
reportCliDiagnostics(reporter, outcome.diagnostics);
|
|
25251
|
+
reportFileDiagnostics(reporter, outcome.fileDiagnostics, outcome.sources);
|
|
25252
|
+
if (outcome.build === null) {
|
|
25253
|
+
reporter.error("Tests failed: the project did not compile.");
|
|
25254
|
+
return EXIT_DIAGNOSTICS;
|
|
25255
|
+
}
|
|
25256
|
+
const paths = new Set(discovered.files.map((file) => file.path));
|
|
25257
|
+
const environments = testEnvironments(outcome.build, paths);
|
|
25258
|
+
const runs = runTests({
|
|
25259
|
+
executable: interpreter.executable,
|
|
25260
|
+
scripts: outcome.build.scripts,
|
|
25261
|
+
environments,
|
|
25262
|
+
...options.spawn === void 0 ? {} : { spawn: options.spawn }
|
|
25263
|
+
});
|
|
25264
|
+
return reportTestResults(reporter, runs, context.root, outcome.map, performance.now() - started) ? EXIT_OK : EXIT_DIAGNOSTICS;
|
|
25265
|
+
}
|
|
25266
|
+
|
|
25267
|
+
// src/cli/registry/test-registration.ts
|
|
25268
|
+
function registerTestCommand(program2, runtime) {
|
|
25269
|
+
const command = program2.command("test").description("Compile the project with its test files and run them on a Lua 5.1 interpreter.");
|
|
25270
|
+
addProjectOptions(command);
|
|
25271
|
+
command.option("--lua <path>", "Path to the Lua 5.1 interpreter that runs the tests.");
|
|
25272
|
+
command.action((options) => {
|
|
25273
|
+
const project = createProjectContext(runtime, "test", options);
|
|
25274
|
+
if (project.context === null) {
|
|
25275
|
+
runtime.exitCode = project.error;
|
|
25276
|
+
return;
|
|
25277
|
+
}
|
|
25278
|
+
runtime.exitCode = runTestCommand(project.context, { lua: options.lua ?? null, env: runtime.env[LUA_ENV_VARIABLE] ?? null });
|
|
25279
|
+
});
|
|
25280
|
+
}
|
|
25281
|
+
|
|
24568
25282
|
// src/commands/trace-command.ts
|
|
24569
|
-
import { existsSync as existsSync13, readFileSync as
|
|
25283
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
|
|
24570
25284
|
function defaultMapPath(root, options, reporter) {
|
|
24571
25285
|
const loaded = loadManifest(root, { path: options.manifestPath, mode: manifestMode("trace"), env: options.env });
|
|
24572
25286
|
if (loaded.config !== null) {
|
|
@@ -24591,7 +25305,7 @@ function readStandardInput() {
|
|
|
24591
25305
|
return "";
|
|
24592
25306
|
}
|
|
24593
25307
|
try {
|
|
24594
|
-
return
|
|
25308
|
+
return readFileSync13(0, "utf8");
|
|
24595
25309
|
} catch {
|
|
24596
25310
|
return "";
|
|
24597
25311
|
}
|
|
@@ -24676,6 +25390,7 @@ var COMMAND_REGISTRARS = [
|
|
|
24676
25390
|
registerInitCommand,
|
|
24677
25391
|
registerServerCommand,
|
|
24678
25392
|
registerSetupCommand,
|
|
25393
|
+
registerTestCommand,
|
|
24679
25394
|
registerTraceCommand
|
|
24680
25395
|
];
|
|
24681
25396
|
|