gdharness 1.1.55 → 1.1.56
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 +123 -33
- package/build/index.js +123 -31
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7840,10 +7840,54 @@ function announced(line) {
|
|
|
7840
7840
|
if (!match) {
|
|
7841
7841
|
return null;
|
|
7842
7842
|
}
|
|
7843
|
-
return {
|
|
7843
|
+
return {
|
|
7844
|
+
severity: match[2] === "WARNING" ? "warning" : "error",
|
|
7845
|
+
text: match[3] ?? "",
|
|
7846
|
+
byScript: match[2] === "SCRIPT ERROR"
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
function reported(entry) {
|
|
7850
|
+
return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
|
|
7851
|
+
}
|
|
7852
|
+
function framesCollapsed(detail) {
|
|
7853
|
+
const out = [];
|
|
7854
|
+
let pending;
|
|
7855
|
+
for (const line of detail) {
|
|
7856
|
+
const match = FRAME.exec(line);
|
|
7857
|
+
const number = match?.[1];
|
|
7858
|
+
const frame = match?.[2];
|
|
7859
|
+
if (frame !== undefined && pending?.frame === frame && Number(number) === Number(pending.last) + 1) {
|
|
7860
|
+
pending.last = String(number);
|
|
7861
|
+
continue;
|
|
7862
|
+
}
|
|
7863
|
+
if (pending !== undefined) {
|
|
7864
|
+
out.push(writtenRun(pending));
|
|
7865
|
+
}
|
|
7866
|
+
pending = number === undefined || frame === undefined ? undefined : { first: number, last: number, frame };
|
|
7867
|
+
if (pending === undefined) {
|
|
7868
|
+
out.push(line);
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
if (pending !== undefined) {
|
|
7872
|
+
out.push(writtenRun(pending));
|
|
7873
|
+
}
|
|
7874
|
+
return out;
|
|
7875
|
+
}
|
|
7876
|
+
function writtenRun(run) {
|
|
7877
|
+
return run.first === run.last ? `[${run.first}] ${run.frame}` : `[${run.first}-${run.last}] ${run.frame}`;
|
|
7844
7878
|
}
|
|
7845
|
-
function
|
|
7846
|
-
|
|
7879
|
+
function foldedRepeats(entries) {
|
|
7880
|
+
const first = new Map;
|
|
7881
|
+
for (const entry of entries) {
|
|
7882
|
+
const key = JSON.stringify([entry.severity, entry.source, entry.text, entry.detail]);
|
|
7883
|
+
const seen = first.get(key);
|
|
7884
|
+
if (seen === undefined) {
|
|
7885
|
+
first.set(key, { entry, times: 1 });
|
|
7886
|
+
} else {
|
|
7887
|
+
seen.times += 1;
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
return [...first.values()].map(({ entry, times }) => times === 1 ? reported(entry) : { ...reported(entry), times });
|
|
7847
7891
|
}
|
|
7848
7892
|
|
|
7849
7893
|
class GameLog {
|
|
@@ -7854,8 +7898,13 @@ class GameLog {
|
|
|
7854
7898
|
transcript: new StringDecoder("utf8")
|
|
7855
7899
|
};
|
|
7856
7900
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
7857
|
-
lastHeadline =
|
|
7901
|
+
lastHeadline = {
|
|
7902
|
+
stdout: null,
|
|
7903
|
+
stderr: null,
|
|
7904
|
+
transcript: null
|
|
7905
|
+
};
|
|
7858
7906
|
seen = { error: 0, warning: 0, info: 0 };
|
|
7907
|
+
byScript = new Set;
|
|
7859
7908
|
append(source, chunk) {
|
|
7860
7909
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
7861
7910
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -7879,8 +7928,9 @@ class GameLog {
|
|
|
7879
7928
|
if (line.trim() === "") {
|
|
7880
7929
|
return;
|
|
7881
7930
|
}
|
|
7882
|
-
|
|
7883
|
-
|
|
7931
|
+
const owner = this.lastHeadline[source];
|
|
7932
|
+
if (owner !== null && /^\s/.test(line)) {
|
|
7933
|
+
owner.detail.push(line.trim());
|
|
7884
7934
|
return;
|
|
7885
7935
|
}
|
|
7886
7936
|
const headline = announced(line);
|
|
@@ -7893,7 +7943,13 @@ class GameLog {
|
|
|
7893
7943
|
detail
|
|
7894
7944
|
};
|
|
7895
7945
|
this.entries.push(entry);
|
|
7896
|
-
|
|
7946
|
+
if (headline?.byScript === true) {
|
|
7947
|
+
this.byScript.add(entry.index);
|
|
7948
|
+
}
|
|
7949
|
+
this.lastHeadline[source] = headline ? { detail } : null;
|
|
7950
|
+
}
|
|
7951
|
+
raisedByScript(entry) {
|
|
7952
|
+
return this.byScript.has(entry.index);
|
|
7897
7953
|
}
|
|
7898
7954
|
record(severity, text) {
|
|
7899
7955
|
this.entries.push({
|
|
@@ -7903,7 +7959,6 @@ class GameLog {
|
|
|
7903
7959
|
text,
|
|
7904
7960
|
detail: []
|
|
7905
7961
|
});
|
|
7906
|
-
this.lastHeadline = null;
|
|
7907
7962
|
}
|
|
7908
7963
|
get all() {
|
|
7909
7964
|
return this.entries;
|
|
@@ -7927,6 +7982,11 @@ class GameLog {
|
|
|
7927
7982
|
const omitted = Math.max(0, matching.length - options.limit);
|
|
7928
7983
|
return { entries: matching.slice(omitted), omitted };
|
|
7929
7984
|
}
|
|
7985
|
+
selectFolded(options) {
|
|
7986
|
+
const folded = foldedRepeats(this.select({ ...options, limit: Number.POSITIVE_INFINITY }).entries);
|
|
7987
|
+
const omitted = Math.max(0, folded.length - options.limit);
|
|
7988
|
+
return { entries: folded.slice(omitted), omitted };
|
|
7989
|
+
}
|
|
7930
7990
|
}
|
|
7931
7991
|
function answersTo(entry, severity, contains) {
|
|
7932
7992
|
if (!ADMITTED[severity].has(entry.severity)) {
|
|
@@ -7938,7 +7998,7 @@ function answersTo(entry, severity, contains) {
|
|
|
7938
7998
|
const needle = contains.toLowerCase();
|
|
7939
7999
|
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
7940
8000
|
}
|
|
7941
|
-
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED;
|
|
8001
|
+
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED, FRAME;
|
|
7942
8002
|
var init_game_log = __esm(() => {
|
|
7943
8003
|
HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
7944
8004
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
@@ -7948,6 +8008,7 @@ var init_game_log = __esm(() => {
|
|
|
7948
8008
|
warning: new Set(["error", "warning"]),
|
|
7949
8009
|
info: new Set(["error", "warning", "info"])
|
|
7950
8010
|
};
|
|
8011
|
+
FRAME = /^\[(\d+)\] (.+)$/;
|
|
7951
8012
|
});
|
|
7952
8013
|
|
|
7953
8014
|
// src/engine-run.ts
|
|
@@ -8166,7 +8227,7 @@ function problemsIn(log) {
|
|
|
8166
8227
|
return [];
|
|
8167
8228
|
});
|
|
8168
8229
|
}
|
|
8169
|
-
function
|
|
8230
|
+
function reported2(problems) {
|
|
8170
8231
|
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
8171
8232
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
8172
8233
|
}
|
|
@@ -8251,12 +8312,12 @@ async function importOnce(godotPath, projectPath) {
|
|
|
8251
8312
|
}
|
|
8252
8313
|
const problems = problemsIn(ran.log);
|
|
8253
8314
|
if (ran.exitCode === 0 && ran.failure === null) {
|
|
8254
|
-
return { ok: true, ...
|
|
8315
|
+
return { ok: true, ...reported2(problems) };
|
|
8255
8316
|
}
|
|
8256
8317
|
return {
|
|
8257
8318
|
ok: false,
|
|
8258
8319
|
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
8259
|
-
...
|
|
8320
|
+
...reported2(problems)
|
|
8260
8321
|
};
|
|
8261
8322
|
}
|
|
8262
8323
|
async function runOperation(engine, operation, params, projectPath) {
|
|
@@ -8290,7 +8351,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8290
8351
|
return {
|
|
8291
8352
|
ok: false,
|
|
8292
8353
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8293
|
-
...
|
|
8354
|
+
...reported2(problems)
|
|
8294
8355
|
};
|
|
8295
8356
|
}
|
|
8296
8357
|
if (answer.kind === "none") {
|
|
@@ -8298,7 +8359,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8298
8359
|
return {
|
|
8299
8360
|
ok: false,
|
|
8300
8361
|
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
8301
|
-
...
|
|
8362
|
+
...reported2(problems)
|
|
8302
8363
|
};
|
|
8303
8364
|
}
|
|
8304
8365
|
const fault = scriptFault(problems, engine.script);
|
|
@@ -8306,14 +8367,14 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8306
8367
|
return {
|
|
8307
8368
|
ok: false,
|
|
8308
8369
|
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
8309
|
-
...
|
|
8370
|
+
...reported2(problems)
|
|
8310
8371
|
};
|
|
8311
8372
|
}
|
|
8312
8373
|
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
8313
8374
|
return {
|
|
8314
8375
|
ok: true,
|
|
8315
8376
|
payload: answer.payload,
|
|
8316
|
-
...
|
|
8377
|
+
...reported2(problems),
|
|
8317
8378
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
8318
8379
|
};
|
|
8319
8380
|
}
|
|
@@ -17646,7 +17707,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17646
17707
|
},
|
|
17647
17708
|
{
|
|
17648
17709
|
name: "project_test",
|
|
17649
|
-
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. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
17710
|
+
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. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17650
17711
|
parameters: {
|
|
17651
17712
|
projectPath: PROJECT_PATH,
|
|
17652
17713
|
path: {
|
|
@@ -18094,7 +18155,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18094
18155
|
limit: {
|
|
18095
18156
|
type: "integer",
|
|
18096
18157
|
minimum: 1,
|
|
18097
|
-
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
18158
|
+
description: "The most entries to answer with, newest kept. Identical entries come back as one, at the first of them, with times saying how many were printed, so the limit counts distinct entries. Default 200, and omitted says how many distinct matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
18098
18159
|
},
|
|
18099
18160
|
before: {
|
|
18100
18161
|
ops: ["editor"],
|
|
@@ -42591,7 +42652,6 @@ function notRunNote(notRun, failFast) {
|
|
|
42591
42652
|
function exportAnswer(ending, asked, before, after) {
|
|
42592
42653
|
const { log } = ending;
|
|
42593
42654
|
const written = after !== null && after !== before;
|
|
42594
|
-
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
42595
42655
|
const verdict = {
|
|
42596
42656
|
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
42597
42657
|
...asked,
|
|
@@ -42600,8 +42660,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
42600
42660
|
failure: ending.failure ?? undefined,
|
|
42601
42661
|
errors: log.count("error"),
|
|
42602
42662
|
warnings: log.count("warning"),
|
|
42603
|
-
|
|
42604
|
-
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
42663
|
+
...foldedEntriesAnswer(log)
|
|
42605
42664
|
};
|
|
42606
42665
|
if (verdict.exported) {
|
|
42607
42666
|
return { content: [{ type: "text", text: answerJson(verdict) }] };
|
|
@@ -42807,6 +42866,32 @@ function aboveTheRunner(entry) {
|
|
|
42807
42866
|
]
|
|
42808
42867
|
};
|
|
42809
42868
|
}
|
|
42869
|
+
function foldedEntriesAnswer(log) {
|
|
42870
|
+
const problems = log.selectFolded({
|
|
42871
|
+
severity: "warning",
|
|
42872
|
+
sinceLastCall: false,
|
|
42873
|
+
limit: MOST_ENGINE_ENTRIES
|
|
42874
|
+
});
|
|
42875
|
+
return {
|
|
42876
|
+
entries: problems.entries,
|
|
42877
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
42878
|
+
};
|
|
42879
|
+
}
|
|
42880
|
+
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
42881
|
+
try {
|
|
42882
|
+
return settingKeys(readFileSync22(join26(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
42883
|
+
} catch {
|
|
42884
|
+
return true;
|
|
42885
|
+
}
|
|
42886
|
+
}
|
|
42887
|
+
function uncaughtNote(uncaught) {
|
|
42888
|
+
const first = uncaught[0];
|
|
42889
|
+
if (first === undefined) {
|
|
42890
|
+
return;
|
|
42891
|
+
}
|
|
42892
|
+
const count = uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`;
|
|
42893
|
+
return `Every case passed, but the run printed ${count} that no case failed on, the first "${first.text}"; ` + "they are under uncaughtScriptErrors. gdUnit4 fails a case on the script errors its logger sees, and " + "it did not see these, so the cases passed on whatever the faulted calls returned. A stack overflow is " + "one it cannot see: it is raised deeper than the logger can be called.";
|
|
42894
|
+
}
|
|
42810
42895
|
function elsewhereIn(projectPath, asked) {
|
|
42811
42896
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
42812
42897
|
if (wanted === "" || existsSync20(join26(projectPath, wanted))) {
|
|
@@ -44591,7 +44676,8 @@ class GodotServer {
|
|
|
44591
44676
|
rmdirSync2(dirname19(reportsDir));
|
|
44592
44677
|
} catch {}
|
|
44593
44678
|
}
|
|
44594
|
-
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit:
|
|
44679
|
+
const engineEntries = foldedRepeats(run.log.select({ severity: "warning", sinceLastCall: false, limit: Number.POSITIVE_INFINITY }).entries.map(aboveTheRunner));
|
|
44680
|
+
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
44595
44681
|
const verdicts = {
|
|
44596
44682
|
0: "passed",
|
|
44597
44683
|
100: "failures",
|
|
@@ -44601,6 +44687,7 @@ class GodotServer {
|
|
|
44601
44687
|
105: "script errors"
|
|
44602
44688
|
};
|
|
44603
44689
|
const exitCode = run.exitCode;
|
|
44690
|
+
const uncaught = report !== null && exitCode === 0 && !timedOut && gdUnitFailsOnScriptErrors(project.value.path) ? run.log.all.filter((entry) => run.log.raisedByScript(entry) && !entry.detail.some((line) => line.includes(ERROR_ASSERT))) : [];
|
|
44604
44691
|
const printed = run.log.select({
|
|
44605
44692
|
severity: "info",
|
|
44606
44693
|
sinceLastCall: false,
|
|
@@ -44614,7 +44701,7 @@ class GodotServer {
|
|
|
44614
44701
|
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
44615
44702
|
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
44616
44703
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
44617
|
-
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
44704
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
44618
44705
|
const nothingAt = missing.join(" and ");
|
|
44619
44706
|
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
44620
44707
|
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
@@ -44664,7 +44751,7 @@ class GodotServer {
|
|
|
44664
44751
|
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
44665
44752
|
...atTheEnd,
|
|
44666
44753
|
arguments: cmdArgs,
|
|
44667
|
-
entries:
|
|
44754
|
+
entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
|
|
44668
44755
|
...printedLines,
|
|
44669
44756
|
savesNote,
|
|
44670
44757
|
...scanned
|
|
@@ -44699,7 +44786,7 @@ class GodotServer {
|
|
|
44699
44786
|
});
|
|
44700
44787
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
44701
44788
|
return this.jsonTextResponse({
|
|
44702
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
44789
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
44703
44790
|
verdict,
|
|
44704
44791
|
...missing.length > 0 ? { missing } : {},
|
|
44705
44792
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
@@ -44712,6 +44799,7 @@ class GodotServer {
|
|
|
44712
44799
|
time: report.time,
|
|
44713
44800
|
failed,
|
|
44714
44801
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
44802
|
+
uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
|
|
44715
44803
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
44716
44804
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
44717
44805
|
notRun: notRun > 0 ? notRun : undefined,
|
|
@@ -44720,6 +44808,7 @@ class GodotServer {
|
|
|
44720
44808
|
...scanned,
|
|
44721
44809
|
note: [
|
|
44722
44810
|
missingNote,
|
|
44811
|
+
uncaughtNote(uncaught),
|
|
44723
44812
|
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)
|
|
44724
44813
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
44725
44814
|
suites: unclean.map((suite) => ({
|
|
@@ -44737,7 +44826,8 @@ class GodotServer {
|
|
|
44737
44826
|
suitesPassed: report.suites.length - unclean.length,
|
|
44738
44827
|
engineErrors: run.log.count("error"),
|
|
44739
44828
|
engineWarnings: run.log.count("warning"),
|
|
44740
|
-
engineEntries,
|
|
44829
|
+
engineEntries: engineEntriesShown,
|
|
44830
|
+
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
44741
44831
|
classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
|
|
44742
44832
|
});
|
|
44743
44833
|
}
|
|
@@ -46446,7 +46536,7 @@ class GodotServer {
|
|
|
46446
46536
|
warnings,
|
|
46447
46537
|
...scanWaitAnswer(scanned),
|
|
46448
46538
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
46449
|
-
|
|
46539
|
+
...foldedEntriesAnswer(boot.log)
|
|
46450
46540
|
});
|
|
46451
46541
|
}
|
|
46452
46542
|
async handleWaitForRun(args) {
|
|
@@ -46530,7 +46620,7 @@ class GodotServer {
|
|
|
46530
46620
|
const asked = readString(args, "severity");
|
|
46531
46621
|
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
46532
46622
|
const contains = readNonEmptyString(args, "contains");
|
|
46533
|
-
const selected = console2.log.
|
|
46623
|
+
const selected = console2.log.selectFolded({
|
|
46534
46624
|
severity,
|
|
46535
46625
|
sinceLastCall: false,
|
|
46536
46626
|
contains,
|
|
@@ -46559,7 +46649,7 @@ class GodotServer {
|
|
|
46559
46649
|
},
|
|
46560
46650
|
repeated: shown,
|
|
46561
46651
|
moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
|
|
46562
|
-
entries:
|
|
46652
|
+
entries: selected.entries,
|
|
46563
46653
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
46564
46654
|
});
|
|
46565
46655
|
}
|
|
@@ -46581,7 +46671,7 @@ class GodotServer {
|
|
|
46581
46671
|
run.endedUnwatched = noCodeWillCome(run);
|
|
46582
46672
|
}
|
|
46583
46673
|
const severity = readString(args, "severity");
|
|
46584
|
-
const selected = run.log.
|
|
46674
|
+
const selected = run.log.selectFolded({
|
|
46585
46675
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
46586
46676
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
46587
46677
|
contains: readNonEmptyString(args, "contains"),
|
|
@@ -46647,7 +46737,7 @@ class GodotServer {
|
|
|
46647
46737
|
cpuSeconds,
|
|
46648
46738
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
46649
46739
|
omitted: selected.omitted,
|
|
46650
|
-
entries:
|
|
46740
|
+
entries: selected.entries
|
|
46651
46741
|
});
|
|
46652
46742
|
}
|
|
46653
46743
|
anotherProjectsRun(runsProject, args, act) {
|
|
@@ -46714,7 +46804,7 @@ class GodotServer {
|
|
|
46714
46804
|
clean: verdict.clean,
|
|
46715
46805
|
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
46716
46806
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
46717
|
-
|
|
46807
|
+
...foldedEntriesAnswer(stopped.log)
|
|
46718
46808
|
});
|
|
46719
46809
|
}
|
|
46720
46810
|
async stopAnnouncedGame(pid, andChildren, args) {
|
|
@@ -47694,7 +47784,7 @@ async function runGodotServer() {
|
|
|
47694
47784
|
const server = new GodotServer;
|
|
47695
47785
|
await server.run();
|
|
47696
47786
|
}
|
|
47697
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, 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, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
47787
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, 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, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
47698
47788
|
var init_server3 = __esm(() => {
|
|
47699
47789
|
init_mcp();
|
|
47700
47790
|
init_stdio2();
|
package/build/index.js
CHANGED
|
@@ -27850,7 +27850,11 @@ function announced(line) {
|
|
|
27850
27850
|
if (!match) {
|
|
27851
27851
|
return null;
|
|
27852
27852
|
}
|
|
27853
|
-
return {
|
|
27853
|
+
return {
|
|
27854
|
+
severity: match[2] === "WARNING" ? "warning" : "error",
|
|
27855
|
+
text: match[3] ?? "",
|
|
27856
|
+
byScript: match[2] === "SCRIPT ERROR"
|
|
27857
|
+
};
|
|
27854
27858
|
}
|
|
27855
27859
|
var STREAMS = ["stdout", "stderr", "transcript"];
|
|
27856
27860
|
var ADMITTED = {
|
|
@@ -27858,8 +27862,49 @@ var ADMITTED = {
|
|
|
27858
27862
|
warning: new Set(["error", "warning"]),
|
|
27859
27863
|
info: new Set(["error", "warning", "info"])
|
|
27860
27864
|
};
|
|
27861
|
-
function
|
|
27862
|
-
return
|
|
27865
|
+
function reported(entry) {
|
|
27866
|
+
return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
|
|
27867
|
+
}
|
|
27868
|
+
var FRAME = /^\[(\d+)\] (.+)$/;
|
|
27869
|
+
function framesCollapsed(detail) {
|
|
27870
|
+
const out = [];
|
|
27871
|
+
let pending;
|
|
27872
|
+
for (const line of detail) {
|
|
27873
|
+
const match = FRAME.exec(line);
|
|
27874
|
+
const number = match?.[1];
|
|
27875
|
+
const frame = match?.[2];
|
|
27876
|
+
if (frame !== undefined && pending?.frame === frame && Number(number) === Number(pending.last) + 1) {
|
|
27877
|
+
pending.last = String(number);
|
|
27878
|
+
continue;
|
|
27879
|
+
}
|
|
27880
|
+
if (pending !== undefined) {
|
|
27881
|
+
out.push(writtenRun(pending));
|
|
27882
|
+
}
|
|
27883
|
+
pending = number === undefined || frame === undefined ? undefined : { first: number, last: number, frame };
|
|
27884
|
+
if (pending === undefined) {
|
|
27885
|
+
out.push(line);
|
|
27886
|
+
}
|
|
27887
|
+
}
|
|
27888
|
+
if (pending !== undefined) {
|
|
27889
|
+
out.push(writtenRun(pending));
|
|
27890
|
+
}
|
|
27891
|
+
return out;
|
|
27892
|
+
}
|
|
27893
|
+
function writtenRun(run) {
|
|
27894
|
+
return run.first === run.last ? `[${run.first}] ${run.frame}` : `[${run.first}-${run.last}] ${run.frame}`;
|
|
27895
|
+
}
|
|
27896
|
+
function foldedRepeats(entries) {
|
|
27897
|
+
const first = new Map;
|
|
27898
|
+
for (const entry of entries) {
|
|
27899
|
+
const key = JSON.stringify([entry.severity, entry.source, entry.text, entry.detail]);
|
|
27900
|
+
const seen = first.get(key);
|
|
27901
|
+
if (seen === undefined) {
|
|
27902
|
+
first.set(key, { entry, times: 1 });
|
|
27903
|
+
} else {
|
|
27904
|
+
seen.times += 1;
|
|
27905
|
+
}
|
|
27906
|
+
}
|
|
27907
|
+
return [...first.values()].map(({ entry, times }) => times === 1 ? reported(entry) : { ...reported(entry), times });
|
|
27863
27908
|
}
|
|
27864
27909
|
|
|
27865
27910
|
class GameLog {
|
|
@@ -27870,8 +27915,13 @@ class GameLog {
|
|
|
27870
27915
|
transcript: new StringDecoder("utf8")
|
|
27871
27916
|
};
|
|
27872
27917
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
27873
|
-
lastHeadline =
|
|
27918
|
+
lastHeadline = {
|
|
27919
|
+
stdout: null,
|
|
27920
|
+
stderr: null,
|
|
27921
|
+
transcript: null
|
|
27922
|
+
};
|
|
27874
27923
|
seen = { error: 0, warning: 0, info: 0 };
|
|
27924
|
+
byScript = new Set;
|
|
27875
27925
|
append(source, chunk) {
|
|
27876
27926
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
27877
27927
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -27895,8 +27945,9 @@ class GameLog {
|
|
|
27895
27945
|
if (line.trim() === "") {
|
|
27896
27946
|
return;
|
|
27897
27947
|
}
|
|
27898
|
-
|
|
27899
|
-
|
|
27948
|
+
const owner = this.lastHeadline[source];
|
|
27949
|
+
if (owner !== null && /^\s/.test(line)) {
|
|
27950
|
+
owner.detail.push(line.trim());
|
|
27900
27951
|
return;
|
|
27901
27952
|
}
|
|
27902
27953
|
const headline = announced(line);
|
|
@@ -27909,7 +27960,13 @@ class GameLog {
|
|
|
27909
27960
|
detail
|
|
27910
27961
|
};
|
|
27911
27962
|
this.entries.push(entry);
|
|
27912
|
-
|
|
27963
|
+
if (headline?.byScript === true) {
|
|
27964
|
+
this.byScript.add(entry.index);
|
|
27965
|
+
}
|
|
27966
|
+
this.lastHeadline[source] = headline ? { detail } : null;
|
|
27967
|
+
}
|
|
27968
|
+
raisedByScript(entry) {
|
|
27969
|
+
return this.byScript.has(entry.index);
|
|
27913
27970
|
}
|
|
27914
27971
|
record(severity, text) {
|
|
27915
27972
|
this.entries.push({
|
|
@@ -27919,7 +27976,6 @@ class GameLog {
|
|
|
27919
27976
|
text,
|
|
27920
27977
|
detail: []
|
|
27921
27978
|
});
|
|
27922
|
-
this.lastHeadline = null;
|
|
27923
27979
|
}
|
|
27924
27980
|
get all() {
|
|
27925
27981
|
return this.entries;
|
|
@@ -27943,6 +27999,11 @@ class GameLog {
|
|
|
27943
27999
|
const omitted = Math.max(0, matching.length - options.limit);
|
|
27944
28000
|
return { entries: matching.slice(omitted), omitted };
|
|
27945
28001
|
}
|
|
28002
|
+
selectFolded(options) {
|
|
28003
|
+
const folded = foldedRepeats(this.select({ ...options, limit: Number.POSITIVE_INFINITY }).entries);
|
|
28004
|
+
const omitted = Math.max(0, folded.length - options.limit);
|
|
28005
|
+
return { entries: folded.slice(omitted), omitted };
|
|
28006
|
+
}
|
|
27946
28007
|
}
|
|
27947
28008
|
function answersTo(entry, severity, contains) {
|
|
27948
28009
|
if (!ADMITTED[severity].has(entry.severity)) {
|
|
@@ -29146,7 +29207,7 @@ function problemsIn(log) {
|
|
|
29146
29207
|
return [];
|
|
29147
29208
|
});
|
|
29148
29209
|
}
|
|
29149
|
-
function
|
|
29210
|
+
function reported2(problems) {
|
|
29150
29211
|
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
29151
29212
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
29152
29213
|
}
|
|
@@ -29231,12 +29292,12 @@ async function importOnce(godotPath, projectPath) {
|
|
|
29231
29292
|
}
|
|
29232
29293
|
const problems = problemsIn(ran.log);
|
|
29233
29294
|
if (ran.exitCode === 0 && ran.failure === null) {
|
|
29234
|
-
return { ok: true, ...
|
|
29295
|
+
return { ok: true, ...reported2(problems) };
|
|
29235
29296
|
}
|
|
29236
29297
|
return {
|
|
29237
29298
|
ok: false,
|
|
29238
29299
|
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
29239
|
-
...
|
|
29300
|
+
...reported2(problems)
|
|
29240
29301
|
};
|
|
29241
29302
|
}
|
|
29242
29303
|
async function runOperation(engine, operation, params, projectPath) {
|
|
@@ -29270,7 +29331,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29270
29331
|
return {
|
|
29271
29332
|
ok: false,
|
|
29272
29333
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
29273
|
-
...
|
|
29334
|
+
...reported2(problems)
|
|
29274
29335
|
};
|
|
29275
29336
|
}
|
|
29276
29337
|
if (answer.kind === "none") {
|
|
@@ -29278,7 +29339,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29278
29339
|
return {
|
|
29279
29340
|
ok: false,
|
|
29280
29341
|
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
29281
|
-
...
|
|
29342
|
+
...reported2(problems)
|
|
29282
29343
|
};
|
|
29283
29344
|
}
|
|
29284
29345
|
const fault = scriptFault(problems, engine.script);
|
|
@@ -29286,14 +29347,14 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29286
29347
|
return {
|
|
29287
29348
|
ok: false,
|
|
29288
29349
|
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
29289
|
-
...
|
|
29350
|
+
...reported2(problems)
|
|
29290
29351
|
};
|
|
29291
29352
|
}
|
|
29292
29353
|
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
29293
29354
|
return {
|
|
29294
29355
|
ok: true,
|
|
29295
29356
|
payload: answer.payload,
|
|
29296
|
-
...
|
|
29357
|
+
...reported2(problems),
|
|
29297
29358
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
29298
29359
|
};
|
|
29299
29360
|
}
|
|
@@ -33528,7 +33589,7 @@ var TOOL_SPECS = [
|
|
|
33528
33589
|
},
|
|
33529
33590
|
{
|
|
33530
33591
|
name: "project_test",
|
|
33531
|
-
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. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
33592
|
+
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. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
33532
33593
|
parameters: {
|
|
33533
33594
|
projectPath: PROJECT_PATH,
|
|
33534
33595
|
path: {
|
|
@@ -33976,7 +34037,7 @@ var TOOL_SPECS = [
|
|
|
33976
34037
|
limit: {
|
|
33977
34038
|
type: "integer",
|
|
33978
34039
|
minimum: 1,
|
|
33979
|
-
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
34040
|
+
description: "The most entries to answer with, newest kept. Identical entries come back as one, at the first of them, with times saying how many were printed, so the limit counts distinct entries. Default 200, and omitted says how many distinct matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
33980
34041
|
},
|
|
33981
34042
|
before: {
|
|
33982
34043
|
ops: ["editor"],
|
|
@@ -35038,7 +35099,6 @@ function notRunNote(notRun, failFast) {
|
|
|
35038
35099
|
function exportAnswer(ending, asked, before, after) {
|
|
35039
35100
|
const { log } = ending;
|
|
35040
35101
|
const written = after !== null && after !== before;
|
|
35041
|
-
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
35042
35102
|
const verdict = {
|
|
35043
35103
|
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
35044
35104
|
...asked,
|
|
@@ -35047,8 +35107,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
35047
35107
|
failure: ending.failure ?? undefined,
|
|
35048
35108
|
errors: log.count("error"),
|
|
35049
35109
|
warnings: log.count("warning"),
|
|
35050
|
-
|
|
35051
|
-
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
35110
|
+
...foldedEntriesAnswer(log)
|
|
35052
35111
|
};
|
|
35053
35112
|
if (verdict.exported) {
|
|
35054
35113
|
return { content: [{ type: "text", text: answerJson(verdict) }] };
|
|
@@ -35298,6 +35357,34 @@ function aboveTheRunner(entry) {
|
|
|
35298
35357
|
]
|
|
35299
35358
|
};
|
|
35300
35359
|
}
|
|
35360
|
+
var MOST_ENGINE_ENTRIES = 200;
|
|
35361
|
+
function foldedEntriesAnswer(log) {
|
|
35362
|
+
const problems = log.selectFolded({
|
|
35363
|
+
severity: "warning",
|
|
35364
|
+
sinceLastCall: false,
|
|
35365
|
+
limit: MOST_ENGINE_ENTRIES
|
|
35366
|
+
});
|
|
35367
|
+
return {
|
|
35368
|
+
entries: problems.entries,
|
|
35369
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
35370
|
+
};
|
|
35371
|
+
}
|
|
35372
|
+
var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
|
|
35373
|
+
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
35374
|
+
try {
|
|
35375
|
+
return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
35376
|
+
} catch {
|
|
35377
|
+
return true;
|
|
35378
|
+
}
|
|
35379
|
+
}
|
|
35380
|
+
function uncaughtNote(uncaught) {
|
|
35381
|
+
const first = uncaught[0];
|
|
35382
|
+
if (first === undefined) {
|
|
35383
|
+
return;
|
|
35384
|
+
}
|
|
35385
|
+
const count = uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`;
|
|
35386
|
+
return `Every case passed, but the run printed ${count} that no case failed on, the first "${first.text}"; ` + "they are under uncaughtScriptErrors. gdUnit4 fails a case on the script errors its logger sees, and " + "it did not see these, so the cases passed on whatever the faulted calls returned. A stack overflow is " + "one it cannot see: it is raised deeper than the logger can be called.";
|
|
35387
|
+
}
|
|
35301
35388
|
function elsewhereIn(projectPath, asked) {
|
|
35302
35389
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
35303
35390
|
if (wanted === "" || existsSync17(join22(projectPath, wanted))) {
|
|
@@ -37083,7 +37170,8 @@ class GodotServer {
|
|
|
37083
37170
|
rmdirSync2(dirname15(reportsDir));
|
|
37084
37171
|
} catch {}
|
|
37085
37172
|
}
|
|
37086
|
-
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit:
|
|
37173
|
+
const engineEntries = foldedRepeats(run.log.select({ severity: "warning", sinceLastCall: false, limit: Number.POSITIVE_INFINITY }).entries.map(aboveTheRunner));
|
|
37174
|
+
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
37087
37175
|
const verdicts = {
|
|
37088
37176
|
0: "passed",
|
|
37089
37177
|
100: "failures",
|
|
@@ -37093,6 +37181,7 @@ class GodotServer {
|
|
|
37093
37181
|
105: "script errors"
|
|
37094
37182
|
};
|
|
37095
37183
|
const exitCode = run.exitCode;
|
|
37184
|
+
const uncaught = report !== null && exitCode === 0 && !timedOut && gdUnitFailsOnScriptErrors(project.value.path) ? run.log.all.filter((entry) => run.log.raisedByScript(entry) && !entry.detail.some((line) => line.includes(ERROR_ASSERT))) : [];
|
|
37096
37185
|
const printed = run.log.select({
|
|
37097
37186
|
severity: "info",
|
|
37098
37187
|
sinceLastCall: false,
|
|
@@ -37106,7 +37195,7 @@ class GodotServer {
|
|
|
37106
37195
|
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
37107
37196
|
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
37108
37197
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
37109
|
-
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
37198
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
37110
37199
|
const nothingAt = missing.join(" and ");
|
|
37111
37200
|
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
37112
37201
|
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
@@ -37156,7 +37245,7 @@ class GodotServer {
|
|
|
37156
37245
|
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
37157
37246
|
...atTheEnd,
|
|
37158
37247
|
arguments: cmdArgs,
|
|
37159
|
-
entries:
|
|
37248
|
+
entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
|
|
37160
37249
|
...printedLines,
|
|
37161
37250
|
savesNote,
|
|
37162
37251
|
...scanned
|
|
@@ -37191,7 +37280,7 @@ class GodotServer {
|
|
|
37191
37280
|
});
|
|
37192
37281
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
37193
37282
|
return this.jsonTextResponse({
|
|
37194
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
37283
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
37195
37284
|
verdict,
|
|
37196
37285
|
...missing.length > 0 ? { missing } : {},
|
|
37197
37286
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
@@ -37204,6 +37293,7 @@ class GodotServer {
|
|
|
37204
37293
|
time: report.time,
|
|
37205
37294
|
failed,
|
|
37206
37295
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
37296
|
+
uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
|
|
37207
37297
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
37208
37298
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
37209
37299
|
notRun: notRun > 0 ? notRun : undefined,
|
|
@@ -37212,6 +37302,7 @@ class GodotServer {
|
|
|
37212
37302
|
...scanned,
|
|
37213
37303
|
note: [
|
|
37214
37304
|
missingNote,
|
|
37305
|
+
uncaughtNote(uncaught),
|
|
37215
37306
|
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)
|
|
37216
37307
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
37217
37308
|
suites: unclean.map((suite) => ({
|
|
@@ -37229,7 +37320,8 @@ class GodotServer {
|
|
|
37229
37320
|
suitesPassed: report.suites.length - unclean.length,
|
|
37230
37321
|
engineErrors: run.log.count("error"),
|
|
37231
37322
|
engineWarnings: run.log.count("warning"),
|
|
37232
|
-
engineEntries,
|
|
37323
|
+
engineEntries: engineEntriesShown,
|
|
37324
|
+
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
37233
37325
|
classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
|
|
37234
37326
|
});
|
|
37235
37327
|
}
|
|
@@ -38938,7 +39030,7 @@ class GodotServer {
|
|
|
38938
39030
|
warnings,
|
|
38939
39031
|
...scanWaitAnswer(scanned),
|
|
38940
39032
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
38941
|
-
|
|
39033
|
+
...foldedEntriesAnswer(boot.log)
|
|
38942
39034
|
});
|
|
38943
39035
|
}
|
|
38944
39036
|
async handleWaitForRun(args) {
|
|
@@ -39022,7 +39114,7 @@ class GodotServer {
|
|
|
39022
39114
|
const asked = readString(args, "severity");
|
|
39023
39115
|
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
39024
39116
|
const contains = readNonEmptyString(args, "contains");
|
|
39025
|
-
const selected = console2.log.
|
|
39117
|
+
const selected = console2.log.selectFolded({
|
|
39026
39118
|
severity,
|
|
39027
39119
|
sinceLastCall: false,
|
|
39028
39120
|
contains,
|
|
@@ -39051,7 +39143,7 @@ class GodotServer {
|
|
|
39051
39143
|
},
|
|
39052
39144
|
repeated: shown,
|
|
39053
39145
|
moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
|
|
39054
|
-
entries:
|
|
39146
|
+
entries: selected.entries,
|
|
39055
39147
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
39056
39148
|
});
|
|
39057
39149
|
}
|
|
@@ -39073,7 +39165,7 @@ class GodotServer {
|
|
|
39073
39165
|
run.endedUnwatched = noCodeWillCome(run);
|
|
39074
39166
|
}
|
|
39075
39167
|
const severity = readString(args, "severity");
|
|
39076
|
-
const selected = run.log.
|
|
39168
|
+
const selected = run.log.selectFolded({
|
|
39077
39169
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
39078
39170
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
39079
39171
|
contains: readNonEmptyString(args, "contains"),
|
|
@@ -39139,7 +39231,7 @@ class GodotServer {
|
|
|
39139
39231
|
cpuSeconds,
|
|
39140
39232
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
39141
39233
|
omitted: selected.omitted,
|
|
39142
|
-
entries:
|
|
39234
|
+
entries: selected.entries
|
|
39143
39235
|
});
|
|
39144
39236
|
}
|
|
39145
39237
|
anotherProjectsRun(runsProject, args, act) {
|
|
@@ -39206,7 +39298,7 @@ class GodotServer {
|
|
|
39206
39298
|
clean: verdict.clean,
|
|
39207
39299
|
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
39208
39300
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
39209
|
-
|
|
39301
|
+
...foldedEntriesAnswer(stopped.log)
|
|
39210
39302
|
});
|
|
39211
39303
|
}
|
|
39212
39304
|
async stopAnnouncedGame(pid, andChildren, args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.56",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|