gdharness 1.3.6 → 1.3.7
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/build/cli.js +788 -624
- package/build/godot/addons/gdharness_editor/reading.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +12 -10
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +11 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +45 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +19 -4
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +1 -1
- package/build/godot/operations/reading.gd +1 -1
- package/build/godot/tests/listed.gd +26 -4
- package/build/keeper.js +25 -3
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8776,6 +8776,7 @@ function parseJUnit(xml) {
|
|
|
8776
8776
|
throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
|
|
8777
8777
|
}
|
|
8778
8778
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
8779
|
+
const where = (suite) => suite.path ?? suite.name;
|
|
8779
8780
|
const suites = suiteElements.map((suite) => {
|
|
8780
8781
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
8781
8782
|
const failures = cases.filter(failedAnAssertion).length;
|
|
@@ -8795,6 +8796,7 @@ function parseJUnit(xml) {
|
|
|
8795
8796
|
cases
|
|
8796
8797
|
};
|
|
8797
8798
|
});
|
|
8799
|
+
suites.sort((a, b) => where(a) < where(b) ? -1 : where(a) > where(b) ? 1 : 0);
|
|
8798
8800
|
const all = suites.flatMap((suite) => suite.cases);
|
|
8799
8801
|
return {
|
|
8800
8802
|
tests: all.length,
|
|
@@ -11856,7 +11858,7 @@ function inspectProject(projectPath) {
|
|
|
11856
11858
|
problems
|
|
11857
11859
|
};
|
|
11858
11860
|
}
|
|
11859
|
-
var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime, GDUNIT4_CI_RUNNER = "addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd";
|
|
11861
|
+
var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", QUIET_SCENES_SETTING = "gdharness/runtime/quiet_scenes_under", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime, GDUNIT4_CI_RUNNER = "addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd";
|
|
11860
11862
|
var init_setup = __esm(() => {
|
|
11861
11863
|
init_class_cache();
|
|
11862
11864
|
init_headless();
|
|
@@ -12448,14 +12450,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
12448
12450
|
name: "project_test",
|
|
12449
12451
|
title: "Run tests",
|
|
12450
12452
|
effect: "additive",
|
|
12451
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed
|
|
12452
|
-
answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. " + IMPORTED_FIRST,
|
|
12453
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
|
|
12454
|
+
answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, with the suite and the case that printed it, and declaredNotPrinted each declared error its case did not print. Either fails the run, and each suite holding one is listed with its count. engineErrors counts every engine error the run printed but those a suite declared. " + IMPORTED_FIRST,
|
|
12453
12455
|
parameters: {
|
|
12454
12456
|
projectPath: PROJECT_PATH,
|
|
12455
12457
|
path: {
|
|
12456
12458
|
type: ["string", "array"],
|
|
12457
12459
|
items: { type: "string" },
|
|
12458
|
-
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default test."
|
|
12460
|
+
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default tests and test, those the project has, which is where gdharness test looks."
|
|
12459
12461
|
},
|
|
12460
12462
|
ignore: {
|
|
12461
12463
|
type: "array",
|
|
@@ -12696,7 +12698,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12696
12698
|
{
|
|
12697
12699
|
name: "scene_animation",
|
|
12698
12700
|
title: "Animations",
|
|
12699
|
-
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key past the animation's end, where it never plays, is written and named under pastLength. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
|
|
12701
|
+
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key past the animation's end, where it never plays, is written and named under pastLength. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
|
|
12700
12702
|
parameters: {
|
|
12701
12703
|
projectPath: PROJECT_PATH,
|
|
12702
12704
|
scenePath: SCENE_PATH,
|
|
@@ -13042,7 +13044,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13042
13044
|
name: "editor_launch",
|
|
13043
13045
|
title: "Open, restart or close the editor, or bind its debug adapter again",
|
|
13044
13046
|
description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it; a debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
13045
|
-
answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up
|
|
13047
|
+
answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had; a scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took; neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
|
|
13046
13048
|
parameters: {
|
|
13047
13049
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
13048
13050
|
hidden: {
|
|
@@ -13595,7 +13597,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
13595
13597
|
},
|
|
13596
13598
|
outputPath: {
|
|
13597
13599
|
type: "string",
|
|
13598
|
-
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
13600
|
+
description: "Also save the picture as a lossless PNG at this absolute path, for somebody to open later or a script to compare; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
13601
|
+
},
|
|
13602
|
+
inline: {
|
|
13603
|
+
type: "boolean",
|
|
13604
|
+
description: "Send the picture back in the answer. Default true, as a JPEG when that is smaller than the PNG and the picture has no transparency (the text says so, with both sizes), since every picture an answer carries stays in the session for as long as it lasts. false sends none and needs outputPath, for a capture a script compares rather than one somebody looks at."
|
|
13599
13605
|
}
|
|
13600
13606
|
},
|
|
13601
13607
|
requires: [],
|
|
@@ -13897,7 +13903,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13897
13903
|
operations: {
|
|
13898
13904
|
pause: {
|
|
13899
13905
|
effect: "additive",
|
|
13900
|
-
summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs",
|
|
13906
|
+
summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs, and stopped in gdharness's runtime addon, an autoload whose _process runs first in every frame, on to the next line of the game's own script, for up to five seconds; one whose stack is still the addon's after them says so in a note",
|
|
13901
13907
|
requires: []
|
|
13902
13908
|
},
|
|
13903
13909
|
continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
|
|
@@ -14428,6 +14434,316 @@ var init_desktop = __esm(() => {
|
|
|
14428
14434
|
DIGEST = createHash4("sha256").update(SOURCE).digest("hex").slice(0, 16);
|
|
14429
14435
|
});
|
|
14430
14436
|
|
|
14437
|
+
// src/suite-judge.ts
|
|
14438
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
14439
|
+
import { stripVTControlCharacters } from "node:util";
|
|
14440
|
+
function secondsOf(time) {
|
|
14441
|
+
let ms = 0;
|
|
14442
|
+
for (const [, amount, unit] of time.matchAll(/(\d+)(h|min|ms|s)\b/g)) {
|
|
14443
|
+
ms += Number(amount) * (unit === "h" ? 3600000 : unit === "min" ? 60000 : unit === "s" ? 1000 : 1);
|
|
14444
|
+
}
|
|
14445
|
+
return ms / 1000;
|
|
14446
|
+
}
|
|
14447
|
+
function funcOf(name) {
|
|
14448
|
+
const colon = name.indexOf(":");
|
|
14449
|
+
return colon < 0 ? name : name.slice(0, colon);
|
|
14450
|
+
}
|
|
14451
|
+
function readRun(printed) {
|
|
14452
|
+
const lines = stripVTControlCharacters(printed).replaceAll(`\r
|
|
14453
|
+
`, `
|
|
14454
|
+
`).split(`
|
|
14455
|
+
`);
|
|
14456
|
+
const suites = new Map;
|
|
14457
|
+
const errors = [];
|
|
14458
|
+
const leaks = [];
|
|
14459
|
+
let executed = null;
|
|
14460
|
+
let open = null;
|
|
14461
|
+
let during = null;
|
|
14462
|
+
let afterCase = [];
|
|
14463
|
+
let lastCase = null;
|
|
14464
|
+
const settleAfterCase = (owner) => {
|
|
14465
|
+
for (const held of afterCase) {
|
|
14466
|
+
errors.push({ ...held, suite: open?.path ?? null, during: owner });
|
|
14467
|
+
}
|
|
14468
|
+
afterCase = [];
|
|
14469
|
+
};
|
|
14470
|
+
for (const [at, line] of lines.entries()) {
|
|
14471
|
+
const opened = SUITE_OPENED.exec(line);
|
|
14472
|
+
if (opened?.[1] !== undefined) {
|
|
14473
|
+
settleAfterCase("after");
|
|
14474
|
+
open = suites.get(opened[1]) ?? { path: opened[1], cases: [], closed: null };
|
|
14475
|
+
suites.set(open.path, open);
|
|
14476
|
+
during = "before";
|
|
14477
|
+
lastCase = null;
|
|
14478
|
+
continue;
|
|
14479
|
+
}
|
|
14480
|
+
const caseLine = CASE_LINE.exec(line);
|
|
14481
|
+
if (caseLine !== null && open !== null) {
|
|
14482
|
+
const name = caseLine[2] ?? "";
|
|
14483
|
+
if (caseLine[3] === "STARTED") {
|
|
14484
|
+
settleAfterCase(lastCase ?? "before");
|
|
14485
|
+
during = name;
|
|
14486
|
+
} else {
|
|
14487
|
+
open.cases.push({
|
|
14488
|
+
name,
|
|
14489
|
+
func: funcOf(name),
|
|
14490
|
+
verdict: caseLine[3] ?? "",
|
|
14491
|
+
seconds: secondsOf(caseLine[4] ?? ""),
|
|
14492
|
+
at
|
|
14493
|
+
});
|
|
14494
|
+
lastCase = name;
|
|
14495
|
+
during = null;
|
|
14496
|
+
}
|
|
14497
|
+
continue;
|
|
14498
|
+
}
|
|
14499
|
+
const statistics = STATISTICS.exec(line);
|
|
14500
|
+
if (statistics !== null && open !== null) {
|
|
14501
|
+
settleAfterCase("after");
|
|
14502
|
+
open.closed = { verdict: statistics[1] ?? "", seconds: secondsOf(statistics[2] ?? "") };
|
|
14503
|
+
open = null;
|
|
14504
|
+
during = null;
|
|
14505
|
+
lastCase = null;
|
|
14506
|
+
continue;
|
|
14507
|
+
}
|
|
14508
|
+
const counted = EXECUTED.exec(line);
|
|
14509
|
+
if (counted?.[1] !== undefined) {
|
|
14510
|
+
executed = Number(counted[1]);
|
|
14511
|
+
continue;
|
|
14512
|
+
}
|
|
14513
|
+
if (LEAK.test(line)) {
|
|
14514
|
+
leaks.push(line.trim());
|
|
14515
|
+
continue;
|
|
14516
|
+
}
|
|
14517
|
+
if (suites.size === 0 && DRIVER_FALLBACK.test(line)) {
|
|
14518
|
+
errors.length = 0;
|
|
14519
|
+
continue;
|
|
14520
|
+
}
|
|
14521
|
+
const error = ERROR_LINE.exec(line);
|
|
14522
|
+
if (error === null) {
|
|
14523
|
+
continue;
|
|
14524
|
+
}
|
|
14525
|
+
const text = error[1] ?? "";
|
|
14526
|
+
if (open === null) {
|
|
14527
|
+
errors.push({ text, at, suite: null, during: null });
|
|
14528
|
+
} else if (during === null) {
|
|
14529
|
+
afterCase.push({ text, at });
|
|
14530
|
+
} else {
|
|
14531
|
+
errors.push({ text, at, suite: open.path, during });
|
|
14532
|
+
}
|
|
14533
|
+
}
|
|
14534
|
+
const last = open;
|
|
14535
|
+
const stoppedIn = last === null ? null : during === null ? `${last.path}, after ${lastCase ?? "its cases"}` : during === "before" ? `${last.path}, in before` : `${last.path} > ${during}`;
|
|
14536
|
+
settleAfterCase("after");
|
|
14537
|
+
const joined = lines.join(`
|
|
14538
|
+
`);
|
|
14539
|
+
const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
|
|
14540
|
+
const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
|
|
14541
|
+
return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
|
|
14542
|
+
}
|
|
14543
|
+
function readExpected(file) {
|
|
14544
|
+
try {
|
|
14545
|
+
const parsed = JSON.parse(readFileSync15(file, "utf8"));
|
|
14546
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
14547
|
+
} catch {
|
|
14548
|
+
return {};
|
|
14549
|
+
}
|
|
14550
|
+
}
|
|
14551
|
+
function exitNamed(code) {
|
|
14552
|
+
const unsigned = code >>> 0;
|
|
14553
|
+
const status = WINDOWS_EXITS.get(unsigned);
|
|
14554
|
+
if (status !== undefined) {
|
|
14555
|
+
return `0x${unsigned.toString(16).toUpperCase()} (${status})`;
|
|
14556
|
+
}
|
|
14557
|
+
const gdunit = GDUNIT_EXITS.get(code);
|
|
14558
|
+
return gdunit === undefined ? String(code) : `${String(code)} (${gdunit})`;
|
|
14559
|
+
}
|
|
14560
|
+
function named(items) {
|
|
14561
|
+
const shown = items.slice(0, MOST_NAMED).join(", ");
|
|
14562
|
+
return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
|
|
14563
|
+
}
|
|
14564
|
+
function judgeEngine(printed, handed, end) {
|
|
14565
|
+
const faults = [];
|
|
14566
|
+
const failing = new Set;
|
|
14567
|
+
const shownAt = new Set;
|
|
14568
|
+
const explained = end.code === GDUNIT_FAILED && [...printed.suites.values()].some((suite) => suite.cases.some((ran) => ran.verdict === "FAILED"));
|
|
14569
|
+
if (end.ended !== null) {
|
|
14570
|
+
faults.push(end.ended);
|
|
14571
|
+
} else if (end.code !== 0 && !explained) {
|
|
14572
|
+
faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
|
|
14573
|
+
}
|
|
14574
|
+
if (printed.unloadable.length > 0) {
|
|
14575
|
+
faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
|
|
14576
|
+
}
|
|
14577
|
+
if (printed.executed === null || printed.executed === 0) {
|
|
14578
|
+
faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
|
|
14579
|
+
}
|
|
14580
|
+
for (const { path, cases } of handed.suites) {
|
|
14581
|
+
const suite = printed.suites.get(path);
|
|
14582
|
+
if (suite === undefined) {
|
|
14583
|
+
faults.push(`${path} never started`);
|
|
14584
|
+
failing.add(path);
|
|
14585
|
+
continue;
|
|
14586
|
+
}
|
|
14587
|
+
if (suite.closed === null) {
|
|
14588
|
+
faults.push(`${path} never finished`);
|
|
14589
|
+
failing.add(path);
|
|
14590
|
+
} else if (suite.closed.verdict !== "PASSED") {
|
|
14591
|
+
if (suite.cases.every((ran) => ran.verdict !== "FAILED")) {
|
|
14592
|
+
faults.push(`${path} reported ${suite.closed.verdict} with every case passing, so a hook failed`);
|
|
14593
|
+
}
|
|
14594
|
+
failing.add(path);
|
|
14595
|
+
}
|
|
14596
|
+
const finished = new Set(suite.cases.map((ran) => ran.func));
|
|
14597
|
+
const missing = cases.filter((func) => !finished.has(func));
|
|
14598
|
+
if (missing.length > 0 && suite.closed !== null) {
|
|
14599
|
+
faults.push(`${path} closed without running ${named(missing)}`);
|
|
14600
|
+
failing.add(path);
|
|
14601
|
+
}
|
|
14602
|
+
const wanted = new Set(cases);
|
|
14603
|
+
const unasked = [...finished].filter((func) => !wanted.has(func));
|
|
14604
|
+
if (unasked.length > 0) {
|
|
14605
|
+
faults.push(`${path} ran ${named(unasked)}, which gdharness did not read as cases of it to run here, so its ` + "plan of the cases was wrong");
|
|
14606
|
+
failing.add(path);
|
|
14607
|
+
}
|
|
14608
|
+
for (const ran of suite.cases) {
|
|
14609
|
+
if (ran.verdict === "FAILED" || ran.verdict === "FLAKY") {
|
|
14610
|
+
faults.push(ran.verdict === "FLAKY" ? `${path} > ${ran.name} failed before it passed on a retry` : `${path} > ${ran.name} failed`);
|
|
14611
|
+
failing.add(path);
|
|
14612
|
+
for (let at = ran.at;at < printed.lines.length && (at === ran.at || printed.lines[at]?.trim()); at += 1) {
|
|
14613
|
+
shownAt.add(at);
|
|
14614
|
+
}
|
|
14615
|
+
}
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
const unexpected = unexpectedErrors(printed, handed.expected, handed.suites.map((suite) => suite.path));
|
|
14619
|
+
for (const error of unexpected.printed) {
|
|
14620
|
+
faults.push(error.suite === null ? `an engine error outside every suite: ${error.text}` : `an engine error in ${error.suite} > ${error.during ?? ""}: ${error.text}; ${declaringAdvice(error.during ?? "")}`);
|
|
14621
|
+
if (error.suite !== null) {
|
|
14622
|
+
failing.add(error.suite);
|
|
14623
|
+
}
|
|
14624
|
+
shownAt.add(error.at);
|
|
14625
|
+
shownAt.add(error.at + 1);
|
|
14626
|
+
}
|
|
14627
|
+
for (const missing of unexpected.neverPrinted) {
|
|
14628
|
+
faults.push(`${missing.suite} > ${missing.during} declares the error "${missing.text}", which it never printed`);
|
|
14629
|
+
failing.add(missing.suite);
|
|
14630
|
+
}
|
|
14631
|
+
for (const leak of printed.leaks) {
|
|
14632
|
+
faults.push(`the engine exited holding what it should have freed: ${leak}`);
|
|
14633
|
+
}
|
|
14634
|
+
const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
|
|
14635
|
+
const starved = memoryExit || printed.starved.length > 0;
|
|
14636
|
+
if (printed.starved.length > 0) {
|
|
14637
|
+
faults.push("the engine ran out of memory");
|
|
14638
|
+
}
|
|
14639
|
+
printed.lines.forEach((line, at) => {
|
|
14640
|
+
if (/^(?:Overall Summary|Executed test|Total execution time| ?Exit code)/.test(line.trim())) {
|
|
14641
|
+
shownAt.add(at);
|
|
14642
|
+
}
|
|
14643
|
+
});
|
|
14644
|
+
const passed = faults.length === 0;
|
|
14645
|
+
const passedSuites = handed.suites.map((suite) => suite.path).filter((path) => !failing.has(path));
|
|
14646
|
+
const cutShort = end.ended !== null || printed.executed === null || handed.suites.some(({ path }) => printed.suites.get(path)?.closed == null);
|
|
14647
|
+
return {
|
|
14648
|
+
passed,
|
|
14649
|
+
faults,
|
|
14650
|
+
passedSuites,
|
|
14651
|
+
starved,
|
|
14652
|
+
shown: passed ? [] : linesShown(printed.lines, shownAt, cutShort)
|
|
14653
|
+
};
|
|
14654
|
+
}
|
|
14655
|
+
function linesShown(lines, picked, cutShort) {
|
|
14656
|
+
const summary = [...picked].filter((at) => at < lines.length).sort((a, b) => a - b);
|
|
14657
|
+
const telling = summary.some((at) => !/^(?:Overall Summary|Executed test|Total execution|\s*Exit code)/.test(lines[at]?.trim() ?? ""));
|
|
14658
|
+
if (!telling && !cutShort) {
|
|
14659
|
+
return [];
|
|
14660
|
+
}
|
|
14661
|
+
if (!telling) {
|
|
14662
|
+
return lines.slice(Math.max(0, lines.length - TAIL)).filter((line, at, all) => at < all.length - 1 || line !== "");
|
|
14663
|
+
}
|
|
14664
|
+
const shown = [];
|
|
14665
|
+
let previous = -1;
|
|
14666
|
+
for (const at of summary) {
|
|
14667
|
+
if (previous >= 0 && at > previous + 1) {
|
|
14668
|
+
shown.push("...");
|
|
14669
|
+
}
|
|
14670
|
+
shown.push(lines[at] ?? "");
|
|
14671
|
+
previous = at;
|
|
14672
|
+
}
|
|
14673
|
+
return shown;
|
|
14674
|
+
}
|
|
14675
|
+
function declaringAdvice(during) {
|
|
14676
|
+
return `if the suite prints it on purpose, list its text under "${funcOf(during)}" in the suite's GDHARNESS_EXPECTED_ERRORS`;
|
|
14677
|
+
}
|
|
14678
|
+
function unexpectedErrors(printed, expected, paths) {
|
|
14679
|
+
const owed = new Map;
|
|
14680
|
+
for (const path of paths) {
|
|
14681
|
+
const suite = printed.suites.get(path);
|
|
14682
|
+
const declared = expected[path];
|
|
14683
|
+
if (suite === undefined || declared === undefined) {
|
|
14684
|
+
continue;
|
|
14685
|
+
}
|
|
14686
|
+
const ranHooks = suite.closed === null ? ["before"] : ["before", "after"];
|
|
14687
|
+
for (const during of [...ranHooks, ...suite.cases.map((ran) => ran.name)]) {
|
|
14688
|
+
const texts = declared[funcOf(during)];
|
|
14689
|
+
if (Array.isArray(texts) && texts.length > 0) {
|
|
14690
|
+
owed.set(`${path}
|
|
14691
|
+
${during}`, {
|
|
14692
|
+
suite: path,
|
|
14693
|
+
during,
|
|
14694
|
+
texts: texts.map((text) => String(text))
|
|
14695
|
+
});
|
|
14696
|
+
}
|
|
14697
|
+
}
|
|
14698
|
+
}
|
|
14699
|
+
const unexpected = [];
|
|
14700
|
+
for (const error of printed.errors) {
|
|
14701
|
+
const debt = error.suite === null || error.during === null ? undefined : owed.get(`${error.suite}
|
|
14702
|
+
${error.during}`);
|
|
14703
|
+
const index = debt?.texts.indexOf(error.text) ?? -1;
|
|
14704
|
+
if (debt === undefined || index < 0) {
|
|
14705
|
+
unexpected.push(error);
|
|
14706
|
+
} else {
|
|
14707
|
+
debt.texts.splice(index, 1);
|
|
14708
|
+
}
|
|
14709
|
+
}
|
|
14710
|
+
const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
|
|
14711
|
+
return { printed: unexpected, neverPrinted };
|
|
14712
|
+
}
|
|
14713
|
+
var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, EXECUTED, DRIVER_FALLBACK, LEAK, STARVED, WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
|
|
14714
|
+
var init_suite_judge = __esm(() => {
|
|
14715
|
+
init_junit();
|
|
14716
|
+
SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
|
|
14717
|
+
TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
|
|
14718
|
+
CASE_LINE = new RegExp(String.raw`^\s*(res://.+?\.gd) > (.+?) (STARTED|PASSED|FAILED|FLAKY|SKIPPED|WARNING)(?: \((?:retry \d+|\d+ retries)\))?(?: (${TIME}))?\s*$`);
|
|
14719
|
+
STATISTICS = new RegExp(String.raw`^Statistics:.*\|\s*([A-Z]+)(?:\s+\(.*?\))?\s+(${TIME})\s*$`);
|
|
14720
|
+
ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
|
|
14721
|
+
EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
|
|
14722
|
+
DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
|
|
14723
|
+
LEAK = /ObjectDB instances (?:were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
|
|
14724
|
+
STARVED = [
|
|
14725
|
+
/Parameter "mem(?:_new)?" is null\.?\s*\n\s*at: \w+ \((?:\.\/)?core\/(?:os\/memory\.cpp|templates\/cowdata\.h):/g,
|
|
14726
|
+
/ERROR: Failed decoding WebP image\.\s*\n\s*at: _webp_unpack/g
|
|
14727
|
+
];
|
|
14728
|
+
WINDOWS_EXITS = new Map([
|
|
14729
|
+
[3221225477, "STATUS_ACCESS_VIOLATION"],
|
|
14730
|
+
[3221225495, "STATUS_NO_MEMORY"],
|
|
14731
|
+
[3221225773, "STATUS_COMMITMENT_LIMIT"],
|
|
14732
|
+
[3221225794, "STATUS_DLL_INIT_FAILED"],
|
|
14733
|
+
[3221226505, "STATUS_STACK_BUFFER_OVERRUN"],
|
|
14734
|
+
[3228369022, "a module that could not be found (VcppException)"],
|
|
14735
|
+
[3228369023, "a procedure that could not be found (VcppException)"]
|
|
14736
|
+
]);
|
|
14737
|
+
MEMORY_EXITS = new Set([3221225495, 3221225773]);
|
|
14738
|
+
GDUNIT_EXITS = new Map([
|
|
14739
|
+
[GDUNIT_FAILED, "gdUnit4's code for a case that failed or errored"],
|
|
14740
|
+
[101, "gdUnit4's code for orphan nodes left behind"],
|
|
14741
|
+
[103, "gdUnit4's code for a suite that cannot run headless"],
|
|
14742
|
+
[104, "gdUnit4's code for a Godot version it does not support"],
|
|
14743
|
+
[105, "gdUnit4's code for scripts that did not compile"]
|
|
14744
|
+
]);
|
|
14745
|
+
});
|
|
14746
|
+
|
|
14431
14747
|
// src/whole-file.ts
|
|
14432
14748
|
import { renameSync as renameSync2, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "node:fs";
|
|
14433
14749
|
function writeWhole(path, text) {
|
|
@@ -14459,6 +14775,227 @@ var init_whole_file = __esm(() => {
|
|
|
14459
14775
|
HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
14460
14776
|
});
|
|
14461
14777
|
|
|
14778
|
+
// src/suite-plan.ts
|
|
14779
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync16 } from "node:fs";
|
|
14780
|
+
import { dirname as dirname10, posix as posix3 } from "node:path";
|
|
14781
|
+
|
|
14782
|
+
class ProjectScripts {
|
|
14783
|
+
sources = new Map;
|
|
14784
|
+
classes = new Map;
|
|
14785
|
+
read = new Map;
|
|
14786
|
+
projectPath;
|
|
14787
|
+
constructor(projectPath) {
|
|
14788
|
+
this.projectPath = projectPath;
|
|
14789
|
+
eachScript(projectPath, (script, source) => {
|
|
14790
|
+
this.sources.set(script, source);
|
|
14791
|
+
const declared = declaredClassOf(source);
|
|
14792
|
+
if (declared !== null) {
|
|
14793
|
+
this.classes.set(declared, script);
|
|
14794
|
+
}
|
|
14795
|
+
});
|
|
14796
|
+
}
|
|
14797
|
+
script(path) {
|
|
14798
|
+
const known = this.read.get(path);
|
|
14799
|
+
if (known !== undefined) {
|
|
14800
|
+
return known;
|
|
14801
|
+
}
|
|
14802
|
+
const source = this.sources.get(path);
|
|
14803
|
+
if (source === undefined) {
|
|
14804
|
+
return;
|
|
14805
|
+
}
|
|
14806
|
+
const shape = shapeOf(source);
|
|
14807
|
+
const read = {
|
|
14808
|
+
source,
|
|
14809
|
+
extendsAs: shape.body.extendsAs,
|
|
14810
|
+
funcs: shape.body.declarations.filter((d) => d.kind === "func").map((d) => d.name),
|
|
14811
|
+
code: codeOf(source)
|
|
14812
|
+
};
|
|
14813
|
+
this.read.set(path, read);
|
|
14814
|
+
return read;
|
|
14815
|
+
}
|
|
14816
|
+
base(path) {
|
|
14817
|
+
const extendsAs = this.script(path)?.extendsAs ?? null;
|
|
14818
|
+
if (extendsAs === null) {
|
|
14819
|
+
return null;
|
|
14820
|
+
}
|
|
14821
|
+
const quote = extendsAs[0];
|
|
14822
|
+
if (quote === '"' || quote === "'") {
|
|
14823
|
+
const target = extendsAs.slice(1, -1);
|
|
14824
|
+
return {
|
|
14825
|
+
path: target.startsWith("res://") ? target : `res://${posix3.join(posix3.dirname(path.slice(6)), target)}`
|
|
14826
|
+
};
|
|
14827
|
+
}
|
|
14828
|
+
const name = extendsAs.split(".")[0] ?? extendsAs;
|
|
14829
|
+
const declaring = this.classes.get(name);
|
|
14830
|
+
return declaring === undefined ? { name } : { path: declaring };
|
|
14831
|
+
}
|
|
14832
|
+
suite(path) {
|
|
14833
|
+
const cases = new Set;
|
|
14834
|
+
let windowed = false;
|
|
14835
|
+
const seen = new Set;
|
|
14836
|
+
let at = path;
|
|
14837
|
+
while (!seen.has(at)) {
|
|
14838
|
+
seen.add(at);
|
|
14839
|
+
const read = this.script(at);
|
|
14840
|
+
if (read === undefined) {
|
|
14841
|
+
return null;
|
|
14842
|
+
}
|
|
14843
|
+
for (const func of read.funcs.filter((name) => name.startsWith(CASE_PREFIX))) {
|
|
14844
|
+
cases.add(func);
|
|
14845
|
+
}
|
|
14846
|
+
windowed ||= NEEDS_WINDOW.test(read.code) || SCENE_RUNNER.test(read.code);
|
|
14847
|
+
const base = this.base(at);
|
|
14848
|
+
if (base === null) {
|
|
14849
|
+
return null;
|
|
14850
|
+
}
|
|
14851
|
+
if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
|
|
14852
|
+
return cases.size > 0 ? { path, cases: [...cases].sort(), windowed } : null;
|
|
14853
|
+
}
|
|
14854
|
+
if ("name" in base) {
|
|
14855
|
+
return null;
|
|
14856
|
+
}
|
|
14857
|
+
at = base.path;
|
|
14858
|
+
}
|
|
14859
|
+
return null;
|
|
14860
|
+
}
|
|
14861
|
+
suitesUnder(roots) {
|
|
14862
|
+
const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
|
|
14863
|
+
return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
|
|
14864
|
+
}
|
|
14865
|
+
}
|
|
14866
|
+
function readTimes(file) {
|
|
14867
|
+
try {
|
|
14868
|
+
const parsed = JSON.parse(readFileSync16(file, "utf8"));
|
|
14869
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
14870
|
+
} catch {
|
|
14871
|
+
return {};
|
|
14872
|
+
}
|
|
14873
|
+
}
|
|
14874
|
+
function writeJson(file, value) {
|
|
14875
|
+
mkdirSync7(dirname10(file), { recursive: true });
|
|
14876
|
+
writeWhole(file, `${JSON.stringify(value, null, 1)}
|
|
14877
|
+
`);
|
|
14878
|
+
}
|
|
14879
|
+
function timesAfter(times, suites, runs) {
|
|
14880
|
+
const next = { ...times };
|
|
14881
|
+
const declared = new Map(suites.map((suite) => [suite.path, suite]));
|
|
14882
|
+
const measured = new Map;
|
|
14883
|
+
for (const run of runs) {
|
|
14884
|
+
for (const printed of run.suites.values()) {
|
|
14885
|
+
if (printed.closed === null || !declared.has(printed.path)) {
|
|
14886
|
+
continue;
|
|
14887
|
+
}
|
|
14888
|
+
const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
|
|
14889
|
+
const own = new Map;
|
|
14890
|
+
for (const ran of printed.cases) {
|
|
14891
|
+
own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
|
|
14892
|
+
}
|
|
14893
|
+
const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
|
|
14894
|
+
entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
|
|
14895
|
+
for (const [func, seconds] of own) {
|
|
14896
|
+
entry.cases.set(func, seconds);
|
|
14897
|
+
}
|
|
14898
|
+
measured.set(printed.path, entry);
|
|
14899
|
+
}
|
|
14900
|
+
}
|
|
14901
|
+
for (const [path, entry] of measured) {
|
|
14902
|
+
const suite = declared.get(path);
|
|
14903
|
+
const cases = {};
|
|
14904
|
+
for (const func of suite?.cases ?? []) {
|
|
14905
|
+
const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
|
|
14906
|
+
if (seconds !== undefined) {
|
|
14907
|
+
cases[func] = round(seconds);
|
|
14908
|
+
}
|
|
14909
|
+
}
|
|
14910
|
+
const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
|
|
14911
|
+
next[path] = { seconds: round(entry.around + inCases), cases };
|
|
14912
|
+
}
|
|
14913
|
+
return next;
|
|
14914
|
+
}
|
|
14915
|
+
function round(seconds) {
|
|
14916
|
+
return Math.round(seconds * 1000) / 1000;
|
|
14917
|
+
}
|
|
14918
|
+
function median(values) {
|
|
14919
|
+
if (values.length === 0) {
|
|
14920
|
+
return;
|
|
14921
|
+
}
|
|
14922
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
14923
|
+
const middle = Math.floor(sorted.length / 2);
|
|
14924
|
+
return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
14925
|
+
}
|
|
14926
|
+
function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
|
|
14927
|
+
const known = Object.values(times);
|
|
14928
|
+
const caseCost = median(known.flatMap((time) => Object.values(time.cases))) ?? UNTIMED_CASE;
|
|
14929
|
+
const aroundCost = median(known.map((time) => time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0))) ?? 0;
|
|
14930
|
+
return suites.flatMap((suite) => {
|
|
14931
|
+
const time = times[suite.path];
|
|
14932
|
+
const costOf = (func) => time?.cases[func] ?? caseCost;
|
|
14933
|
+
const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
|
|
14934
|
+
const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
|
|
14935
|
+
if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
|
|
14936
|
+
return [{ suite: suite.path, cases: suite.cases, cost: whole, windowed: suite.windowed }];
|
|
14937
|
+
}
|
|
14938
|
+
return suite.cases.map((func) => ({
|
|
14939
|
+
suite: suite.path,
|
|
14940
|
+
cases: [func],
|
|
14941
|
+
cost: around + costOf(func),
|
|
14942
|
+
windowed: false
|
|
14943
|
+
}));
|
|
14944
|
+
});
|
|
14945
|
+
}
|
|
14946
|
+
function dealt(units, declared, most) {
|
|
14947
|
+
const windowed = units.filter((unit) => unit.windowed);
|
|
14948
|
+
const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
|
|
14949
|
+
const total = headless.reduce((sum, unit) => sum + unit.cost, 0);
|
|
14950
|
+
const slowest = headless[0]?.cost ?? 0;
|
|
14951
|
+
const room = Math.max(1, most - (windowed.length > 0 && most > 1 ? 1 : 0));
|
|
14952
|
+
const count = headless.length === 0 ? 0 : Math.max(1, Math.min(room, headless.length, Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
|
|
14953
|
+
const bins = Array.from({ length: count }, () => []);
|
|
14954
|
+
const loads = Array.from({ length: count }, () => 0);
|
|
14955
|
+
for (const unit of headless) {
|
|
14956
|
+
const lightest = loads.indexOf(Math.min(...loads));
|
|
14957
|
+
bins[lightest]?.push(unit);
|
|
14958
|
+
loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
|
|
14959
|
+
}
|
|
14960
|
+
const plans = bins.map((bin) => planOf(bin, declared, false));
|
|
14961
|
+
return windowed.length > 0 ? [planOf(windowed, declared, true), ...plans] : plans;
|
|
14962
|
+
}
|
|
14963
|
+
function inOneEngine(units, declared) {
|
|
14964
|
+
return planOf(units, declared, units.some((unit) => unit.windowed));
|
|
14965
|
+
}
|
|
14966
|
+
function planOf(bin, declared, windowed) {
|
|
14967
|
+
const held = new Map;
|
|
14968
|
+
for (const unit of bin) {
|
|
14969
|
+
held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
|
|
14970
|
+
}
|
|
14971
|
+
const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
|
|
14972
|
+
return {
|
|
14973
|
+
suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
|
|
14974
|
+
skipping,
|
|
14975
|
+
cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
|
|
14976
|
+
windowed
|
|
14977
|
+
};
|
|
14978
|
+
}
|
|
14979
|
+
function viewportOf(projectPath) {
|
|
14980
|
+
const file = `${projectPath}/project.godot`;
|
|
14981
|
+
if (!existsSync17(file)) {
|
|
14982
|
+
return null;
|
|
14983
|
+
}
|
|
14984
|
+
const text = readFileSync16(file, "utf8");
|
|
14985
|
+
const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
14986
|
+
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
14987
|
+
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
14988
|
+
}
|
|
14989
|
+
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5;
|
|
14990
|
+
var init_suite_plan = __esm(() => {
|
|
14991
|
+
init_class_cache();
|
|
14992
|
+
init_gdscript_source();
|
|
14993
|
+
init_whole_file();
|
|
14994
|
+
NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*true\b/m;
|
|
14995
|
+
TEST_DIRECTORIES = ["tests", "test"];
|
|
14996
|
+
SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
14997
|
+
});
|
|
14998
|
+
|
|
14462
14999
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
14463
15000
|
var __create2, __defProp2, __getOwnPropDesc, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __exportAll = (all, symbols) => {
|
|
14464
15001
|
let target = {};
|
|
@@ -36060,13 +36597,13 @@ var init_stdio = __esm(() => {
|
|
|
36060
36597
|
});
|
|
36061
36598
|
|
|
36062
36599
|
// src/autoload-failure.ts
|
|
36063
|
-
import { existsSync as existsSync19, readFileSync as
|
|
36600
|
+
import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
|
|
36064
36601
|
import { join as join22 } from "node:path";
|
|
36065
36602
|
function scriptsAutoloaded(projectPath) {
|
|
36066
36603
|
const scripts = new Map;
|
|
36067
36604
|
let settings;
|
|
36068
36605
|
try {
|
|
36069
|
-
settings = parseProjectGodot(
|
|
36606
|
+
settings = parseProjectGodot(readFileSync19(join22(projectPath, "project.godot"), "utf8"));
|
|
36070
36607
|
} catch {
|
|
36071
36608
|
return scripts;
|
|
36072
36609
|
}
|
|
@@ -36080,7 +36617,7 @@ function scriptsAutoloaded(projectPath) {
|
|
|
36080
36617
|
continue;
|
|
36081
36618
|
}
|
|
36082
36619
|
try {
|
|
36083
|
-
for (const named of
|
|
36620
|
+
for (const named of readFileSync19(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
|
|
36084
36621
|
scripts.set(named[1] ?? "", name);
|
|
36085
36622
|
}
|
|
36086
36623
|
} catch {}
|
|
@@ -36099,7 +36636,7 @@ var init_autoload_failure = __esm(() => {
|
|
|
36099
36636
|
});
|
|
36100
36637
|
|
|
36101
36638
|
// src/runtime-client.ts
|
|
36102
|
-
import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as
|
|
36639
|
+
import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync20, statSync as statSync12, unlinkSync as unlinkSync2 } from "node:fs";
|
|
36103
36640
|
import { createConnection } from "node:net";
|
|
36104
36641
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36105
36642
|
import { dirname as dirname12, join as join23, resolve as resolve5 } from "node:path";
|
|
@@ -36154,7 +36691,7 @@ function reportAlreadyInTranscript(transcript, report) {
|
|
|
36154
36691
|
function parseAnnouncement(file, pid) {
|
|
36155
36692
|
let fields;
|
|
36156
36693
|
try {
|
|
36157
|
-
fields = asParams(JSON.parse(
|
|
36694
|
+
fields = asParams(JSON.parse(readFileSync20(file, "utf8")));
|
|
36158
36695
|
} catch {
|
|
36159
36696
|
return { kind: "rubbish" };
|
|
36160
36697
|
}
|
|
@@ -36224,7 +36761,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
36224
36761
|
function wentAway(file, pid) {
|
|
36225
36762
|
let project = "";
|
|
36226
36763
|
try {
|
|
36227
|
-
const fields = asParams(JSON.parse(
|
|
36764
|
+
const fields = asParams(JSON.parse(readFileSync20(file, "utf8")));
|
|
36228
36765
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
36229
36766
|
} catch {
|
|
36230
36767
|
return;
|
|
@@ -36739,14 +37276,14 @@ var init_runtime_client = __esm(() => {
|
|
|
36739
37276
|
});
|
|
36740
37277
|
|
|
36741
37278
|
// src/boot-note.ts
|
|
36742
|
-
import { mkdirSync as mkdirSync9, readFileSync as
|
|
37279
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "node:fs";
|
|
36743
37280
|
import { dirname as dirname13, join as join24 } from "node:path";
|
|
36744
37281
|
function bootNotePath(projectPath) {
|
|
36745
37282
|
return join24(projectPath, ".godot", "gdharness-boot.json");
|
|
36746
37283
|
}
|
|
36747
37284
|
function readScenes(projectPath) {
|
|
36748
37285
|
try {
|
|
36749
|
-
return asParams(asParams(JSON.parse(
|
|
37286
|
+
return asParams(asParams(JSON.parse(readFileSync21(bootNotePath(projectPath), "utf8")))["scenes"]);
|
|
36750
37287
|
} catch {
|
|
36751
37288
|
return {};
|
|
36752
37289
|
}
|
|
@@ -36788,7 +37325,7 @@ var init_boot_note = __esm(() => {
|
|
|
36788
37325
|
});
|
|
36789
37326
|
|
|
36790
37327
|
// src/breakpoint-note.ts
|
|
36791
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
37328
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync22, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
|
|
36792
37329
|
import { dirname as dirname14, isAbsolute as isAbsolute5, join as join25, relative as relative4 } from "node:path";
|
|
36793
37330
|
function breakpointNotePath(projectPath) {
|
|
36794
37331
|
return join25(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
@@ -36817,7 +37354,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
36817
37354
|
}
|
|
36818
37355
|
function readBreakpointNote(projectPath) {
|
|
36819
37356
|
try {
|
|
36820
|
-
const parsed = JSON.parse(
|
|
37357
|
+
const parsed = JSON.parse(readFileSync22(breakpointNotePath(projectPath), "utf8"));
|
|
36821
37358
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
36822
37359
|
return [];
|
|
36823
37360
|
}
|
|
@@ -36850,14 +37387,14 @@ var init_breakpoint_note = __esm(() => {
|
|
|
36850
37387
|
});
|
|
36851
37388
|
|
|
36852
37389
|
// src/class-note.ts
|
|
36853
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
37390
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync23 } from "node:fs";
|
|
36854
37391
|
import { dirname as dirname15, join as join26 } from "node:path";
|
|
36855
37392
|
function classNotePath(projectPath) {
|
|
36856
37393
|
return join26(projectPath, ".godot", "gdharness-classes.json");
|
|
36857
37394
|
}
|
|
36858
37395
|
function readClassNote(projectPath) {
|
|
36859
37396
|
try {
|
|
36860
|
-
const fields = asParams(JSON.parse(
|
|
37397
|
+
const fields = asParams(JSON.parse(readFileSync23(classNotePath(projectPath), "utf8")));
|
|
36861
37398
|
const rebuiltWith = readArray(fields, "rebuiltWith");
|
|
36862
37399
|
if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
|
|
36863
37400
|
return null;
|
|
@@ -36899,7 +37436,7 @@ var init_class_note = __esm(() => {
|
|
|
36899
37436
|
});
|
|
36900
37437
|
|
|
36901
37438
|
// src/config-pin.ts
|
|
36902
|
-
import { existsSync as existsSync21, readFileSync as
|
|
37439
|
+
import { existsSync as existsSync21, readFileSync as readFileSync24 } from "node:fs";
|
|
36903
37440
|
import { join as join27 } from "node:path";
|
|
36904
37441
|
function versionsNamedInConfig(projectPath) {
|
|
36905
37442
|
const config = join27(projectPath, ".mcp.json");
|
|
@@ -36908,7 +37445,7 @@ function versionsNamedInConfig(projectPath) {
|
|
|
36908
37445
|
}
|
|
36909
37446
|
let text;
|
|
36910
37447
|
try {
|
|
36911
|
-
text =
|
|
37448
|
+
text = readFileSync24(config, "utf8");
|
|
36912
37449
|
JSON.parse(text);
|
|
36913
37450
|
} catch {
|
|
36914
37451
|
return [];
|
|
@@ -36929,7 +37466,7 @@ var init_config_pin = __esm(() => {
|
|
|
36929
37466
|
});
|
|
36930
37467
|
|
|
36931
37468
|
// src/cut-test-run.ts
|
|
36932
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as
|
|
37469
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync25, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
|
|
36933
37470
|
import { dirname as dirname16, join as join28 } from "node:path";
|
|
36934
37471
|
function keptAt(projectPath) {
|
|
36935
37472
|
return join28(projectPath, ".godot", "gdharness-reports", "cut-run.json");
|
|
@@ -36948,7 +37485,7 @@ function takeCutTestRun(projectPath) {
|
|
|
36948
37485
|
return null;
|
|
36949
37486
|
}
|
|
36950
37487
|
try {
|
|
36951
|
-
const run = JSON.parse(
|
|
37488
|
+
const run = JSON.parse(readFileSync25(path, "utf8"));
|
|
36952
37489
|
rmSync12(path, { force: true });
|
|
36953
37490
|
return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
|
|
36954
37491
|
} catch {
|
|
@@ -37764,7 +38301,7 @@ import {
|
|
|
37764
38301
|
existsSync as existsSync23,
|
|
37765
38302
|
mkdirSync as mkdirSync13,
|
|
37766
38303
|
openSync as openSync4,
|
|
37767
|
-
readFileSync as
|
|
38304
|
+
readFileSync as readFileSync26,
|
|
37768
38305
|
readSync as readSync2,
|
|
37769
38306
|
rmSync as rmSync13,
|
|
37770
38307
|
statSync as statSync13,
|
|
@@ -37779,7 +38316,7 @@ function editorLogNotePath(projectPath) {
|
|
|
37779
38316
|
}
|
|
37780
38317
|
function readEditorLogNote(projectPath) {
|
|
37781
38318
|
try {
|
|
37782
|
-
const fields = asParams(JSON.parse(
|
|
38319
|
+
const fields = asParams(JSON.parse(readFileSync26(editorLogNotePath(projectPath), "utf8")));
|
|
37783
38320
|
const pid = readNumber(fields, "pid");
|
|
37784
38321
|
const startedAt = readString(fields, "startedAt");
|
|
37785
38322
|
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
@@ -37878,7 +38415,7 @@ function foldProgress(text) {
|
|
|
37878
38415
|
function theFirstOf(path) {
|
|
37879
38416
|
const whole = statSync13(path).size;
|
|
37880
38417
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
37881
|
-
return { text:
|
|
38418
|
+
return { text: readFileSync26(path, "utf8") };
|
|
37882
38419
|
}
|
|
37883
38420
|
const handle = openSync4(path, "r");
|
|
37884
38421
|
try {
|
|
@@ -38039,7 +38576,7 @@ var init_editor_log = __esm(() => {
|
|
|
38039
38576
|
});
|
|
38040
38577
|
|
|
38041
38578
|
// src/rename.ts
|
|
38042
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as
|
|
38579
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync27, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
|
|
38043
38580
|
import { dirname as dirname18, posix as posix4 } from "node:path";
|
|
38044
38581
|
|
|
38045
38582
|
class Script {
|
|
@@ -38744,7 +39281,7 @@ function rewritesOf(plan, texts, move) {
|
|
|
38744
39281
|
}
|
|
38745
39282
|
function applyRewrites(rewrites) {
|
|
38746
39283
|
for (const rewrite of rewrites) {
|
|
38747
|
-
const now =
|
|
39284
|
+
const now = readFileSync27(rewrite.absolute, "utf8");
|
|
38748
39285
|
if (now !== rewrite.before) {
|
|
38749
39286
|
throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
|
|
38750
39287
|
}
|
|
@@ -40144,7 +40681,7 @@ import {
|
|
|
40144
40681
|
existsSync as existsSync27,
|
|
40145
40682
|
mkdirSync as mkdirSync16,
|
|
40146
40683
|
readdirSync as readdirSync14,
|
|
40147
|
-
readFileSync as
|
|
40684
|
+
readFileSync as readFileSync28,
|
|
40148
40685
|
renameSync as renameSync4,
|
|
40149
40686
|
statSync as statSync14,
|
|
40150
40687
|
writeFileSync as writeFileSync17
|
|
@@ -40304,7 +40841,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40304
40841
|
plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
|
|
40305
40842
|
const binaries = [];
|
|
40306
40843
|
for (const path of binaryResources(projectPath)) {
|
|
40307
|
-
const bytes =
|
|
40844
|
+
const bytes = readFileSync28(join32(projectPath, path.slice("res://".length)));
|
|
40308
40845
|
const names = [...moves.keys()].find((moving) => bytes.includes(moving));
|
|
40309
40846
|
if (names !== undefined) {
|
|
40310
40847
|
binaries.push({
|
|
@@ -40365,7 +40902,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40365
40902
|
}
|
|
40366
40903
|
function applyFileMove(outcome) {
|
|
40367
40904
|
for (const rewrite of outcome.inPlace) {
|
|
40368
|
-
if (
|
|
40905
|
+
if (readFileSync28(rewrite.absolute, "utf8") !== rewrite.before) {
|
|
40369
40906
|
throw new Error(`${rewrite.path} changed on disk while the move was being worked out; nothing was written`);
|
|
40370
40907
|
}
|
|
40371
40908
|
}
|
|
@@ -40373,7 +40910,7 @@ function applyFileMove(outcome) {
|
|
|
40373
40910
|
if (!existsSync27(file.absoluteFrom)) {
|
|
40374
40911
|
throw new Error(`${file.from} is gone from disk; nothing was written`);
|
|
40375
40912
|
}
|
|
40376
|
-
if (file.text !== null &&
|
|
40913
|
+
if (file.text !== null && readFileSync28(file.absoluteFrom, "utf8") !== file.text.before) {
|
|
40377
40914
|
throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
|
|
40378
40915
|
}
|
|
40379
40916
|
if (existsSync27(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
|
|
@@ -40437,7 +40974,7 @@ var init_godot_variant = __esm(() => {
|
|
|
40437
40974
|
});
|
|
40438
40975
|
|
|
40439
40976
|
// src/game-debugger.ts
|
|
40440
|
-
var PAUSE_MS = 30000, STEP_MS = 1e4, READ_MS = 5000, REPLIES;
|
|
40977
|
+
var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", READ_MS = 5000, REPLIES;
|
|
40441
40978
|
var init_game_debugger = __esm(() => {
|
|
40442
40979
|
init_godot_variant();
|
|
40443
40980
|
REPLIES = new Set([
|
|
@@ -44045,7 +44582,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
44045
44582
|
|
|
44046
44583
|
// src/import-freshness.ts
|
|
44047
44584
|
import { createHash as createHash6 } from "node:crypto";
|
|
44048
|
-
import { existsSync as existsSync28, readdirSync as readdirSync15, readFileSync as
|
|
44585
|
+
import { existsSync as existsSync28, readdirSync as readdirSync15, readFileSync as readFileSync29, statSync as statSync15 } from "node:fs";
|
|
44049
44586
|
import { join as join33 } from "node:path";
|
|
44050
44587
|
function md5Of(file) {
|
|
44051
44588
|
let stats;
|
|
@@ -44060,7 +44597,7 @@ function md5Of(file) {
|
|
|
44060
44597
|
}
|
|
44061
44598
|
let md5;
|
|
44062
44599
|
try {
|
|
44063
|
-
md5 = createHash6("md5").update(
|
|
44600
|
+
md5 = createHash6("md5").update(readFileSync29(file)).digest("hex");
|
|
44064
44601
|
} catch {
|
|
44065
44602
|
return null;
|
|
44066
44603
|
}
|
|
@@ -44106,7 +44643,7 @@ function behind(projectPath, resPath, sidecar) {
|
|
|
44106
44643
|
const record = join33(projectPath, ".godot", "imported", `${name}-${createHash6("md5").update(resPath, "utf8").digest("hex")}.md5`);
|
|
44107
44644
|
let recorded = "";
|
|
44108
44645
|
try {
|
|
44109
|
-
recorded = /^source_md5="([0-9a-f]*)"/m.exec(
|
|
44646
|
+
recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync29(record, "utf8"))?.[1] ?? "";
|
|
44110
44647
|
} catch {
|
|
44111
44648
|
recorded = "";
|
|
44112
44649
|
}
|
|
@@ -44148,7 +44685,7 @@ function staleImports(projectPath) {
|
|
|
44148
44685
|
}
|
|
44149
44686
|
let sidecar;
|
|
44150
44687
|
try {
|
|
44151
|
-
sidecar =
|
|
44688
|
+
sidecar = readFileSync29(join33(directory, entry.name), "utf8");
|
|
44152
44689
|
} catch {
|
|
44153
44690
|
continue;
|
|
44154
44691
|
}
|
|
@@ -44636,7 +45173,7 @@ var init_prompts = __esm(() => {
|
|
|
44636
45173
|
});
|
|
44637
45174
|
|
|
44638
45175
|
// src/restart-note.ts
|
|
44639
|
-
import { mkdirSync as mkdirSync18, readFileSync as
|
|
45176
|
+
import { mkdirSync as mkdirSync18, readFileSync as readFileSync30, rmSync as rmSync15, writeFileSync as writeFileSync18 } from "node:fs";
|
|
44640
45177
|
import { dirname as dirname23, join as join35 } from "node:path";
|
|
44641
45178
|
function restartNotePath(projectPath) {
|
|
44642
45179
|
return join35(projectPath, ".godot", "gdharness-restart.json");
|
|
@@ -44653,7 +45190,7 @@ function noteRestartBegun(note) {
|
|
|
44653
45190
|
}
|
|
44654
45191
|
function restartOwed(projectPath) {
|
|
44655
45192
|
try {
|
|
44656
|
-
const parsed = JSON.parse(
|
|
45193
|
+
const parsed = JSON.parse(readFileSync30(restartNotePath(projectPath), "utf8"));
|
|
44657
45194
|
if (typeof parsed !== "object" || parsed === null) {
|
|
44658
45195
|
return null;
|
|
44659
45196
|
}
|
|
@@ -44679,7 +45216,7 @@ var init_restart_note = __esm(() => {
|
|
|
44679
45216
|
|
|
44680
45217
|
// src/run-record.ts
|
|
44681
45218
|
import { createHash as createHash7 } from "node:crypto";
|
|
44682
|
-
import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as
|
|
45219
|
+
import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as readFileSync31, rmSync as rmSync16, statSync as statSync17 } from "node:fs";
|
|
44683
45220
|
import { basename as basename10, join as join36, resolve as resolve8 } from "node:path";
|
|
44684
45221
|
function runsDirectory() {
|
|
44685
45222
|
return join36(runtimeDirectory(), "runs");
|
|
@@ -44703,7 +45240,7 @@ function writeLatestRun(servedBy, latest) {
|
|
|
44703
45240
|
}
|
|
44704
45241
|
function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
44705
45242
|
try {
|
|
44706
|
-
const fields = JSON.parse(
|
|
45243
|
+
const fields = JSON.parse(readFileSync31(latestRunPath(servedBy, directory), "utf8"));
|
|
44707
45244
|
if (typeof fields !== "object" || fields === null) {
|
|
44708
45245
|
return null;
|
|
44709
45246
|
}
|
|
@@ -44752,7 +45289,7 @@ function readEditorRunNote(projectPath) {
|
|
|
44752
45289
|
function editorNoteAt(path) {
|
|
44753
45290
|
let parsed;
|
|
44754
45291
|
try {
|
|
44755
|
-
parsed = JSON.parse(
|
|
45292
|
+
parsed = JSON.parse(readFileSync31(path, "utf8"));
|
|
44756
45293
|
} catch {
|
|
44757
45294
|
return null;
|
|
44758
45295
|
}
|
|
@@ -44802,7 +45339,7 @@ function everyRunRecord() {
|
|
|
44802
45339
|
function recordAt(path) {
|
|
44803
45340
|
let parsed;
|
|
44804
45341
|
try {
|
|
44805
|
-
parsed = JSON.parse(
|
|
45342
|
+
parsed = JSON.parse(readFileSync31(path, "utf8"));
|
|
44806
45343
|
} catch {
|
|
44807
45344
|
return null;
|
|
44808
45345
|
}
|
|
@@ -44861,7 +45398,7 @@ async function runningAs(pid) {
|
|
|
44861
45398
|
const began = await startedAt(pid, null);
|
|
44862
45399
|
const when = began === null ? {} : { startedAt: began };
|
|
44863
45400
|
if (process.platform === "linux") {
|
|
44864
|
-
const raw =
|
|
45401
|
+
const raw = readFileSync31(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
44865
45402
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
44866
45403
|
}
|
|
44867
45404
|
const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
|
|
@@ -45049,10 +45586,10 @@ var init_run_record = __esm(() => {
|
|
|
45049
45586
|
});
|
|
45050
45587
|
|
|
45051
45588
|
// src/script-profile.ts
|
|
45052
|
-
import { readFileSync as
|
|
45589
|
+
import { readFileSync as readFileSync32 } from "node:fs";
|
|
45053
45590
|
function readProfileFile(path) {
|
|
45054
45591
|
try {
|
|
45055
|
-
const parsed = JSON.parse(
|
|
45592
|
+
const parsed = JSON.parse(readFileSync32(path, "utf8"));
|
|
45056
45593
|
return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
|
|
45057
45594
|
} catch {
|
|
45058
45595
|
return null;
|
|
@@ -45109,7 +45646,7 @@ function writeProfileMark(path, mark) {
|
|
|
45109
45646
|
}
|
|
45110
45647
|
function readProfileMark(path) {
|
|
45111
45648
|
try {
|
|
45112
|
-
const parsed = JSON.parse(
|
|
45649
|
+
const parsed = JSON.parse(readFileSync32(path, "utf8"));
|
|
45113
45650
|
return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
|
|
45114
45651
|
} catch {
|
|
45115
45652
|
return null;
|
|
@@ -45251,8 +45788,9 @@ function callersOf(totals, name, scripts) {
|
|
|
45251
45788
|
property
|
|
45252
45789
|
};
|
|
45253
45790
|
}
|
|
45254
|
-
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler",
|
|
45791
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
|
|
45255
45792
|
var init_script_profile = __esm(() => {
|
|
45793
|
+
init_game_debugger();
|
|
45256
45794
|
init_gdscript_source();
|
|
45257
45795
|
init_godot_variant();
|
|
45258
45796
|
init_resources();
|
|
@@ -45276,10 +45814,10 @@ var init_server_instructions = __esm(() => {
|
|
|
45276
45814
|
});
|
|
45277
45815
|
|
|
45278
45816
|
// src/test-selection.ts
|
|
45279
|
-
import { existsSync as existsSync29, readFileSync as
|
|
45817
|
+
import { existsSync as existsSync29, readFileSync as readFileSync33 } from "node:fs";
|
|
45280
45818
|
import { basename as basename11, join as join37 } from "node:path";
|
|
45281
45819
|
function casesIn(projectPath, suite) {
|
|
45282
|
-
const text =
|
|
45820
|
+
const text = readFileSync33(join37(projectPath, suite.slice("res://".length)), "utf8");
|
|
45283
45821
|
return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
|
|
45284
45822
|
}
|
|
45285
45823
|
function suitesUnder(projectPath, asked) {
|
|
@@ -45351,7 +45889,7 @@ import {
|
|
|
45351
45889
|
mkdirSync as mkdirSync20,
|
|
45352
45890
|
openSync as openSync6,
|
|
45353
45891
|
readdirSync as readdirSync17,
|
|
45354
|
-
readFileSync as
|
|
45892
|
+
readFileSync as readFileSync34,
|
|
45355
45893
|
readSync as readSync3,
|
|
45356
45894
|
rmdirSync as rmdirSync2,
|
|
45357
45895
|
statSync as statSync18,
|
|
@@ -45370,6 +45908,25 @@ function waitFromEnv(variable, fallback) {
|
|
|
45370
45908
|
function greetingWaitMs() {
|
|
45371
45909
|
return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
|
|
45372
45910
|
}
|
|
45911
|
+
function pictureToSend(pngPath, jpegPath) {
|
|
45912
|
+
const png = readFileSync34(pngPath);
|
|
45913
|
+
const beside = pngPath.replace(/\.png$/, ".jpg");
|
|
45914
|
+
const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync30(beside) ? readFileSync34(beside) : null;
|
|
45915
|
+
const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
|
|
45916
|
+
return {
|
|
45917
|
+
data: sent.toString("base64"),
|
|
45918
|
+
mimeType: sent === png ? "image/png" : "image/jpeg",
|
|
45919
|
+
bytes: sent.length,
|
|
45920
|
+
pngBytes: png.length
|
|
45921
|
+
};
|
|
45922
|
+
}
|
|
45923
|
+
function kilobytes(bytes) {
|
|
45924
|
+
return `${String(Math.max(1, Math.round(bytes / 1024)))} KB`;
|
|
45925
|
+
}
|
|
45926
|
+
function savedScenesOf(asked, as = "savedScenes") {
|
|
45927
|
+
const saved = readStringArray(asParams(JSON.parse(asked.content[0]?.text ?? "{}")), "saved");
|
|
45928
|
+
return saved === undefined ? {} : { [as]: saved };
|
|
45929
|
+
}
|
|
45373
45930
|
function restartUnderway(note, now = Date.now()) {
|
|
45374
45931
|
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
45375
45932
|
}
|
|
@@ -45787,7 +46344,7 @@ function endedToStartThis(ended) {
|
|
|
45787
46344
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
45788
46345
|
}
|
|
45789
46346
|
function lineBeyondScript(absolutePath, relativePath, line) {
|
|
45790
|
-
const text =
|
|
46347
|
+
const text = readFileSync34(absolutePath, "utf8");
|
|
45791
46348
|
const lines = text === "" ? 0 : text.split(`
|
|
45792
46349
|
`).length - (text.endsWith(`
|
|
45793
46350
|
`) ? 1 : 0);
|
|
@@ -45987,7 +46544,7 @@ function notRunNote(notRun, failFast) {
|
|
|
45987
46544
|
function exportPresetRefusal(projectPath, preset) {
|
|
45988
46545
|
let text;
|
|
45989
46546
|
try {
|
|
45990
|
-
text =
|
|
46547
|
+
text = readFileSync34(join38(projectPath, "export_presets.cfg"), "utf8");
|
|
45991
46548
|
} catch {
|
|
45992
46549
|
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
|
|
45993
46550
|
}
|
|
@@ -46350,7 +46907,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
46350
46907
|
let projectGodot = null;
|
|
46351
46908
|
if (projectPath !== null) {
|
|
46352
46909
|
try {
|
|
46353
|
-
projectGodot =
|
|
46910
|
+
projectGodot = readFileSync34(join38(projectPath, "project.godot"), "utf8");
|
|
46354
46911
|
} catch {}
|
|
46355
46912
|
}
|
|
46356
46913
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -46374,7 +46931,7 @@ function projectSourceOf(projectPath) {
|
|
|
46374
46931
|
let source = null;
|
|
46375
46932
|
if (inside.ok) {
|
|
46376
46933
|
try {
|
|
46377
|
-
source =
|
|
46934
|
+
source = readFileSync34(inside.absolutePath, "utf8");
|
|
46378
46935
|
} catch {}
|
|
46379
46936
|
}
|
|
46380
46937
|
read.set(script, source);
|
|
@@ -46529,7 +47086,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
46529
47086
|
}
|
|
46530
47087
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
46531
47088
|
try {
|
|
46532
|
-
return settingKeys(
|
|
47089
|
+
return settingKeys(readFileSync34(join38(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
46533
47090
|
} catch {
|
|
46534
47091
|
return true;
|
|
46535
47092
|
}
|
|
@@ -46558,12 +47115,13 @@ function elsewhereIn(projectPath, asked) {
|
|
|
46558
47115
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
46559
47116
|
}
|
|
46560
47117
|
function testPathsIn(projectPath, given) {
|
|
46561
|
-
const
|
|
47118
|
+
const defaults = TEST_DIRECTORIES.filter((directory) => existsSync30(join38(projectPath, directory)));
|
|
47119
|
+
const entries = given === undefined || given === null || given === "" ? defaults.length > 0 ? defaults : TEST_DIRECTORIES : given;
|
|
46562
47120
|
const listed = Array.isArray(entries) ? entries : [entries];
|
|
46563
47121
|
if (listed.length === 0) {
|
|
46564
47122
|
return {
|
|
46565
47123
|
ok: false,
|
|
46566
|
-
reason: "path is an empty list: name a suite file or a directory, or leave path out for test."
|
|
47124
|
+
reason: "path is an empty list: name a suite file or a directory, or leave path out for tests and test."
|
|
46567
47125
|
};
|
|
46568
47126
|
}
|
|
46569
47127
|
const resolved = [];
|
|
@@ -46665,11 +47223,11 @@ function scriptsUnder(directory, prefix) {
|
|
|
46665
47223
|
return found;
|
|
46666
47224
|
}
|
|
46667
47225
|
function mainSceneOf(projectFile) {
|
|
46668
|
-
const scene = parseProjectGodot(
|
|
47226
|
+
const scene = parseProjectGodot(readFileSync34(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
46669
47227
|
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
46670
47228
|
}
|
|
46671
47229
|
function editorPlaysHeadless(projectFile) {
|
|
46672
|
-
const runArgs = parseProjectGodot(
|
|
47230
|
+
const runArgs = parseProjectGodot(readFileSync34(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
46673
47231
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
46674
47232
|
}
|
|
46675
47233
|
function ownArguments(params) {
|
|
@@ -48496,7 +49054,7 @@ class GodotServer {
|
|
|
48496
49054
|
if (!engine.ok) {
|
|
48497
49055
|
return engine.response;
|
|
48498
49056
|
}
|
|
48499
|
-
const application = parseProjectGodot(
|
|
49057
|
+
const application = parseProjectGodot(readFileSync34(project.value.file, "utf8"))["application"] ?? {};
|
|
48500
49058
|
const name = application["config/name"];
|
|
48501
49059
|
const mainScene = application["run/main_scene"];
|
|
48502
49060
|
const info = {
|
|
@@ -48767,6 +49325,8 @@ class GodotServer {
|
|
|
48767
49325
|
const userData = scratchDirectory("tests");
|
|
48768
49326
|
const suitesFile = join38(userData, "suites.txt");
|
|
48769
49327
|
const skippingFile = join38(userData, "skipping.txt");
|
|
49328
|
+
const engineLog = join38(userData, "engine.log");
|
|
49329
|
+
const expectedFile = join38(userData, "expected.json");
|
|
48770
49330
|
writeFileSync19(suitesFile, `${(selection.value?.suites ?? asked).join(`
|
|
48771
49331
|
`)}
|
|
48772
49332
|
`);
|
|
@@ -48778,6 +49338,8 @@ class GodotServer {
|
|
|
48778
49338
|
"--path",
|
|
48779
49339
|
project.value.path,
|
|
48780
49340
|
...flags.value,
|
|
49341
|
+
"--log-file",
|
|
49342
|
+
engineLog,
|
|
48781
49343
|
"-s",
|
|
48782
49344
|
this.listedRunner,
|
|
48783
49345
|
"--ignoreHeadlessMode",
|
|
@@ -48791,7 +49353,9 @@ class GodotServer {
|
|
|
48791
49353
|
suitesFile,
|
|
48792
49354
|
"--skipping-in",
|
|
48793
49355
|
skippingFile,
|
|
48794
|
-
"--html-report-off"
|
|
49356
|
+
"--html-report-off",
|
|
49357
|
+
"--expected-errors-out",
|
|
49358
|
+
expectedFile
|
|
48795
49359
|
];
|
|
48796
49360
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
48797
49361
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
@@ -48846,11 +49410,13 @@ class GodotServer {
|
|
|
48846
49410
|
const reportsDir = join38(project.value.path, ".godot", ...ours.split("/"));
|
|
48847
49411
|
let report = null;
|
|
48848
49412
|
let reportProblem = null;
|
|
49413
|
+
const casePrinted = readRun(existsSync30(engineLog) ? readFileSync34(engineLog, "utf8") : "");
|
|
49414
|
+
const declaredErrors = readExpected(expectedFile);
|
|
48849
49415
|
try {
|
|
48850
49416
|
const written = existsSync30(reportsDir) && notCompiled === null ? readdirSync17(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
48851
49417
|
const newest = written.at(-1);
|
|
48852
49418
|
if (newest !== undefined) {
|
|
48853
|
-
report = parseJUnit(
|
|
49419
|
+
report = parseJUnit(readFileSync34(join38(reportsDir, newest, "results.xml"), "utf8"));
|
|
48854
49420
|
}
|
|
48855
49421
|
} catch (error) {
|
|
48856
49422
|
reportProblem = errorMessage(error);
|
|
@@ -48961,7 +49527,44 @@ class GodotServer {
|
|
|
48961
49527
|
orphans: suite.orphans,
|
|
48962
49528
|
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
48963
49529
|
}));
|
|
48964
|
-
const
|
|
49530
|
+
const judgedErrors = unexpectedErrors(casePrinted, declaredErrors, [...casePrinted.suites.keys()]);
|
|
49531
|
+
const uncaughtTexts = new Set(uncaught.map((entry) => entry.text));
|
|
49532
|
+
const undeclaredPrinted = judgedErrors.printed.filter((error) => !uncaughtTexts.has(error.text));
|
|
49533
|
+
const undeclaredFolded = new Map;
|
|
49534
|
+
for (const error of undeclaredPrinted) {
|
|
49535
|
+
const key = `${error.suite ?? ""}
|
|
49536
|
+
${error.during ?? ""}
|
|
49537
|
+
${error.text}`;
|
|
49538
|
+
const seen = undeclaredFolded.get(key);
|
|
49539
|
+
if (seen === undefined) {
|
|
49540
|
+
undeclaredFolded.set(key, {
|
|
49541
|
+
suite: error.suite ?? undefined,
|
|
49542
|
+
case: error.during ?? undefined,
|
|
49543
|
+
text: error.text,
|
|
49544
|
+
times: 1
|
|
49545
|
+
});
|
|
49546
|
+
} else {
|
|
49547
|
+
seen.times += 1;
|
|
49548
|
+
}
|
|
49549
|
+
}
|
|
49550
|
+
const undeclaredAll = [...undeclaredFolded.values()].map((error) => ({
|
|
49551
|
+
...error,
|
|
49552
|
+
times: error.times > 1 ? error.times : undefined
|
|
49553
|
+
}));
|
|
49554
|
+
const undeclaredErrors = undeclaredAll.slice(0, MOST_UNDECLARED_ERRORS);
|
|
49555
|
+
const declaredNotPrinted = judgedErrors.neverPrinted.map((missing) => ({
|
|
49556
|
+
suite: missing.suite,
|
|
49557
|
+
case: missing.during,
|
|
49558
|
+
text: missing.text
|
|
49559
|
+
}));
|
|
49560
|
+
const erring = (path) => ({
|
|
49561
|
+
undeclared: undeclaredPrinted.filter((error) => error.suite === path).length,
|
|
49562
|
+
notPrinted: judgedErrors.neverPrinted.filter((missing) => missing.suite === path).length
|
|
49563
|
+
});
|
|
49564
|
+
const declaredAndPrinted = casePrinted.errors.length - judgedErrors.printed.length;
|
|
49565
|
+
const ranIn = (suite) => suite.path === null ? suite.tests : casePrinted.suites.get(suite.path)?.cases.length ?? suite.tests;
|
|
49566
|
+
const notRunIn = (suite) => Math.max(0, suite.discovered - ranIn(suite));
|
|
49567
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.hookFailures > 0 || suite.hookErrors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0 || erring(suite.path).undeclared + erring(suite.path).notPrinted > 0);
|
|
48965
49568
|
const hooksPrinted = hookFailuresPrinted(said);
|
|
48966
49569
|
const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
|
|
48967
49570
|
const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
|
|
@@ -48973,7 +49576,7 @@ class GodotServer {
|
|
|
48973
49576
|
}
|
|
48974
49577
|
];
|
|
48975
49578
|
});
|
|
48976
|
-
const notRun = report.suites.reduce((sum, suite) => sum +
|
|
49579
|
+
const notRun = report.suites.reduce((sum, suite) => sum + notRunIn(suite), 0);
|
|
48977
49580
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
48978
49581
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
48979
49582
|
const notApplied = ignoredYetRan(report, ignored);
|
|
@@ -48981,8 +49584,13 @@ class GodotServer {
|
|
|
48981
49584
|
...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename12(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename12(one.suite, ".gd")}:${one.test}`))
|
|
48982
49585
|
];
|
|
48983
49586
|
return this.jsonTextResponse({
|
|
48984
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0 && testsNotRun.length === 0,
|
|
48985
|
-
verdict:
|
|
49587
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && missing.length === 0 && testsNotRun.length === 0,
|
|
49588
|
+
verdict: [
|
|
49589
|
+
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length > 0 ? undefined : verdict,
|
|
49590
|
+
undeclaredPrinted.length > 0 ? `${undeclaredPrinted.length === 1 ? "one engine error" : `${String(undeclaredPrinted.length)} engine errors`} no suite declared` : undefined,
|
|
49591
|
+
declaredNotPrinted.length > 0 ? `${declaredNotPrinted.length === 1 ? "one declared error" : `${String(declaredNotPrinted.length)} declared errors`} never printed` : undefined,
|
|
49592
|
+
testsNotRun.length > 0 ? `not run: ${testsNotRun.join(", ")}` : undefined
|
|
49593
|
+
].filter((part) => part !== undefined).join("; "),
|
|
48986
49594
|
...missing.length > 0 ? { missing } : {},
|
|
48987
49595
|
...testsNotRun.length > 0 ? { testsNotRun } : {},
|
|
48988
49596
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
@@ -48998,6 +49606,9 @@ class GodotServer {
|
|
|
48998
49606
|
failedWithoutDetail: briefed > 0 ? briefed : undefined,
|
|
48999
49607
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
49000
49608
|
uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
|
|
49609
|
+
undeclaredErrors: undeclaredErrors.length > 0 ? undeclaredErrors : undefined,
|
|
49610
|
+
undeclaredErrorsOmitted: undeclaredAll.length > undeclaredErrors.length ? undeclaredAll.length - undeclaredErrors.length : undefined,
|
|
49611
|
+
declaredNotPrinted: declaredNotPrinted.length > 0 ? declaredNotPrinted : undefined,
|
|
49001
49612
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
49002
49613
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
49003
49614
|
notRun: notRun > 0 ? notRun : undefined,
|
|
@@ -49011,13 +49622,17 @@ class GodotServer {
|
|
|
49011
49622
|
briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
|
|
49012
49623
|
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
49013
49624
|
uncaughtNote(uncaught),
|
|
49625
|
+
undeclaredErrors.length > 0 ? `An engine error a suite does not declare fails the run, as gdharness test judges it; ${declaringAdvice(undeclaredErrors[0]?.case ?? "test_name")}, once for each time it prints it.` : undefined,
|
|
49626
|
+
declaredNotPrinted.length > 0 ? "A declared error the case did not print fails the run too, since the case no longer reaches what it was written to test." : undefined,
|
|
49014
49627
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
49015
49628
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
49016
49629
|
suites: unclean.map((suite) => ({
|
|
49017
49630
|
name: suite.name,
|
|
49018
49631
|
path: suite.path,
|
|
49019
|
-
tests: suite
|
|
49020
|
-
notRun: suite
|
|
49632
|
+
tests: ranIn(suite),
|
|
49633
|
+
notRun: notRunIn(suite) > 0 ? notRunIn(suite) : undefined,
|
|
49634
|
+
undeclaredErrors: erring(suite.path).undeclared > 0 ? erring(suite.path).undeclared : undefined,
|
|
49635
|
+
declaredNotPrinted: erring(suite.path).notPrinted > 0 ? erring(suite.path).notPrinted : undefined,
|
|
49021
49636
|
failures: suite.failures,
|
|
49022
49637
|
errors: suite.errors,
|
|
49023
49638
|
hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
|
|
@@ -49026,7 +49641,7 @@ class GodotServer {
|
|
|
49026
49641
|
time: suite.time
|
|
49027
49642
|
})),
|
|
49028
49643
|
suitesPassed: report.suites.length - unclean.length,
|
|
49029
|
-
engineErrors: run.log.count("error"),
|
|
49644
|
+
engineErrors: Math.max(0, run.log.count("error") - declaredAndPrinted),
|
|
49030
49645
|
engineWarnings: run.log.count("warning"),
|
|
49031
49646
|
engineEntries: engineEntriesShown.map((entry) => listedAsUncaught.has(entry.index) ? { ...entry, detail: undefined, detailUnder: "uncaughtScriptErrors" } : entry),
|
|
49032
49647
|
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
@@ -49242,7 +49857,7 @@ class GodotServer {
|
|
|
49242
49857
|
return null;
|
|
49243
49858
|
}
|
|
49244
49859
|
try {
|
|
49245
|
-
return
|
|
49860
|
+
return readFileSync34(contained.absolutePath, "utf8");
|
|
49246
49861
|
} catch {
|
|
49247
49862
|
return null;
|
|
49248
49863
|
}
|
|
@@ -49336,7 +49951,7 @@ class GodotServer {
|
|
|
49336
49951
|
return null;
|
|
49337
49952
|
}
|
|
49338
49953
|
try {
|
|
49339
|
-
return
|
|
49954
|
+
return readFileSync34(contained.absolutePath, "utf8");
|
|
49340
49955
|
} catch {
|
|
49341
49956
|
return null;
|
|
49342
49957
|
}
|
|
@@ -49772,8 +50387,9 @@ class GodotServer {
|
|
|
49772
50387
|
if (servers.dapListening !== false && servers.lspListening !== false) {
|
|
49773
50388
|
return { servers, report: {} };
|
|
49774
50389
|
}
|
|
49775
|
-
const { rebound,
|
|
50390
|
+
const { rebound, before, since } = await this.editorServersSaid(servers.dapListening === false);
|
|
49776
50391
|
const after = rebound ? await this.editorServersListening() : servers;
|
|
50392
|
+
const said = [...before, ...since];
|
|
49777
50393
|
return {
|
|
49778
50394
|
servers: after,
|
|
49779
50395
|
report: {
|
|
@@ -49791,17 +50407,18 @@ class GodotServer {
|
|
|
49791
50407
|
}
|
|
49792
50408
|
return {
|
|
49793
50409
|
rebound: readBoolean(answer, "rebound") === true,
|
|
49794
|
-
|
|
50410
|
+
before: readStringArray(answer, "said") ?? [],
|
|
50411
|
+
since: readStringArray(answer, "saidSince") ?? []
|
|
49795
50412
|
};
|
|
49796
50413
|
}
|
|
49797
50414
|
async handleRebindEditorServers() {
|
|
49798
50415
|
if (!this.godotBridge.getStatus().connected) {
|
|
49799
|
-
|
|
49800
|
-
|
|
49801
|
-
|
|
49802
|
-
|
|
50416
|
+
const refused = await this.noEditorYet({}, []);
|
|
50417
|
+
if (refused !== null) {
|
|
50418
|
+
return refused;
|
|
50419
|
+
}
|
|
49803
50420
|
}
|
|
49804
|
-
const { rebound, said } = await this.editorServersSaid(true);
|
|
50421
|
+
const { rebound, since: said } = await this.editorServersSaid(true);
|
|
49805
50422
|
if (!rebound) {
|
|
49806
50423
|
return this.createErrorResponse("The editor has no debug adapter to bind again: no DebugAdapterServer is in its tree.", ["editor_status says what the editor serves"]);
|
|
49807
50424
|
}
|
|
@@ -49817,7 +50434,7 @@ class GodotServer {
|
|
|
49817
50434
|
return null;
|
|
49818
50435
|
}
|
|
49819
50436
|
try {
|
|
49820
|
-
return settingKeys(
|
|
50437
|
+
return settingKeys(readFileSync34(join38(projectPath, "project.godot"), "utf8"));
|
|
49821
50438
|
} catch {
|
|
49822
50439
|
return null;
|
|
49823
50440
|
}
|
|
@@ -49865,6 +50482,7 @@ class GodotServer {
|
|
|
49865
50482
|
serverVersion: SERVER_VERSION,
|
|
49866
50483
|
addonIsStale: this.godotBridge.anotherRelease() !== undefined,
|
|
49867
50484
|
staleNote: this.godotBridge.anotherRelease(),
|
|
50485
|
+
...savedScenesOf(asked),
|
|
49868
50486
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
49869
50487
|
...servers,
|
|
49870
50488
|
...rebinding,
|
|
@@ -49908,6 +50526,7 @@ class GodotServer {
|
|
|
49908
50526
|
closed: true,
|
|
49909
50527
|
editorPid: pid,
|
|
49910
50528
|
projectPath: before.projectPath,
|
|
50529
|
+
...savedScenesOf(asked),
|
|
49911
50530
|
...this.whatTheEditorDropped(settingsBefore, this.settingKeysOf(before.projectPath)),
|
|
49912
50531
|
tookMs: Date.now() - began
|
|
49913
50532
|
});
|
|
@@ -49991,7 +50610,7 @@ class GodotServer {
|
|
|
49991
50610
|
if (opened.error !== null) {
|
|
49992
50611
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
49993
50612
|
}
|
|
49994
|
-
return this.jsonTextResponse({ restarting: true, pid: opened.pid });
|
|
50613
|
+
return this.jsonTextResponse({ restarting: true, pid: opened.pid, ...savedScenesOf(asked, "saved") });
|
|
49995
50614
|
}
|
|
49996
50615
|
async waitForBridge(reached, deadline) {
|
|
49997
50616
|
for (;; ) {
|
|
@@ -50050,7 +50669,7 @@ class GodotServer {
|
|
|
50050
50669
|
dapPort: ports.dap,
|
|
50051
50670
|
arguments: opened.arguments,
|
|
50052
50671
|
finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
|
|
50053
|
-
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
50672
|
+
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected, and leaves settingsDropped out when nothing was."
|
|
50054
50673
|
});
|
|
50055
50674
|
}
|
|
50056
50675
|
async editorsNotYetConnected(projectPath) {
|
|
@@ -50542,7 +51161,7 @@ class GodotServer {
|
|
|
50542
51161
|
return 0;
|
|
50543
51162
|
}
|
|
50544
51163
|
try {
|
|
50545
|
-
return reportAlreadyInTranscript(
|
|
51164
|
+
return reportAlreadyInTranscript(readFileSync34(game.transcript), readFileSync34(report));
|
|
50546
51165
|
} catch (error) {
|
|
50547
51166
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
50548
51167
|
return 0;
|
|
@@ -52627,7 +53246,7 @@ class GodotServer {
|
|
|
52627
53246
|
}
|
|
52628
53247
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52629
53248
|
const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
|
|
52630
|
-
if (declared.test(
|
|
53249
|
+
if (declared.test(readFileSync34(scriptPath, "utf8"))) {
|
|
52631
53250
|
return null;
|
|
52632
53251
|
}
|
|
52633
53252
|
const checked = await this.classesTheEditorCannotSee(args);
|
|
@@ -52692,6 +53311,10 @@ class GodotServer {
|
|
|
52692
53311
|
"editor_launch open starts another"
|
|
52693
53312
|
]);
|
|
52694
53313
|
}
|
|
53314
|
+
const closed = this.closedHere;
|
|
53315
|
+
if (closed !== null && (project === null || (await this.editorsNotYetConnected(project) ?? []).length === 0)) {
|
|
53316
|
+
return this.createErrorResponse(`Editor not connected: this server closed the editor, pid ${closed.editorPid}, on request at ${closed.at}, so nothing is coming until one is opened.`, [...instead, "editor_launch open starts one on the project"]);
|
|
53317
|
+
}
|
|
52695
53318
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
52696
53319
|
...instead,
|
|
52697
53320
|
"Open the project in the editor with editor_launch, or by hand",
|
|
@@ -52840,7 +53463,8 @@ class GodotServer {
|
|
|
52840
53463
|
}
|
|
52841
53464
|
async handleRuntimeCommand(command, args, asked) {
|
|
52842
53465
|
const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
|
|
52843
|
-
const
|
|
53466
|
+
const callArgs = asParams(args);
|
|
53467
|
+
const { op: _op, projectPath, pid, outputPath, inline: _inline, ...params } = callArgs;
|
|
52844
53468
|
const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
|
|
52845
53469
|
if (waited !== null) {
|
|
52846
53470
|
return this.createErrorResponse(waited, [
|
|
@@ -52873,6 +53497,13 @@ class GodotServer {
|
|
|
52873
53497
|
}
|
|
52874
53498
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
52875
53499
|
const saveTo = expectsScreenshot && typeof outputPath === "string" ? outputPath : null;
|
|
53500
|
+
const sendsThePicture = readBoolean(callArgs, "inline") !== false;
|
|
53501
|
+
if (expectsScreenshot && !sendsThePicture && saveTo === null) {
|
|
53502
|
+
return this.createErrorResponse("inline: false sends no picture back, so it needs outputPath to save one to; without it the capture would answer nothing to look at.", [
|
|
53503
|
+
"Pass outputPath, an absolute path ending in .png outside the project",
|
|
53504
|
+
"Or leave inline out to have the picture sent"
|
|
53505
|
+
]);
|
|
53506
|
+
}
|
|
52876
53507
|
if (saveTo !== null) {
|
|
52877
53508
|
const refusal = captureDestinationRefusal(saveTo, [
|
|
52878
53509
|
choice.endpoint.project.path,
|
|
@@ -52940,15 +53571,17 @@ class GodotServer {
|
|
|
52940
53571
|
}
|
|
52941
53572
|
}
|
|
52942
53573
|
const ranWith = [...onGame.alongside];
|
|
53574
|
+
const sent = sendsThePicture ? pictureToSend(screenshotPath, readString(payload, "jpegPath")) : null;
|
|
53575
|
+
const how = sent === null ? ". Not sent, as inline: false asked" : sent.mimeType === "image/jpeg" ? `. Sent as JPEG, ${kilobytes(sent.bytes)} against the PNG's ${kilobytes(sent.pngBytes)}${saveTo === null ? "" : "; the saved PNG is lossless"}` : "";
|
|
52943
53576
|
return {
|
|
52944
53577
|
content: [
|
|
52945
53578
|
{
|
|
52946
53579
|
type: "text",
|
|
52947
|
-
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
|
|
53580
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${how}${ranWith.length === 0 ? "" : `
|
|
52948
53581
|
|
|
52949
53582
|
Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
|
|
52950
53583
|
},
|
|
52951
|
-
{ type: "image", data:
|
|
53584
|
+
...sent === null ? [] : [{ type: "image", data: sent.data, mimeType: sent.mimeType }]
|
|
52952
53585
|
]
|
|
52953
53586
|
};
|
|
52954
53587
|
} finally {
|
|
@@ -53065,7 +53698,7 @@ async function runGodotServer() {
|
|
|
53065
53698
|
const server = new GodotServer;
|
|
53066
53699
|
await server.run();
|
|
53067
53700
|
}
|
|
53068
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
53701
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
53069
53702
|
var init_server = __esm(() => {
|
|
53070
53703
|
init_dist();
|
|
53071
53704
|
init_stdio();
|
|
@@ -53123,6 +53756,8 @@ var init_server = __esm(() => {
|
|
|
53123
53756
|
init_server_version();
|
|
53124
53757
|
init_setup();
|
|
53125
53758
|
init_spawn();
|
|
53759
|
+
init_suite_judge();
|
|
53760
|
+
init_suite_plan();
|
|
53126
53761
|
init_test_selection();
|
|
53127
53762
|
init_tool_definitions();
|
|
53128
53763
|
init_tool_reference();
|
|
@@ -54298,6 +54933,11 @@ engines would otherwise announce sixteen games that are not games. Set \`${SCRIP
|
|
|
54298
54933
|
true in \`project.godot\` to serve one anyway, which is for driving a \`-s\` script on purpose. Read
|
|
54299
54934
|
that setting rather than assuming the quiet: it is a default and not a property of script runs.
|
|
54300
54935
|
|
|
54936
|
+
A scene that is not the game, such as a bench, is run as a scene and is served like the game unless
|
|
54937
|
+
the project lists its folder in \`${QUIET_SCENES_SETTING}\`, a \`PackedStringArray\` of \`res://\`
|
|
54938
|
+
prefixes, empty by default. A run started on a scene under one of them neither serves nor
|
|
54939
|
+
announces, nor do the engines it starts, unless it was started to be profiled from its start.
|
|
54940
|
+
|
|
54301
54941
|
The autoload reaches an export. \`gdharness runtime off\` in the project before shipping.
|
|
54302
54942
|
|
|
54303
54943
|
## At a breakpoint
|
|
@@ -54490,13 +55130,13 @@ import {
|
|
|
54490
55130
|
mkdirSync as mkdirSync8,
|
|
54491
55131
|
openSync as openSync3,
|
|
54492
55132
|
readdirSync as readdirSync11,
|
|
54493
|
-
readFileSync as
|
|
55133
|
+
readFileSync as readFileSync18,
|
|
54494
55134
|
readSync,
|
|
54495
55135
|
rmSync as rmSync10,
|
|
54496
55136
|
statSync as statSync11,
|
|
54497
55137
|
writeFileSync as writeFileSync10
|
|
54498
55138
|
} from "node:fs";
|
|
54499
|
-
import { availableParallelism, freemem } from "node:os";
|
|
55139
|
+
import { availableParallelism, freemem, totalmem } from "node:os";
|
|
54500
55140
|
import { isAbsolute as isAbsolute4, join as join21, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
|
|
54501
55141
|
import process12 from "node:process";
|
|
54502
55142
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
@@ -54785,516 +55425,12 @@ function extensionsOf(projectPath) {
|
|
|
54785
55425
|
`).map((line) => line.trim()).filter((line) => line.startsWith("res://")) : [];
|
|
54786
55426
|
}
|
|
54787
55427
|
|
|
54788
|
-
// src/suite-
|
|
54789
|
-
|
|
54790
|
-
|
|
54791
|
-
var SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
|
|
54792
|
-
var TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
|
|
54793
|
-
var CASE_LINE = new RegExp(String.raw`^\s*(res://.+?\.gd) > (.+?) (STARTED|PASSED|FAILED|FLAKY|SKIPPED|WARNING)(?: \((?:retry \d+|\d+ retries)\))?(?: (${TIME}))?\s*$`);
|
|
54794
|
-
var STATISTICS = new RegExp(String.raw`^Statistics:.*\|\s*([A-Z]+)(?:\s+\(.*?\))?\s+(${TIME})\s*$`);
|
|
54795
|
-
var ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
|
|
54796
|
-
var EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
|
|
54797
|
-
var DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
|
|
54798
|
-
var LEAK = /ObjectDB instances (?:were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
|
|
54799
|
-
var STARVED = [
|
|
54800
|
-
/Parameter "mem(?:_new)?" is null\.?\s*\n\s*at: \w+ \((?:\.\/)?core\/(?:os\/memory\.cpp|templates\/cowdata\.h):/g,
|
|
54801
|
-
/ERROR: Failed decoding WebP image\.\s*\n\s*at: _webp_unpack/g
|
|
54802
|
-
];
|
|
54803
|
-
function secondsOf(time) {
|
|
54804
|
-
let ms = 0;
|
|
54805
|
-
for (const [, amount, unit] of time.matchAll(/(\d+)(h|min|ms|s)\b/g)) {
|
|
54806
|
-
ms += Number(amount) * (unit === "h" ? 3600000 : unit === "min" ? 60000 : unit === "s" ? 1000 : 1);
|
|
54807
|
-
}
|
|
54808
|
-
return ms / 1000;
|
|
54809
|
-
}
|
|
54810
|
-
function funcOf(name) {
|
|
54811
|
-
const colon = name.indexOf(":");
|
|
54812
|
-
return colon < 0 ? name : name.slice(0, colon);
|
|
54813
|
-
}
|
|
54814
|
-
function readRun(printed) {
|
|
54815
|
-
const lines = stripVTControlCharacters(printed).replaceAll(`\r
|
|
54816
|
-
`, `
|
|
54817
|
-
`).split(`
|
|
54818
|
-
`);
|
|
54819
|
-
const suites = new Map;
|
|
54820
|
-
const errors = [];
|
|
54821
|
-
const leaks = [];
|
|
54822
|
-
let executed = null;
|
|
54823
|
-
let open = null;
|
|
54824
|
-
let during = null;
|
|
54825
|
-
let afterCase = [];
|
|
54826
|
-
let lastCase = null;
|
|
54827
|
-
const settleAfterCase = (owner) => {
|
|
54828
|
-
for (const held of afterCase) {
|
|
54829
|
-
errors.push({ ...held, suite: open?.path ?? null, during: owner });
|
|
54830
|
-
}
|
|
54831
|
-
afterCase = [];
|
|
54832
|
-
};
|
|
54833
|
-
for (const [at, line] of lines.entries()) {
|
|
54834
|
-
const opened = SUITE_OPENED.exec(line);
|
|
54835
|
-
if (opened?.[1] !== undefined) {
|
|
54836
|
-
settleAfterCase("after");
|
|
54837
|
-
open = suites.get(opened[1]) ?? { path: opened[1], cases: [], closed: null };
|
|
54838
|
-
suites.set(open.path, open);
|
|
54839
|
-
during = "before";
|
|
54840
|
-
lastCase = null;
|
|
54841
|
-
continue;
|
|
54842
|
-
}
|
|
54843
|
-
const caseLine = CASE_LINE.exec(line);
|
|
54844
|
-
if (caseLine !== null && open !== null) {
|
|
54845
|
-
const name = caseLine[2] ?? "";
|
|
54846
|
-
if (caseLine[3] === "STARTED") {
|
|
54847
|
-
settleAfterCase(lastCase ?? "before");
|
|
54848
|
-
during = name;
|
|
54849
|
-
} else {
|
|
54850
|
-
open.cases.push({
|
|
54851
|
-
name,
|
|
54852
|
-
func: funcOf(name),
|
|
54853
|
-
verdict: caseLine[3] ?? "",
|
|
54854
|
-
seconds: secondsOf(caseLine[4] ?? ""),
|
|
54855
|
-
at
|
|
54856
|
-
});
|
|
54857
|
-
lastCase = name;
|
|
54858
|
-
during = null;
|
|
54859
|
-
}
|
|
54860
|
-
continue;
|
|
54861
|
-
}
|
|
54862
|
-
const statistics = STATISTICS.exec(line);
|
|
54863
|
-
if (statistics !== null && open !== null) {
|
|
54864
|
-
settleAfterCase("after");
|
|
54865
|
-
open.closed = { verdict: statistics[1] ?? "", seconds: secondsOf(statistics[2] ?? "") };
|
|
54866
|
-
open = null;
|
|
54867
|
-
during = null;
|
|
54868
|
-
lastCase = null;
|
|
54869
|
-
continue;
|
|
54870
|
-
}
|
|
54871
|
-
const counted = EXECUTED.exec(line);
|
|
54872
|
-
if (counted?.[1] !== undefined) {
|
|
54873
|
-
executed = Number(counted[1]);
|
|
54874
|
-
continue;
|
|
54875
|
-
}
|
|
54876
|
-
if (LEAK.test(line)) {
|
|
54877
|
-
leaks.push(line.trim());
|
|
54878
|
-
continue;
|
|
54879
|
-
}
|
|
54880
|
-
if (suites.size === 0 && DRIVER_FALLBACK.test(line)) {
|
|
54881
|
-
errors.length = 0;
|
|
54882
|
-
continue;
|
|
54883
|
-
}
|
|
54884
|
-
const error = ERROR_LINE.exec(line);
|
|
54885
|
-
if (error === null) {
|
|
54886
|
-
continue;
|
|
54887
|
-
}
|
|
54888
|
-
const text = error[1] ?? "";
|
|
54889
|
-
if (open === null) {
|
|
54890
|
-
errors.push({ text, at, suite: null, during: null });
|
|
54891
|
-
} else if (during === null) {
|
|
54892
|
-
afterCase.push({ text, at });
|
|
54893
|
-
} else {
|
|
54894
|
-
errors.push({ text, at, suite: open.path, during });
|
|
54895
|
-
}
|
|
54896
|
-
}
|
|
54897
|
-
const last = open;
|
|
54898
|
-
const stoppedIn = last === null ? null : during === null ? `${last.path}, after ${lastCase ?? "its cases"}` : during === "before" ? `${last.path}, in before` : `${last.path} > ${during}`;
|
|
54899
|
-
settleAfterCase("after");
|
|
54900
|
-
const joined = lines.join(`
|
|
54901
|
-
`);
|
|
54902
|
-
const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
|
|
54903
|
-
const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
|
|
54904
|
-
return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
|
|
54905
|
-
}
|
|
54906
|
-
var WINDOWS_EXITS = new Map([
|
|
54907
|
-
[3221225477, "STATUS_ACCESS_VIOLATION"],
|
|
54908
|
-
[3221225495, "STATUS_NO_MEMORY"],
|
|
54909
|
-
[3221225773, "STATUS_COMMITMENT_LIMIT"],
|
|
54910
|
-
[3221225794, "STATUS_DLL_INIT_FAILED"],
|
|
54911
|
-
[3221226505, "STATUS_STACK_BUFFER_OVERRUN"],
|
|
54912
|
-
[3228369022, "a module that could not be found (VcppException)"],
|
|
54913
|
-
[3228369023, "a procedure that could not be found (VcppException)"]
|
|
54914
|
-
]);
|
|
54915
|
-
var MEMORY_EXITS = new Set([3221225495, 3221225773]);
|
|
54916
|
-
function exitNamed(code) {
|
|
54917
|
-
const unsigned = code >>> 0;
|
|
54918
|
-
const name = WINDOWS_EXITS.get(unsigned);
|
|
54919
|
-
return name === undefined ? String(code) : `0x${unsigned.toString(16).toUpperCase()} (${name})`;
|
|
54920
|
-
}
|
|
54921
|
-
var TAIL = 60;
|
|
54922
|
-
var MOST_NAMED = 8;
|
|
54923
|
-
function named(items) {
|
|
54924
|
-
const shown = items.slice(0, MOST_NAMED).join(", ");
|
|
54925
|
-
return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
|
|
54926
|
-
}
|
|
54927
|
-
function judgeEngine(printed, handed, end) {
|
|
54928
|
-
const faults = [];
|
|
54929
|
-
const failing = new Set;
|
|
54930
|
-
const shownAt = new Set;
|
|
54931
|
-
if (end.ended !== null) {
|
|
54932
|
-
faults.push(end.ended);
|
|
54933
|
-
} else if (end.code !== 0) {
|
|
54934
|
-
faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
|
|
54935
|
-
}
|
|
54936
|
-
if (printed.unloadable.length > 0) {
|
|
54937
|
-
faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
|
|
54938
|
-
}
|
|
54939
|
-
if (printed.executed === null || printed.executed === 0) {
|
|
54940
|
-
faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
|
|
54941
|
-
}
|
|
54942
|
-
for (const { path, cases } of handed.suites) {
|
|
54943
|
-
const suite = printed.suites.get(path);
|
|
54944
|
-
if (suite === undefined) {
|
|
54945
|
-
faults.push(`${path} never started`);
|
|
54946
|
-
failing.add(path);
|
|
54947
|
-
continue;
|
|
54948
|
-
}
|
|
54949
|
-
if (suite.closed === null) {
|
|
54950
|
-
faults.push(`${path} never finished`);
|
|
54951
|
-
failing.add(path);
|
|
54952
|
-
} else if (suite.closed.verdict !== "PASSED") {
|
|
54953
|
-
if (suite.cases.every((ran) => ran.verdict !== "FAILED")) {
|
|
54954
|
-
faults.push(`${path} reported ${suite.closed.verdict} with every case passing, so a hook failed`);
|
|
54955
|
-
}
|
|
54956
|
-
failing.add(path);
|
|
54957
|
-
}
|
|
54958
|
-
const finished = new Set(suite.cases.map((ran) => ran.func));
|
|
54959
|
-
const missing = cases.filter((func) => !finished.has(func));
|
|
54960
|
-
if (missing.length > 0 && suite.closed !== null) {
|
|
54961
|
-
faults.push(`${path} closed without running ${named(missing)}`);
|
|
54962
|
-
failing.add(path);
|
|
54963
|
-
}
|
|
54964
|
-
const wanted = new Set(cases);
|
|
54965
|
-
const unasked = [...finished].filter((func) => !wanted.has(func));
|
|
54966
|
-
if (unasked.length > 0) {
|
|
54967
|
-
faults.push(`${path} ran ${named(unasked)}, which gdharness did not read as cases of it to run here, so its ` + "plan of the cases was wrong");
|
|
54968
|
-
failing.add(path);
|
|
54969
|
-
}
|
|
54970
|
-
for (const ran of suite.cases) {
|
|
54971
|
-
if (ran.verdict === "FAILED" || ran.verdict === "FLAKY") {
|
|
54972
|
-
faults.push(ran.verdict === "FLAKY" ? `${path} > ${ran.name} failed before it passed on a retry` : `${path} > ${ran.name} failed`);
|
|
54973
|
-
failing.add(path);
|
|
54974
|
-
for (let at = ran.at;at < printed.lines.length && (at === ran.at || printed.lines[at]?.trim()); at += 1) {
|
|
54975
|
-
shownAt.add(at);
|
|
54976
|
-
}
|
|
54977
|
-
}
|
|
54978
|
-
}
|
|
54979
|
-
}
|
|
54980
|
-
const unexpected = unexpectedErrors(printed, handed);
|
|
54981
|
-
for (const error of unexpected.printed) {
|
|
54982
|
-
const where = error.suite === null ? "outside every suite" : `in ${error.suite} > ${error.during ?? ""}`;
|
|
54983
|
-
faults.push(`an engine error ${where}: ${error.text}`);
|
|
54984
|
-
if (error.suite !== null) {
|
|
54985
|
-
failing.add(error.suite);
|
|
54986
|
-
}
|
|
54987
|
-
shownAt.add(error.at);
|
|
54988
|
-
shownAt.add(error.at + 1);
|
|
54989
|
-
}
|
|
54990
|
-
for (const missing of unexpected.neverPrinted) {
|
|
54991
|
-
faults.push(`${missing.suite} > ${missing.during} declares the error "${missing.text}", which it never printed`);
|
|
54992
|
-
failing.add(missing.suite);
|
|
54993
|
-
}
|
|
54994
|
-
for (const leak of printed.leaks) {
|
|
54995
|
-
faults.push(`the engine exited holding what it should have freed: ${leak}`);
|
|
54996
|
-
}
|
|
54997
|
-
const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
|
|
54998
|
-
const starved = memoryExit || printed.starved.length > 0;
|
|
54999
|
-
if (printed.starved.length > 0) {
|
|
55000
|
-
faults.push("the engine ran out of memory");
|
|
55001
|
-
}
|
|
55002
|
-
printed.lines.forEach((line, at) => {
|
|
55003
|
-
if (/^(?:Overall Summary|Executed test|Total execution time| ?Exit code)/.test(line.trim())) {
|
|
55004
|
-
shownAt.add(at);
|
|
55005
|
-
}
|
|
55006
|
-
});
|
|
55007
|
-
const passed = faults.length === 0;
|
|
55008
|
-
const passedSuites = handed.suites.map((suite) => suite.path).filter((path) => !failing.has(path));
|
|
55009
|
-
const cutShort = end.ended !== null || printed.executed === null || handed.suites.some(({ path }) => printed.suites.get(path)?.closed == null);
|
|
55010
|
-
return {
|
|
55011
|
-
passed,
|
|
55012
|
-
faults,
|
|
55013
|
-
passedSuites,
|
|
55014
|
-
starved,
|
|
55015
|
-
shown: passed ? [] : linesShown(printed.lines, shownAt, cutShort)
|
|
55016
|
-
};
|
|
55017
|
-
}
|
|
55018
|
-
function linesShown(lines, picked, cutShort) {
|
|
55019
|
-
const summary = [...picked].filter((at) => at < lines.length).sort((a, b) => a - b);
|
|
55020
|
-
const telling = summary.some((at) => !/^(?:Overall Summary|Executed test|Total execution|\s*Exit code)/.test(lines[at]?.trim() ?? ""));
|
|
55021
|
-
if (!telling && !cutShort) {
|
|
55022
|
-
return [];
|
|
55023
|
-
}
|
|
55024
|
-
if (!telling) {
|
|
55025
|
-
return lines.slice(Math.max(0, lines.length - TAIL)).filter((line, at, all) => at < all.length - 1 || line !== "");
|
|
55026
|
-
}
|
|
55027
|
-
const shown = [];
|
|
55028
|
-
let previous = -1;
|
|
55029
|
-
for (const at of summary) {
|
|
55030
|
-
if (previous >= 0 && at > previous + 1) {
|
|
55031
|
-
shown.push("...");
|
|
55032
|
-
}
|
|
55033
|
-
shown.push(lines[at] ?? "");
|
|
55034
|
-
previous = at;
|
|
55035
|
-
}
|
|
55036
|
-
return shown;
|
|
55037
|
-
}
|
|
55038
|
-
function unexpectedErrors(printed, handed) {
|
|
55039
|
-
const owed = new Map;
|
|
55040
|
-
for (const { path } of handed.suites) {
|
|
55041
|
-
const suite = printed.suites.get(path);
|
|
55042
|
-
const declared = handed.expected[path];
|
|
55043
|
-
if (suite === undefined || declared === undefined) {
|
|
55044
|
-
continue;
|
|
55045
|
-
}
|
|
55046
|
-
const ranHooks = suite.closed === null ? ["before"] : ["before", "after"];
|
|
55047
|
-
for (const during of [...ranHooks, ...suite.cases.map((ran) => ran.name)]) {
|
|
55048
|
-
const texts = declared[funcOf(during)];
|
|
55049
|
-
if (Array.isArray(texts) && texts.length > 0) {
|
|
55050
|
-
owed.set(`${path}
|
|
55051
|
-
${during}`, {
|
|
55052
|
-
suite: path,
|
|
55053
|
-
during,
|
|
55054
|
-
texts: texts.map((text) => String(text))
|
|
55055
|
-
});
|
|
55056
|
-
}
|
|
55057
|
-
}
|
|
55058
|
-
}
|
|
55059
|
-
const unexpected = [];
|
|
55060
|
-
for (const error of printed.errors) {
|
|
55061
|
-
const debt = error.suite === null || error.during === null ? undefined : owed.get(`${error.suite}
|
|
55062
|
-
${error.during}`);
|
|
55063
|
-
const index = debt?.texts.indexOf(error.text) ?? -1;
|
|
55064
|
-
if (debt === undefined || index < 0) {
|
|
55065
|
-
unexpected.push(error);
|
|
55066
|
-
} else {
|
|
55067
|
-
debt.texts.splice(index, 1);
|
|
55068
|
-
}
|
|
55069
|
-
}
|
|
55070
|
-
const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
|
|
55071
|
-
return { printed: unexpected, neverPrinted };
|
|
55072
|
-
}
|
|
55073
|
-
|
|
55074
|
-
// src/suite-plan.ts
|
|
55075
|
-
init_class_cache();
|
|
55076
|
-
init_gdscript_source();
|
|
55077
|
-
init_whole_file();
|
|
55078
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync15 } from "node:fs";
|
|
55079
|
-
import { dirname as dirname10, posix as posix3 } from "node:path";
|
|
55080
|
-
var SUITE_BASE = "GdUnitTestSuite";
|
|
55081
|
-
var CASE_PREFIX = "test_";
|
|
55082
|
-
var NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*true\b/m;
|
|
55083
|
-
var SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
55084
|
-
|
|
55085
|
-
class ProjectScripts {
|
|
55086
|
-
sources = new Map;
|
|
55087
|
-
classes = new Map;
|
|
55088
|
-
read = new Map;
|
|
55089
|
-
projectPath;
|
|
55090
|
-
constructor(projectPath) {
|
|
55091
|
-
this.projectPath = projectPath;
|
|
55092
|
-
eachScript(projectPath, (script, source) => {
|
|
55093
|
-
this.sources.set(script, source);
|
|
55094
|
-
const declared = declaredClassOf(source);
|
|
55095
|
-
if (declared !== null) {
|
|
55096
|
-
this.classes.set(declared, script);
|
|
55097
|
-
}
|
|
55098
|
-
});
|
|
55099
|
-
}
|
|
55100
|
-
script(path) {
|
|
55101
|
-
const known = this.read.get(path);
|
|
55102
|
-
if (known !== undefined) {
|
|
55103
|
-
return known;
|
|
55104
|
-
}
|
|
55105
|
-
const source = this.sources.get(path);
|
|
55106
|
-
if (source === undefined) {
|
|
55107
|
-
return;
|
|
55108
|
-
}
|
|
55109
|
-
const shape = shapeOf(source);
|
|
55110
|
-
const read = {
|
|
55111
|
-
source,
|
|
55112
|
-
extendsAs: shape.body.extendsAs,
|
|
55113
|
-
funcs: shape.body.declarations.filter((d) => d.kind === "func").map((d) => d.name),
|
|
55114
|
-
code: codeOf(source)
|
|
55115
|
-
};
|
|
55116
|
-
this.read.set(path, read);
|
|
55117
|
-
return read;
|
|
55118
|
-
}
|
|
55119
|
-
base(path) {
|
|
55120
|
-
const extendsAs = this.script(path)?.extendsAs ?? null;
|
|
55121
|
-
if (extendsAs === null) {
|
|
55122
|
-
return null;
|
|
55123
|
-
}
|
|
55124
|
-
const quote = extendsAs[0];
|
|
55125
|
-
if (quote === '"' || quote === "'") {
|
|
55126
|
-
const target = extendsAs.slice(1, -1);
|
|
55127
|
-
return {
|
|
55128
|
-
path: target.startsWith("res://") ? target : `res://${posix3.join(posix3.dirname(path.slice(6)), target)}`
|
|
55129
|
-
};
|
|
55130
|
-
}
|
|
55131
|
-
const name = extendsAs.split(".")[0] ?? extendsAs;
|
|
55132
|
-
const declaring = this.classes.get(name);
|
|
55133
|
-
return declaring === undefined ? { name } : { path: declaring };
|
|
55134
|
-
}
|
|
55135
|
-
suite(path) {
|
|
55136
|
-
const cases = new Set;
|
|
55137
|
-
let windowed = false;
|
|
55138
|
-
const seen = new Set;
|
|
55139
|
-
let at = path;
|
|
55140
|
-
while (!seen.has(at)) {
|
|
55141
|
-
seen.add(at);
|
|
55142
|
-
const read = this.script(at);
|
|
55143
|
-
if (read === undefined) {
|
|
55144
|
-
return null;
|
|
55145
|
-
}
|
|
55146
|
-
for (const func of read.funcs.filter((name) => name.startsWith(CASE_PREFIX))) {
|
|
55147
|
-
cases.add(func);
|
|
55148
|
-
}
|
|
55149
|
-
windowed ||= NEEDS_WINDOW.test(read.code) || SCENE_RUNNER.test(read.code);
|
|
55150
|
-
const base = this.base(at);
|
|
55151
|
-
if (base === null) {
|
|
55152
|
-
return null;
|
|
55153
|
-
}
|
|
55154
|
-
if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
|
|
55155
|
-
return cases.size > 0 ? { path, cases: [...cases].sort(), windowed } : null;
|
|
55156
|
-
}
|
|
55157
|
-
if ("name" in base) {
|
|
55158
|
-
return null;
|
|
55159
|
-
}
|
|
55160
|
-
at = base.path;
|
|
55161
|
-
}
|
|
55162
|
-
return null;
|
|
55163
|
-
}
|
|
55164
|
-
suitesUnder(roots) {
|
|
55165
|
-
const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
|
|
55166
|
-
return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
|
|
55167
|
-
}
|
|
55168
|
-
}
|
|
55169
|
-
function readTimes(file) {
|
|
55170
|
-
try {
|
|
55171
|
-
const parsed = JSON.parse(readFileSync15(file, "utf8"));
|
|
55172
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
55173
|
-
} catch {
|
|
55174
|
-
return {};
|
|
55175
|
-
}
|
|
55176
|
-
}
|
|
55177
|
-
function writeJson(file, value) {
|
|
55178
|
-
mkdirSync7(dirname10(file), { recursive: true });
|
|
55179
|
-
writeWhole(file, `${JSON.stringify(value, null, 1)}
|
|
55180
|
-
`);
|
|
55181
|
-
}
|
|
55182
|
-
function timesAfter(times, suites, runs) {
|
|
55183
|
-
const next = { ...times };
|
|
55184
|
-
const declared = new Map(suites.map((suite) => [suite.path, suite]));
|
|
55185
|
-
const measured = new Map;
|
|
55186
|
-
for (const run of runs) {
|
|
55187
|
-
for (const printed of run.suites.values()) {
|
|
55188
|
-
if (printed.closed === null || !declared.has(printed.path)) {
|
|
55189
|
-
continue;
|
|
55190
|
-
}
|
|
55191
|
-
const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
|
|
55192
|
-
const own = new Map;
|
|
55193
|
-
for (const ran of printed.cases) {
|
|
55194
|
-
own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
|
|
55195
|
-
}
|
|
55196
|
-
const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
|
|
55197
|
-
entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
|
|
55198
|
-
for (const [func, seconds] of own) {
|
|
55199
|
-
entry.cases.set(func, seconds);
|
|
55200
|
-
}
|
|
55201
|
-
measured.set(printed.path, entry);
|
|
55202
|
-
}
|
|
55203
|
-
}
|
|
55204
|
-
for (const [path, entry] of measured) {
|
|
55205
|
-
const suite = declared.get(path);
|
|
55206
|
-
const cases = {};
|
|
55207
|
-
for (const func of suite?.cases ?? []) {
|
|
55208
|
-
const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
|
|
55209
|
-
if (seconds !== undefined) {
|
|
55210
|
-
cases[func] = round(seconds);
|
|
55211
|
-
}
|
|
55212
|
-
}
|
|
55213
|
-
const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
|
|
55214
|
-
next[path] = { seconds: round(entry.around + inCases), cases };
|
|
55215
|
-
}
|
|
55216
|
-
return next;
|
|
55217
|
-
}
|
|
55218
|
-
function round(seconds) {
|
|
55219
|
-
return Math.round(seconds * 1000) / 1000;
|
|
55220
|
-
}
|
|
55221
|
-
var UNTIMED_CASE = 0.02;
|
|
55222
|
-
var SPLIT_PAST_SECONDS = 10;
|
|
55223
|
-
var ENGINE_START_SECONDS = 5;
|
|
55224
|
-
function median(values) {
|
|
55225
|
-
if (values.length === 0) {
|
|
55226
|
-
return;
|
|
55227
|
-
}
|
|
55228
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
55229
|
-
const middle = Math.floor(sorted.length / 2);
|
|
55230
|
-
return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
55231
|
-
}
|
|
55232
|
-
function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
|
|
55233
|
-
const known = Object.values(times);
|
|
55234
|
-
const caseCost = median(known.flatMap((time) => Object.values(time.cases))) ?? UNTIMED_CASE;
|
|
55235
|
-
const aroundCost = median(known.map((time) => time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0))) ?? 0;
|
|
55236
|
-
return suites.flatMap((suite) => {
|
|
55237
|
-
const time = times[suite.path];
|
|
55238
|
-
const costOf = (func) => time?.cases[func] ?? caseCost;
|
|
55239
|
-
const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
|
|
55240
|
-
const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
|
|
55241
|
-
if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
|
|
55242
|
-
return [{ suite: suite.path, cases: suite.cases, cost: whole, windowed: suite.windowed }];
|
|
55243
|
-
}
|
|
55244
|
-
return suite.cases.map((func) => ({
|
|
55245
|
-
suite: suite.path,
|
|
55246
|
-
cases: [func],
|
|
55247
|
-
cost: around + costOf(func),
|
|
55248
|
-
windowed: false
|
|
55249
|
-
}));
|
|
55250
|
-
});
|
|
55251
|
-
}
|
|
55252
|
-
function dealt(units, declared, most) {
|
|
55253
|
-
const windowed = units.filter((unit) => unit.windowed);
|
|
55254
|
-
const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
|
|
55255
|
-
const total = headless.reduce((sum, unit) => sum + unit.cost, 0);
|
|
55256
|
-
const slowest = headless[0]?.cost ?? 0;
|
|
55257
|
-
const room = Math.max(1, most - (windowed.length > 0 && most > 1 ? 1 : 0));
|
|
55258
|
-
const count = headless.length === 0 ? 0 : Math.max(1, Math.min(room, headless.length, Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
|
|
55259
|
-
const bins = Array.from({ length: count }, () => []);
|
|
55260
|
-
const loads = Array.from({ length: count }, () => 0);
|
|
55261
|
-
for (const unit of headless) {
|
|
55262
|
-
const lightest = loads.indexOf(Math.min(...loads));
|
|
55263
|
-
bins[lightest]?.push(unit);
|
|
55264
|
-
loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
|
|
55265
|
-
}
|
|
55266
|
-
const plans = bins.map((bin) => planOf(bin, declared, false));
|
|
55267
|
-
return windowed.length > 0 ? [planOf(windowed, declared, true), ...plans] : plans;
|
|
55268
|
-
}
|
|
55269
|
-
function inOneEngine(units, declared) {
|
|
55270
|
-
return planOf(units, declared, units.some((unit) => unit.windowed));
|
|
55271
|
-
}
|
|
55272
|
-
function planOf(bin, declared, windowed) {
|
|
55273
|
-
const held = new Map;
|
|
55274
|
-
for (const unit of bin) {
|
|
55275
|
-
held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
|
|
55276
|
-
}
|
|
55277
|
-
const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
|
|
55278
|
-
return {
|
|
55279
|
-
suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
|
|
55280
|
-
skipping,
|
|
55281
|
-
cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
|
|
55282
|
-
windowed
|
|
55283
|
-
};
|
|
55284
|
-
}
|
|
55285
|
-
function viewportOf(projectPath) {
|
|
55286
|
-
const file = `${projectPath}/project.godot`;
|
|
55287
|
-
if (!existsSync17(file)) {
|
|
55288
|
-
return null;
|
|
55289
|
-
}
|
|
55290
|
-
const text = readFileSync15(file, "utf8");
|
|
55291
|
-
const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
55292
|
-
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
55293
|
-
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
55294
|
-
}
|
|
55428
|
+
// src/suite-run.ts
|
|
55429
|
+
init_suite_judge();
|
|
55430
|
+
init_suite_plan();
|
|
55295
55431
|
|
|
55296
55432
|
// src/version.ts
|
|
55297
|
-
import { readFileSync as
|
|
55433
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
55298
55434
|
import { dirname as dirname11, join as join20 } from "node:path";
|
|
55299
55435
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
55300
55436
|
function getLocalVersion() {
|
|
@@ -55305,7 +55441,7 @@ function getLocalVersion() {
|
|
|
55305
55441
|
];
|
|
55306
55442
|
for (const packagePath of packageCandidates) {
|
|
55307
55443
|
try {
|
|
55308
|
-
const value = JSON.parse(
|
|
55444
|
+
const value = JSON.parse(readFileSync17(packagePath, "utf-8"));
|
|
55309
55445
|
if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
|
|
55310
55446
|
return value.version;
|
|
55311
55447
|
}
|
|
@@ -55402,7 +55538,8 @@ function resPathOf(entry, projectPath, cwd) {
|
|
|
55402
55538
|
const caseAt = /^(.*\.gd):(\w+)$/.exec(entry);
|
|
55403
55539
|
const file = caseAt?.[1] ?? entry;
|
|
55404
55540
|
const fromProject = resolve4(projectPath, file);
|
|
55405
|
-
const
|
|
55541
|
+
const fromHere = resolve4(cwd, file);
|
|
55542
|
+
const absolute = isAbsolute4(file) || !existsSync18(fromProject) && existsSync18(fromHere) ? fromHere : fromProject;
|
|
55406
55543
|
const inside = relative3(projectPath, absolute);
|
|
55407
55544
|
if (inside.startsWith("..") || isAbsolute4(inside)) {
|
|
55408
55545
|
throw new TestUsageError(`${entry} is not inside the project at ${projectPath}.`);
|
|
@@ -55414,7 +55551,7 @@ function homeOf(projectPath) {
|
|
|
55414
55551
|
return mainCheckoutOf(projectPath) ?? projectPath;
|
|
55415
55552
|
}
|
|
55416
55553
|
function selectedSuites(scripts, options) {
|
|
55417
|
-
const roots = options.roots.length > 0 ? options.roots :
|
|
55554
|
+
const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync18(join21(options.projectPath, root))).map((root) => `res://${root}`);
|
|
55418
55555
|
if (options.selected.length === 0) {
|
|
55419
55556
|
const found = scripts.suitesUnder(roots);
|
|
55420
55557
|
if (found.length === 0) {
|
|
@@ -55429,7 +55566,7 @@ function selectedSuites(scripts, options) {
|
|
|
55429
55566
|
const directory = join21(options.projectPath, path.slice(6));
|
|
55430
55567
|
const suites = existsSync18(directory) && statSync11(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
|
|
55431
55568
|
if (suites.length === 0) {
|
|
55432
|
-
throw new TestUsageError(existsSync18(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path}.`);
|
|
55569
|
+
throw new TestUsageError(existsSync18(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
|
|
55433
55570
|
}
|
|
55434
55571
|
for (const suite of suites) {
|
|
55435
55572
|
const picked = chosen.get(suite.path) ?? { suite, cases: new Set };
|
|
@@ -55448,9 +55585,32 @@ function selectedSuites(scripts, options) {
|
|
|
55448
55585
|
cases: suite.cases.filter((c) => cases.has(c))
|
|
55449
55586
|
}));
|
|
55450
55587
|
}
|
|
55451
|
-
function roomFor(most) {
|
|
55452
|
-
const byMemory = Math.floor((
|
|
55453
|
-
|
|
55588
|
+
function roomFor(most, processors, available) {
|
|
55589
|
+
const byMemory = Math.floor((available - MEMORY_KEPT) / MEMORY_PER_ENGINE);
|
|
55590
|
+
const engines = Math.max(1, Math.min(most, processors, byMemory));
|
|
55591
|
+
if (engines >= most) {
|
|
55592
|
+
return { engines, held: undefined };
|
|
55593
|
+
}
|
|
55594
|
+
const gigabytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
55595
|
+
return {
|
|
55596
|
+
engines,
|
|
55597
|
+
held: byMemory < processors ? `the machine has ${gigabytes(Math.max(0, available))} of memory free, ${gigabytes(MEMORY_KEPT)} of it kept for everything else and ${gigabytes(MEMORY_PER_ENGINE)} taken for each engine` : `the machine has ${plural(processors, "processor")}`
|
|
55598
|
+
};
|
|
55599
|
+
}
|
|
55600
|
+
async function memoryAvailable() {
|
|
55601
|
+
const level = await macMemoryLevel();
|
|
55602
|
+
return level === null ? freemem() : totalmem() * level / 100;
|
|
55603
|
+
}
|
|
55604
|
+
async function macMemoryLevel() {
|
|
55605
|
+
if (process12.platform !== "darwin") {
|
|
55606
|
+
return null;
|
|
55607
|
+
}
|
|
55608
|
+
try {
|
|
55609
|
+
const level = Number((await runHidden("sysctl", ["-n", "kern.memorystatus_level"])).stdout.trim());
|
|
55610
|
+
return Number.isFinite(level) && level > 0 && level <= 100 ? level : null;
|
|
55611
|
+
} catch {
|
|
55612
|
+
return null;
|
|
55613
|
+
}
|
|
55454
55614
|
}
|
|
55455
55615
|
function ordered(units, order) {
|
|
55456
55616
|
const sorted = [...units].sort((a, b) => a.suite.localeCompare(b.suite));
|
|
@@ -55522,9 +55682,9 @@ async function runTestCommand(options, godotPath) {
|
|
|
55522
55682
|
}
|
|
55523
55683
|
const fingerprints = new SuiteFingerprints(scripts, [
|
|
55524
55684
|
getLocalVersion(),
|
|
55525
|
-
|
|
55685
|
+
readFileSync18(shippedListedRunner(), "utf8"),
|
|
55526
55686
|
...options.reachAlso.map((path) => `${path}
|
|
55527
|
-
${existsSync18(path) ?
|
|
55687
|
+
${existsSync18(path) ? readFileSync18(path, "latin1") : "missing"}`)
|
|
55528
55688
|
]);
|
|
55529
55689
|
const printsOf = new Map(suites.map((suite) => [suite.path, fingerprints.of(suite)]));
|
|
55530
55690
|
if (options.changedOnly) {
|
|
@@ -55535,7 +55695,7 @@ ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
|
|
|
55535
55695
|
});
|
|
55536
55696
|
suites = suites.filter((suite) => !unchanged.includes(suite));
|
|
55537
55697
|
if (unchanged.length > 0) {
|
|
55538
|
-
console.log(`${plural(unchanged.length, "suite")} passed with everything ${unchanged.length === 1 ? "it reaches" : "they reach"} as it stands now, and ${unchanged.length === 1 ? "is" : "are"} not run.`);
|
|
55698
|
+
console.log(`${plural(unchanged.length, "suite")} passed with everything ${unchanged.length === 1 ? "it reaches" : "they reach"} as it stands now, and ${unchanged.length === 1 ? "is" : "are"} not run: ${named(unchanged.map((suite) => suite.path))}.`);
|
|
55539
55699
|
}
|
|
55540
55700
|
if (suites.length === 0) {
|
|
55541
55701
|
console.log("Nothing has changed since every suite last passed.");
|
|
@@ -55545,9 +55705,16 @@ ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
|
|
|
55545
55705
|
}
|
|
55546
55706
|
const times = readTimes(timesFile);
|
|
55547
55707
|
const units = unitsOf(suites, times);
|
|
55548
|
-
const
|
|
55708
|
+
const room = roomFor(options.engines, availableParallelism(), await memoryAvailable());
|
|
55709
|
+
const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
|
|
55549
55710
|
const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
|
|
55550
|
-
|
|
55711
|
+
const held = options.order === null && room.held !== undefined && plans.length === room.engines ? `, of the ${String(options.engines)} asked for, since ${room.held}` : "";
|
|
55712
|
+
const how = options.order === null ? `${plans.some((plan) => plan.windowed) ? ", one of them windowed" : ""}${held}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
|
|
55713
|
+
console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
|
|
55714
|
+
const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path).length > 1);
|
|
55715
|
+
if (plans.length > 1 && split.length > 0) {
|
|
55716
|
+
console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(split)}.`);
|
|
55717
|
+
}
|
|
55551
55718
|
let ran = await Promise.all(plans.map((plan, index) => runEngine2(launch, plan, index, units)));
|
|
55552
55719
|
if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
|
|
55553
55720
|
console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
|
|
@@ -55570,7 +55737,7 @@ function plural(count, noun) {
|
|
|
55570
55737
|
}
|
|
55571
55738
|
function readRecord(file) {
|
|
55572
55739
|
try {
|
|
55573
|
-
const parsed = JSON.parse(
|
|
55740
|
+
const parsed = JSON.parse(readFileSync18(file, "utf8"));
|
|
55574
55741
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
55575
55742
|
} catch {
|
|
55576
55743
|
return {};
|
|
@@ -55673,7 +55840,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
|
|
|
55673
55840
|
} finally {
|
|
55674
55841
|
launch.slots.release(lock);
|
|
55675
55842
|
}
|
|
55676
|
-
const printed = readRun(
|
|
55843
|
+
const printed = readRun(readFileSync18(log, "utf8"));
|
|
55677
55844
|
const assignment = { suites: plan.suites, expected: readExpected(expectedFile) };
|
|
55678
55845
|
return {
|
|
55679
55846
|
index,
|
|
@@ -55684,14 +55851,6 @@ async function runEngine2(launch, plan, index, units, again = "") {
|
|
|
55684
55851
|
seconds: (Date.now() - began) / 1000
|
|
55685
55852
|
};
|
|
55686
55853
|
}
|
|
55687
|
-
function readExpected(file) {
|
|
55688
|
-
try {
|
|
55689
|
-
const parsed = JSON.parse(readFileSync17(file, "utf8"));
|
|
55690
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
55691
|
-
} catch {
|
|
55692
|
-
return {};
|
|
55693
|
-
}
|
|
55694
|
-
}
|
|
55695
55854
|
async function watched(launch, args, directory, log, windowed, silenceSeconds) {
|
|
55696
55855
|
const user = join21(directory, "user");
|
|
55697
55856
|
const env = { ...process12.env, APPDATA: user, XDG_DATA_HOME: user };
|
|
@@ -55813,12 +55972,17 @@ function report(ran, startedAt, runDirectory) {
|
|
|
55813
55972
|
const suites = new Set(ran.flatMap((engine) => engine.plan.suites.map((suite) => suite.path))).size;
|
|
55814
55973
|
const longest = Math.max(0, ...ran.map((engine) => engine.seconds));
|
|
55815
55974
|
const took = (Date.now() - startedAt) / 1000;
|
|
55975
|
+
const totals = `${plural(cases, "case")} in ${plural(suites, "suite")} across ${plural(ran.length, "engine")}, ${took.toFixed(1)} s; the longest engine took ${longest.toFixed(1)} s.`;
|
|
55976
|
+
console.log("");
|
|
55816
55977
|
if (failed.length === 0) {
|
|
55817
55978
|
rmSync10(runDirectory, { recursive: true, force: true });
|
|
55979
|
+
console.log(`Passed: ${totals}`);
|
|
55980
|
+
return 0;
|
|
55818
55981
|
}
|
|
55819
|
-
|
|
55820
|
-
console.log(
|
|
55821
|
-
|
|
55982
|
+
const problems = failed.reduce((sum, engine) => sum + engine.verdict.faults.length, 0);
|
|
55983
|
+
console.log(`Failed: ${plural(problems, "problem")}, listed above. Ran ${totals}`);
|
|
55984
|
+
console.log(`Every engine's output is kept under ${runDirectory} until the next run.`);
|
|
55985
|
+
return 1;
|
|
55822
55986
|
}
|
|
55823
55987
|
|
|
55824
55988
|
// src/cli.ts
|