gdharness 1.2.39 → 1.2.40
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 +1418 -1400
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +15 -2
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +175 -0
- package/build/godot/addons/gdharness_runtime/runtime_items.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +43 -10
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +50 -14
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +48 -18
- package/build/index.js +7954 -7937
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8577,512 +8577,1127 @@ var init_scratch = __esm(() => {
|
|
|
8577
8577
|
];
|
|
8578
8578
|
});
|
|
8579
8579
|
|
|
8580
|
-
// src/
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
return result;
|
|
8590
|
-
}
|
|
8591
|
-
function engineExtras(outcome) {
|
|
8592
|
-
const after = outcome.afterAnswerMessages ?? [];
|
|
8593
|
-
const before = outcome.beforeOperationMessages ?? [];
|
|
8594
|
-
return {
|
|
8595
|
-
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
8596
|
-
...before.length > 0 ? {
|
|
8597
|
-
engine_messages_before_operation: before,
|
|
8598
|
-
engine_messages_before_operation_note: BEFORE_OPERATION_NOTE
|
|
8599
|
-
} : {},
|
|
8600
|
-
...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
|
|
8601
|
-
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
8602
|
-
...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
|
|
8603
|
-
...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
|
|
8604
|
-
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
8605
|
-
};
|
|
8606
|
-
}
|
|
8607
|
-
function problemsIn(log) {
|
|
8608
|
-
return problemsAmong(log.everything());
|
|
8609
|
-
}
|
|
8610
|
-
function problemsAmong(entries) {
|
|
8611
|
-
return entries.flatMap((entry) => {
|
|
8612
|
-
if (entry.severity !== "info") {
|
|
8613
|
-
return [entry];
|
|
8580
|
+
// src/junit.ts
|
|
8581
|
+
function decoded(text) {
|
|
8582
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
|
|
8583
|
+
if (entity.startsWith("#")) {
|
|
8584
|
+
const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
|
|
8585
|
+
if (!Number.isInteger(code) || code < 0 || code > 1114111) {
|
|
8586
|
+
throw new MalformedReportError(`Not a character: ${whole}`);
|
|
8587
|
+
}
|
|
8588
|
+
return String.fromCodePoint(code);
|
|
8614
8589
|
}
|
|
8615
|
-
|
|
8616
|
-
|
|
8590
|
+
const named = ENTITIES[entity];
|
|
8591
|
+
if (named === undefined) {
|
|
8592
|
+
throw new MalformedReportError(`Unknown entity ${whole}`);
|
|
8617
8593
|
}
|
|
8618
|
-
return
|
|
8619
|
-
});
|
|
8620
|
-
}
|
|
8621
|
-
function reported2(problems) {
|
|
8622
|
-
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
8623
|
-
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
8624
|
-
}
|
|
8625
|
-
function reason(log, problems) {
|
|
8626
|
-
const own = refusalsIn(log);
|
|
8627
|
-
if (own.length > 0) {
|
|
8628
|
-
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
8629
|
-
}
|
|
8630
|
-
const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
8631
|
-
if (errors.length > 0) {
|
|
8632
|
-
const rest = errors.length - ERRORS_NAMED;
|
|
8633
|
-
return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
|
|
8634
|
-
}
|
|
8635
|
-
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
8636
|
-
return printed.at(-1)?.text ?? "no output at all";
|
|
8637
|
-
}
|
|
8638
|
-
function refusalsIn(log) {
|
|
8639
|
-
return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
8640
|
-
}
|
|
8641
|
-
function scriptFault(problems, script) {
|
|
8642
|
-
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
8643
|
-
const directories = [...new Set([spelled(dirname6(script)), spelled(resolvedOr(dirname6(script)))])];
|
|
8644
|
-
return problems.find((entry) => {
|
|
8645
|
-
const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
|
|
8646
|
-
return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
|
|
8594
|
+
return named;
|
|
8647
8595
|
});
|
|
8648
8596
|
}
|
|
8649
|
-
function
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8597
|
+
function tagEnd(source, from) {
|
|
8598
|
+
let quote = null;
|
|
8599
|
+
for (let at = from + 1;at < source.length; at += 1) {
|
|
8600
|
+
const character = source[at];
|
|
8601
|
+
if (quote !== null) {
|
|
8602
|
+
if (character === quote) {
|
|
8603
|
+
quote = null;
|
|
8604
|
+
}
|
|
8605
|
+
} else if (character === '"' || character === "'") {
|
|
8606
|
+
quote = character;
|
|
8607
|
+
} else if (character === ">") {
|
|
8608
|
+
return at;
|
|
8609
|
+
}
|
|
8654
8610
|
}
|
|
8611
|
+
return -1;
|
|
8655
8612
|
}
|
|
8656
|
-
function
|
|
8657
|
-
const
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8613
|
+
function parseXml(source) {
|
|
8614
|
+
const root = { name: "", attributes: {}, children: [], text: "" };
|
|
8615
|
+
const open = [root];
|
|
8616
|
+
let at = 0;
|
|
8617
|
+
const current = () => {
|
|
8618
|
+
const element = open.at(-1);
|
|
8619
|
+
if (element === undefined) {
|
|
8620
|
+
throw new MalformedReportError("A close tag with nothing open");
|
|
8621
|
+
}
|
|
8622
|
+
return element;
|
|
8623
|
+
};
|
|
8624
|
+
while (at < source.length) {
|
|
8625
|
+
const next = source.indexOf("<", at);
|
|
8626
|
+
if (next === -1) {
|
|
8627
|
+
current().text += decoded(source.slice(at));
|
|
8628
|
+
break;
|
|
8629
|
+
}
|
|
8630
|
+
current().text += decoded(source.slice(at, next));
|
|
8631
|
+
at = next;
|
|
8632
|
+
if (source.startsWith("<![CDATA[", at)) {
|
|
8633
|
+
const end = source.indexOf("]]>", at);
|
|
8634
|
+
if (end === -1) {
|
|
8635
|
+
throw new MalformedReportError("CDATA that never ends");
|
|
8636
|
+
}
|
|
8637
|
+
current().text += source.slice(at + "<![CDATA[".length, end);
|
|
8638
|
+
at = end + "]]>".length;
|
|
8639
|
+
continue;
|
|
8640
|
+
}
|
|
8641
|
+
if (source.startsWith("<!--", at)) {
|
|
8642
|
+
const end = source.indexOf("-->", at);
|
|
8643
|
+
if (end === -1) {
|
|
8644
|
+
throw new MalformedReportError("A comment that never ends");
|
|
8645
|
+
}
|
|
8646
|
+
at = end + "-->".length;
|
|
8647
|
+
continue;
|
|
8648
|
+
}
|
|
8649
|
+
if (source.startsWith("<?", at) || source.startsWith("<!", at)) {
|
|
8650
|
+
const end = source.indexOf(">", at);
|
|
8651
|
+
if (end === -1) {
|
|
8652
|
+
throw new MalformedReportError("A declaration that never ends");
|
|
8653
|
+
}
|
|
8654
|
+
at = end + 1;
|
|
8655
|
+
continue;
|
|
8656
|
+
}
|
|
8657
|
+
const end = tagEnd(source, at);
|
|
8658
|
+
if (end === -1) {
|
|
8659
|
+
throw new MalformedReportError("A tag that never ends");
|
|
8660
|
+
}
|
|
8661
|
+
const tag = source.slice(at + 1, end);
|
|
8662
|
+
at = end + 1;
|
|
8663
|
+
if (tag.startsWith("/")) {
|
|
8664
|
+
const closing = tag.slice(1).trim();
|
|
8665
|
+
const element = current();
|
|
8666
|
+
if (element === root || element.name !== closing) {
|
|
8667
|
+
throw new MalformedReportError(`</${closing}> closes <${element.name || "nothing"}>`);
|
|
8668
|
+
}
|
|
8669
|
+
open.pop();
|
|
8670
|
+
continue;
|
|
8671
|
+
}
|
|
8672
|
+
const selfClosing = tag.endsWith("/");
|
|
8673
|
+
const body = selfClosing ? tag.slice(0, -1) : tag;
|
|
8674
|
+
const nameMatch = /^([^\s/>]+)/.exec(body);
|
|
8675
|
+
if (!nameMatch?.[1]) {
|
|
8676
|
+
throw new MalformedReportError(`A tag with no name: <${tag}>`);
|
|
8677
|
+
}
|
|
8678
|
+
const attributes = {};
|
|
8679
|
+
let read = nameMatch[0].length;
|
|
8680
|
+
ATTRIBUTE.lastIndex = read;
|
|
8681
|
+
let attribute = ATTRIBUTE.exec(body);
|
|
8682
|
+
while (attribute) {
|
|
8683
|
+
attributes[attribute[1] ?? ""] = decoded(attribute[2] ?? attribute[3] ?? "");
|
|
8684
|
+
read = ATTRIBUTE.lastIndex;
|
|
8685
|
+
attribute = ATTRIBUTE.exec(body);
|
|
8686
|
+
}
|
|
8687
|
+
if (body.slice(read).trim() !== "") {
|
|
8688
|
+
throw new MalformedReportError(`Unreadable attributes in <${tag}>`);
|
|
8689
|
+
}
|
|
8690
|
+
const element = { name: nameMatch[1], attributes, children: [], text: "" };
|
|
8691
|
+
current().children.push(element);
|
|
8692
|
+
if (!selfClosing) {
|
|
8693
|
+
open.push(element);
|
|
8662
8694
|
}
|
|
8663
8695
|
}
|
|
8664
|
-
|
|
8696
|
+
if (open.length !== 1) {
|
|
8697
|
+
throw new MalformedReportError(`<${current().name}> is never closed`);
|
|
8698
|
+
}
|
|
8699
|
+
return root;
|
|
8665
8700
|
}
|
|
8666
|
-
function
|
|
8667
|
-
const
|
|
8668
|
-
return
|
|
8701
|
+
function count(attributes, name) {
|
|
8702
|
+
const value = Number(attributes[name] ?? "0");
|
|
8703
|
+
return Number.isFinite(value) ? value : 0;
|
|
8669
8704
|
}
|
|
8670
|
-
|
|
8671
|
-
const
|
|
8672
|
-
const
|
|
8673
|
-
|
|
8674
|
-
|
|
8705
|
+
function ignoredYetRan(report, ignored) {
|
|
8706
|
+
const found = [];
|
|
8707
|
+
for (const entry of ignored) {
|
|
8708
|
+
const bare = entry.replace("res://", "");
|
|
8709
|
+
const at = bare.lastIndexOf(":");
|
|
8710
|
+
const suiteNamed = at === -1 ? null : bare.slice(0, at);
|
|
8711
|
+
const testNamed = at === -1 ? bare : bare.slice(at + 1);
|
|
8712
|
+
for (const suite of report.suites) {
|
|
8713
|
+
const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
|
|
8714
|
+
const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
|
|
8715
|
+
if (ran !== undefined) {
|
|
8716
|
+
found.push({ entry, ran: `${suite.name}:${ran.name}` });
|
|
8717
|
+
}
|
|
8675
8718
|
}
|
|
8676
|
-
freeLibraryCopies(projectPath);
|
|
8677
|
-
return once(godotPath, projectPath);
|
|
8678
|
-
};
|
|
8679
|
-
const first = await pass();
|
|
8680
|
-
const collided = librariesNotCopied(first.messages);
|
|
8681
|
-
const crash = first.docCacheNote;
|
|
8682
|
-
if (collided.length === 0 && crash === undefined) {
|
|
8683
|
-
return first;
|
|
8684
8719
|
}
|
|
8685
|
-
|
|
8686
|
-
const second = again.ok || crash === undefined ? again : { ...again, message: `${again.message.replace(/[.\s]+$/, "")}. ${crash}` };
|
|
8687
|
-
const still = librariesNotCopied(second.messages);
|
|
8688
|
-
return {
|
|
8689
|
-
...second,
|
|
8690
|
-
...collided.length === 0 ? {} : {
|
|
8691
|
-
librariesRetried: collided,
|
|
8692
|
-
...still.length === 0 ? {} : { librariesNotLoaded: still },
|
|
8693
|
-
extensionNote: extensionRetryNote(collided, still)
|
|
8694
|
-
},
|
|
8695
|
-
...crash === undefined ? {} : { docCacheNote: crash }
|
|
8696
|
-
};
|
|
8720
|
+
return found;
|
|
8697
8721
|
}
|
|
8698
|
-
|
|
8699
|
-
const
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
"--headless",
|
|
8706
|
-
"--log-file",
|
|
8707
|
-
join9(logDir, "engine.log"),
|
|
8708
|
-
"--path",
|
|
8709
|
-
projectPath,
|
|
8710
|
-
"--import"
|
|
8711
|
-
]);
|
|
8712
|
-
} finally {
|
|
8713
|
-
discard(logDir);
|
|
8714
|
-
}
|
|
8715
|
-
const problems = problemsIn(ran.log);
|
|
8716
|
-
const crash = docCacheCrash(ran, cachesBefore, docCachesWritten(caches), "import");
|
|
8717
|
-
if (ran.exitCode === 0 && ran.failure === null) {
|
|
8718
|
-
return { ok: true, ...reported2(problems), ...crash === null ? {} : { docCacheNote: crash } };
|
|
8719
|
-
}
|
|
8722
|
+
function caseOf(suite, element) {
|
|
8723
|
+
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
8724
|
+
const detail = child.text.trim();
|
|
8725
|
+
return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
|
|
8726
|
+
});
|
|
8727
|
+
const reports = [...of("failure"), ...of("error"), ...of("skipped")];
|
|
8728
|
+
const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
|
|
8720
8729
|
return {
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8730
|
+
suite: suite.attributes["name"] ?? "",
|
|
8731
|
+
name: element.attributes["name"] ?? "",
|
|
8732
|
+
status,
|
|
8733
|
+
time: count(element.attributes, "time"),
|
|
8734
|
+
message: reports[0]?.message ?? null,
|
|
8735
|
+
detail: reports[0]?.detail ?? null,
|
|
8736
|
+
reports
|
|
8725
8737
|
};
|
|
8726
8738
|
}
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
const
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
operation,
|
|
8741
|
-
`@file:${paramsFile}`,
|
|
8742
|
-
answerFile,
|
|
8743
|
-
...engine.debug ? ["--debug-godot"] : []
|
|
8744
|
-
];
|
|
8745
|
-
let ran;
|
|
8746
|
-
let answer;
|
|
8747
|
-
try {
|
|
8748
|
-
ran = await runEngine(engine.godotPath, args);
|
|
8749
|
-
answer = answerIn(answerFile);
|
|
8750
|
-
} finally {
|
|
8751
|
-
discard(paramsDir);
|
|
8752
|
-
}
|
|
8753
|
-
const everything = ran.log.everything();
|
|
8754
|
-
const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
|
|
8755
|
-
const answered = marked(ANSWERED);
|
|
8756
|
-
const waiting = marked(WAITING);
|
|
8757
|
-
const begun = marked(BEGUN);
|
|
8758
|
-
const ended = answered === -1 ? everything.length : answered;
|
|
8759
|
-
const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
|
|
8760
|
-
...everything.slice(0, waiting),
|
|
8761
|
-
...begun === -1 ? [] : everything.slice(begun + 1, ended)
|
|
8762
|
-
]);
|
|
8763
|
-
const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
|
|
8764
|
-
const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
|
|
8765
|
-
const afterAnswered = {
|
|
8766
|
-
...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
|
|
8767
|
-
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
|
|
8768
|
-
...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
|
|
8769
|
-
...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
|
|
8770
|
-
};
|
|
8771
|
-
if (answer.kind === "unreadable") {
|
|
8772
|
-
return {
|
|
8773
|
-
ok: false,
|
|
8774
|
-
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8775
|
-
...reported2(problems),
|
|
8776
|
-
...afterAnswered
|
|
8777
|
-
};
|
|
8778
|
-
}
|
|
8779
|
-
if (answer.kind === "none") {
|
|
8780
|
-
if (waiting !== -1 && begun === -1) {
|
|
8781
|
-
return {
|
|
8782
|
-
ok: false,
|
|
8783
|
-
message: `${operation} never ran: the engine ended (${howItEnded(ran)}) while it started the project's ` + `autoloads, which this operation waits for. The last it reported: ${reason(ran.log, startup)}`,
|
|
8784
|
-
...reported2(problems),
|
|
8785
|
-
...afterAnswered
|
|
8786
|
-
};
|
|
8787
|
-
}
|
|
8788
|
-
const refusals = refusalsIn(ran.log);
|
|
8789
|
-
if (refusals.length > 0) {
|
|
8790
|
-
const said = new Set(refusals.map((entry) => entry.index));
|
|
8791
|
-
return {
|
|
8792
|
-
ok: false,
|
|
8793
|
-
message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
|
|
8794
|
-
...reported2(problems.filter((entry) => !said.has(entry.index)))
|
|
8795
|
-
};
|
|
8796
|
-
}
|
|
8797
|
-
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
8798
|
-
return {
|
|
8799
|
-
ok: false,
|
|
8800
|
-
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
8801
|
-
...reported2(problems)
|
|
8802
|
-
};
|
|
8739
|
+
function failedAnAssertion(entry) {
|
|
8740
|
+
return entry.reports.some((report) => report.kind === "failure");
|
|
8741
|
+
}
|
|
8742
|
+
function pathOf(suite) {
|
|
8743
|
+
const directory = suite.attributes["package"];
|
|
8744
|
+
const name = suite.attributes["name"];
|
|
8745
|
+
return directory && name ? `res://${directory}/${name}.gd` : null;
|
|
8746
|
+
}
|
|
8747
|
+
function parseJUnit(xml) {
|
|
8748
|
+
const document = parseXml(xml);
|
|
8749
|
+
const top = document.children.find((child) => child.name === "testsuites" || child.name === "testsuite");
|
|
8750
|
+
if (top === undefined) {
|
|
8751
|
+
throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
|
|
8803
8752
|
}
|
|
8804
|
-
const
|
|
8805
|
-
|
|
8753
|
+
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
8754
|
+
const suites = suiteElements.map((suite) => {
|
|
8755
|
+
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
8756
|
+
const failures = cases.filter(failedAnAssertion).length;
|
|
8757
|
+
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
8758
|
+
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
8806
8759
|
return {
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8760
|
+
name: suite.attributes["name"] ?? "",
|
|
8761
|
+
path: pathOf(suite),
|
|
8762
|
+
tests: cases.length,
|
|
8763
|
+
discovered: count(suite.attributes, "tests"),
|
|
8764
|
+
failures,
|
|
8765
|
+
errors,
|
|
8766
|
+
hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
|
|
8767
|
+
hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
|
|
8768
|
+
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
8769
|
+
time: count(suite.attributes, "time"),
|
|
8770
|
+
cases
|
|
8811
8771
|
};
|
|
8812
|
-
}
|
|
8813
|
-
const
|
|
8772
|
+
});
|
|
8773
|
+
const all = suites.flatMap((suite) => suite.cases);
|
|
8814
8774
|
return {
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8775
|
+
tests: all.length,
|
|
8776
|
+
failures: all.filter(failedAnAssertion).length,
|
|
8777
|
+
errors: all.filter((entry) => entry.status === "error").length,
|
|
8778
|
+
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
8779
|
+
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
8780
|
+
skipped: all.filter((entry) => entry.status === "skipped").length,
|
|
8781
|
+
time: suites.reduce((sum, suite) => sum + suite.time, 0),
|
|
8782
|
+
suites
|
|
8820
8783
|
};
|
|
8821
8784
|
}
|
|
8822
|
-
function
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
try {
|
|
8828
|
-
const parsed = JSON.parse(text);
|
|
8829
|
-
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
|
|
8830
|
-
} catch (error) {
|
|
8831
|
-
return {
|
|
8832
|
-
kind: "unreadable",
|
|
8833
|
-
why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
|
|
8834
|
-
};
|
|
8835
|
-
}
|
|
8836
|
-
}
|
|
8837
|
-
var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", WAITING = "[gdharness] waiting for the autoloads", BEGUN = "[gdharness] begun", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
8838
|
-
var init_headless = __esm(() => {
|
|
8839
|
-
init_doc_cache();
|
|
8840
|
-
init_engine_run();
|
|
8841
|
-
init_library_copies();
|
|
8842
|
-
init_reimport();
|
|
8843
|
-
init_scratch();
|
|
8844
|
-
});
|
|
8845
|
-
|
|
8846
|
-
// src/update-check.ts
|
|
8847
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8848
|
-
import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
|
|
8849
|
-
import { join as join10 } from "node:path";
|
|
8850
|
-
function packageUrl(root) {
|
|
8851
|
-
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
8785
|
+
function hookFailuresPrinted(printed) {
|
|
8786
|
+
return printed.flatMap((line) => {
|
|
8787
|
+
const found = HOOK_FAILURE.exec(line);
|
|
8788
|
+
return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
|
|
8789
|
+
});
|
|
8852
8790
|
}
|
|
8853
|
-
function
|
|
8854
|
-
const
|
|
8855
|
-
if (
|
|
8856
|
-
return
|
|
8857
|
-
}
|
|
8858
|
-
try {
|
|
8859
|
-
return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
|
|
8860
|
-
} catch {
|
|
8861
|
-
return packageUrl(REGISTRY_ROOT);
|
|
8791
|
+
function whyNoReport(printed, asked) {
|
|
8792
|
+
const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
|
|
8793
|
+
if (missing !== undefined) {
|
|
8794
|
+
return `nothing at ${missing[1] ?? asked}`;
|
|
8862
8795
|
}
|
|
8796
|
+
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
8863
8797
|
}
|
|
8864
|
-
function
|
|
8865
|
-
|
|
8866
|
-
const value = environment[name];
|
|
8867
|
-
return value !== undefined && value !== "" ? value : null;
|
|
8868
|
-
};
|
|
8869
|
-
const home = set("HOME") ?? homedir4();
|
|
8870
|
-
if (process.platform === "win32") {
|
|
8871
|
-
return join10(set("LOCALAPPDATA") ?? home, "gdharness");
|
|
8872
|
-
}
|
|
8873
|
-
if (process.platform === "darwin") {
|
|
8874
|
-
return join10(home, "Library", "Caches", "gdharness");
|
|
8875
|
-
}
|
|
8876
|
-
return join10(set("XDG_CACHE_HOME") ?? join10(home, ".cache"), "gdharness");
|
|
8798
|
+
function scriptErrorsPrinted(printed) {
|
|
8799
|
+
return discoveryListing(printed)?.errors ?? [];
|
|
8877
8800
|
}
|
|
8878
|
-
function
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
return join10(directory, "update-check.json");
|
|
8883
|
-
} catch {
|
|
8884
|
-
return join10(tmpdir3(), "gdharness-update-check.json");
|
|
8801
|
+
function discoveryListing(printed) {
|
|
8802
|
+
const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
|
|
8803
|
+
if (start < 0) {
|
|
8804
|
+
return null;
|
|
8885
8805
|
}
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
const latest = record["latest"];
|
|
8899
|
-
if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
|
|
8900
|
-
return null;
|
|
8806
|
+
const found = [];
|
|
8807
|
+
let message = null;
|
|
8808
|
+
let to = start + 1;
|
|
8809
|
+
for (const line of printed.slice(start + 1)) {
|
|
8810
|
+
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
8811
|
+
if (at !== null && message !== null) {
|
|
8812
|
+
found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
|
|
8813
|
+
message = null;
|
|
8814
|
+
} else if (at === null && message === null && /^ {2}\S/.test(line)) {
|
|
8815
|
+
message = line.trim();
|
|
8816
|
+
} else {
|
|
8817
|
+
break;
|
|
8901
8818
|
}
|
|
8902
|
-
|
|
8903
|
-
} catch {
|
|
8904
|
-
return null;
|
|
8819
|
+
to += 1;
|
|
8905
8820
|
}
|
|
8821
|
+
const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
8822
|
+
return { errors, from: start, to };
|
|
8906
8823
|
}
|
|
8907
|
-
function
|
|
8908
|
-
|
|
8909
|
-
writeFileSync5(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
|
|
8910
|
-
} catch {}
|
|
8824
|
+
function unspelled(text) {
|
|
8825
|
+
return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
|
|
8911
8826
|
}
|
|
8912
|
-
function
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8827
|
+
function readDiff(printed) {
|
|
8828
|
+
let merged = "";
|
|
8829
|
+
let actual = "";
|
|
8830
|
+
let background = null;
|
|
8831
|
+
let last = 0;
|
|
8832
|
+
const take = (text) => {
|
|
8833
|
+
merged += text;
|
|
8834
|
+
if (background === null) {
|
|
8835
|
+
actual += text;
|
|
8836
|
+
} else if (background === "added") {
|
|
8837
|
+
actual += unspelled(text);
|
|
8838
|
+
}
|
|
8839
|
+
};
|
|
8840
|
+
for (const match of printed.matchAll(COLOUR)) {
|
|
8841
|
+
take(printed.slice(last, match.index));
|
|
8842
|
+
last = match.index + match[0].length;
|
|
8843
|
+
const codes = (match[1] ?? "").split(";");
|
|
8844
|
+
if (codes[0] === "48" && codes[1] === "2") {
|
|
8845
|
+
background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
|
|
8846
|
+
} else if (codes[0] === "0" || codes[0] === "") {
|
|
8847
|
+
background = null;
|
|
8929
8848
|
}
|
|
8930
8849
|
}
|
|
8931
|
-
|
|
8850
|
+
take(printed.slice(last));
|
|
8851
|
+
return { merged, actual };
|
|
8932
8852
|
}
|
|
8933
|
-
|
|
8934
|
-
const
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8853
|
+
function withActualsPrinted(failed, printed) {
|
|
8854
|
+
const diffs = [...printed.replaceAll(`\r
|
|
8855
|
+
`, `
|
|
8856
|
+
`).matchAll(PRINTED_ACTUAL)].map((match) => ({
|
|
8857
|
+
suffix: match[3] ?? "",
|
|
8858
|
+
at: match[5] ?? "",
|
|
8859
|
+
...readDiff(match[1] ?? "")
|
|
8860
|
+
}));
|
|
8861
|
+
const used = new Set;
|
|
8862
|
+
return failed.map((entry) => {
|
|
8863
|
+
if (entry.detail === null) {
|
|
8864
|
+
return entry;
|
|
8865
|
+
}
|
|
8866
|
+
let detail = entry.detail;
|
|
8867
|
+
let recovered = false;
|
|
8868
|
+
for (const [index, diff] of diffs.entries()) {
|
|
8869
|
+
if (used.has(index)) {
|
|
8870
|
+
continue;
|
|
8871
|
+
}
|
|
8872
|
+
const wrong = ` but was
|
|
8873
|
+
'${diff.merged.replaceAll("\r", "")}'${diff.suffix}
|
|
8874
|
+
at ${diff.at}`;
|
|
8875
|
+
if (detail.includes(wrong)) {
|
|
8876
|
+
detail = detail.replace(wrong, () => ` but was
|
|
8877
|
+
'${diff.actual}'${diff.suffix}
|
|
8878
|
+
at ${diff.at}`);
|
|
8879
|
+
recovered = true;
|
|
8880
|
+
used.add(index);
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
const quoted = QUOTED_EQUALITY.exec(detail);
|
|
8884
|
+
if (quoted !== null) {
|
|
8885
|
+
const alike = quoted[1] === quoted[2];
|
|
8886
|
+
const note = recovered ? alike ? RENDERED_AWAY_NOTE : null : alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE;
|
|
8887
|
+
detail = note === null ? detail : `${detail}
|
|
8888
|
+
${note}`;
|
|
8889
|
+
}
|
|
8890
|
+
if (quoted !== null && masked(quoted[1] ?? "", quoted[2] ?? "")) {
|
|
8891
|
+
const end = quoted[0].length - `
|
|
8892
|
+
at `.length;
|
|
8893
|
+
detail = detail.slice(0, end).replaceAll(MASKED_BRACKET, "[") + detail.slice(end);
|
|
8894
|
+
}
|
|
8895
|
+
return detail === entry.detail ? entry : { ...entry, detail };
|
|
8938
8896
|
});
|
|
8939
|
-
|
|
8940
|
-
|
|
8897
|
+
}
|
|
8898
|
+
function shortened(detail) {
|
|
8899
|
+
if (detail === null || detail.length <= LONGEST_DETAIL) {
|
|
8900
|
+
return detail;
|
|
8941
8901
|
}
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8902
|
+
return `${detail.slice(0, LONGEST_DETAIL)}
|
|
8903
|
+
[${detail.length - LONGEST_DETAIL} more characters left out]`;
|
|
8904
|
+
}
|
|
8905
|
+
function foldedReports(reports) {
|
|
8906
|
+
const first = new Map;
|
|
8907
|
+
for (const report of reports) {
|
|
8908
|
+
const key = JSON.stringify([report.kind, report.message, report.detail]);
|
|
8909
|
+
const seen = first.get(key);
|
|
8910
|
+
if (seen === undefined) {
|
|
8911
|
+
first.set(key, { report, times: 1 });
|
|
8912
|
+
} else {
|
|
8913
|
+
seen.times += 1;
|
|
8914
|
+
}
|
|
8945
8915
|
}
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8916
|
+
return [...first.values()].map(({ report, times }) => {
|
|
8917
|
+
const kept = { ...report, detail: shortened(report.detail) };
|
|
8918
|
+
return times === 1 ? kept : { ...kept, times };
|
|
8919
|
+
});
|
|
8920
|
+
}
|
|
8921
|
+
function failedCases(report, printed) {
|
|
8922
|
+
const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
|
|
8923
|
+
const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
|
|
8924
|
+
let next = 0;
|
|
8925
|
+
return cases.map(({ entry, path }) => {
|
|
8926
|
+
const reports = read.slice(next, next + entry.reports.length);
|
|
8927
|
+
next += entry.reports.length;
|
|
8928
|
+
const folded = foldedReports(reports);
|
|
8929
|
+
const shown = folded.slice(0, MOST_REPORTS_SHOWN);
|
|
8930
|
+
return {
|
|
8931
|
+
suite: entry.suite,
|
|
8932
|
+
name: entry.name,
|
|
8933
|
+
status: entry.status,
|
|
8934
|
+
time: entry.time,
|
|
8935
|
+
path,
|
|
8936
|
+
message: reports[0]?.message ?? null,
|
|
8937
|
+
detail: shortened(reports[0]?.detail ?? null),
|
|
8938
|
+
...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
|
|
8939
|
+
...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
|
|
8940
|
+
};
|
|
8941
|
+
});
|
|
8942
|
+
}
|
|
8943
|
+
function failedWithin(failed, budget) {
|
|
8944
|
+
const sizes = failed.map((entry) => JSON.stringify(entry).length);
|
|
8945
|
+
const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
|
|
8946
|
+
const whole = new Set;
|
|
8947
|
+
let spent = 0;
|
|
8948
|
+
for (const index of smallestFirst) {
|
|
8949
|
+
const size = sizes[index] ?? 0;
|
|
8950
|
+
if (spent + size > budget) {
|
|
8951
|
+
break;
|
|
8952
8952
|
}
|
|
8953
|
-
|
|
8953
|
+
spent += size;
|
|
8954
|
+
whole.add(index);
|
|
8954
8955
|
}
|
|
8955
|
-
const
|
|
8956
|
-
|
|
8957
|
-
|
|
8956
|
+
const kept = failed.map((entry, index) => {
|
|
8957
|
+
if (whole.has(index)) {
|
|
8958
|
+
return entry;
|
|
8959
|
+
}
|
|
8960
|
+
const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
|
|
8961
|
+
return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
|
|
8962
|
+
});
|
|
8963
|
+
return { failed: kept, briefed: failed.length - whole.size };
|
|
8964
|
+
}
|
|
8965
|
+
function masked(expected, actual) {
|
|
8966
|
+
const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
|
|
8967
|
+
return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
|
|
8968
|
+
}
|
|
8969
|
+
function orphansPrinted(printed) {
|
|
8970
|
+
const suites = [];
|
|
8971
|
+
let total = null;
|
|
8972
|
+
let running = null;
|
|
8973
|
+
for (const line of printed) {
|
|
8974
|
+
const started = RUNNING_SUITE.exec(line);
|
|
8975
|
+
if (started?.[1] !== undefined) {
|
|
8976
|
+
running = started[1];
|
|
8977
|
+
continue;
|
|
8978
|
+
}
|
|
8979
|
+
const counted = ORPHANS.exec(line);
|
|
8980
|
+
if (counted?.[1] === undefined) {
|
|
8981
|
+
continue;
|
|
8982
|
+
}
|
|
8983
|
+
const orphans = Number(counted[1]);
|
|
8984
|
+
if (line.includes(OVERALL)) {
|
|
8985
|
+
total = orphans;
|
|
8986
|
+
running = null;
|
|
8987
|
+
continue;
|
|
8988
|
+
}
|
|
8989
|
+
if (running !== null && orphans > 0) {
|
|
8990
|
+
suites.push({ path: running, orphans });
|
|
8991
|
+
}
|
|
8992
|
+
running = null;
|
|
8958
8993
|
}
|
|
8959
|
-
|
|
8960
|
-
|
|
8994
|
+
return {
|
|
8995
|
+
total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
|
|
8996
|
+
suites
|
|
8997
|
+
};
|
|
8961
8998
|
}
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
const
|
|
8980
|
-
if (
|
|
8981
|
-
|
|
8982
|
-
|
|
8999
|
+
function finishesASuite(line) {
|
|
9000
|
+
return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
|
|
9001
|
+
}
|
|
9002
|
+
function suitesPrinted(printed) {
|
|
9003
|
+
const finished = [];
|
|
9004
|
+
let running = null;
|
|
9005
|
+
let failed = [];
|
|
9006
|
+
for (const line of printed) {
|
|
9007
|
+
const started = RUNNING_SUITE.exec(line);
|
|
9008
|
+
if (started?.[1] !== undefined) {
|
|
9009
|
+
running = started[1];
|
|
9010
|
+
failed = [];
|
|
9011
|
+
continue;
|
|
9012
|
+
}
|
|
9013
|
+
if (running === null) {
|
|
9014
|
+
continue;
|
|
9015
|
+
}
|
|
9016
|
+
const failing = CASE_FAILED.exec(line)?.[1];
|
|
9017
|
+
if (failing !== undefined) {
|
|
9018
|
+
failed.push(failing);
|
|
9019
|
+
continue;
|
|
9020
|
+
}
|
|
9021
|
+
const counted = finishesASuite(line) ? SUITE_COUNTS.exec(line) : null;
|
|
9022
|
+
if (counted !== null) {
|
|
9023
|
+
finished.push({
|
|
9024
|
+
path: running,
|
|
9025
|
+
cases: Number(counted[1]),
|
|
9026
|
+
errors: Number(counted[2]),
|
|
9027
|
+
failures: Number(counted[3]),
|
|
9028
|
+
failed
|
|
9029
|
+
});
|
|
9030
|
+
running = null;
|
|
8983
9031
|
}
|
|
8984
9032
|
}
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
9033
|
+
return { finished, running };
|
|
9034
|
+
}
|
|
9035
|
+
function testProgressMessage(printed, elapsedMs) {
|
|
9036
|
+
const { finished, running } = suitesPrinted(printed);
|
|
9037
|
+
const failing = finished.filter((suite) => suite.errors + suite.failures > 0).length;
|
|
9038
|
+
const seconds = Math.floor(elapsedMs / 1000);
|
|
9039
|
+
const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
9040
|
+
const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
|
|
9041
|
+
return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
|
|
9042
|
+
}
|
|
9043
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
|
|
9044
|
+
var init_junit = __esm(() => {
|
|
9045
|
+
MalformedReportError = class MalformedReportError extends Error {
|
|
9046
|
+
constructor(message) {
|
|
9047
|
+
super(message);
|
|
9048
|
+
this.name = "MalformedReportError";
|
|
8988
9049
|
}
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
9050
|
+
};
|
|
9051
|
+
ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
|
|
9052
|
+
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
9053
|
+
HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
9054
|
+
NOTHING_RAN = /no test cases found/i;
|
|
9055
|
+
NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
9056
|
+
ESCAPE = String.fromCharCode(27);
|
|
9057
|
+
PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
9058
|
+
QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
|
|
9059
|
+
COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
9060
|
+
SPELLED = {
|
|
9061
|
+
"<LF>": `
|
|
9062
|
+
`,
|
|
9063
|
+
"<CR>": "\r",
|
|
9064
|
+
"<TAB>": "\t",
|
|
9065
|
+
"<BS>": "\b",
|
|
9066
|
+
"<FF>": "\f",
|
|
9067
|
+
"<VT>": "\v",
|
|
9068
|
+
"<BEL>": "\x07",
|
|
9069
|
+
"<ESC>": ESCAPE,
|
|
9070
|
+
"<DEL>": ""
|
|
9071
|
+
};
|
|
9072
|
+
PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
9073
|
+
RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
9074
|
+
ORPHANS = /(\d+)\s+orphans/;
|
|
9075
|
+
SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
|
|
9076
|
+
CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
|
|
9077
|
+
});
|
|
9078
|
+
|
|
9079
|
+
// src/script-origins.ts
|
|
9080
|
+
import { join as join9 } from "node:path";
|
|
9081
|
+
function unresolvedClassIn(message) {
|
|
9082
|
+
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
9083
|
+
}
|
|
9084
|
+
async function originsOf(errors, declared, check) {
|
|
9085
|
+
const listed = new Set(errors.map((error) => error.path));
|
|
9086
|
+
const origins = [];
|
|
9087
|
+
const unconfirmed = [];
|
|
9088
|
+
const asked = new Set;
|
|
9089
|
+
const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
|
|
9090
|
+
while (waiting.length > 0 && asked.size < MOST_CHECKED) {
|
|
9091
|
+
const name = waiting.shift() ?? "";
|
|
9092
|
+
const path = declared.get(name);
|
|
9093
|
+
if (path === undefined || listed.has(path) || asked.has(path)) {
|
|
9094
|
+
continue;
|
|
9095
|
+
}
|
|
9096
|
+
asked.add(path);
|
|
9097
|
+
const checked = await check(path);
|
|
9098
|
+
const own = checked.errors.filter((error) => error.path === path);
|
|
9099
|
+
if (own.length === 0) {
|
|
9100
|
+
unconfirmed.push({
|
|
9101
|
+
path,
|
|
9102
|
+
declares: name,
|
|
9103
|
+
why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
|
|
9104
|
+
});
|
|
9105
|
+
continue;
|
|
9106
|
+
}
|
|
9107
|
+
for (const error of own) {
|
|
9108
|
+
origins.push({ ...error, declares: name });
|
|
9109
|
+
const deeper = unresolvedClassIn(error.message);
|
|
9110
|
+
if (deeper !== null) {
|
|
9111
|
+
waiting.push(deeper);
|
|
8995
9112
|
}
|
|
8996
|
-
|
|
8997
|
-
this.checkedAt = Date.now();
|
|
8998
|
-
this.backoffMs = FIRST_RETRY_MS;
|
|
8999
|
-
this.retryAt = 0;
|
|
9000
|
-
writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
|
|
9001
|
-
}).catch(() => {
|
|
9002
|
-
this.scheduleRetry(now);
|
|
9003
|
-
}).finally(() => {
|
|
9004
|
-
this.checking = false;
|
|
9005
|
-
});
|
|
9113
|
+
}
|
|
9006
9114
|
}
|
|
9007
|
-
|
|
9008
|
-
|
|
9115
|
+
return { origins, unconfirmed };
|
|
9116
|
+
}
|
|
9117
|
+
function notCompiledIn(entry) {
|
|
9118
|
+
return NOT_COMPILED.exec(entry.text)?.[1];
|
|
9119
|
+
}
|
|
9120
|
+
function compileErrorsIn(entries) {
|
|
9121
|
+
return entries.flatMap((entry) => {
|
|
9122
|
+
const at = entry.severity === "error" ? reloadedAt(entry) : null;
|
|
9123
|
+
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
9124
|
+
});
|
|
9125
|
+
}
|
|
9126
|
+
function reloadedAt(entry) {
|
|
9127
|
+
for (const line of entry.detail) {
|
|
9128
|
+
const at = RELOADED_AT.exec(line.trim());
|
|
9129
|
+
if (at !== null) {
|
|
9130
|
+
return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
|
|
9131
|
+
}
|
|
9009
9132
|
}
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9133
|
+
return null;
|
|
9134
|
+
}
|
|
9135
|
+
function checkWithEngine(godotPath, projectPath) {
|
|
9136
|
+
return async (path) => {
|
|
9137
|
+
const logDir = scratchDirectory("tests");
|
|
9138
|
+
try {
|
|
9139
|
+
const ran = await runEngine(godotPath, [
|
|
9140
|
+
"--headless",
|
|
9141
|
+
"--log-file",
|
|
9142
|
+
join9(logDir, "engine.log"),
|
|
9143
|
+
"--path",
|
|
9144
|
+
projectPath,
|
|
9145
|
+
"--check-only",
|
|
9146
|
+
"-s",
|
|
9147
|
+
path
|
|
9148
|
+
], { timeout: { ms: 60000, said: "minute" } });
|
|
9149
|
+
const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
|
|
9150
|
+
const at = reloadedAt(entry);
|
|
9151
|
+
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
9152
|
+
});
|
|
9153
|
+
return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
|
|
9154
|
+
} finally {
|
|
9155
|
+
discard(logDir);
|
|
9156
|
+
}
|
|
9157
|
+
};
|
|
9158
|
+
}
|
|
9159
|
+
function withoutListed(entries, errors) {
|
|
9160
|
+
if (errors.length === 0) {
|
|
9161
|
+
return [...entries];
|
|
9013
9162
|
}
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9163
|
+
const listing = discoveryListing(entries.map((entry) => entry.text));
|
|
9164
|
+
const paths = new Set(errors.map((error) => error.path));
|
|
9165
|
+
return entries.filter((entry, at) => {
|
|
9166
|
+
if (listing !== null && at >= listing.from && at < listing.to) {
|
|
9167
|
+
return false;
|
|
9018
9168
|
}
|
|
9019
|
-
|
|
9020
|
-
|
|
9169
|
+
const reloaded = reloadedAt(entry);
|
|
9170
|
+
if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
|
|
9171
|
+
return false;
|
|
9021
9172
|
}
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
releaseNotes: `${RELEASES}/v${latest}`,
|
|
9026
|
-
upgrade: runLine(currentRunner(), latest, "upgrade")
|
|
9027
|
-
};
|
|
9028
|
-
}
|
|
9173
|
+
const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
|
|
9174
|
+
return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
|
|
9175
|
+
});
|
|
9029
9176
|
}
|
|
9030
|
-
var
|
|
9031
|
-
var
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9177
|
+
var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT, NOT_COMPILED;
|
|
9178
|
+
var init_script_origins = __esm(() => {
|
|
9179
|
+
init_engine_run();
|
|
9180
|
+
init_junit();
|
|
9181
|
+
init_scratch();
|
|
9182
|
+
UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
9183
|
+
RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
9184
|
+
NOT_COMPILED = /^Failed to load script "(res:\/\/[^"]+)" with error "(?:Parse error|Compilation failed)"/;
|
|
9037
9185
|
});
|
|
9038
9186
|
|
|
9039
|
-
// src/
|
|
9040
|
-
import { readFileSync as
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9187
|
+
// src/headless.ts
|
|
9188
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9189
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
9190
|
+
function snakeCased(params) {
|
|
9191
|
+
const result = emptyRecord();
|
|
9192
|
+
for (const [key, value] of Object.entries(params)) {
|
|
9193
|
+
const name = key.startsWith("_") ? key : key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
9194
|
+
result[name] = value;
|
|
9044
9195
|
}
|
|
9045
|
-
return
|
|
9196
|
+
return result;
|
|
9046
9197
|
}
|
|
9047
|
-
function
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9198
|
+
function engineExtras(outcome) {
|
|
9199
|
+
const after = outcome.afterAnswerMessages ?? [];
|
|
9200
|
+
const before = outcome.beforeOperationMessages ?? [];
|
|
9201
|
+
return {
|
|
9202
|
+
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
9203
|
+
...before.length > 0 ? {
|
|
9204
|
+
engine_messages_before_operation: before,
|
|
9205
|
+
engine_messages_before_operation_note: withNotCompiled(BEFORE_OPERATION_NOTE, before)
|
|
9206
|
+
} : {},
|
|
9207
|
+
...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
|
|
9208
|
+
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
9209
|
+
...after.length > 0 ? {
|
|
9210
|
+
engine_messages_after_answer: after,
|
|
9211
|
+
engine_messages_after_answer_note: withNotCompiled(AFTER_ANSWER_NOTE, after)
|
|
9212
|
+
} : {},
|
|
9213
|
+
...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
|
|
9214
|
+
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
9215
|
+
};
|
|
9053
9216
|
}
|
|
9054
|
-
function
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
}
|
|
9058
|
-
const reported = addonVersion ?? "";
|
|
9059
|
-
const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: run ${gdharnessCommand(serverVersion, "upgrade")} in a shell in the project directory, which installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
|
|
9060
|
-
if (reported === serverVersion) {
|
|
9061
|
-
return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
|
|
9062
|
-
}
|
|
9063
|
-
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
9064
|
-
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
9065
|
-
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
9066
|
-
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
9067
|
-
}
|
|
9068
|
-
return `${both} ${restart}`;
|
|
9217
|
+
function withNotCompiled(note, entries) {
|
|
9218
|
+
const path = entries.map(notCompiledIn).find((found) => found !== undefined);
|
|
9219
|
+
return path === undefined ? note : `${note} ${path} did not compile, so every run of this project starts its autoloads without it until it is fixed; its errors are among these.`;
|
|
9069
9220
|
}
|
|
9070
|
-
function
|
|
9071
|
-
return
|
|
9221
|
+
function problemsIn(log) {
|
|
9222
|
+
return problemsAmong(log.everything());
|
|
9072
9223
|
}
|
|
9073
|
-
function
|
|
9074
|
-
return
|
|
9224
|
+
function problemsAmong(entries) {
|
|
9225
|
+
return entries.flatMap((entry) => {
|
|
9226
|
+
if (entry.severity !== "info") {
|
|
9227
|
+
return [entry];
|
|
9228
|
+
}
|
|
9229
|
+
if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
|
|
9230
|
+
return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
|
|
9231
|
+
}
|
|
9232
|
+
return [];
|
|
9233
|
+
});
|
|
9075
9234
|
}
|
|
9076
|
-
function
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9235
|
+
function reported2(problems) {
|
|
9236
|
+
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
9237
|
+
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
9238
|
+
}
|
|
9239
|
+
function reason(log, problems) {
|
|
9240
|
+
const own = refusalsIn(log);
|
|
9241
|
+
if (own.length > 0) {
|
|
9242
|
+
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
9082
9243
|
}
|
|
9083
|
-
const
|
|
9084
|
-
if (
|
|
9085
|
-
|
|
9244
|
+
const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
9245
|
+
if (errors.length > 0) {
|
|
9246
|
+
const rest = errors.length - ERRORS_NAMED;
|
|
9247
|
+
return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
|
|
9248
|
+
}
|
|
9249
|
+
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
9250
|
+
return printed.at(-1)?.text ?? "no output at all";
|
|
9251
|
+
}
|
|
9252
|
+
function refusalsIn(log) {
|
|
9253
|
+
return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
9254
|
+
}
|
|
9255
|
+
function scriptFault(problems, script) {
|
|
9256
|
+
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
9257
|
+
const directories = [...new Set([spelled(dirname6(script)), spelled(resolvedOr(dirname6(script)))])];
|
|
9258
|
+
return problems.find((entry) => {
|
|
9259
|
+
const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
|
|
9260
|
+
return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
|
|
9261
|
+
});
|
|
9262
|
+
}
|
|
9263
|
+
function resolvedOr(path) {
|
|
9264
|
+
try {
|
|
9265
|
+
return realpathSync2.native(path);
|
|
9266
|
+
} catch {
|
|
9267
|
+
return path;
|
|
9268
|
+
}
|
|
9269
|
+
}
|
|
9270
|
+
function librariesNotCopied(messages) {
|
|
9271
|
+
const found = new Set;
|
|
9272
|
+
for (const entry of messages) {
|
|
9273
|
+
const library = /Error copying library: (.+)$/.exec(entry.text)?.[1]?.trim();
|
|
9274
|
+
if (library !== undefined && library !== "") {
|
|
9275
|
+
found.add(library);
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
return [...found];
|
|
9279
|
+
}
|
|
9280
|
+
function extensionRetryNote(libraries, stillNotLoaded = []) {
|
|
9281
|
+
const second = stillNotLoaded.length === 0 ? "so the import was run again and loaded the extension, and the messages are from that second run." : `so the import was run again, and it still could not load ${stillNotLoaded.join(" and ")}: this answer is from an import without ${stillNotLoaded.length === 1 ? "it" : "them"}, and its messages say what the engine hit.`;
|
|
9282
|
+
return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "the next import, export or editor this server starts on the project deletes once the editor has exited.";
|
|
9283
|
+
}
|
|
9284
|
+
async function runImport(godotPath, projectPath, options = {}) {
|
|
9285
|
+
const once = options.once ?? importOnce;
|
|
9286
|
+
const pass = () => {
|
|
9287
|
+
for (const target of options.forcing ?? []) {
|
|
9288
|
+
forceNextImport(projectPath, target);
|
|
9289
|
+
}
|
|
9290
|
+
freeLibraryCopies(projectPath);
|
|
9291
|
+
return once(godotPath, projectPath);
|
|
9292
|
+
};
|
|
9293
|
+
const first = await pass();
|
|
9294
|
+
const collided = librariesNotCopied(first.messages);
|
|
9295
|
+
const crash = first.docCacheNote;
|
|
9296
|
+
if (collided.length === 0 && crash === undefined) {
|
|
9297
|
+
return first;
|
|
9298
|
+
}
|
|
9299
|
+
const { docCacheNote: _, ...again } = await pass();
|
|
9300
|
+
const second = again.ok || crash === undefined ? again : { ...again, message: `${again.message.replace(/[.\s]+$/, "")}. ${crash}` };
|
|
9301
|
+
const still = librariesNotCopied(second.messages);
|
|
9302
|
+
return {
|
|
9303
|
+
...second,
|
|
9304
|
+
...collided.length === 0 ? {} : {
|
|
9305
|
+
librariesRetried: collided,
|
|
9306
|
+
...still.length === 0 ? {} : { librariesNotLoaded: still },
|
|
9307
|
+
extensionNote: extensionRetryNote(collided, still)
|
|
9308
|
+
},
|
|
9309
|
+
...crash === undefined ? {} : { docCacheNote: crash }
|
|
9310
|
+
};
|
|
9311
|
+
}
|
|
9312
|
+
async function importOnce(godotPath, projectPath) {
|
|
9313
|
+
const logDir = scratchDirectory("import");
|
|
9314
|
+
const caches = editorCacheDirectory(godotPath);
|
|
9315
|
+
const cachesBefore = docCachesWritten(caches);
|
|
9316
|
+
let ran;
|
|
9317
|
+
try {
|
|
9318
|
+
ran = await runEngine(godotPath, [
|
|
9319
|
+
"--headless",
|
|
9320
|
+
"--log-file",
|
|
9321
|
+
join10(logDir, "engine.log"),
|
|
9322
|
+
"--path",
|
|
9323
|
+
projectPath,
|
|
9324
|
+
"--import"
|
|
9325
|
+
]);
|
|
9326
|
+
} finally {
|
|
9327
|
+
discard(logDir);
|
|
9328
|
+
}
|
|
9329
|
+
const problems = problemsIn(ran.log);
|
|
9330
|
+
const crash = docCacheCrash(ran, cachesBefore, docCachesWritten(caches), "import");
|
|
9331
|
+
if (ran.exitCode === 0 && ran.failure === null) {
|
|
9332
|
+
return { ok: true, ...reported2(problems), ...crash === null ? {} : { docCacheNote: crash } };
|
|
9333
|
+
}
|
|
9334
|
+
return {
|
|
9335
|
+
ok: false,
|
|
9336
|
+
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
9337
|
+
...reported2(problems),
|
|
9338
|
+
...crash === null ? {} : { docCacheNote: crash }
|
|
9339
|
+
};
|
|
9340
|
+
}
|
|
9341
|
+
async function runOperation(engine, operation, params, projectPath) {
|
|
9342
|
+
const paramsDir = scratchDirectory("params");
|
|
9343
|
+
const paramsFile = join10(paramsDir, `${operation}.json`);
|
|
9344
|
+
const answerFile = join10(paramsDir, "answer.json");
|
|
9345
|
+
writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
9346
|
+
const args = [
|
|
9347
|
+
"--headless",
|
|
9348
|
+
"--log-file",
|
|
9349
|
+
join10(paramsDir, "engine.log"),
|
|
9350
|
+
"--path",
|
|
9351
|
+
projectPath,
|
|
9352
|
+
"--script",
|
|
9353
|
+
engine.script,
|
|
9354
|
+
operation,
|
|
9355
|
+
`@file:${paramsFile}`,
|
|
9356
|
+
answerFile,
|
|
9357
|
+
...engine.debug ? ["--debug-godot"] : []
|
|
9358
|
+
];
|
|
9359
|
+
let ran;
|
|
9360
|
+
let answer;
|
|
9361
|
+
try {
|
|
9362
|
+
ran = await runEngine(engine.godotPath, args);
|
|
9363
|
+
answer = answerIn(answerFile);
|
|
9364
|
+
} finally {
|
|
9365
|
+
discard(paramsDir);
|
|
9366
|
+
}
|
|
9367
|
+
const everything = ran.log.everything();
|
|
9368
|
+
const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
|
|
9369
|
+
const answered = marked(ANSWERED);
|
|
9370
|
+
const waiting = marked(WAITING);
|
|
9371
|
+
const begun = marked(BEGUN);
|
|
9372
|
+
const ended = answered === -1 ? everything.length : answered;
|
|
9373
|
+
const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
|
|
9374
|
+
...everything.slice(0, waiting),
|
|
9375
|
+
...begun === -1 ? [] : everything.slice(begun + 1, ended)
|
|
9376
|
+
]);
|
|
9377
|
+
const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
|
|
9378
|
+
const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
|
|
9379
|
+
const afterAnswered = {
|
|
9380
|
+
...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
|
|
9381
|
+
...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
|
|
9382
|
+
...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
|
|
9383
|
+
...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
|
|
9384
|
+
};
|
|
9385
|
+
if (answer.kind === "unreadable") {
|
|
9386
|
+
return {
|
|
9387
|
+
ok: false,
|
|
9388
|
+
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
9389
|
+
...reported2(problems),
|
|
9390
|
+
...afterAnswered
|
|
9391
|
+
};
|
|
9392
|
+
}
|
|
9393
|
+
if (answer.kind === "none") {
|
|
9394
|
+
if (waiting !== -1 && begun === -1) {
|
|
9395
|
+
return {
|
|
9396
|
+
ok: false,
|
|
9397
|
+
message: `${operation} never ran: the engine ended (${howItEnded(ran)}) while it started the project's ` + `autoloads, which this operation waits for. The last it reported: ${reason(ran.log, startup)}`,
|
|
9398
|
+
...reported2(problems),
|
|
9399
|
+
...afterAnswered
|
|
9400
|
+
};
|
|
9401
|
+
}
|
|
9402
|
+
const refusals = refusalsIn(ran.log);
|
|
9403
|
+
if (refusals.length > 0) {
|
|
9404
|
+
const said = new Set(refusals.map((entry) => entry.index));
|
|
9405
|
+
return {
|
|
9406
|
+
ok: false,
|
|
9407
|
+
message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
|
|
9408
|
+
...reported2(problems.filter((entry) => !said.has(entry.index)))
|
|
9409
|
+
};
|
|
9410
|
+
}
|
|
9411
|
+
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
9412
|
+
return {
|
|
9413
|
+
ok: false,
|
|
9414
|
+
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
9415
|
+
...reported2(problems)
|
|
9416
|
+
};
|
|
9417
|
+
}
|
|
9418
|
+
const fault = scriptFault(problems, engine.script);
|
|
9419
|
+
if (fault !== undefined) {
|
|
9420
|
+
return {
|
|
9421
|
+
ok: false,
|
|
9422
|
+
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"})`,
|
|
9423
|
+
...reported2(problems),
|
|
9424
|
+
...afterAnswered
|
|
9425
|
+
};
|
|
9426
|
+
}
|
|
9427
|
+
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 ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
|
|
9428
|
+
return {
|
|
9429
|
+
ok: true,
|
|
9430
|
+
payload: answer.payload,
|
|
9431
|
+
...reported2(problems),
|
|
9432
|
+
...afterAnswered,
|
|
9433
|
+
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
9434
|
+
};
|
|
9435
|
+
}
|
|
9436
|
+
function answerIn(file) {
|
|
9437
|
+
if (!existsSync9(file)) {
|
|
9438
|
+
return { kind: "none" };
|
|
9439
|
+
}
|
|
9440
|
+
const text = readFileSync5(file, "utf8");
|
|
9441
|
+
try {
|
|
9442
|
+
const parsed = JSON.parse(text);
|
|
9443
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
|
|
9444
|
+
} catch (error) {
|
|
9445
|
+
return {
|
|
9446
|
+
kind: "unreadable",
|
|
9447
|
+
why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
|
|
9448
|
+
};
|
|
9449
|
+
}
|
|
9450
|
+
}
|
|
9451
|
+
var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", WAITING = "[gdharness] waiting for the autoloads", BEGUN = "[gdharness] begun", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
|
|
9452
|
+
var init_headless = __esm(() => {
|
|
9453
|
+
init_doc_cache();
|
|
9454
|
+
init_engine_run();
|
|
9455
|
+
init_library_copies();
|
|
9456
|
+
init_reimport();
|
|
9457
|
+
init_scratch();
|
|
9458
|
+
init_script_origins();
|
|
9459
|
+
});
|
|
9460
|
+
|
|
9461
|
+
// src/update-check.ts
|
|
9462
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9463
|
+
import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
|
|
9464
|
+
import { join as join11 } from "node:path";
|
|
9465
|
+
function packageUrl(root) {
|
|
9466
|
+
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
9467
|
+
}
|
|
9468
|
+
function registryFor(environment) {
|
|
9469
|
+
const named = environment["GDHARNESS_REGISTRY"] ?? "";
|
|
9470
|
+
if (named === "") {
|
|
9471
|
+
return packageUrl(REGISTRY_ROOT);
|
|
9472
|
+
}
|
|
9473
|
+
try {
|
|
9474
|
+
return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
|
|
9475
|
+
} catch {
|
|
9476
|
+
return packageUrl(REGISTRY_ROOT);
|
|
9477
|
+
}
|
|
9478
|
+
}
|
|
9479
|
+
function cacheDirectory(environment) {
|
|
9480
|
+
const set = (name) => {
|
|
9481
|
+
const value = environment[name];
|
|
9482
|
+
return value !== undefined && value !== "" ? value : null;
|
|
9483
|
+
};
|
|
9484
|
+
const home = set("HOME") ?? homedir4();
|
|
9485
|
+
if (process.platform === "win32") {
|
|
9486
|
+
return join11(set("LOCALAPPDATA") ?? home, "gdharness");
|
|
9487
|
+
}
|
|
9488
|
+
if (process.platform === "darwin") {
|
|
9489
|
+
return join11(home, "Library", "Caches", "gdharness");
|
|
9490
|
+
}
|
|
9491
|
+
return join11(set("XDG_CACHE_HOME") ?? join11(home, ".cache"), "gdharness");
|
|
9492
|
+
}
|
|
9493
|
+
function cacheFile(environment = process.env) {
|
|
9494
|
+
try {
|
|
9495
|
+
const directory = cacheDirectory(environment);
|
|
9496
|
+
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
9497
|
+
return join11(directory, "update-check.json");
|
|
9498
|
+
} catch {
|
|
9499
|
+
return join11(tmpdir3(), "gdharness-update-check.json");
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
function readCache(path) {
|
|
9503
|
+
try {
|
|
9504
|
+
if (!existsSync10(path)) {
|
|
9505
|
+
return null;
|
|
9506
|
+
}
|
|
9507
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
9508
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
9509
|
+
return null;
|
|
9510
|
+
}
|
|
9511
|
+
const record = parsed;
|
|
9512
|
+
const checkedAt = record["checkedAt"];
|
|
9513
|
+
const latest = record["latest"];
|
|
9514
|
+
if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
|
|
9515
|
+
return null;
|
|
9516
|
+
}
|
|
9517
|
+
return { checkedAt, latest };
|
|
9518
|
+
} catch {
|
|
9519
|
+
return null;
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
function writeCache(path, entry) {
|
|
9523
|
+
try {
|
|
9524
|
+
writeFileSync5(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
|
|
9525
|
+
} catch {}
|
|
9526
|
+
}
|
|
9527
|
+
function parts(version) {
|
|
9528
|
+
const withoutBuild = version.split("+")[0] ?? version;
|
|
9529
|
+
const dash = withoutBuild.indexOf("-");
|
|
9530
|
+
const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
|
|
9531
|
+
return {
|
|
9532
|
+
numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
|
|
9533
|
+
prerelease: dash !== -1
|
|
9534
|
+
};
|
|
9535
|
+
}
|
|
9536
|
+
function isNewer(candidate, current) {
|
|
9537
|
+
const left = parts(candidate);
|
|
9538
|
+
const right = parts(current);
|
|
9539
|
+
for (let index = 0;index < 3; index += 1) {
|
|
9540
|
+
const a = left.numbers[index] ?? 0;
|
|
9541
|
+
const b = right.numbers[index] ?? 0;
|
|
9542
|
+
if (a !== b) {
|
|
9543
|
+
return a > b;
|
|
9544
|
+
}
|
|
9545
|
+
}
|
|
9546
|
+
return !left.prerelease && right.prerelease;
|
|
9547
|
+
}
|
|
9548
|
+
async function fetchLatest(registry) {
|
|
9549
|
+
const response = await fetch(registry, {
|
|
9550
|
+
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
9551
|
+
redirect: "error",
|
|
9552
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
9553
|
+
});
|
|
9554
|
+
if (!response.ok || response.body === null) {
|
|
9555
|
+
return null;
|
|
9556
|
+
}
|
|
9557
|
+
const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
9558
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
9559
|
+
return null;
|
|
9560
|
+
}
|
|
9561
|
+
const chunks = [];
|
|
9562
|
+
let size = 0;
|
|
9563
|
+
for await (const chunk of response.body) {
|
|
9564
|
+
size += chunk.byteLength;
|
|
9565
|
+
if (size > MAX_BODY_BYTES) {
|
|
9566
|
+
return null;
|
|
9567
|
+
}
|
|
9568
|
+
chunks.push(chunk);
|
|
9569
|
+
}
|
|
9570
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
9571
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
9572
|
+
return null;
|
|
9573
|
+
}
|
|
9574
|
+
const version = parsed["version"];
|
|
9575
|
+
return typeof version === "string" && VERSION.test(version) ? version : null;
|
|
9576
|
+
}
|
|
9577
|
+
|
|
9578
|
+
class UpdateCheck {
|
|
9579
|
+
latest = null;
|
|
9580
|
+
checking = false;
|
|
9581
|
+
asked = false;
|
|
9582
|
+
checkedAt = 0;
|
|
9583
|
+
retryAt = 0;
|
|
9584
|
+
backoffMs = FIRST_RETRY_MS;
|
|
9585
|
+
enabled;
|
|
9586
|
+
current;
|
|
9587
|
+
cachePath;
|
|
9588
|
+
ask;
|
|
9589
|
+
constructor(current, environment = process.env, ask = () => fetchLatest(registryFor(environment))) {
|
|
9590
|
+
this.current = current;
|
|
9591
|
+
this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
|
|
9592
|
+
this.cachePath = cacheFile(environment);
|
|
9593
|
+
this.ask = ask;
|
|
9594
|
+
const cached = this.enabled ? readCache(this.cachePath) : null;
|
|
9595
|
+
if (cached !== null) {
|
|
9596
|
+
this.latest = cached.latest;
|
|
9597
|
+
this.checkedAt = cached.checkedAt;
|
|
9598
|
+
}
|
|
9599
|
+
}
|
|
9600
|
+
refresh(now = Date.now()) {
|
|
9601
|
+
if (!this.enabled || this.checking || now < this.retryAt || this.asked && !this.stale(now)) {
|
|
9602
|
+
return;
|
|
9603
|
+
}
|
|
9604
|
+
this.asked = true;
|
|
9605
|
+
this.checking = true;
|
|
9606
|
+
this.ask().then((version) => {
|
|
9607
|
+
if (version === null) {
|
|
9608
|
+
this.scheduleRetry(now);
|
|
9609
|
+
return;
|
|
9610
|
+
}
|
|
9611
|
+
this.latest = version;
|
|
9612
|
+
this.checkedAt = Date.now();
|
|
9613
|
+
this.backoffMs = FIRST_RETRY_MS;
|
|
9614
|
+
this.retryAt = 0;
|
|
9615
|
+
writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
|
|
9616
|
+
}).catch(() => {
|
|
9617
|
+
this.scheduleRetry(now);
|
|
9618
|
+
}).finally(() => {
|
|
9619
|
+
this.checking = false;
|
|
9620
|
+
});
|
|
9621
|
+
}
|
|
9622
|
+
stale(now) {
|
|
9623
|
+
return now - this.checkedAt >= CACHE_MS;
|
|
9624
|
+
}
|
|
9625
|
+
scheduleRetry(now) {
|
|
9626
|
+
this.retryAt = now + this.backoffMs;
|
|
9627
|
+
this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
|
|
9628
|
+
}
|
|
9629
|
+
notice(now = Date.now()) {
|
|
9630
|
+
const latest = this.latest;
|
|
9631
|
+
if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
|
|
9632
|
+
return null;
|
|
9633
|
+
}
|
|
9634
|
+
if (this.checking && this.stale(now)) {
|
|
9635
|
+
return null;
|
|
9636
|
+
}
|
|
9637
|
+
return {
|
|
9638
|
+
current: this.current,
|
|
9639
|
+
latest,
|
|
9640
|
+
releaseNotes: `${RELEASES}/v${latest}`,
|
|
9641
|
+
upgrade: runLine(currentRunner(), latest, "upgrade")
|
|
9642
|
+
};
|
|
9643
|
+
}
|
|
9644
|
+
}
|
|
9645
|
+
var REGISTRY_ROOT = "https://registry.npmjs.org", RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag", CACHE_MS, REQUEST_TIMEOUT_MS = 1e4, MAX_BODY_BYTES, FIRST_RETRY_MS = 30000, MAX_RETRY_MS, VERSION;
|
|
9646
|
+
var init_update_check = __esm(() => {
|
|
9647
|
+
init_runner();
|
|
9648
|
+
CACHE_MS = 10 * 60 * 1000;
|
|
9649
|
+
MAX_BODY_BYTES = 1 << 20;
|
|
9650
|
+
MAX_RETRY_MS = 60 * 60 * 1000;
|
|
9651
|
+
VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
9652
|
+
});
|
|
9653
|
+
|
|
9654
|
+
// src/server-version.ts
|
|
9655
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
9656
|
+
function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
9657
|
+
if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
|
|
9658
|
+
return addonDigest !== shippedDigest;
|
|
9659
|
+
}
|
|
9660
|
+
return addonVersion !== serverVersion;
|
|
9661
|
+
}
|
|
9662
|
+
function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
9663
|
+
if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
|
|
9664
|
+
return;
|
|
9665
|
+
}
|
|
9666
|
+
const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
|
|
9667
|
+
return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
|
|
9668
|
+
}
|
|
9669
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
9670
|
+
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
9671
|
+
return;
|
|
9672
|
+
}
|
|
9673
|
+
const reported = addonVersion ?? "";
|
|
9674
|
+
const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: run ${gdharnessCommand(serverVersion, "upgrade")} in a shell in the project directory, which installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
|
|
9675
|
+
if (reported === serverVersion) {
|
|
9676
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
|
|
9677
|
+
}
|
|
9678
|
+
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
9679
|
+
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
9680
|
+
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
9681
|
+
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
9682
|
+
}
|
|
9683
|
+
return `${both} ${restart}`;
|
|
9684
|
+
}
|
|
9685
|
+
function gdharnessCommand(version, args) {
|
|
9686
|
+
return `npx -y gdharness@${version} ${args}`;
|
|
9687
|
+
}
|
|
9688
|
+
function runtimeAddonUnknown(addon) {
|
|
9689
|
+
return addon?.version === "" && addon.digest === "";
|
|
9690
|
+
}
|
|
9691
|
+
function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, projectPath) {
|
|
9692
|
+
if (runtimeAddonUnknown(addon)) {
|
|
9693
|
+
return;
|
|
9694
|
+
}
|
|
9695
|
+
if (addon !== undefined && !editorIsStale(addon.version, serverVersion, addon.digest, shippedDigest)) {
|
|
9696
|
+
return;
|
|
9697
|
+
}
|
|
9698
|
+
const game = `The game (pid ${pid})`;
|
|
9699
|
+
if (addon !== undefined && addon.version !== "" && isNewer(addon.version, serverVersion)) {
|
|
9700
|
+
return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
|
|
9086
9701
|
}
|
|
9087
9702
|
let loaded;
|
|
9088
9703
|
if (addon === undefined) {
|
|
@@ -9897,7 +10512,7 @@ var init_gdscript_source = __esm(() => {
|
|
|
9897
10512
|
|
|
9898
10513
|
// src/class-cache.ts
|
|
9899
10514
|
import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "node:fs";
|
|
9900
|
-
import { join as
|
|
10515
|
+
import { join as join12, posix } from "node:path";
|
|
9901
10516
|
function isToolScript(source) {
|
|
9902
10517
|
return TOOL.test(codeOf(source));
|
|
9903
10518
|
}
|
|
@@ -9913,14 +10528,14 @@ function declaredClasses(projectPath) {
|
|
|
9913
10528
|
}
|
|
9914
10529
|
function eachScript(projectPath, each) {
|
|
9915
10530
|
const visit = (directory, prefix) => {
|
|
9916
|
-
if (existsSync11(
|
|
10531
|
+
if (existsSync11(join12(directory, ".gdignore"))) {
|
|
9917
10532
|
return;
|
|
9918
10533
|
}
|
|
9919
10534
|
for (const entry of readdirSync6(directory, { withFileTypes: true })) {
|
|
9920
10535
|
if (entry.name.startsWith(".")) {
|
|
9921
10536
|
continue;
|
|
9922
10537
|
}
|
|
9923
|
-
const path =
|
|
10538
|
+
const path = join12(directory, entry.name);
|
|
9924
10539
|
if (entry.isDirectory()) {
|
|
9925
10540
|
visit(path, `${prefix}${entry.name}/`);
|
|
9926
10541
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
@@ -9946,7 +10561,7 @@ function scriptsNaming(projectPath, classes) {
|
|
|
9946
10561
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
9947
10562
|
}
|
|
9948
10563
|
function cachedClasses(projectPath) {
|
|
9949
|
-
const cache =
|
|
10564
|
+
const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
9950
10565
|
if (!existsSync11(cache)) {
|
|
9951
10566
|
return null;
|
|
9952
10567
|
}
|
|
@@ -10217,7 +10832,7 @@ function uncachedClassNote(types) {
|
|
|
10217
10832
|
return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
|
|
10218
10833
|
}
|
|
10219
10834
|
function cacheWrittenAt(projectPath) {
|
|
10220
|
-
const cache =
|
|
10835
|
+
const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
10221
10836
|
try {
|
|
10222
10837
|
return statSync6(cache).mtimeMs;
|
|
10223
10838
|
} catch {
|
|
@@ -10231,14 +10846,14 @@ function declaredSince(projectPath, paths, names, since) {
|
|
|
10231
10846
|
return false;
|
|
10232
10847
|
}
|
|
10233
10848
|
try {
|
|
10234
|
-
return statSync6(
|
|
10849
|
+
return statSync6(join12(projectPath, path.slice("res://".length))).mtimeMs > since;
|
|
10235
10850
|
} catch {
|
|
10236
10851
|
return false;
|
|
10237
10852
|
}
|
|
10238
10853
|
});
|
|
10239
10854
|
}
|
|
10240
10855
|
function cachedAtMissingPaths(cached, projectPath) {
|
|
10241
|
-
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(
|
|
10856
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join12(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
10242
10857
|
}
|
|
10243
10858
|
function staleAgainst(cached, projectPath) {
|
|
10244
10859
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
@@ -10264,7 +10879,7 @@ var init_class_cache = __esm(() => {
|
|
|
10264
10879
|
|
|
10265
10880
|
// src/project-scan.ts
|
|
10266
10881
|
import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
10267
|
-
import { extname, join as
|
|
10882
|
+
import { extname, join as join13 } from "node:path";
|
|
10268
10883
|
function skipped(name) {
|
|
10269
10884
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
10270
10885
|
}
|
|
@@ -10272,10 +10887,10 @@ function steppedOver(directory) {
|
|
|
10272
10887
|
return gdignored(directory) || installedByUs(directory);
|
|
10273
10888
|
}
|
|
10274
10889
|
function gdignored(directory) {
|
|
10275
|
-
return existsSync12(
|
|
10890
|
+
return existsSync12(join13(directory, ".gdignore"));
|
|
10276
10891
|
}
|
|
10277
10892
|
function installedByUs(directory) {
|
|
10278
|
-
return existsSync12(
|
|
10893
|
+
return existsSync12(join13(directory, VERSION_MARKER));
|
|
10279
10894
|
}
|
|
10280
10895
|
function projectStructure(projectPath) {
|
|
10281
10896
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -10288,7 +10903,7 @@ function projectStructure(projectPath) {
|
|
|
10288
10903
|
continue;
|
|
10289
10904
|
}
|
|
10290
10905
|
if (entry.isDirectory()) {
|
|
10291
|
-
visit(
|
|
10906
|
+
visit(join13(directory, entry.name));
|
|
10292
10907
|
} else if (entry.isFile()) {
|
|
10293
10908
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
10294
10909
|
if (SCENE_EXTENSIONS.has(extension)) {
|
|
@@ -10318,7 +10933,7 @@ function projectFiles(projectPath, extensions) {
|
|
|
10318
10933
|
}
|
|
10319
10934
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
10320
10935
|
if (entry.isDirectory()) {
|
|
10321
|
-
visit(
|
|
10936
|
+
visit(join13(directory, entry.name), spelled);
|
|
10322
10937
|
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
10323
10938
|
found.push(spelled);
|
|
10324
10939
|
}
|
|
@@ -10339,14 +10954,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
10339
10954
|
}
|
|
10340
10955
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
10341
10956
|
if (entry.isDirectory()) {
|
|
10342
|
-
visit(
|
|
10957
|
+
visit(join13(directory, entry.name), spelled);
|
|
10343
10958
|
continue;
|
|
10344
10959
|
}
|
|
10345
10960
|
if (!entry.isFile()) {
|
|
10346
10961
|
continue;
|
|
10347
10962
|
}
|
|
10348
10963
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
10349
|
-
if (CARRIES_A_UID.has(extension) && !existsSync12(
|
|
10964
|
+
if (CARRIES_A_UID.has(extension) && !existsSync12(join13(directory, `${entry.name}.uid`))) {
|
|
10350
10965
|
found.push(spelled);
|
|
10351
10966
|
}
|
|
10352
10967
|
}
|
|
@@ -10367,7 +10982,7 @@ function projectTexts(projectPath) {
|
|
|
10367
10982
|
if (skipped(entry.name)) {
|
|
10368
10983
|
continue;
|
|
10369
10984
|
}
|
|
10370
|
-
const absolute =
|
|
10985
|
+
const absolute = join13(directory, entry.name);
|
|
10371
10986
|
const spelled = `${prefix}${entry.name}`;
|
|
10372
10987
|
if (entry.isDirectory()) {
|
|
10373
10988
|
visit(absolute, `${spelled}/`);
|
|
@@ -10397,7 +11012,7 @@ function ignoredTexts(projectPath) {
|
|
|
10397
11012
|
if (skipped(entry.name)) {
|
|
10398
11013
|
continue;
|
|
10399
11014
|
}
|
|
10400
|
-
const absolute =
|
|
11015
|
+
const absolute = join13(directory, entry.name);
|
|
10401
11016
|
const spelled = `${prefix}${entry.name}`;
|
|
10402
11017
|
if (entry.isDirectory()) {
|
|
10403
11018
|
visit(absolute, `${spelled}/`, under);
|
|
@@ -10437,7 +11052,7 @@ function searchProject(projectPath, options) {
|
|
|
10437
11052
|
if (skipped(entry.name)) {
|
|
10438
11053
|
continue;
|
|
10439
11054
|
}
|
|
10440
|
-
const entryPath =
|
|
11055
|
+
const entryPath = join13(directory, entry.name);
|
|
10441
11056
|
if (entry.isDirectory()) {
|
|
10442
11057
|
visit(entryPath);
|
|
10443
11058
|
continue;
|
|
@@ -10891,7 +11506,7 @@ import {
|
|
|
10891
11506
|
statSync as statSync7,
|
|
10892
11507
|
writeFileSync as writeFileSync6
|
|
10893
11508
|
} from "node:fs";
|
|
10894
|
-
import { dirname as dirname7, join as
|
|
11509
|
+
import { dirname as dirname7, join as join14 } from "node:path";
|
|
10895
11510
|
import { fileURLToPath } from "node:url";
|
|
10896
11511
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
10897
11512
|
return addonsDigest(EDITOR_PLUGINS, from);
|
|
@@ -10906,7 +11521,7 @@ function addonsDigest(names, from) {
|
|
|
10906
11521
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
10907
11522
|
continue;
|
|
10908
11523
|
}
|
|
10909
|
-
const path =
|
|
11524
|
+
const path = join14(directory, name);
|
|
10910
11525
|
const inside = `${relative}/${name}`;
|
|
10911
11526
|
if (statSync7(path).isDirectory()) {
|
|
10912
11527
|
walk(path, inside);
|
|
@@ -10920,7 +11535,7 @@ function addonsDigest(names, from) {
|
|
|
10920
11535
|
}
|
|
10921
11536
|
};
|
|
10922
11537
|
for (const name of names) {
|
|
10923
|
-
walk(
|
|
11538
|
+
walk(join14(from, name), name);
|
|
10924
11539
|
}
|
|
10925
11540
|
return hash.digest("hex");
|
|
10926
11541
|
}
|
|
@@ -10951,7 +11566,7 @@ function installedRuntimeDigest(projectPath) {
|
|
|
10951
11566
|
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
10952
11567
|
}
|
|
10953
11568
|
function digestMarker(projectPath, addon) {
|
|
10954
|
-
const marker =
|
|
11569
|
+
const marker = join14(projectPath, "addons", addon, DIGEST_MARKER);
|
|
10955
11570
|
if (!existsSync13(marker)) {
|
|
10956
11571
|
return null;
|
|
10957
11572
|
}
|
|
@@ -10959,35 +11574,35 @@ function digestMarker(projectPath, addon) {
|
|
|
10959
11574
|
return said === "" ? null : said;
|
|
10960
11575
|
}
|
|
10961
11576
|
function shippedAddonsDirectory() {
|
|
10962
|
-
return
|
|
11577
|
+
return join14(dirname7(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
10963
11578
|
}
|
|
10964
11579
|
function shippedOperationsScript() {
|
|
10965
|
-
return
|
|
11580
|
+
return join14(dirname7(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
|
|
10966
11581
|
}
|
|
10967
11582
|
function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
10968
11583
|
const installed = [];
|
|
10969
11584
|
for (const name of ADDONS) {
|
|
10970
|
-
const source =
|
|
10971
|
-
if (!existsSync13(
|
|
11585
|
+
const source = join14(from, name);
|
|
11586
|
+
if (!existsSync13(join14(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
10972
11587
|
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
10973
11588
|
}
|
|
10974
|
-
const target =
|
|
11589
|
+
const target = join14(projectPath, "addons", name);
|
|
10975
11590
|
const replaced = existsSync13(target);
|
|
10976
11591
|
rmSync5(target, { recursive: true, force: true });
|
|
10977
11592
|
mkdirSync4(dirname7(target), { recursive: true });
|
|
10978
11593
|
cpSync(source, target, { recursive: true });
|
|
10979
|
-
writeFileSync6(
|
|
11594
|
+
writeFileSync6(join14(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
10980
11595
|
`);
|
|
10981
11596
|
installed.push({ name, path: target, replaced });
|
|
10982
11597
|
}
|
|
10983
|
-
writeFileSync6(
|
|
11598
|
+
writeFileSync6(join14(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
10984
11599
|
`);
|
|
10985
|
-
writeFileSync6(
|
|
11600
|
+
writeFileSync6(join14(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
|
|
10986
11601
|
`);
|
|
10987
11602
|
return installed;
|
|
10988
11603
|
}
|
|
10989
11604
|
function installedAddonVersion(projectPath) {
|
|
10990
|
-
const marker =
|
|
11605
|
+
const marker = join14(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
10991
11606
|
if (!existsSync13(marker)) {
|
|
10992
11607
|
return null;
|
|
10993
11608
|
}
|
|
@@ -10997,13 +11612,13 @@ function installedAddonVersion(projectPath) {
|
|
|
10997
11612
|
function removeAddons(projectPath) {
|
|
10998
11613
|
const removed = [];
|
|
10999
11614
|
for (const name of ADDONS) {
|
|
11000
|
-
const target =
|
|
11615
|
+
const target = join14(projectPath, "addons", name);
|
|
11001
11616
|
if (existsSync13(target)) {
|
|
11002
11617
|
rmSync5(target, { recursive: true, force: true });
|
|
11003
11618
|
removed.push(target);
|
|
11004
11619
|
}
|
|
11005
11620
|
}
|
|
11006
|
-
const addons =
|
|
11621
|
+
const addons = join14(projectPath, "addons");
|
|
11007
11622
|
if (removed.length > 0 && existsSync13(addons) && readdirSync8(addons).length === 0) {
|
|
11008
11623
|
rmdirSync(addons);
|
|
11009
11624
|
removed.push(addons);
|
|
@@ -11038,7 +11653,7 @@ function loaderAmong(projectPath, named) {
|
|
|
11038
11653
|
}
|
|
11039
11654
|
let source = null;
|
|
11040
11655
|
try {
|
|
11041
|
-
source = readFileSync11(
|
|
11656
|
+
source = readFileSync11(join14(projectPath, path), "utf8");
|
|
11042
11657
|
} catch {}
|
|
11043
11658
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
11044
11659
|
return { name: entry, path, exists: true };
|
|
@@ -11080,8 +11695,8 @@ function enabledPlugins(settings) {
|
|
|
11080
11695
|
function inspectProject(projectPath) {
|
|
11081
11696
|
const problems = [];
|
|
11082
11697
|
const addons = ADDONS.map((name) => {
|
|
11083
|
-
const directory =
|
|
11084
|
-
const marker =
|
|
11698
|
+
const directory = join14(projectPath, "addons", name);
|
|
11699
|
+
const marker = join14(directory, VERSION_MARKER);
|
|
11085
11700
|
const installed = existsSync13(directory);
|
|
11086
11701
|
const version = existsSync13(marker) ? readFileSync11(marker, "utf8").trim() : null;
|
|
11087
11702
|
const current = version === SERVER_VERSION;
|
|
@@ -11092,7 +11707,7 @@ function inspectProject(projectPath) {
|
|
|
11092
11707
|
}
|
|
11093
11708
|
return { name, installed, version, current };
|
|
11094
11709
|
});
|
|
11095
|
-
const settings = parseProjectGodot(readFileSync11(
|
|
11710
|
+
const settings = parseProjectGodot(readFileSync11(join14(projectPath, "project.godot"), "utf8"));
|
|
11096
11711
|
const pluginsEnabled = enabledPlugins(settings);
|
|
11097
11712
|
for (const name of EDITOR_PLUGINS) {
|
|
11098
11713
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -11108,7 +11723,7 @@ function inspectProject(projectPath) {
|
|
|
11108
11723
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
11109
11724
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
11110
11725
|
for (const [name, path] of named) {
|
|
11111
|
-
if (!existsSync13(
|
|
11726
|
+
if (!existsSync13(join14(projectPath, path))) {
|
|
11112
11727
|
if (!addonMissing(path)) {
|
|
11113
11728
|
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
11114
11729
|
}
|
|
@@ -11256,7 +11871,7 @@ function annotationsOf(spec) {
|
|
|
11256
11871
|
openWorldHint: false
|
|
11257
11872
|
};
|
|
11258
11873
|
}
|
|
11259
|
-
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "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, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set 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.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS,
|
|
11874
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "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, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set 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.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
|
|
11260
11875
|
var init_tool_definitions = __esm(() => {
|
|
11261
11876
|
init_launch();
|
|
11262
11877
|
PROJECT_INFO_SECTIONS = [
|
|
@@ -11412,7 +12027,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11412
12027
|
additionalProperties: false
|
|
11413
12028
|
}
|
|
11414
12029
|
};
|
|
11415
|
-
|
|
12030
|
+
COLOUR2 = {
|
|
11416
12031
|
type: "object",
|
|
11417
12032
|
description: "set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default.",
|
|
11418
12033
|
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
|
|
@@ -11725,7 +12340,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
11725
12340
|
name: "project_test",
|
|
11726
12341
|
title: "Run tests",
|
|
11727
12342
|
effect: "additive",
|
|
11728
|
-
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
|
|
12343
|
+
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, 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, 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: 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. 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, 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, 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
12344
|
+
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.",
|
|
11729
12345
|
parameters: {
|
|
11730
12346
|
projectPath: PROJECT_PATH,
|
|
11731
12347
|
path: {
|
|
@@ -12210,7 +12826,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12210
12826
|
ops: ["set_theme_color"],
|
|
12211
12827
|
description: 'set_theme_color: such as "font_color".'
|
|
12212
12828
|
},
|
|
12213
|
-
color: { ...
|
|
12829
|
+
color: { ...COLOUR2, ops: ["set_theme_color"] },
|
|
12214
12830
|
fontSizeName: {
|
|
12215
12831
|
type: "string",
|
|
12216
12832
|
ops: ["set_theme_font_size"],
|
|
@@ -12421,7 +13037,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12421
13037
|
requires: []
|
|
12422
13038
|
}
|
|
12423
13039
|
},
|
|
12424
|
-
answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
13040
|
+
answer: "start: scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
12425
13041
|
defaultOperation: "start"
|
|
12426
13042
|
},
|
|
12427
13043
|
{
|
|
@@ -12589,7 +13205,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12589
13205
|
property: {
|
|
12590
13206
|
type: "string",
|
|
12591
13207
|
ops: ["property", "find"],
|
|
12592
|
-
description:
|
|
13208
|
+
description: `property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
|
|
12593
13209
|
},
|
|
12594
13210
|
depth: {
|
|
12595
13211
|
type: "integer",
|
|
@@ -12809,7 +13425,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12809
13425
|
name: "runtime_input",
|
|
12810
13426
|
title: "Send input to the game",
|
|
12811
13427
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
12812
|
-
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
13428
|
+
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, found.item names the tab, list entry, tree row or menu bar title pressed (kind, index where its control numbers them, says, and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
12813
13429
|
parameters: {
|
|
12814
13430
|
projectPath: RUNNING_PROJECT_PATH,
|
|
12815
13431
|
pid: RUNNING_PID,
|
|
@@ -12821,7 +13437,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12821
13437
|
says: {
|
|
12822
13438
|
type: "string",
|
|
12823
13439
|
ops: ["click"],
|
|
12824
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
|
|
13440
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. A tab, a list entry, a tree row and a menu bar title each count as a match of their own and are pressed where they are drawn, after their control scrolls them into view; a disabled one is refused. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
|
|
12825
13441
|
},
|
|
12826
13442
|
action: {
|
|
12827
13443
|
type: "string",
|
|
@@ -34901,12 +35517,12 @@ var init_stdio = __esm(() => {
|
|
|
34901
35517
|
|
|
34902
35518
|
// src/autoload-failure.ts
|
|
34903
35519
|
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
|
|
34904
|
-
import { join as
|
|
35520
|
+
import { join as join17 } from "node:path";
|
|
34905
35521
|
function scriptsAutoloaded(projectPath) {
|
|
34906
35522
|
const scripts = new Map;
|
|
34907
35523
|
let settings;
|
|
34908
35524
|
try {
|
|
34909
|
-
settings = parseProjectGodot(readFileSync13(
|
|
35525
|
+
settings = parseProjectGodot(readFileSync13(join17(projectPath, "project.godot"), "utf8"));
|
|
34910
35526
|
} catch {
|
|
34911
35527
|
return scripts;
|
|
34912
35528
|
}
|
|
@@ -34915,7 +35531,7 @@ function scriptsAutoloaded(projectPath) {
|
|
|
34915
35531
|
scripts.set(`res://${path}`, name);
|
|
34916
35532
|
continue;
|
|
34917
35533
|
}
|
|
34918
|
-
const scene =
|
|
35534
|
+
const scene = join17(projectPath, path);
|
|
34919
35535
|
if (!existsSync15(scene)) {
|
|
34920
35536
|
continue;
|
|
34921
35537
|
}
|
|
@@ -34928,22 +35544,21 @@ function scriptsAutoloaded(projectPath) {
|
|
|
34928
35544
|
return scripts;
|
|
34929
35545
|
}
|
|
34930
35546
|
function autoloadNotCompiled(entry, autoloaded) {
|
|
34931
|
-
const path =
|
|
35547
|
+
const path = notCompiledIn(entry);
|
|
34932
35548
|
const name = path === undefined ? undefined : autoloaded.get(path);
|
|
34933
35549
|
return path === undefined || name === undefined ? null : { name, path };
|
|
34934
35550
|
}
|
|
34935
|
-
var NOT_COMPILED;
|
|
34936
35551
|
var init_autoload_failure = __esm(() => {
|
|
34937
35552
|
init_resources();
|
|
35553
|
+
init_script_origins();
|
|
34938
35554
|
init_setup();
|
|
34939
|
-
NOT_COMPILED = /^Failed to load script "(res:\/\/[^"]+)" with error "Compilation failed"/;
|
|
34940
35555
|
});
|
|
34941
35556
|
|
|
34942
35557
|
// src/runtime-client.ts
|
|
34943
35558
|
import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync8, unlinkSync as unlinkSync2 } from "node:fs";
|
|
34944
35559
|
import { createConnection } from "node:net";
|
|
34945
35560
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
34946
|
-
import { dirname as dirname10, join as
|
|
35561
|
+
import { dirname as dirname10, join as join18, resolve as resolve4 } from "node:path";
|
|
34947
35562
|
import { setTimeout as delay } from "node:timers/promises";
|
|
34948
35563
|
function runtimeDirectory(variables = process.env) {
|
|
34949
35564
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -34951,19 +35566,19 @@ function runtimeDirectory(variables = process.env) {
|
|
|
34951
35566
|
return explicit;
|
|
34952
35567
|
}
|
|
34953
35568
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
34954
|
-
return
|
|
35569
|
+
return join18(perUser ?? tmpdir4(), "gdharness");
|
|
34955
35570
|
}
|
|
34956
35571
|
function runtimeDirectories(variables = process.env) {
|
|
34957
35572
|
const candidates = [runtimeDirectory(variables)];
|
|
34958
35573
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
34959
35574
|
for (const base of fallbacks) {
|
|
34960
|
-
candidates.push(
|
|
35575
|
+
candidates.push(join18(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
34961
35576
|
}
|
|
34962
35577
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
34963
35578
|
}
|
|
34964
35579
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
34965
35580
|
for (const directory of directories) {
|
|
34966
|
-
const path =
|
|
35581
|
+
const path = join18(directory, `runtime-${pid}.log`);
|
|
34967
35582
|
if (existsSync16(path)) {
|
|
34968
35583
|
return path;
|
|
34969
35584
|
}
|
|
@@ -35113,14 +35728,14 @@ function announcedIn(directory) {
|
|
|
35113
35728
|
for (const entry of readdirSync10(directory)) {
|
|
35114
35729
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
35115
35730
|
if (report) {
|
|
35116
|
-
sweepErrorReport(
|
|
35731
|
+
sweepErrorReport(join18(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
35117
35732
|
continue;
|
|
35118
35733
|
}
|
|
35119
35734
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
35120
35735
|
if (!match) {
|
|
35121
35736
|
continue;
|
|
35122
35737
|
}
|
|
35123
|
-
const file =
|
|
35738
|
+
const file = join18(directory, entry);
|
|
35124
35739
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
35125
35740
|
const running = alive(pid);
|
|
35126
35741
|
entries.push({ file, pid, running });
|
|
@@ -35148,7 +35763,7 @@ function announcedIn(directory) {
|
|
|
35148
35763
|
found.push(announced);
|
|
35149
35764
|
}
|
|
35150
35765
|
const held = new Set(entries.map((one) => one.file));
|
|
35151
|
-
const here =
|
|
35766
|
+
const here = join18(directory, ".");
|
|
35152
35767
|
for (const file of verdicts.keys()) {
|
|
35153
35768
|
if (dirname10(file) === here && !held.has(file)) {
|
|
35154
35769
|
verdicts.delete(file);
|
|
@@ -35157,7 +35772,7 @@ function announcedIn(directory) {
|
|
|
35157
35772
|
return found;
|
|
35158
35773
|
}
|
|
35159
35774
|
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
35160
|
-
const files = directories.map((directory) =>
|
|
35775
|
+
const files = directories.map((directory) => join18(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
|
|
35161
35776
|
const first = files[0];
|
|
35162
35777
|
if (first === undefined) {
|
|
35163
35778
|
return;
|
|
@@ -35569,9 +36184,9 @@ var init_runtime_client = __esm(() => {
|
|
|
35569
36184
|
|
|
35570
36185
|
// src/boot-note.ts
|
|
35571
36186
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35572
|
-
import { dirname as dirname11, join as
|
|
36187
|
+
import { dirname as dirname11, join as join19 } from "node:path";
|
|
35573
36188
|
function bootNotePath(projectPath) {
|
|
35574
|
-
return
|
|
36189
|
+
return join19(projectPath, ".godot", "gdharness-boot.json");
|
|
35575
36190
|
}
|
|
35576
36191
|
function readScenes(projectPath) {
|
|
35577
36192
|
try {
|
|
@@ -35618,9 +36233,9 @@ var init_boot_note = __esm(() => {
|
|
|
35618
36233
|
|
|
35619
36234
|
// src/breakpoint-note.ts
|
|
35620
36235
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
35621
|
-
import { dirname as dirname12, isAbsolute as isAbsolute4, join as
|
|
36236
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, join as join20, relative as relative3 } from "node:path";
|
|
35622
36237
|
function breakpointNotePath(projectPath) {
|
|
35623
|
-
return
|
|
36238
|
+
return join20(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
35624
36239
|
}
|
|
35625
36240
|
function writeBreakpointNote(projectPath, held) {
|
|
35626
36241
|
const path = breakpointNotePath(projectPath);
|
|
@@ -35661,7 +36276,7 @@ function readBreakpointNote(projectPath) {
|
|
|
35661
36276
|
}
|
|
35662
36277
|
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
35663
36278
|
if (lines.length > 0) {
|
|
35664
|
-
held.push({ scriptPath: realPathOr(
|
|
36279
|
+
held.push({ scriptPath: realPathOr(join20(projectPath, script)), lines });
|
|
35665
36280
|
}
|
|
35666
36281
|
}
|
|
35667
36282
|
return held;
|
|
@@ -35711,9 +36326,9 @@ var init_whole_file = __esm(() => {
|
|
|
35711
36326
|
|
|
35712
36327
|
// src/class-note.ts
|
|
35713
36328
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync17 } from "node:fs";
|
|
35714
|
-
import { dirname as dirname13, join as
|
|
36329
|
+
import { dirname as dirname13, join as join21 } from "node:path";
|
|
35715
36330
|
function classNotePath(projectPath) {
|
|
35716
|
-
return
|
|
36331
|
+
return join21(projectPath, ".godot", "gdharness-classes.json");
|
|
35717
36332
|
}
|
|
35718
36333
|
function readClassNote(projectPath) {
|
|
35719
36334
|
try {
|
|
@@ -35760,9 +36375,9 @@ var init_class_note = __esm(() => {
|
|
|
35760
36375
|
|
|
35761
36376
|
// src/config-pin.ts
|
|
35762
36377
|
import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
|
|
35763
|
-
import { join as
|
|
36378
|
+
import { join as join22 } from "node:path";
|
|
35764
36379
|
function versionsNamedInConfig(projectPath) {
|
|
35765
|
-
const config =
|
|
36380
|
+
const config = join22(projectPath, ".mcp.json");
|
|
35766
36381
|
if (!existsSync17(config)) {
|
|
35767
36382
|
return [];
|
|
35768
36383
|
}
|
|
@@ -35790,9 +36405,9 @@ var init_config_pin = __esm(() => {
|
|
|
35790
36405
|
|
|
35791
36406
|
// src/cut-test-run.ts
|
|
35792
36407
|
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync19, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
|
|
35793
|
-
import { dirname as dirname14, join as
|
|
36408
|
+
import { dirname as dirname14, join as join23 } from "node:path";
|
|
35794
36409
|
function keptAt(projectPath) {
|
|
35795
|
-
return
|
|
36410
|
+
return join23(projectPath, ".godot", "gdharness-reports", "cut-run.json");
|
|
35796
36411
|
}
|
|
35797
36412
|
function keepCutTestRun(projectPath, run) {
|
|
35798
36413
|
const path = keptAt(projectPath);
|
|
@@ -36631,10 +37246,10 @@ import {
|
|
|
36631
37246
|
writeSync
|
|
36632
37247
|
} from "node:fs";
|
|
36633
37248
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36634
|
-
import { join as
|
|
37249
|
+
import { join as join24 } from "node:path";
|
|
36635
37250
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36636
37251
|
function helperAssembly(directory = tmpdir5()) {
|
|
36637
|
-
return
|
|
37252
|
+
return join24(directory, `gdharness-desktop-${DIGEST}.dll`);
|
|
36638
37253
|
}
|
|
36639
37254
|
async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COMPILE_MS, wait = true) {
|
|
36640
37255
|
const assembly = helperAssembly(directory);
|
|
@@ -36674,8 +37289,8 @@ async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COM
|
|
|
36674
37289
|
return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
|
|
36675
37290
|
}
|
|
36676
37291
|
async function compileHelper(directory, assembly, budgetMs) {
|
|
36677
|
-
const scratch = mkdtempSync2(
|
|
36678
|
-
const source =
|
|
37292
|
+
const scratch = mkdtempSync2(join24(directory, "gdharness-desktop-source-"));
|
|
37293
|
+
const source = join24(scratch, "helper.cs");
|
|
36679
37294
|
writeFileSync12(source, SOURCE, "utf8");
|
|
36680
37295
|
const literal = (path) => `'${path.replaceAll("'", "''")}'`;
|
|
36681
37296
|
const script = [
|
|
@@ -36889,12 +37504,12 @@ import {
|
|
|
36889
37504
|
statSync as statSync10,
|
|
36890
37505
|
writeFileSync as writeFileSync13
|
|
36891
37506
|
} from "node:fs";
|
|
36892
|
-
import { dirname as dirname15, join as
|
|
37507
|
+
import { dirname as dirname15, join as join25 } from "node:path";
|
|
36893
37508
|
function editorLogPath(projectPath) {
|
|
36894
|
-
return
|
|
37509
|
+
return join25(projectPath, ".godot", "gdharness-editor.log");
|
|
36895
37510
|
}
|
|
36896
37511
|
function editorLogNotePath(projectPath) {
|
|
36897
|
-
return
|
|
37512
|
+
return join25(projectPath, ".godot", "gdharness-editor.json");
|
|
36898
37513
|
}
|
|
36899
37514
|
function readEditorLogNote(projectPath) {
|
|
36900
37515
|
try {
|
|
@@ -37930,7 +38545,7 @@ var init_rename = __esm(() => {
|
|
|
37930
38545
|
import { existsSync as existsSync22, realpathSync as realpathSync4 } from "node:fs";
|
|
37931
38546
|
import { readFile, realpath } from "node:fs/promises";
|
|
37932
38547
|
import { createConnection as createConnection3 } from "node:net";
|
|
37933
|
-
import { dirname as dirname17, join as
|
|
38548
|
+
import { dirname as dirname17, join as join26, resolve as resolve5 } from "node:path";
|
|
37934
38549
|
import { setTimeout as delay4 } from "node:timers/promises";
|
|
37935
38550
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
37936
38551
|
function initializeTimeoutMs() {
|
|
@@ -38608,7 +39223,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
38608
39223
|
} catch {
|
|
38609
39224
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
38610
39225
|
}
|
|
38611
|
-
if (!existsSync22(
|
|
39226
|
+
if (!existsSync22(join26(projectPath, "project.godot"))) {
|
|
38612
39227
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
38613
39228
|
}
|
|
38614
39229
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -38769,7 +39384,7 @@ var init_lsp_client = __esm(() => {
|
|
|
38769
39384
|
|
|
38770
39385
|
// src/rename-symbol.ts
|
|
38771
39386
|
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync11, realpathSync as realpathSync5, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
|
|
38772
|
-
import { basename as basename7, dirname as dirname18, join as
|
|
39387
|
+
import { basename as basename7, dirname as dirname18, join as join27, relative as relative4, resolve as resolve6 } from "node:path";
|
|
38773
39388
|
function resourcePathOf(projectPath, absolute) {
|
|
38774
39389
|
const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
|
|
38775
39390
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
@@ -38782,7 +39397,7 @@ function onDiskSpelling(path) {
|
|
|
38782
39397
|
let at = resolve6(path);
|
|
38783
39398
|
for (;; ) {
|
|
38784
39399
|
try {
|
|
38785
|
-
return
|
|
39400
|
+
return join27(realpathSync5.native(at), ...missing.reverse());
|
|
38786
39401
|
} catch {
|
|
38787
39402
|
const parent = dirname18(at);
|
|
38788
39403
|
if (parent === at) {
|
|
@@ -39210,15 +39825,15 @@ ${mention.kind}`;
|
|
|
39210
39825
|
};
|
|
39211
39826
|
}
|
|
39212
39827
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
39213
|
-
const directory =
|
|
39828
|
+
const directory = join27(projectPath, ".godot", "gdharness-renames");
|
|
39214
39829
|
mkdirSync12(directory, { recursive: true });
|
|
39215
39830
|
detailWrites += 1;
|
|
39216
|
-
const path =
|
|
39831
|
+
const path = join27(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
39217
39832
|
writeFileSync15(path, `${JSON.stringify(detail, null, 2)}
|
|
39218
39833
|
`, "utf8");
|
|
39219
39834
|
const kept = readdirSync11(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
39220
39835
|
for (const old of kept.slice(10)) {
|
|
39221
|
-
rmSync12(
|
|
39836
|
+
rmSync12(join27(directory, old), { force: true });
|
|
39222
39837
|
}
|
|
39223
39838
|
return path;
|
|
39224
39839
|
}
|
|
@@ -39267,10 +39882,10 @@ import {
|
|
|
39267
39882
|
statSync as statSync11,
|
|
39268
39883
|
writeFileSync as writeFileSync16
|
|
39269
39884
|
} from "node:fs";
|
|
39270
|
-
import { basename as basename8, dirname as dirname19, extname as extname3, join as
|
|
39885
|
+
import { basename as basename8, dirname as dirname19, extname as extname3, join as join28, relative as relative5 } from "node:path";
|
|
39271
39886
|
function filesUnder(directory) {
|
|
39272
39887
|
return readdirSync12(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
39273
|
-
const path =
|
|
39888
|
+
const path = join28(directory, entry.name);
|
|
39274
39889
|
return entry.isDirectory() ? filesUnder(path) : [path];
|
|
39275
39890
|
});
|
|
39276
39891
|
}
|
|
@@ -39291,8 +39906,8 @@ function unseenByTheEngine(projectPath, path) {
|
|
|
39291
39906
|
return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
|
|
39292
39907
|
}
|
|
39293
39908
|
if (at < steps.length - 1) {
|
|
39294
|
-
directory =
|
|
39295
|
-
if (existsSync24(
|
|
39909
|
+
directory = join28(directory, step);
|
|
39910
|
+
if (existsSync24(join28(directory, ".gdignore"))) {
|
|
39296
39911
|
return `${path} is under a directory holding a .gdignore, which the engine does not load`;
|
|
39297
39912
|
}
|
|
39298
39913
|
}
|
|
@@ -39304,12 +39919,12 @@ function installedAddonOf(projectPath, path) {
|
|
|
39304
39919
|
if (steps[0] !== "addons" || steps[1] === undefined) {
|
|
39305
39920
|
return null;
|
|
39306
39921
|
}
|
|
39307
|
-
return existsSync24(
|
|
39922
|
+
return existsSync24(join28(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
|
|
39308
39923
|
}
|
|
39309
39924
|
function binaryResources(projectPath) {
|
|
39310
39925
|
const found = [];
|
|
39311
39926
|
const visit = (directory, prefix) => {
|
|
39312
|
-
if (existsSync24(
|
|
39927
|
+
if (existsSync24(join28(directory, ".gdignore"))) {
|
|
39313
39928
|
return;
|
|
39314
39929
|
}
|
|
39315
39930
|
for (const entry of readdirSync12(directory, { withFileTypes: true })) {
|
|
@@ -39318,7 +39933,7 @@ function binaryResources(projectPath) {
|
|
|
39318
39933
|
}
|
|
39319
39934
|
const path = `${prefix}${entry.name}`;
|
|
39320
39935
|
if (entry.isDirectory()) {
|
|
39321
|
-
visit(
|
|
39936
|
+
visit(join28(directory, entry.name), `${path}/`);
|
|
39322
39937
|
} else if (/\.(scn|res)$/i.test(entry.name)) {
|
|
39323
39938
|
found.push(path);
|
|
39324
39939
|
}
|
|
@@ -39328,7 +39943,7 @@ function binaryResources(projectPath) {
|
|
|
39328
39943
|
return found;
|
|
39329
39944
|
}
|
|
39330
39945
|
function targetPathOf(projectPath, absolute) {
|
|
39331
|
-
const inside = relative5(onDiskSpelling(projectPath),
|
|
39946
|
+
const inside = relative5(onDiskSpelling(projectPath), join28(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
|
|
39332
39947
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
39333
39948
|
return null;
|
|
39334
39949
|
}
|
|
@@ -39383,7 +39998,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
39383
39998
|
return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
|
|
39384
39999
|
}
|
|
39385
40000
|
if (!isDirectory) {
|
|
39386
|
-
const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(
|
|
40001
|
+
const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(join28(projectPath, path.slice("res://".length))));
|
|
39387
40002
|
if (owner !== undefined && owner !== null) {
|
|
39388
40003
|
return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
|
|
39389
40004
|
}
|
|
@@ -39417,12 +40032,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
39417
40032
|
scripts,
|
|
39418
40033
|
moves,
|
|
39419
40034
|
named: [from],
|
|
39420
|
-
exists: (path) => existsSync24(
|
|
40035
|
+
exists: (path) => existsSync24(join28(projectPath, path.slice("res://".length)))
|
|
39421
40036
|
}, { edits: [], mentions: [] });
|
|
39422
40037
|
plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
|
|
39423
40038
|
const binaries = [];
|
|
39424
40039
|
for (const path of binaryResources(projectPath)) {
|
|
39425
|
-
const bytes = readFileSync23(
|
|
40040
|
+
const bytes = readFileSync23(join28(projectPath, path.slice("res://".length)));
|
|
39426
40041
|
const names = [...moves.keys()].find((moving) => bytes.includes(moving));
|
|
39427
40042
|
if (names !== undefined) {
|
|
39428
40043
|
binaries.push({
|
|
@@ -39441,7 +40056,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
39441
40056
|
}));
|
|
39442
40057
|
const left = leftAloneReport(mentions);
|
|
39443
40058
|
const rewrites = rewritesOf(plan, texts, null);
|
|
39444
|
-
const absoluteOf = (path) =>
|
|
40059
|
+
const absoluteOf = (path) => join28(projectPath, path.slice("res://".length));
|
|
39445
40060
|
const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
|
|
39446
40061
|
const moved = [...moves].map(([source, target]) => {
|
|
39447
40062
|
const rewrite = rewritten.get(source);
|
|
@@ -42882,742 +43497,243 @@ var init_godot_bridge = __esm(() => {
|
|
|
42882
43497
|
this.connectionInfo.greeted = true;
|
|
42883
43498
|
const pid = this.connectionInfo.editorPid;
|
|
42884
43499
|
if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
|
|
42885
|
-
this.identify(pid);
|
|
42886
|
-
return;
|
|
42887
|
-
}
|
|
42888
|
-
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
42889
|
-
}
|
|
42890
|
-
return;
|
|
42891
|
-
case "pong":
|
|
42892
|
-
if (this.connectionInfo) {
|
|
42893
|
-
this.connectionInfo.lastPongAt = new Date;
|
|
42894
|
-
}
|
|
42895
|
-
return;
|
|
42896
|
-
}
|
|
42897
|
-
}
|
|
42898
|
-
invokeToolDirect(toolName, args, counted, resourceKey) {
|
|
42899
|
-
if (!this.isConnected()) {
|
|
42900
|
-
return Promise.reject(new Error("Godot is not connected"));
|
|
42901
|
-
}
|
|
42902
|
-
const requestId = randomUUID();
|
|
42903
|
-
const message = {
|
|
42904
|
-
type: "tool_invoke",
|
|
42905
|
-
id: requestId,
|
|
42906
|
-
tool: toolName,
|
|
42907
|
-
args
|
|
42908
|
-
};
|
|
42909
|
-
return new Promise((resolve, reject) => {
|
|
42910
|
-
const timeout = setTimeout(() => {
|
|
42911
|
-
this.pendingRequests.delete(requestId);
|
|
42912
|
-
reject(new BridgeTimeout(toolName, this.timeoutMs));
|
|
42913
|
-
}, this.timeoutMs);
|
|
42914
|
-
this.pendingRequests.set(requestId, {
|
|
42915
|
-
toolName,
|
|
42916
|
-
timeout,
|
|
42917
|
-
resolve,
|
|
42918
|
-
reject,
|
|
42919
|
-
startedAt: Date.now(),
|
|
42920
|
-
resourceKey,
|
|
42921
|
-
counted
|
|
42922
|
-
});
|
|
42923
|
-
this.emitBridgeEvent("tool_start", {
|
|
42924
|
-
tool: toolName,
|
|
42925
|
-
id: requestId,
|
|
42926
|
-
args
|
|
42927
|
-
});
|
|
42928
|
-
try {
|
|
42929
|
-
this.sendMessage(message);
|
|
42930
|
-
} catch (error) {
|
|
42931
|
-
clearTimeout(timeout);
|
|
42932
|
-
this.pendingRequests.delete(requestId);
|
|
42933
|
-
reject(toError(error));
|
|
42934
|
-
}
|
|
42935
|
-
});
|
|
42936
|
-
}
|
|
42937
|
-
sendMessage(message) {
|
|
42938
|
-
if (this.socket?.readyState !== import_websocket.default.OPEN) {
|
|
42939
|
-
throw new Refusal("Godot is not connected");
|
|
42940
|
-
}
|
|
42941
|
-
this.socket.send(JSON.stringify(message));
|
|
42942
|
-
}
|
|
42943
|
-
startKeepalive() {
|
|
42944
|
-
this.stopKeepalive();
|
|
42945
|
-
this.pingInterval = setInterval(() => {
|
|
42946
|
-
if (!this.isConnected()) {
|
|
42947
|
-
return;
|
|
42948
|
-
}
|
|
42949
|
-
try {
|
|
42950
|
-
const ping = { type: "ping" };
|
|
42951
|
-
this.sendMessage(ping);
|
|
42952
|
-
} catch (error) {
|
|
42953
|
-
this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
|
|
42954
|
-
}
|
|
42955
|
-
}, KEEPALIVE_INTERVAL_MS);
|
|
42956
|
-
}
|
|
42957
|
-
stopKeepalive() {
|
|
42958
|
-
if (!this.pingInterval) {
|
|
42959
|
-
return;
|
|
42960
|
-
}
|
|
42961
|
-
clearInterval(this.pingInterval);
|
|
42962
|
-
this.pingInterval = null;
|
|
42963
|
-
}
|
|
42964
|
-
belongsElsewhere(theirProject) {
|
|
42965
|
-
if (this.ownProject === null || theirProject.trim() === "") {
|
|
42966
|
-
return false;
|
|
42967
|
-
}
|
|
42968
|
-
return !isSameDirectory(this.ownProject, theirProject);
|
|
42969
|
-
}
|
|
42970
|
-
sendElsewhere(theirProject) {
|
|
42971
|
-
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
42972
|
-
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
42973
|
-
const stranger = this.socket;
|
|
42974
|
-
this.socket = null;
|
|
42975
|
-
this.connectionInfo = null;
|
|
42976
|
-
this.stopKeepalive();
|
|
42977
|
-
const fits = closeReason(said, "start") === said;
|
|
42978
|
-
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
42979
|
-
}
|
|
42980
|
-
elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
|
|
42981
|
-
const known = pid === undefined ? undefined : this.oneShots.get(pid);
|
|
42982
|
-
if (pid !== undefined && known !== undefined) {
|
|
42983
|
-
this.turnAway(pid, known);
|
|
42984
|
-
return;
|
|
42985
|
-
}
|
|
42986
|
-
const info = this.connectionInfo;
|
|
42987
|
-
const socket = this.socket;
|
|
42988
|
-
const reader = this.oneShotReader;
|
|
42989
|
-
if (pid === undefined || openedByAServer || info === null || reader === null) {
|
|
42990
|
-
this.sendElsewhere(theirProject);
|
|
42991
|
-
return;
|
|
42992
|
-
}
|
|
42993
|
-
info.identifying = true;
|
|
42994
|
-
reader(pid).catch(() => {
|
|
42995
|
-
return;
|
|
42996
|
-
}).then((option) => {
|
|
42997
|
-
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
42998
|
-
return;
|
|
42999
|
-
}
|
|
43000
|
-
if (typeof option === "string") {
|
|
43001
|
-
this.oneShots.set(pid, option);
|
|
43002
|
-
this.turnAway(pid, option);
|
|
43003
|
-
return;
|
|
43004
|
-
}
|
|
43005
|
-
this.sendElsewhere(theirProject);
|
|
43006
|
-
});
|
|
43007
|
-
}
|
|
43008
|
-
identify(pid) {
|
|
43009
|
-
const known = this.oneShots.get(pid);
|
|
43010
|
-
if (known !== undefined) {
|
|
43011
|
-
this.turnAway(pid, known);
|
|
43012
|
-
return;
|
|
43013
|
-
}
|
|
43014
|
-
const info = this.connectionInfo;
|
|
43015
|
-
const socket = this.socket;
|
|
43016
|
-
const reader = this.oneShotReader;
|
|
43017
|
-
if (info === null || reader === null) {
|
|
43018
|
-
return;
|
|
43019
|
-
}
|
|
43020
|
-
info.identifying = true;
|
|
43021
|
-
reader(pid).catch(() => {
|
|
43022
|
-
return;
|
|
43023
|
-
}).then((option) => {
|
|
43024
|
-
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
43025
|
-
return;
|
|
43026
|
-
}
|
|
43027
|
-
if (typeof option === "string") {
|
|
43028
|
-
this.oneShots.set(pid, option);
|
|
43029
|
-
this.turnAway(pid, option);
|
|
43030
|
-
return;
|
|
43031
|
-
}
|
|
43032
|
-
info.identifying = false;
|
|
43033
|
-
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
43034
|
-
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
43035
|
-
});
|
|
43036
|
-
}
|
|
43037
|
-
turnAway(pid, option) {
|
|
43038
|
-
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
43039
|
-
this.log("info", `Turning away an engine run: ${said}`);
|
|
43040
|
-
this.turnedAway = { pid, option, at: new Date };
|
|
43041
|
-
const run = this.socket;
|
|
43042
|
-
this.socket = null;
|
|
43043
|
-
this.connectionInfo = null;
|
|
43044
|
-
this.stopKeepalive();
|
|
43045
|
-
run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
|
|
43046
|
-
}
|
|
43047
|
-
handleDisconnect(disconnectedSocket, reason) {
|
|
43048
|
-
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
43049
|
-
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
43050
|
-
return;
|
|
43051
|
-
}
|
|
43052
|
-
this.stopKeepalive();
|
|
43053
|
-
this.socket = null;
|
|
43054
|
-
this.connectionInfo = null;
|
|
43055
|
-
this.emitBridgeEvent("godot_disconnected", {});
|
|
43056
|
-
this.rejectAllPending(reason);
|
|
43057
|
-
this.resourceQueues.clear();
|
|
43058
|
-
}
|
|
43059
|
-
emitBridgeEvent(eventName, payload) {
|
|
43060
|
-
this.emit(eventName, payload);
|
|
43061
|
-
}
|
|
43062
|
-
rejectAllPending(error) {
|
|
43063
|
-
for (const pending of this.pendingRequests.values()) {
|
|
43064
|
-
clearTimeout(pending.timeout);
|
|
43065
|
-
pending.reject(error);
|
|
43066
|
-
}
|
|
43067
|
-
this.pendingRequests.clear();
|
|
43068
|
-
}
|
|
43069
|
-
enqueueResourceRequest(resourceKey, task) {
|
|
43070
|
-
const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
|
|
43071
|
-
const taskPromise = previous.catch(() => {
|
|
43072
|
-
return;
|
|
43073
|
-
}).then(task);
|
|
43074
|
-
const tail = taskPromise.then(() => {
|
|
43075
|
-
return;
|
|
43076
|
-
}, () => {
|
|
43077
|
-
return;
|
|
43078
|
-
});
|
|
43079
|
-
this.resourceQueues.set(resourceKey, tail);
|
|
43080
|
-
return taskPromise.finally(() => {
|
|
43081
|
-
if (this.resourceQueues.get(resourceKey) === tail) {
|
|
43082
|
-
this.resourceQueues.delete(resourceKey);
|
|
43083
|
-
}
|
|
43084
|
-
});
|
|
43085
|
-
}
|
|
43086
|
-
getResourceKey(args) {
|
|
43087
|
-
const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
|
|
43088
|
-
if (scenePath) {
|
|
43089
|
-
return `scene:${scenePath}`;
|
|
43090
|
-
}
|
|
43091
|
-
const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
|
|
43092
|
-
if (resourcePath) {
|
|
43093
|
-
return `resource:${resourcePath}`;
|
|
43094
|
-
}
|
|
43095
|
-
return;
|
|
43096
|
-
}
|
|
43097
|
-
getStringArg(args, key) {
|
|
43098
|
-
const value = args[key];
|
|
43099
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
43100
|
-
}
|
|
43101
|
-
isIncomingMessage(value) {
|
|
43102
|
-
if (!value || typeof value !== "object") {
|
|
43103
|
-
return false;
|
|
43104
|
-
}
|
|
43105
|
-
const message = value;
|
|
43106
|
-
const type = message["type"];
|
|
43107
|
-
if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
|
|
43108
|
-
return false;
|
|
43109
|
-
}
|
|
43110
|
-
if (type === "pong") {
|
|
43111
|
-
return true;
|
|
43112
|
-
}
|
|
43113
|
-
if (type === "godot_ready") {
|
|
43114
|
-
return typeof message["project_path"] === "string";
|
|
43115
|
-
}
|
|
43116
|
-
return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
|
|
43117
|
-
}
|
|
43118
|
-
log(level, message) {
|
|
43119
|
-
console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
|
|
43120
|
-
}
|
|
43121
|
-
};
|
|
43122
|
-
});
|
|
43123
|
-
|
|
43124
|
-
// src/junit.ts
|
|
43125
|
-
function decoded(text) {
|
|
43126
|
-
return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
|
|
43127
|
-
if (entity.startsWith("#")) {
|
|
43128
|
-
const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
|
|
43129
|
-
if (!Number.isInteger(code) || code < 0 || code > 1114111) {
|
|
43130
|
-
throw new MalformedReportError(`Not a character: ${whole}`);
|
|
43131
|
-
}
|
|
43132
|
-
return String.fromCodePoint(code);
|
|
43133
|
-
}
|
|
43134
|
-
const named = ENTITIES[entity];
|
|
43135
|
-
if (named === undefined) {
|
|
43136
|
-
throw new MalformedReportError(`Unknown entity ${whole}`);
|
|
43137
|
-
}
|
|
43138
|
-
return named;
|
|
43139
|
-
});
|
|
43140
|
-
}
|
|
43141
|
-
function tagEnd(source, from) {
|
|
43142
|
-
let quote = null;
|
|
43143
|
-
for (let at = from + 1;at < source.length; at += 1) {
|
|
43144
|
-
const character = source[at];
|
|
43145
|
-
if (quote !== null) {
|
|
43146
|
-
if (character === quote) {
|
|
43147
|
-
quote = null;
|
|
43148
|
-
}
|
|
43149
|
-
} else if (character === '"' || character === "'") {
|
|
43150
|
-
quote = character;
|
|
43151
|
-
} else if (character === ">") {
|
|
43152
|
-
return at;
|
|
43153
|
-
}
|
|
43154
|
-
}
|
|
43155
|
-
return -1;
|
|
43156
|
-
}
|
|
43157
|
-
function parseXml(source) {
|
|
43158
|
-
const root = { name: "", attributes: {}, children: [], text: "" };
|
|
43159
|
-
const open = [root];
|
|
43160
|
-
let at = 0;
|
|
43161
|
-
const current = () => {
|
|
43162
|
-
const element = open.at(-1);
|
|
43163
|
-
if (element === undefined) {
|
|
43164
|
-
throw new MalformedReportError("A close tag with nothing open");
|
|
43165
|
-
}
|
|
43166
|
-
return element;
|
|
43167
|
-
};
|
|
43168
|
-
while (at < source.length) {
|
|
43169
|
-
const next = source.indexOf("<", at);
|
|
43170
|
-
if (next === -1) {
|
|
43171
|
-
current().text += decoded(source.slice(at));
|
|
43172
|
-
break;
|
|
43173
|
-
}
|
|
43174
|
-
current().text += decoded(source.slice(at, next));
|
|
43175
|
-
at = next;
|
|
43176
|
-
if (source.startsWith("<![CDATA[", at)) {
|
|
43177
|
-
const end = source.indexOf("]]>", at);
|
|
43178
|
-
if (end === -1) {
|
|
43179
|
-
throw new MalformedReportError("CDATA that never ends");
|
|
43180
|
-
}
|
|
43181
|
-
current().text += source.slice(at + "<![CDATA[".length, end);
|
|
43182
|
-
at = end + "]]>".length;
|
|
43183
|
-
continue;
|
|
43184
|
-
}
|
|
43185
|
-
if (source.startsWith("<!--", at)) {
|
|
43186
|
-
const end = source.indexOf("-->", at);
|
|
43187
|
-
if (end === -1) {
|
|
43188
|
-
throw new MalformedReportError("A comment that never ends");
|
|
43189
|
-
}
|
|
43190
|
-
at = end + "-->".length;
|
|
43191
|
-
continue;
|
|
43192
|
-
}
|
|
43193
|
-
if (source.startsWith("<?", at) || source.startsWith("<!", at)) {
|
|
43194
|
-
const end = source.indexOf(">", at);
|
|
43195
|
-
if (end === -1) {
|
|
43196
|
-
throw new MalformedReportError("A declaration that never ends");
|
|
43197
|
-
}
|
|
43198
|
-
at = end + 1;
|
|
43199
|
-
continue;
|
|
43200
|
-
}
|
|
43201
|
-
const end = tagEnd(source, at);
|
|
43202
|
-
if (end === -1) {
|
|
43203
|
-
throw new MalformedReportError("A tag that never ends");
|
|
43204
|
-
}
|
|
43205
|
-
const tag = source.slice(at + 1, end);
|
|
43206
|
-
at = end + 1;
|
|
43207
|
-
if (tag.startsWith("/")) {
|
|
43208
|
-
const closing = tag.slice(1).trim();
|
|
43209
|
-
const element = current();
|
|
43210
|
-
if (element === root || element.name !== closing) {
|
|
43211
|
-
throw new MalformedReportError(`</${closing}> closes <${element.name || "nothing"}>`);
|
|
43212
|
-
}
|
|
43213
|
-
open.pop();
|
|
43214
|
-
continue;
|
|
43215
|
-
}
|
|
43216
|
-
const selfClosing = tag.endsWith("/");
|
|
43217
|
-
const body = selfClosing ? tag.slice(0, -1) : tag;
|
|
43218
|
-
const nameMatch = /^([^\s/>]+)/.exec(body);
|
|
43219
|
-
if (!nameMatch?.[1]) {
|
|
43220
|
-
throw new MalformedReportError(`A tag with no name: <${tag}>`);
|
|
43221
|
-
}
|
|
43222
|
-
const attributes = {};
|
|
43223
|
-
let read = nameMatch[0].length;
|
|
43224
|
-
ATTRIBUTE.lastIndex = read;
|
|
43225
|
-
let attribute = ATTRIBUTE.exec(body);
|
|
43226
|
-
while (attribute) {
|
|
43227
|
-
attributes[attribute[1] ?? ""] = decoded(attribute[2] ?? attribute[3] ?? "");
|
|
43228
|
-
read = ATTRIBUTE.lastIndex;
|
|
43229
|
-
attribute = ATTRIBUTE.exec(body);
|
|
43230
|
-
}
|
|
43231
|
-
if (body.slice(read).trim() !== "") {
|
|
43232
|
-
throw new MalformedReportError(`Unreadable attributes in <${tag}>`);
|
|
43233
|
-
}
|
|
43234
|
-
const element = { name: nameMatch[1], attributes, children: [], text: "" };
|
|
43235
|
-
current().children.push(element);
|
|
43236
|
-
if (!selfClosing) {
|
|
43237
|
-
open.push(element);
|
|
43238
|
-
}
|
|
43239
|
-
}
|
|
43240
|
-
if (open.length !== 1) {
|
|
43241
|
-
throw new MalformedReportError(`<${current().name}> is never closed`);
|
|
43242
|
-
}
|
|
43243
|
-
return root;
|
|
43244
|
-
}
|
|
43245
|
-
function count(attributes, name) {
|
|
43246
|
-
const value = Number(attributes[name] ?? "0");
|
|
43247
|
-
return Number.isFinite(value) ? value : 0;
|
|
43248
|
-
}
|
|
43249
|
-
function ignoredYetRan(report, ignored) {
|
|
43250
|
-
const found = [];
|
|
43251
|
-
for (const entry of ignored) {
|
|
43252
|
-
const bare = entry.replace("res://", "");
|
|
43253
|
-
const at = bare.lastIndexOf(":");
|
|
43254
|
-
const suiteNamed = at === -1 ? null : bare.slice(0, at);
|
|
43255
|
-
const testNamed = at === -1 ? bare : bare.slice(at + 1);
|
|
43256
|
-
for (const suite of report.suites) {
|
|
43257
|
-
const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
|
|
43258
|
-
const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
|
|
43259
|
-
if (ran !== undefined) {
|
|
43260
|
-
found.push({ entry, ran: `${suite.name}:${ran.name}` });
|
|
43261
|
-
}
|
|
43262
|
-
}
|
|
43263
|
-
}
|
|
43264
|
-
return found;
|
|
43265
|
-
}
|
|
43266
|
-
function caseOf(suite, element) {
|
|
43267
|
-
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
43268
|
-
const detail = child.text.trim();
|
|
43269
|
-
return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
|
|
43270
|
-
});
|
|
43271
|
-
const reports = [...of("failure"), ...of("error"), ...of("skipped")];
|
|
43272
|
-
const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
|
|
43273
|
-
return {
|
|
43274
|
-
suite: suite.attributes["name"] ?? "",
|
|
43275
|
-
name: element.attributes["name"] ?? "",
|
|
43276
|
-
status,
|
|
43277
|
-
time: count(element.attributes, "time"),
|
|
43278
|
-
message: reports[0]?.message ?? null,
|
|
43279
|
-
detail: reports[0]?.detail ?? null,
|
|
43280
|
-
reports
|
|
43281
|
-
};
|
|
43282
|
-
}
|
|
43283
|
-
function failedAnAssertion(entry) {
|
|
43284
|
-
return entry.reports.some((report) => report.kind === "failure");
|
|
43285
|
-
}
|
|
43286
|
-
function pathOf(suite) {
|
|
43287
|
-
const directory = suite.attributes["package"];
|
|
43288
|
-
const name = suite.attributes["name"];
|
|
43289
|
-
return directory && name ? `res://${directory}/${name}.gd` : null;
|
|
43290
|
-
}
|
|
43291
|
-
function parseJUnit(xml) {
|
|
43292
|
-
const document = parseXml(xml);
|
|
43293
|
-
const top = document.children.find((child) => child.name === "testsuites" || child.name === "testsuite");
|
|
43294
|
-
if (top === undefined) {
|
|
43295
|
-
throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
|
|
43296
|
-
}
|
|
43297
|
-
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
43298
|
-
const suites = suiteElements.map((suite) => {
|
|
43299
|
-
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
43300
|
-
const failures = cases.filter(failedAnAssertion).length;
|
|
43301
|
-
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
43302
|
-
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
43303
|
-
return {
|
|
43304
|
-
name: suite.attributes["name"] ?? "",
|
|
43305
|
-
path: pathOf(suite),
|
|
43306
|
-
tests: cases.length,
|
|
43307
|
-
discovered: count(suite.attributes, "tests"),
|
|
43308
|
-
failures,
|
|
43309
|
-
errors,
|
|
43310
|
-
hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
|
|
43311
|
-
hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
|
|
43312
|
-
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
43313
|
-
time: count(suite.attributes, "time"),
|
|
43314
|
-
cases
|
|
43315
|
-
};
|
|
43316
|
-
});
|
|
43317
|
-
const all = suites.flatMap((suite) => suite.cases);
|
|
43318
|
-
return {
|
|
43319
|
-
tests: all.length,
|
|
43320
|
-
failures: all.filter(failedAnAssertion).length,
|
|
43321
|
-
errors: all.filter((entry) => entry.status === "error").length,
|
|
43322
|
-
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
43323
|
-
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
43324
|
-
skipped: all.filter((entry) => entry.status === "skipped").length,
|
|
43325
|
-
time: suites.reduce((sum, suite) => sum + suite.time, 0),
|
|
43326
|
-
suites
|
|
43327
|
-
};
|
|
43328
|
-
}
|
|
43329
|
-
function hookFailuresPrinted(printed) {
|
|
43330
|
-
return printed.flatMap((line) => {
|
|
43331
|
-
const found = HOOK_FAILURE.exec(line);
|
|
43332
|
-
return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
|
|
43333
|
-
});
|
|
43334
|
-
}
|
|
43335
|
-
function whyNoReport(printed, asked) {
|
|
43336
|
-
const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
|
|
43337
|
-
if (missing !== undefined) {
|
|
43338
|
-
return `nothing at ${missing[1] ?? asked}`;
|
|
43339
|
-
}
|
|
43340
|
-
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
43341
|
-
}
|
|
43342
|
-
function scriptErrorsPrinted(printed) {
|
|
43343
|
-
return discoveryListing(printed)?.errors ?? [];
|
|
43344
|
-
}
|
|
43345
|
-
function discoveryListing(printed) {
|
|
43346
|
-
const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
|
|
43347
|
-
if (start < 0) {
|
|
43348
|
-
return null;
|
|
43349
|
-
}
|
|
43350
|
-
const found = [];
|
|
43351
|
-
let message = null;
|
|
43352
|
-
let to = start + 1;
|
|
43353
|
-
for (const line of printed.slice(start + 1)) {
|
|
43354
|
-
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
43355
|
-
if (at !== null && message !== null) {
|
|
43356
|
-
found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
|
|
43357
|
-
message = null;
|
|
43358
|
-
} else if (at === null && message === null && /^ {2}\S/.test(line)) {
|
|
43359
|
-
message = line.trim();
|
|
43360
|
-
} else {
|
|
43361
|
-
break;
|
|
43362
|
-
}
|
|
43363
|
-
to += 1;
|
|
43364
|
-
}
|
|
43365
|
-
const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
43366
|
-
return { errors, from: start, to };
|
|
43367
|
-
}
|
|
43368
|
-
function unspelled(text) {
|
|
43369
|
-
return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
|
|
43370
|
-
}
|
|
43371
|
-
function readDiff(printed) {
|
|
43372
|
-
let merged = "";
|
|
43373
|
-
let actual = "";
|
|
43374
|
-
let background = null;
|
|
43375
|
-
let last = 0;
|
|
43376
|
-
const take = (text) => {
|
|
43377
|
-
merged += text;
|
|
43378
|
-
if (background === null) {
|
|
43379
|
-
actual += text;
|
|
43380
|
-
} else if (background === "added") {
|
|
43381
|
-
actual += unspelled(text);
|
|
43382
|
-
}
|
|
43383
|
-
};
|
|
43384
|
-
for (const match of printed.matchAll(COLOUR2)) {
|
|
43385
|
-
take(printed.slice(last, match.index));
|
|
43386
|
-
last = match.index + match[0].length;
|
|
43387
|
-
const codes = (match[1] ?? "").split(";");
|
|
43388
|
-
if (codes[0] === "48" && codes[1] === "2") {
|
|
43389
|
-
background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
|
|
43390
|
-
} else if (codes[0] === "0" || codes[0] === "") {
|
|
43391
|
-
background = null;
|
|
43392
|
-
}
|
|
43393
|
-
}
|
|
43394
|
-
take(printed.slice(last));
|
|
43395
|
-
return { merged, actual };
|
|
43396
|
-
}
|
|
43397
|
-
function withActualsPrinted(failed, printed) {
|
|
43398
|
-
const diffs = [...printed.replaceAll(`\r
|
|
43399
|
-
`, `
|
|
43400
|
-
`).matchAll(PRINTED_ACTUAL)].map((match) => ({
|
|
43401
|
-
suffix: match[3] ?? "",
|
|
43402
|
-
at: match[5] ?? "",
|
|
43403
|
-
...readDiff(match[1] ?? "")
|
|
43404
|
-
}));
|
|
43405
|
-
const used = new Set;
|
|
43406
|
-
return failed.map((entry) => {
|
|
43407
|
-
if (entry.detail === null) {
|
|
43408
|
-
return entry;
|
|
43500
|
+
this.identify(pid);
|
|
43501
|
+
return;
|
|
43502
|
+
}
|
|
43503
|
+
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
43504
|
+
}
|
|
43505
|
+
return;
|
|
43506
|
+
case "pong":
|
|
43507
|
+
if (this.connectionInfo) {
|
|
43508
|
+
this.connectionInfo.lastPongAt = new Date;
|
|
43509
|
+
}
|
|
43510
|
+
return;
|
|
43511
|
+
}
|
|
43409
43512
|
}
|
|
43410
|
-
|
|
43411
|
-
|
|
43412
|
-
|
|
43413
|
-
if (used.has(index)) {
|
|
43414
|
-
continue;
|
|
43513
|
+
invokeToolDirect(toolName, args, counted, resourceKey) {
|
|
43514
|
+
if (!this.isConnected()) {
|
|
43515
|
+
return Promise.reject(new Error("Godot is not connected"));
|
|
43415
43516
|
}
|
|
43416
|
-
const
|
|
43417
|
-
|
|
43418
|
-
|
|
43419
|
-
|
|
43420
|
-
|
|
43421
|
-
|
|
43422
|
-
|
|
43423
|
-
|
|
43424
|
-
|
|
43517
|
+
const requestId = randomUUID();
|
|
43518
|
+
const message = {
|
|
43519
|
+
type: "tool_invoke",
|
|
43520
|
+
id: requestId,
|
|
43521
|
+
tool: toolName,
|
|
43522
|
+
args
|
|
43523
|
+
};
|
|
43524
|
+
return new Promise((resolve, reject) => {
|
|
43525
|
+
const timeout = setTimeout(() => {
|
|
43526
|
+
this.pendingRequests.delete(requestId);
|
|
43527
|
+
reject(new BridgeTimeout(toolName, this.timeoutMs));
|
|
43528
|
+
}, this.timeoutMs);
|
|
43529
|
+
this.pendingRequests.set(requestId, {
|
|
43530
|
+
toolName,
|
|
43531
|
+
timeout,
|
|
43532
|
+
resolve,
|
|
43533
|
+
reject,
|
|
43534
|
+
startedAt: Date.now(),
|
|
43535
|
+
resourceKey,
|
|
43536
|
+
counted
|
|
43537
|
+
});
|
|
43538
|
+
this.emitBridgeEvent("tool_start", {
|
|
43539
|
+
tool: toolName,
|
|
43540
|
+
id: requestId,
|
|
43541
|
+
args
|
|
43542
|
+
});
|
|
43543
|
+
try {
|
|
43544
|
+
this.sendMessage(message);
|
|
43545
|
+
} catch (error) {
|
|
43546
|
+
clearTimeout(timeout);
|
|
43547
|
+
this.pendingRequests.delete(requestId);
|
|
43548
|
+
reject(toError(error));
|
|
43549
|
+
}
|
|
43550
|
+
});
|
|
43551
|
+
}
|
|
43552
|
+
sendMessage(message) {
|
|
43553
|
+
if (this.socket?.readyState !== import_websocket.default.OPEN) {
|
|
43554
|
+
throw new Refusal("Godot is not connected");
|
|
43425
43555
|
}
|
|
43556
|
+
this.socket.send(JSON.stringify(message));
|
|
43426
43557
|
}
|
|
43427
|
-
|
|
43428
|
-
|
|
43429
|
-
|
|
43430
|
-
|
|
43431
|
-
|
|
43432
|
-
|
|
43558
|
+
startKeepalive() {
|
|
43559
|
+
this.stopKeepalive();
|
|
43560
|
+
this.pingInterval = setInterval(() => {
|
|
43561
|
+
if (!this.isConnected()) {
|
|
43562
|
+
return;
|
|
43563
|
+
}
|
|
43564
|
+
try {
|
|
43565
|
+
const ping = { type: "ping" };
|
|
43566
|
+
this.sendMessage(ping);
|
|
43567
|
+
} catch (error) {
|
|
43568
|
+
this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
|
|
43569
|
+
}
|
|
43570
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
43433
43571
|
}
|
|
43434
|
-
|
|
43435
|
-
|
|
43436
|
-
|
|
43437
|
-
|
|
43572
|
+
stopKeepalive() {
|
|
43573
|
+
if (!this.pingInterval) {
|
|
43574
|
+
return;
|
|
43575
|
+
}
|
|
43576
|
+
clearInterval(this.pingInterval);
|
|
43577
|
+
this.pingInterval = null;
|
|
43438
43578
|
}
|
|
43439
|
-
|
|
43440
|
-
|
|
43441
|
-
|
|
43442
|
-
|
|
43443
|
-
|
|
43444
|
-
return detail;
|
|
43445
|
-
}
|
|
43446
|
-
return `${detail.slice(0, LONGEST_DETAIL)}
|
|
43447
|
-
[${detail.length - LONGEST_DETAIL} more characters left out]`;
|
|
43448
|
-
}
|
|
43449
|
-
function foldedReports(reports) {
|
|
43450
|
-
const first = new Map;
|
|
43451
|
-
for (const report of reports) {
|
|
43452
|
-
const key = JSON.stringify([report.kind, report.message, report.detail]);
|
|
43453
|
-
const seen = first.get(key);
|
|
43454
|
-
if (seen === undefined) {
|
|
43455
|
-
first.set(key, { report, times: 1 });
|
|
43456
|
-
} else {
|
|
43457
|
-
seen.times += 1;
|
|
43579
|
+
belongsElsewhere(theirProject) {
|
|
43580
|
+
if (this.ownProject === null || theirProject.trim() === "") {
|
|
43581
|
+
return false;
|
|
43582
|
+
}
|
|
43583
|
+
return !isSameDirectory(this.ownProject, theirProject);
|
|
43458
43584
|
}
|
|
43459
|
-
|
|
43460
|
-
|
|
43461
|
-
|
|
43462
|
-
|
|
43463
|
-
|
|
43464
|
-
|
|
43465
|
-
|
|
43466
|
-
|
|
43467
|
-
|
|
43468
|
-
let next = 0;
|
|
43469
|
-
return cases.map(({ entry, path }) => {
|
|
43470
|
-
const reports = read.slice(next, next + entry.reports.length);
|
|
43471
|
-
next += entry.reports.length;
|
|
43472
|
-
const folded = foldedReports(reports);
|
|
43473
|
-
const shown = folded.slice(0, MOST_REPORTS_SHOWN);
|
|
43474
|
-
return {
|
|
43475
|
-
suite: entry.suite,
|
|
43476
|
-
name: entry.name,
|
|
43477
|
-
status: entry.status,
|
|
43478
|
-
time: entry.time,
|
|
43479
|
-
path,
|
|
43480
|
-
message: reports[0]?.message ?? null,
|
|
43481
|
-
detail: shortened(reports[0]?.detail ?? null),
|
|
43482
|
-
...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
|
|
43483
|
-
...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
|
|
43484
|
-
};
|
|
43485
|
-
});
|
|
43486
|
-
}
|
|
43487
|
-
function failedWithin(failed, budget) {
|
|
43488
|
-
const sizes = failed.map((entry) => JSON.stringify(entry).length);
|
|
43489
|
-
const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
|
|
43490
|
-
const whole = new Set;
|
|
43491
|
-
let spent = 0;
|
|
43492
|
-
for (const index of smallestFirst) {
|
|
43493
|
-
const size = sizes[index] ?? 0;
|
|
43494
|
-
if (spent + size > budget) {
|
|
43495
|
-
break;
|
|
43585
|
+
sendElsewhere(theirProject) {
|
|
43586
|
+
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
43587
|
+
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
43588
|
+
const stranger = this.socket;
|
|
43589
|
+
this.socket = null;
|
|
43590
|
+
this.connectionInfo = null;
|
|
43591
|
+
this.stopKeepalive();
|
|
43592
|
+
const fits = closeReason(said, "start") === said;
|
|
43593
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
43496
43594
|
}
|
|
43497
|
-
|
|
43498
|
-
|
|
43499
|
-
|
|
43500
|
-
|
|
43501
|
-
|
|
43502
|
-
|
|
43595
|
+
elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
|
|
43596
|
+
const known = pid === undefined ? undefined : this.oneShots.get(pid);
|
|
43597
|
+
if (pid !== undefined && known !== undefined) {
|
|
43598
|
+
this.turnAway(pid, known);
|
|
43599
|
+
return;
|
|
43600
|
+
}
|
|
43601
|
+
const info = this.connectionInfo;
|
|
43602
|
+
const socket = this.socket;
|
|
43603
|
+
const reader = this.oneShotReader;
|
|
43604
|
+
if (pid === undefined || openedByAServer || info === null || reader === null) {
|
|
43605
|
+
this.sendElsewhere(theirProject);
|
|
43606
|
+
return;
|
|
43607
|
+
}
|
|
43608
|
+
info.identifying = true;
|
|
43609
|
+
reader(pid).catch(() => {
|
|
43610
|
+
return;
|
|
43611
|
+
}).then((option) => {
|
|
43612
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
43613
|
+
return;
|
|
43614
|
+
}
|
|
43615
|
+
if (typeof option === "string") {
|
|
43616
|
+
this.oneShots.set(pid, option);
|
|
43617
|
+
this.turnAway(pid, option);
|
|
43618
|
+
return;
|
|
43619
|
+
}
|
|
43620
|
+
this.sendElsewhere(theirProject);
|
|
43621
|
+
});
|
|
43503
43622
|
}
|
|
43504
|
-
|
|
43505
|
-
|
|
43506
|
-
|
|
43507
|
-
|
|
43508
|
-
|
|
43509
|
-
|
|
43510
|
-
|
|
43511
|
-
|
|
43512
|
-
|
|
43513
|
-
|
|
43514
|
-
|
|
43515
|
-
|
|
43516
|
-
|
|
43517
|
-
|
|
43518
|
-
|
|
43519
|
-
|
|
43520
|
-
|
|
43521
|
-
|
|
43623
|
+
identify(pid) {
|
|
43624
|
+
const known = this.oneShots.get(pid);
|
|
43625
|
+
if (known !== undefined) {
|
|
43626
|
+
this.turnAway(pid, known);
|
|
43627
|
+
return;
|
|
43628
|
+
}
|
|
43629
|
+
const info = this.connectionInfo;
|
|
43630
|
+
const socket = this.socket;
|
|
43631
|
+
const reader = this.oneShotReader;
|
|
43632
|
+
if (info === null || reader === null) {
|
|
43633
|
+
return;
|
|
43634
|
+
}
|
|
43635
|
+
info.identifying = true;
|
|
43636
|
+
reader(pid).catch(() => {
|
|
43637
|
+
return;
|
|
43638
|
+
}).then((option) => {
|
|
43639
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
43640
|
+
return;
|
|
43641
|
+
}
|
|
43642
|
+
if (typeof option === "string") {
|
|
43643
|
+
this.oneShots.set(pid, option);
|
|
43644
|
+
this.turnAway(pid, option);
|
|
43645
|
+
return;
|
|
43646
|
+
}
|
|
43647
|
+
info.identifying = false;
|
|
43648
|
+
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
43649
|
+
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
43650
|
+
});
|
|
43522
43651
|
}
|
|
43523
|
-
|
|
43524
|
-
|
|
43525
|
-
|
|
43652
|
+
turnAway(pid, option) {
|
|
43653
|
+
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
43654
|
+
this.log("info", `Turning away an engine run: ${said}`);
|
|
43655
|
+
this.turnedAway = { pid, option, at: new Date };
|
|
43656
|
+
const run = this.socket;
|
|
43657
|
+
this.socket = null;
|
|
43658
|
+
this.connectionInfo = null;
|
|
43659
|
+
this.stopKeepalive();
|
|
43660
|
+
run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
|
|
43526
43661
|
}
|
|
43527
|
-
|
|
43528
|
-
|
|
43529
|
-
|
|
43530
|
-
|
|
43531
|
-
|
|
43662
|
+
handleDisconnect(disconnectedSocket, reason) {
|
|
43663
|
+
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
43664
|
+
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
43665
|
+
return;
|
|
43666
|
+
}
|
|
43667
|
+
this.stopKeepalive();
|
|
43668
|
+
this.socket = null;
|
|
43669
|
+
this.connectionInfo = null;
|
|
43670
|
+
this.emitBridgeEvent("godot_disconnected", {});
|
|
43671
|
+
this.rejectAllPending(reason);
|
|
43672
|
+
this.resourceQueues.clear();
|
|
43532
43673
|
}
|
|
43533
|
-
|
|
43534
|
-
|
|
43674
|
+
emitBridgeEvent(eventName, payload) {
|
|
43675
|
+
this.emit(eventName, payload);
|
|
43535
43676
|
}
|
|
43536
|
-
|
|
43537
|
-
|
|
43538
|
-
|
|
43539
|
-
|
|
43540
|
-
|
|
43541
|
-
|
|
43542
|
-
}
|
|
43543
|
-
function finishesASuite(line) {
|
|
43544
|
-
return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
|
|
43545
|
-
}
|
|
43546
|
-
function suitesPrinted(printed) {
|
|
43547
|
-
const finished = [];
|
|
43548
|
-
let running = null;
|
|
43549
|
-
let failed = [];
|
|
43550
|
-
for (const line of printed) {
|
|
43551
|
-
const started = RUNNING_SUITE.exec(line);
|
|
43552
|
-
if (started?.[1] !== undefined) {
|
|
43553
|
-
running = started[1];
|
|
43554
|
-
failed = [];
|
|
43555
|
-
continue;
|
|
43677
|
+
rejectAllPending(error) {
|
|
43678
|
+
for (const pending of this.pendingRequests.values()) {
|
|
43679
|
+
clearTimeout(pending.timeout);
|
|
43680
|
+
pending.reject(error);
|
|
43681
|
+
}
|
|
43682
|
+
this.pendingRequests.clear();
|
|
43556
43683
|
}
|
|
43557
|
-
|
|
43558
|
-
|
|
43684
|
+
enqueueResourceRequest(resourceKey, task) {
|
|
43685
|
+
const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
|
|
43686
|
+
const taskPromise = previous.catch(() => {
|
|
43687
|
+
return;
|
|
43688
|
+
}).then(task);
|
|
43689
|
+
const tail = taskPromise.then(() => {
|
|
43690
|
+
return;
|
|
43691
|
+
}, () => {
|
|
43692
|
+
return;
|
|
43693
|
+
});
|
|
43694
|
+
this.resourceQueues.set(resourceKey, tail);
|
|
43695
|
+
return taskPromise.finally(() => {
|
|
43696
|
+
if (this.resourceQueues.get(resourceKey) === tail) {
|
|
43697
|
+
this.resourceQueues.delete(resourceKey);
|
|
43698
|
+
}
|
|
43699
|
+
});
|
|
43559
43700
|
}
|
|
43560
|
-
|
|
43561
|
-
|
|
43562
|
-
|
|
43563
|
-
|
|
43701
|
+
getResourceKey(args) {
|
|
43702
|
+
const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
|
|
43703
|
+
if (scenePath) {
|
|
43704
|
+
return `scene:${scenePath}`;
|
|
43705
|
+
}
|
|
43706
|
+
const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
|
|
43707
|
+
if (resourcePath) {
|
|
43708
|
+
return `resource:${resourcePath}`;
|
|
43709
|
+
}
|
|
43710
|
+
return;
|
|
43564
43711
|
}
|
|
43565
|
-
|
|
43566
|
-
|
|
43567
|
-
|
|
43568
|
-
path: running,
|
|
43569
|
-
cases: Number(counted[1]),
|
|
43570
|
-
errors: Number(counted[2]),
|
|
43571
|
-
failures: Number(counted[3]),
|
|
43572
|
-
failed
|
|
43573
|
-
});
|
|
43574
|
-
running = null;
|
|
43712
|
+
getStringArg(args, key) {
|
|
43713
|
+
const value = args[key];
|
|
43714
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
43575
43715
|
}
|
|
43576
|
-
|
|
43577
|
-
|
|
43578
|
-
|
|
43579
|
-
|
|
43580
|
-
|
|
43581
|
-
|
|
43582
|
-
|
|
43583
|
-
|
|
43584
|
-
|
|
43585
|
-
|
|
43586
|
-
|
|
43587
|
-
|
|
43588
|
-
|
|
43589
|
-
|
|
43590
|
-
|
|
43591
|
-
|
|
43592
|
-
|
|
43716
|
+
isIncomingMessage(value) {
|
|
43717
|
+
if (!value || typeof value !== "object") {
|
|
43718
|
+
return false;
|
|
43719
|
+
}
|
|
43720
|
+
const message = value;
|
|
43721
|
+
const type = message["type"];
|
|
43722
|
+
if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
|
|
43723
|
+
return false;
|
|
43724
|
+
}
|
|
43725
|
+
if (type === "pong") {
|
|
43726
|
+
return true;
|
|
43727
|
+
}
|
|
43728
|
+
if (type === "godot_ready") {
|
|
43729
|
+
return typeof message["project_path"] === "string";
|
|
43730
|
+
}
|
|
43731
|
+
return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
|
|
43732
|
+
}
|
|
43733
|
+
log(level, message) {
|
|
43734
|
+
console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
|
|
43593
43735
|
}
|
|
43594
43736
|
};
|
|
43595
|
-
ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
|
|
43596
|
-
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
43597
|
-
HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
43598
|
-
NOTHING_RAN = /no test cases found/i;
|
|
43599
|
-
NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
43600
|
-
ESCAPE = String.fromCharCode(27);
|
|
43601
|
-
PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
43602
|
-
QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
|
|
43603
|
-
COLOUR2 = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
43604
|
-
SPELLED = {
|
|
43605
|
-
"<LF>": `
|
|
43606
|
-
`,
|
|
43607
|
-
"<CR>": "\r",
|
|
43608
|
-
"<TAB>": "\t",
|
|
43609
|
-
"<BS>": "\b",
|
|
43610
|
-
"<FF>": "\f",
|
|
43611
|
-
"<VT>": "\v",
|
|
43612
|
-
"<BEL>": "\x07",
|
|
43613
|
-
"<ESC>": ESCAPE,
|
|
43614
|
-
"<DEL>": ""
|
|
43615
|
-
};
|
|
43616
|
-
PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
43617
|
-
RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
43618
|
-
ORPHANS = /(\d+)\s+orphans/;
|
|
43619
|
-
SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
|
|
43620
|
-
CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
|
|
43621
43737
|
});
|
|
43622
43738
|
|
|
43623
43739
|
// src/keeper-channel.ts
|
|
@@ -43687,10 +43803,10 @@ var init_keeper_channel = () => {};
|
|
|
43687
43803
|
|
|
43688
43804
|
// src/lifecycle-log.ts
|
|
43689
43805
|
import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync12 } from "node:fs";
|
|
43690
|
-
import { dirname as dirname20, join as
|
|
43806
|
+
import { dirname as dirname20, join as join29 } from "node:path";
|
|
43691
43807
|
import process11 from "node:process";
|
|
43692
43808
|
function lifecycleLogPath() {
|
|
43693
|
-
return
|
|
43809
|
+
return join29(runtimeDirectory(), "servers.log");
|
|
43694
43810
|
}
|
|
43695
43811
|
function writeLifecycle(event, fields = {}) {
|
|
43696
43812
|
const path = lifecycleLogPath();
|
|
@@ -44011,9 +44127,9 @@ var init_prompts = __esm(() => {
|
|
|
44011
44127
|
|
|
44012
44128
|
// src/restart-note.ts
|
|
44013
44129
|
import { mkdirSync as mkdirSync15, readFileSync as readFileSync24, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
|
|
44014
|
-
import { dirname as dirname21, join as
|
|
44130
|
+
import { dirname as dirname21, join as join30 } from "node:path";
|
|
44015
44131
|
function restartNotePath(projectPath) {
|
|
44016
|
-
return
|
|
44132
|
+
return join30(projectPath, ".godot", "gdharness-restart.json");
|
|
44017
44133
|
}
|
|
44018
44134
|
function noteRestartBegun(note) {
|
|
44019
44135
|
const path = restartNotePath(note.projectPath);
|
|
@@ -44054,16 +44170,16 @@ var init_restart_note = __esm(() => {
|
|
|
44054
44170
|
// src/run-record.ts
|
|
44055
44171
|
import { createHash as createHash5 } from "node:crypto";
|
|
44056
44172
|
import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync13, readFileSync as readFileSync25, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync13 } from "node:fs";
|
|
44057
|
-
import { basename as basename9, dirname as dirname22, join as
|
|
44173
|
+
import { basename as basename9, dirname as dirname22, join as join31, resolve as resolve7 } from "node:path";
|
|
44058
44174
|
function runsDirectory() {
|
|
44059
|
-
return
|
|
44175
|
+
return join31(runtimeDirectory(), "runs");
|
|
44060
44176
|
}
|
|
44061
44177
|
function realAsFarAsItExists(path) {
|
|
44062
44178
|
const missing = [];
|
|
44063
44179
|
let at = path;
|
|
44064
44180
|
for (;; ) {
|
|
44065
44181
|
try {
|
|
44066
|
-
return
|
|
44182
|
+
return join31(realpathSync6.native(at), ...missing);
|
|
44067
44183
|
} catch {
|
|
44068
44184
|
const parent = dirname22(at);
|
|
44069
44185
|
if (parent === at) {
|
|
@@ -44080,10 +44196,10 @@ function projectKey(projectPath) {
|
|
|
44080
44196
|
return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
44081
44197
|
}
|
|
44082
44198
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
44083
|
-
return
|
|
44199
|
+
return join31(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
44084
44200
|
}
|
|
44085
44201
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
44086
|
-
return
|
|
44202
|
+
return join31(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
44087
44203
|
}
|
|
44088
44204
|
function writeLatestRun(servedBy, latest) {
|
|
44089
44205
|
try {
|
|
@@ -44108,20 +44224,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
44108
44224
|
}
|
|
44109
44225
|
function openTranscript(startedAt) {
|
|
44110
44226
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
44111
|
-
const path =
|
|
44227
|
+
const path = join31(runsDirectory(), `run-${startedAt}.log`);
|
|
44112
44228
|
return { path, fd: openSync3(path, "a") };
|
|
44113
44229
|
}
|
|
44114
44230
|
function profilePath(startedAt) {
|
|
44115
|
-
return
|
|
44231
|
+
return join31(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
44116
44232
|
}
|
|
44117
44233
|
function profileMarkPath(startedAt) {
|
|
44118
|
-
return
|
|
44234
|
+
return join31(runsDirectory(), `run-${startedAt}.profile-mark.json`);
|
|
44119
44235
|
}
|
|
44120
44236
|
function noteIsAbout(note, playing) {
|
|
44121
44237
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
44122
44238
|
}
|
|
44123
44239
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
44124
|
-
return
|
|
44240
|
+
return join31(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
44125
44241
|
}
|
|
44126
44242
|
function writeEditorRunNote(note) {
|
|
44127
44243
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
@@ -44132,7 +44248,7 @@ function writeEditorRunNote(note) {
|
|
|
44132
44248
|
}
|
|
44133
44249
|
function readEditorRunNote(projectPath) {
|
|
44134
44250
|
for (const directory of runtimeDirectories()) {
|
|
44135
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
44251
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join31(directory, "runs", "editor-run.json"));
|
|
44136
44252
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
44137
44253
|
return note;
|
|
44138
44254
|
}
|
|
@@ -44166,7 +44282,7 @@ function readRunRecord(projectPath) {
|
|
|
44166
44282
|
if (own !== null) {
|
|
44167
44283
|
return own;
|
|
44168
44284
|
}
|
|
44169
|
-
const shared = recordAt(
|
|
44285
|
+
const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
|
|
44170
44286
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
44171
44287
|
return shared;
|
|
44172
44288
|
}
|
|
@@ -44178,13 +44294,13 @@ function everyRunRecord() {
|
|
|
44178
44294
|
for (const directory of runtimeDirectories()) {
|
|
44179
44295
|
let entries;
|
|
44180
44296
|
try {
|
|
44181
|
-
entries = readdirSync13(
|
|
44297
|
+
entries = readdirSync13(join31(directory, "runs"));
|
|
44182
44298
|
} catch {
|
|
44183
44299
|
continue;
|
|
44184
44300
|
}
|
|
44185
44301
|
for (const entry of entries) {
|
|
44186
44302
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
44187
|
-
const record = recordAt(
|
|
44303
|
+
const record = recordAt(join31(directory, "runs", entry));
|
|
44188
44304
|
if (record !== null) {
|
|
44189
44305
|
found.push(record);
|
|
44190
44306
|
}
|
|
@@ -44392,7 +44508,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
44392
44508
|
}
|
|
44393
44509
|
function clearRunRecord(projectPath) {
|
|
44394
44510
|
for (const directory of runtimeDirectories()) {
|
|
44395
|
-
const shared =
|
|
44511
|
+
const shared = join31(directory, "runs", SHARED_NOTE);
|
|
44396
44512
|
const sharedRecord = recordAt(shared);
|
|
44397
44513
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
44398
44514
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -44427,7 +44543,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44427
44543
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
44428
44544
|
continue;
|
|
44429
44545
|
}
|
|
44430
|
-
const path =
|
|
44546
|
+
const path = join31(directory, entry);
|
|
44431
44547
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
44432
44548
|
continue;
|
|
44433
44549
|
}
|
|
@@ -44447,110 +44563,6 @@ var init_run_record = __esm(() => {
|
|
|
44447
44563
|
init_windows_ask();
|
|
44448
44564
|
});
|
|
44449
44565
|
|
|
44450
|
-
// src/script-origins.ts
|
|
44451
|
-
import { join as join31 } from "node:path";
|
|
44452
|
-
function unresolvedClassIn(message) {
|
|
44453
|
-
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
44454
|
-
}
|
|
44455
|
-
async function originsOf(errors, declared, check) {
|
|
44456
|
-
const listed = new Set(errors.map((error) => error.path));
|
|
44457
|
-
const origins = [];
|
|
44458
|
-
const unconfirmed = [];
|
|
44459
|
-
const asked = new Set;
|
|
44460
|
-
const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
|
|
44461
|
-
while (waiting.length > 0 && asked.size < MOST_CHECKED) {
|
|
44462
|
-
const name = waiting.shift() ?? "";
|
|
44463
|
-
const path = declared.get(name);
|
|
44464
|
-
if (path === undefined || listed.has(path) || asked.has(path)) {
|
|
44465
|
-
continue;
|
|
44466
|
-
}
|
|
44467
|
-
asked.add(path);
|
|
44468
|
-
const checked = await check(path);
|
|
44469
|
-
const own = checked.errors.filter((error) => error.path === path);
|
|
44470
|
-
if (own.length === 0) {
|
|
44471
|
-
unconfirmed.push({
|
|
44472
|
-
path,
|
|
44473
|
-
declares: name,
|
|
44474
|
-
why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
|
|
44475
|
-
});
|
|
44476
|
-
continue;
|
|
44477
|
-
}
|
|
44478
|
-
for (const error of own) {
|
|
44479
|
-
origins.push({ ...error, declares: name });
|
|
44480
|
-
const deeper = unresolvedClassIn(error.message);
|
|
44481
|
-
if (deeper !== null) {
|
|
44482
|
-
waiting.push(deeper);
|
|
44483
|
-
}
|
|
44484
|
-
}
|
|
44485
|
-
}
|
|
44486
|
-
return { origins, unconfirmed };
|
|
44487
|
-
}
|
|
44488
|
-
function compileErrorsIn(entries) {
|
|
44489
|
-
return entries.flatMap((entry) => {
|
|
44490
|
-
const at = entry.severity === "error" ? reloadedAt(entry) : null;
|
|
44491
|
-
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
44492
|
-
});
|
|
44493
|
-
}
|
|
44494
|
-
function reloadedAt(entry) {
|
|
44495
|
-
for (const line of entry.detail) {
|
|
44496
|
-
const at = RELOADED_AT.exec(line.trim());
|
|
44497
|
-
if (at !== null) {
|
|
44498
|
-
return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
|
|
44499
|
-
}
|
|
44500
|
-
}
|
|
44501
|
-
return null;
|
|
44502
|
-
}
|
|
44503
|
-
function checkWithEngine(godotPath, projectPath) {
|
|
44504
|
-
return async (path) => {
|
|
44505
|
-
const logDir = scratchDirectory("tests");
|
|
44506
|
-
try {
|
|
44507
|
-
const ran = await runEngine(godotPath, [
|
|
44508
|
-
"--headless",
|
|
44509
|
-
"--log-file",
|
|
44510
|
-
join31(logDir, "engine.log"),
|
|
44511
|
-
"--path",
|
|
44512
|
-
projectPath,
|
|
44513
|
-
"--check-only",
|
|
44514
|
-
"-s",
|
|
44515
|
-
path
|
|
44516
|
-
], { timeout: { ms: 60000, said: "minute" } });
|
|
44517
|
-
const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
|
|
44518
|
-
const at = reloadedAt(entry);
|
|
44519
|
-
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
44520
|
-
});
|
|
44521
|
-
return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
|
|
44522
|
-
} finally {
|
|
44523
|
-
discard(logDir);
|
|
44524
|
-
}
|
|
44525
|
-
};
|
|
44526
|
-
}
|
|
44527
|
-
function withoutListed(entries, errors) {
|
|
44528
|
-
if (errors.length === 0) {
|
|
44529
|
-
return [...entries];
|
|
44530
|
-
}
|
|
44531
|
-
const listing = discoveryListing(entries.map((entry) => entry.text));
|
|
44532
|
-
const paths = new Set(errors.map((error) => error.path));
|
|
44533
|
-
return entries.filter((entry, at) => {
|
|
44534
|
-
if (listing !== null && at >= listing.from && at < listing.to) {
|
|
44535
|
-
return false;
|
|
44536
|
-
}
|
|
44537
|
-
const reloaded = reloadedAt(entry);
|
|
44538
|
-
if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
|
|
44539
|
-
return false;
|
|
44540
|
-
}
|
|
44541
|
-
const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
|
|
44542
|
-
return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
|
|
44543
|
-
});
|
|
44544
|
-
}
|
|
44545
|
-
var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT;
|
|
44546
|
-
var init_script_origins = __esm(() => {
|
|
44547
|
-
init_engine_run();
|
|
44548
|
-
init_junit();
|
|
44549
|
-
init_scratch();
|
|
44550
|
-
UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
44551
|
-
RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
44552
|
-
});
|
|
44553
|
-
|
|
44554
44566
|
// src/script-profile.ts
|
|
44555
44567
|
import { readFileSync as readFileSync26 } from "node:fs";
|
|
44556
44568
|
function readProfileFile(path) {
|
|
@@ -45660,7 +45672,8 @@ function repeatingScriptError(entries, byScript) {
|
|
|
45660
45672
|
if (last === undefined || !byScript(last)) {
|
|
45661
45673
|
return null;
|
|
45662
45674
|
}
|
|
45663
|
-
const
|
|
45675
|
+
const whole = last.detail.length > 0 ? last : entries.findLast((entry) => entry.text === last.text && entry.detail.length > 0 && byScript(entry)) ?? last;
|
|
45676
|
+
const where = whole.detail[0] ?? null;
|
|
45664
45677
|
const times = entries.filter((entry) => entry.text === last.text && (entry.detail[0] ?? null) === where && byScript(entry)).length;
|
|
45665
45678
|
return times >= LOOPING_AFTER ? { text: last.text, where: where?.replace(/^at: /, "") ?? null, times } : null;
|
|
45666
45679
|
}
|
|
@@ -46056,9 +46069,9 @@ function scriptsUnder(directory, prefix) {
|
|
|
46056
46069
|
}
|
|
46057
46070
|
return found;
|
|
46058
46071
|
}
|
|
46059
|
-
function
|
|
46072
|
+
function mainSceneOf(projectFile) {
|
|
46060
46073
|
const scene = parseProjectGodot(readFileSync27(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
46061
|
-
return typeof scene === "string" && scene !== "";
|
|
46074
|
+
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
46062
46075
|
}
|
|
46063
46076
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
46064
46077
|
if (projectPath === null) {
|
|
@@ -48038,6 +48051,7 @@ class GodotServer {
|
|
|
48038
48051
|
if (!engine.ok) {
|
|
48039
48052
|
return engine.response;
|
|
48040
48053
|
}
|
|
48054
|
+
const began = Date.now();
|
|
48041
48055
|
const classes = await this.rebuildClassCache(project.value.path);
|
|
48042
48056
|
if (!classes.ok) {
|
|
48043
48057
|
return this.answer(classes);
|
|
@@ -48111,6 +48125,7 @@ class GodotServer {
|
|
|
48111
48125
|
await untilGone([run.process.pid]);
|
|
48112
48126
|
}
|
|
48113
48127
|
await run.settled;
|
|
48128
|
+
const tookMs = Date.now() - began;
|
|
48114
48129
|
const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
|
|
48115
48130
|
const reportsDir = join32(project.value.path, ".godot", ...ours.split("/"));
|
|
48116
48131
|
let report = null;
|
|
@@ -48200,6 +48215,7 @@ class GodotServer {
|
|
|
48200
48215
|
passed: false,
|
|
48201
48216
|
verdict,
|
|
48202
48217
|
tests: 0,
|
|
48218
|
+
tookMs,
|
|
48203
48219
|
exitCode,
|
|
48204
48220
|
exitSignal: run.exitSignal ?? undefined,
|
|
48205
48221
|
hung,
|
|
@@ -48257,6 +48273,7 @@ class GodotServer {
|
|
|
48257
48273
|
errors: report.errors,
|
|
48258
48274
|
skipped: report.skipped,
|
|
48259
48275
|
time: report.time,
|
|
48276
|
+
tookMs,
|
|
48260
48277
|
failed,
|
|
48261
48278
|
failedWithoutDetail: briefed > 0 ? briefed : undefined,
|
|
48262
48279
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
@@ -49363,7 +49380,7 @@ class GodotServer {
|
|
|
49363
49380
|
if (!asked.ok) {
|
|
49364
49381
|
return asked.response;
|
|
49365
49382
|
}
|
|
49366
|
-
if (exported === null && !sceneToRun &&
|
|
49383
|
+
if (exported === null && !sceneToRun && mainSceneOf(project.value.file) === null) {
|
|
49367
49384
|
return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
|
|
49368
49385
|
"Pass scene to run one scene",
|
|
49369
49386
|
"Choose the main scene with project_settings set_main_scene"
|
|
@@ -49440,6 +49457,7 @@ class GodotServer {
|
|
|
49440
49457
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
49441
49458
|
pid: started.pid,
|
|
49442
49459
|
executable: exported?.ok === true ? exported.relativePath : undefined,
|
|
49460
|
+
scene: exported !== null ? undefined : sceneArgument === null ? mainSceneOf(project.value.file) : `res://${sceneArgument}`,
|
|
49443
49461
|
arguments: cmdArgs,
|
|
49444
49462
|
argsNote: engineFlagsNote(given.value),
|
|
49445
49463
|
transcript: started.transcript,
|
|
@@ -53360,9 +53378,9 @@ init_setup();
|
|
|
53360
53378
|
init_tool_definitions();
|
|
53361
53379
|
init_tool_reference();
|
|
53362
53380
|
import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
53363
|
-
import { basename as basename6, dirname as dirname8, join as
|
|
53381
|
+
import { basename as basename6, dirname as dirname8, join as join15 } from "node:path";
|
|
53364
53382
|
var SKILL_NAME = "gdharness";
|
|
53365
|
-
var SHARED_SKILLS =
|
|
53383
|
+
var SHARED_SKILLS = join15(".agents", "skills");
|
|
53366
53384
|
function skillMarkdown(version) {
|
|
53367
53385
|
return `---
|
|
53368
53386
|
name: ${SKILL_NAME}
|
|
@@ -53587,27 +53605,27 @@ what they report while starting comes back under \`engine_messages_before_operat
|
|
|
53587
53605
|
function skillFiles(version) {
|
|
53588
53606
|
return new Map([
|
|
53589
53607
|
["SKILL.md", skillMarkdown(version)],
|
|
53590
|
-
[
|
|
53608
|
+
[join15("references", "tools.md"), renderToolsMarkdown()]
|
|
53591
53609
|
]);
|
|
53592
53610
|
}
|
|
53593
53611
|
function skillDirectories(harnesses, projectPath, exists) {
|
|
53594
|
-
const directories = [
|
|
53612
|
+
const directories = [join15(projectPath, SHARED_SKILLS, SKILL_NAME)];
|
|
53595
53613
|
for (const harness of harnesses) {
|
|
53596
53614
|
if (harness.skills === undefined) {
|
|
53597
53615
|
continue;
|
|
53598
53616
|
}
|
|
53599
|
-
const own =
|
|
53600
|
-
if ((!harness.skills.shared || exists(own)) && !directories.includes(
|
|
53601
|
-
directories.push(
|
|
53617
|
+
const own = join15(projectPath, harness.skills.dir);
|
|
53618
|
+
if ((!harness.skills.shared || exists(own)) && !directories.includes(join15(own, SKILL_NAME))) {
|
|
53619
|
+
directories.push(join15(own, SKILL_NAME));
|
|
53602
53620
|
}
|
|
53603
53621
|
}
|
|
53604
53622
|
return directories;
|
|
53605
53623
|
}
|
|
53606
53624
|
function everySkillDirectory(projectPath, harnesses) {
|
|
53607
|
-
const directories = new Set([
|
|
53625
|
+
const directories = new Set([join15(projectPath, SHARED_SKILLS, SKILL_NAME)]);
|
|
53608
53626
|
for (const harness of harnesses) {
|
|
53609
53627
|
if (harness.skills !== undefined) {
|
|
53610
|
-
directories.add(
|
|
53628
|
+
directories.add(join15(projectPath, harness.skills.dir, SKILL_NAME));
|
|
53611
53629
|
}
|
|
53612
53630
|
}
|
|
53613
53631
|
return [...directories];
|
|
@@ -53624,7 +53642,7 @@ function removeSkill(directories, projectPath) {
|
|
|
53624
53642
|
return removed;
|
|
53625
53643
|
}
|
|
53626
53644
|
function pruneEmpty(directory, projectPath) {
|
|
53627
|
-
const shared =
|
|
53645
|
+
const shared = join15(projectPath, ".agents");
|
|
53628
53646
|
let path = directory;
|
|
53629
53647
|
while (path !== projectPath && (basename6(path) === "skills" || path === shared)) {
|
|
53630
53648
|
if (readdirSync9(path).length > 0) {
|
|
@@ -53640,7 +53658,7 @@ function writeSkill(directories, version) {
|
|
|
53640
53658
|
const replaced = existsSync14(directory);
|
|
53641
53659
|
rmSync6(directory, { recursive: true, force: true });
|
|
53642
53660
|
for (const [name, contents] of files) {
|
|
53643
|
-
const path =
|
|
53661
|
+
const path = join15(directory, name);
|
|
53644
53662
|
mkdirSync5(dirname8(path), { recursive: true });
|
|
53645
53663
|
writeFileSync7(path, contents, "utf8");
|
|
53646
53664
|
}
|
|
@@ -53650,13 +53668,13 @@ function writeSkill(directories, version) {
|
|
|
53650
53668
|
|
|
53651
53669
|
// src/version.ts
|
|
53652
53670
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
53653
|
-
import { dirname as dirname9, join as
|
|
53671
|
+
import { dirname as dirname9, join as join16 } from "node:path";
|
|
53654
53672
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
53655
53673
|
function getLocalVersion() {
|
|
53656
53674
|
const currentDirectory = dirname9(fileURLToPath2(import.meta.url));
|
|
53657
53675
|
const packageCandidates = [
|
|
53658
|
-
|
|
53659
|
-
|
|
53676
|
+
join16(currentDirectory, "..", "package.json"),
|
|
53677
|
+
join16(currentDirectory, "..", "..", "package.json")
|
|
53660
53678
|
];
|
|
53661
53679
|
for (const packagePath of packageCandidates) {
|
|
53662
53680
|
try {
|