gdharness 1.3.38 → 1.3.39

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 CHANGED
@@ -11334,7 +11334,8 @@ import {
11334
11334
  readSync,
11335
11335
  rmSync as rmSync4,
11336
11336
  statSync as statSync5,
11337
- writeFileSync as writeFileSync4
11337
+ writeFileSync as writeFileSync4,
11338
+ writeSync as writeSync2
11338
11339
  } from "node:fs";
11339
11340
  import { availableParallelism, freemem, totalmem } from "node:os";
11340
11341
  import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
@@ -11489,6 +11490,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11489
11490
  const output = openSync2(log, "w");
11490
11491
  let child;
11491
11492
  let stop;
11493
+ let piped = null;
11492
11494
  let helper = null;
11493
11495
  try {
11494
11496
  if (process6.platform === "win32") {
@@ -11512,11 +11514,13 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11512
11514
  stop: stopFile,
11513
11515
  step: helper.step
11514
11516
  });
11515
- child = spawnHidden(wrapper.command, wrapper.args, {
11516
- stdio: ["ignore", output, output],
11517
+ const helped = spawnHidden(wrapper.command, wrapper.args, {
11518
+ stdio: ["ignore", "pipe", "pipe"],
11517
11519
  env: { ...env, [DESKTOP_VARIABLE]: HIDDEN_DESKTOP },
11518
11520
  cwd: directory
11519
11521
  });
11522
+ piped = [helped.stdout, helped.stderr];
11523
+ child = helped;
11520
11524
  stop = () => {
11521
11525
  writeFileSync4(stopFile, "");
11522
11526
  };
@@ -11533,7 +11537,9 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11533
11537
  };
11534
11538
  }
11535
11539
  } finally {
11536
- closeSync2(output);
11540
+ if (piped === null) {
11541
+ closeSync2(output);
11542
+ }
11537
11543
  }
11538
11544
  launch.running.add(stop);
11539
11545
  let ended = null;
@@ -11546,6 +11552,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11546
11552
  });
11547
11553
  })
11548
11554
  ]);
11555
+ const copied = piped === null ? null : copiedInto(output, piped, exited);
11549
11556
  const finished = { now: false };
11550
11557
  exited.then(() => {
11551
11558
  finished.now = true;
@@ -11598,12 +11605,42 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11598
11605
  }
11599
11606
  const { code, signal } = await exited;
11600
11607
  const freeBytes = code !== null && code >>> 0 >= WINDOWS_EXCEPTION ? freemem() : undefined;
11608
+ if (copied !== null) {
11609
+ await copied;
11610
+ closeSync2(output);
11611
+ }
11601
11612
  const failed = helper === null ? null : helperFailure(helper.error);
11602
11613
  if (failed !== null && helper !== null) {
11603
11614
  ended ??= `the engine could not be started ${helper.how}: ${failed}`;
11604
11615
  }
11605
11616
  return { code, signal, ended, ...freeBytes === undefined ? {} : { freeBytes } };
11606
11617
  }
11618
+ function copiedInto(output, streams, exited) {
11619
+ let writing = true;
11620
+ const ended = streams.map((stream) => new Promise((settle) => {
11621
+ stream.on("data", (chunk) => {
11622
+ if (!writing) {
11623
+ return;
11624
+ }
11625
+ try {
11626
+ writeSync2(output, chunk);
11627
+ } catch {
11628
+ writing = false;
11629
+ }
11630
+ });
11631
+ stream.once("close", settle);
11632
+ }));
11633
+ const all = Promise.all(ended).then(() => {
11634
+ return;
11635
+ });
11636
+ const drained = exited.then(() => delay2(DRAIN_MS, undefined, { ref: false }));
11637
+ return Promise.race([all, drained]).then(() => {
11638
+ writing = false;
11639
+ for (const stream of streams) {
11640
+ stream.destroy();
11641
+ }
11642
+ });
11643
+ }
11607
11644
  function endTree(child) {
11608
11645
  if (child.pid === undefined) {
11609
11646
  return;
@@ -11616,7 +11653,7 @@ function endTree(child) {
11616
11653
  process6.kill(-child.pid, "SIGKILL");
11617
11654
  } catch {}
11618
11655
  }
11619
- var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload", KEPT_RUN_DIRECTORIES = 5;
11656
+ var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload", KEPT_RUN_DIRECTORIES = 5, DRAIN_MS = 5000;
11620
11657
  var init_engine_launch = __esm(() => {
11621
11658
  init_alive();
11622
11659
  init_desktop();
@@ -11744,7 +11781,7 @@ import {
11744
11781
  rmSync as rmSync6,
11745
11782
  statSync as statSync6,
11746
11783
  watch,
11747
- writeSync as writeSync2
11784
+ writeSync as writeSync3
11748
11785
  } from "node:fs";
11749
11786
  import { availableParallelism as availableParallelism2, homedir as homedir2 } from "node:os";
11750
11787
  import { join as join8, resolve as resolve4 } from "node:path";
@@ -11970,7 +12007,7 @@ class EngineSlots {
11970
12007
  return false;
11971
12008
  }
11972
12009
  try {
11973
- writeSync2(made, JSON.stringify(this.holder));
12010
+ writeSync3(made, JSON.stringify(this.holder));
11974
12011
  } finally {
11975
12012
  closeSync3(made);
11976
12013
  }
@@ -12084,7 +12121,7 @@ async function inASlot(body) {
12084
12121
  held.release();
12085
12122
  }
12086
12123
  }
12087
- var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", waitsMarked = 0, ours, saidTo, takenBy;
12124
+ var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", waitsMarked = 0, ours, saidTo, takenBy;
12088
12125
  var init_engine_slots = __esm(() => {
12089
12126
  init_alive();
12090
12127
  init_call_signal();
@@ -17364,7 +17401,7 @@ var init_tool_definitions = __esm(() => {
17364
17401
  name: "script_diagnostics",
17365
17402
  title: "Script diagnostics",
17366
17403
  effect: "read",
17367
- description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine rather than language server: every error that stops a script compiling, up to its first syntax error, with its line and no column (a call to a method another class lacks is not one, since GDScript checks those only on self: the object may be a subclass that has it, so it fails at run time instead), dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and warnings null, since the engine reports warnings only to an editor. Of several scripts, a language server that stops answering partway leaves the rest to the engine, and checkedBy then says language server, then engine, with checkedByLanguageServer counting the scripts it answered. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
17404
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine rather than language server: every error that stops a script compiling, up to its first syntax error, with its line and no column (a call to a method another class lacks is not one, since GDScript checks those only on self: the object may be a subclass that has it, so it fails at run time instead), dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and the warnings at the levels the project sets, which a second engine reports by raising each warning set to warn as an error for that run only, each at its line with no column, warnings being null when that engine could not answer. Of several scripts, a language server that stops answering partway leaves the rest to the engine, and checkedBy then says language server, then engine, with checkedByLanguageServer counting the scripts it answered. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
17368
17405
  parameters: {
17369
17406
  projectPath: PROJECT_PATH,
17370
17407
  scriptPath: {
@@ -18665,6 +18702,7 @@ class ProjectScripts {
18665
18702
  suite(path) {
18666
18703
  const cases = new Set;
18667
18704
  let said;
18705
+ let together;
18668
18706
  let guessed = false;
18669
18707
  const seen = new Set;
18670
18708
  let at = path;
@@ -18679,13 +18717,21 @@ class ProjectScripts {
18679
18717
  }
18680
18718
  const declared = NEEDS_WINDOW.exec(read.code)?.[1];
18681
18719
  said ??= declared === undefined ? undefined : declared === "true";
18720
+ const kept = TOGETHER.exec(read.code)?.[1];
18721
+ together ??= kept === undefined ? undefined : kept === "true";
18682
18722
  guessed ||= this.guessWindow && SCENE_RUNNER.test(read.code);
18683
18723
  const base = this.base(at);
18684
18724
  if (base === null) {
18685
18725
  return null;
18686
18726
  }
18687
18727
  if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
18688
- return cases.size > 0 ? { path, cases: [...cases].sort(), windowed: said ?? guessed, headless: said === false } : null;
18728
+ return cases.size > 0 ? {
18729
+ path,
18730
+ cases: [...cases].sort(),
18731
+ windowed: said ?? guessed,
18732
+ headless: said === false,
18733
+ together: together === true
18734
+ } : null;
18689
18735
  }
18690
18736
  if ("name" in base) {
18691
18737
  return null;
@@ -18808,6 +18854,10 @@ function rowsAfter(before, measured, cutShort) {
18808
18854
  function round(seconds) {
18809
18855
  return Math.round(seconds * 1000) / 1000;
18810
18856
  }
18857
+ function splitPastFor(suites, times, engines) {
18858
+ const whole = unitsOf(suites, times, Number.POSITIVE_INFINITY).reduce((sum, unit) => sum + unit.cost, 0);
18859
+ return Math.max(SPLIT_PAST_SECONDS, whole / Math.max(1, engines));
18860
+ }
18811
18861
  function median(values) {
18812
18862
  if (values.length === 0) {
18813
18863
  return;
@@ -18834,7 +18884,7 @@ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
18834
18884
  windowed: suite.windowed,
18835
18885
  headless: suite.headless
18836
18886
  };
18837
- if (suite.windowed || whole <= splitPast) {
18887
+ if (suite.windowed || suite.together || whole <= splitPast) {
18838
18888
  return [wholly];
18839
18889
  }
18840
18890
  const units = suite.cases.flatMap((func) => {
@@ -19003,13 +19053,14 @@ function viewportOf(projectPath) {
19003
19053
  const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
19004
19054
  return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
19005
19055
  }
19006
- var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
19056
+ var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TOGETHER, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
19007
19057
  var init_suite_plan = __esm(() => {
19008
19058
  init_class_cache();
19009
19059
  init_gdscript_source();
19010
19060
  init_resources();
19011
19061
  init_whole_file();
19012
19062
  NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*(true|false)\b/m;
19063
+ TOGETHER = /^const\s+GDHARNESS_TOGETHER\b[^=\n]*=\s*(true|false)\b/m;
19013
19064
  TEST_DIRECTORIES = ["tests", "test"];
19014
19065
  SCENE_RUNNER = /\bscene_runner\s*\(/;
19015
19066
  SHARE_FLOOR_SECONDS = 6 * ENGINE_START_SECONDS;
@@ -50416,9 +50467,13 @@ function engineChecked(why) {
50416
50467
  return {
50417
50468
  checkedBy: "engine",
50418
50469
  languageServer: why,
50419
- note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, but stops reading a script at its first syntax error, so an error after one shows once it is fixed. It reports no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both. Until then clean says only that every script compiles."
50470
+ note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, but stops reading a script at its first syntax error, so an error after one shows once it is fixed. A second engine reports the warnings, at the levels the project sets, by raising each warning set to warn as an error for that run only, since the engine tells a warning as one only to an editor; each comes at its line without a column, and warnings is null when that engine could not answer. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both with their columns."
50420
50471
  };
50421
50472
  }
50473
+ function asWarning(raised) {
50474
+ const bare = raised.endsWith(RAISED_WARNING) ? raised.slice(0, -RAISED_WARNING.length) : raised;
50475
+ return bare.startsWith(PARSE_ERROR2) ? bare.slice(PARSE_ERROR2.length) : bare;
50476
+ }
50422
50477
  function isDirectory(path) {
50423
50478
  try {
50424
50479
  return statSync20(path).isDirectory();
@@ -53206,6 +53261,7 @@ ${error.text}`;
53206
53261
  const nothingReported = [];
53207
53262
  let errors = 0;
53208
53263
  let warnings = 0;
53264
+ let engineWarned = 0;
53209
53265
  let unread = 0;
53210
53266
  let byEngine = null;
53211
53267
  let byServer = 0;
@@ -53224,6 +53280,7 @@ ${error.text}`;
53224
53280
  }
53225
53281
  for (const answer of checked.answers) {
53226
53282
  errors += Number(answer["errors"]);
53283
+ engineWarned = answer["warnings"] === null || engineWarned === null ? null : engineWarned + Number(answer["warnings"]);
53227
53284
  if (readArray(answer, "diagnostics")?.length === 0) {
53228
53285
  nothingReported.push(String(answer["scriptPath"]));
53229
53286
  } else {
@@ -53254,7 +53311,7 @@ ${error.text}`;
53254
53311
  clean: errors === 0 && unread === 0,
53255
53312
  scriptsChecked: scripts.value.length,
53256
53313
  errors,
53257
- warnings: byEngine === null ? warnings : null,
53314
+ warnings: engineWarned === null ? null : warnings + engineWarned,
53258
53315
  ...unread > 0 ? { scriptsNotRead: unread } : {},
53259
53316
  scripts: reported,
53260
53317
  nothingReported,
@@ -53289,6 +53346,20 @@ ${error.text}`;
53289
53346
  }
53290
53347
  return { ok: true, payload, extras: engineExtras({ messages }) };
53291
53348
  }
53349
+ async engineWarnings(projectPath, paths) {
53350
+ const ran = await this.operation("check_warnings", { paths: [...paths] }, projectPath);
53351
+ if (!ran.ok) {
53352
+ this.logDebug(`check_warnings did not answer: ${ran.message}`);
53353
+ return null;
53354
+ }
53355
+ return new Map((readArray(ran.payload, "scripts") ?? []).map((entry) => [
53356
+ String(asParams(entry)["path"]),
53357
+ (readArray(asParams(entry), "warnings") ?? []).map((warning) => ({
53358
+ line: Number(asParams(warning)["line"]),
53359
+ message: String(asParams(warning)["message"])
53360
+ }))
53361
+ ]));
53362
+ }
53292
53363
  async engineDiagnostics(projectPath, scripts, why) {
53293
53364
  const named = scripts.map((script) => {
53294
53365
  const contained = resolveWithinProject(projectPath, script);
@@ -53305,21 +53376,25 @@ ${error.text}`;
53305
53376
  asParams(entry)
53306
53377
  ]));
53307
53378
  const errorsOf = scripts.map((_, at) => readArray(failures.get(named[at] ?? "") ?? {}, "errors") ?? []);
53379
+ const warned = await this.engineWarnings(projectPath, named);
53308
53380
  const uncachedPer = uncachedFor(projectPath, errorsOf.map((errors) => errors.map((entry) => String(asParams(entry)["message"]))));
53309
53381
  return {
53310
53382
  answers: scripts.map((scriptPath, at) => {
53311
53383
  const failure = failures.get(named[at] ?? "");
53312
- const diagnostics = (errorsOf[at] ?? []).map((entry) => {
53384
+ const errors = (errorsOf[at] ?? []).map((entry) => {
53313
53385
  const { line, message } = asParams(entry);
53314
53386
  return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
53315
53387
  });
53388
+ const raised = new Set(errors.map(({ message, line }) => `${line ?? 0}:${String(message)}`));
53389
+ const warnings = warned === null ? [] : (warned.get(named[at] ?? "") ?? []).filter(({ line, message }) => !raised.has(`${line}:${message}`)).map(({ line, message }) => ({ severity: "warning", message: asWarning(message), line }));
53390
+ const diagnostics = [...errors, ...warnings].sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
53316
53391
  const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
53317
53392
  const uncached = uncachedPer[at] ?? [];
53318
53393
  return {
53319
53394
  scriptPath,
53320
- clean: diagnostics.length === 0,
53321
- errors: diagnostics.length,
53322
- warnings: null,
53395
+ clean: errors.length === 0,
53396
+ errors: errors.length,
53397
+ warnings: warned === null ? null : warnings.length,
53323
53398
  diagnostics,
53324
53399
  ...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies },
53325
53400
  ...uncached.length === 0 ? {} : {
@@ -57669,7 +57744,7 @@ async function runGodotServer() {
57669
57744
  const server = new GodotServer;
57670
57745
  await server.run();
57671
57746
  }
57672
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
57747
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
57673
57748
  var init_server = __esm(() => {
57674
57749
  init_dist2();
57675
57750
  init_stdio();
@@ -59116,7 +59191,6 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59116
59191
  }
59117
59192
  }
59118
59193
  const times = readTimes(timesFile);
59119
- const units = unitsOf(suites, times);
59120
59194
  const memory = await memoryAvailable();
59121
59195
  if (memory.unread !== undefined) {
59122
59196
  console.log(`Windows did not say how much memory it can still commit (${memory.unread}), so engines are sized by the physical memory free.`);
@@ -59125,9 +59199,13 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59125
59199
  if (options.cores !== null && process14.platform === "darwin") {
59126
59200
  console.log("macOS offers no way to hold a process to some processors, so --cores holds only the number of engines here.");
59127
59201
  }
59202
+ const splitPast = splitPastFor(suites, times, room.engines);
59203
+ const units = unitsOf(suites, times, splitPast);
59204
+ const past = splitPast > SPLIT_PAST_SECONDS ? `an engine's share of this run (${String(Math.round(splitPast))} s)` : `${String(SPLIT_PAST_SECONDS)} s`;
59128
59205
  const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
59129
59206
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
59130
- const how = options.order === null ? `${windowedSaid(plans)}${roomSaid(plans.length, lanesOf(plans), lanesOf(dealt(units, declared, options.engines)), room.held, options.engines)}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
59207
+ const asked = unitsOf(suites, times, splitPastFor(suites, times, options.engines));
59208
+ const how = options.order === null ? `${windowedSaid(plans)}${roomSaid(plans.length, lanesOf(plans), lanesOf(dealt(asked, declared, options.engines)), room.held, options.engines)}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
59131
59209
  console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59132
59210
  const windowedAnyway = headlessInAWindow(plans, units);
59133
59211
  if (windowedAnyway.length > 0) {
@@ -59136,13 +59214,18 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59136
59214
  }
59137
59215
  const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
59138
59216
  if (plans.length > 1 && split.length > 0) {
59139
- console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(split)}.`);
59217
+ console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59140
59218
  }
59141
59219
  const byRows = [
59142
59220
  ...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
59143
59221
  ];
59144
59222
  if (plans.length > 1 && byRows.length > 0) {
59145
- console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(byRows)}.`);
59223
+ console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59224
+ }
59225
+ const keptWhole = units.flatMap((unit) => unit.cost > splitPast && !unit.windowed && suites.some((s) => s.path === unit.suite && s.together) ? [unit.suite] : []);
59226
+ if (options.order === null && room.engines > 1 && keptWhole.length > 0) {
59227
+ const one = keptWhole.length === 1;
59228
+ console.log(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59146
59229
  }
59147
59230
  const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
59148
59231
  const taken = [];
@@ -51,6 +51,7 @@ const BEGUN: String = "[gdharness] begun"
51
51
  ## scene or a resource, loads the scripts of the file it opens.
52
52
  const AFTER_THE_AUTOLOADS: Array[String] = [
53
53
  "check_scripts",
54
+ "check_warnings",
54
55
  "create_script",
55
56
  # An exporter made in _init, before an editor's main loop has started, crashed the engine.
56
57
  "export_options",
@@ -99,6 +100,8 @@ func _init() -> void:
99
100
  var params: Dictionary = read
100
101
 
101
102
  _log.info("Executing operation: " + operation)
103
+ if operation == "check_warnings":
104
+ GdhScriptCheck.raise_warnings()
102
105
 
103
106
  var answer_path: String = args[answer_index]
104
107
  if operation in AFTER_THE_AUTOLOADS:
@@ -279,6 +282,8 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
279
282
  payload = GdhRenameChecks.new(_log).names_taken(params)
280
283
  "check_scripts":
281
284
  payload = GdhScriptCheck.new(_log).check_scripts(params)
285
+ "check_warnings":
286
+ payload = GdhScriptCheck.new(_log).check_warnings(params)
282
287
 
283
288
  # Plugins and input
284
289
  "list_plugins":
@@ -19,6 +19,15 @@ const GdhLog = preload("logger.gd")
19
19
  ## Where the engine raises a script's compile errors from.
20
20
  const RELOAD: String = "GDScript::reload"
21
21
 
22
+ ## What the engine puts on the end of a warning it raised as an error. Kept in step with
23
+ ## `RAISED_WARNING` in src/server.ts.
24
+ const RAISED: String = " (Warning treated as error.)"
25
+
26
+ ## Where the project's warning levels are, each 0 to ignore, 1 to warn and 2 to raise as an error.
27
+ const WARNING_LEVELS: String = "debug/gdscript/warnings/"
28
+ const WARN: int = 1
29
+ const ERROR_LEVEL: int = 2
30
+
22
31
  var _log: GdhLog
23
32
 
24
33
 
@@ -112,3 +121,52 @@ func check_scripts(params: Dictionary) -> Dictionary:
112
121
  break
113
122
  OS.remove_logger(caught)
114
123
  return {"checked": checked, "failed": failed, "scripts": scripts}
124
+
125
+
126
+ ## Raises every warning the project sets to warn so that the engine reports it as an error, which is
127
+ ## the one way it reports a warning outside an editor or a debugger. Not saved. The analyser reads
128
+ ## the levels again only once the settings change has been flushed, at the next frame, so this is
129
+ ## called in `_init` and the scripts are loaded after it; the project's autoloads, compiled before
130
+ ## that frame, compile under the project's own levels. A warning set to ignore stays ignored, and
131
+ ## @warning_ignore and the directory rules hold as they do in the editor, since the analyser applies
132
+ ## them itself.
133
+ static func raise_warnings() -> void:
134
+ for property: Dictionary in ProjectSettings.get_property_list():
135
+ var setting: String = str(property.get("name", ""))
136
+ if (
137
+ not setting.begins_with(WARNING_LEVELS)
138
+ or GdhRead.as_int(property.get("type", TYPE_NIL)) != TYPE_INT
139
+ ):
140
+ continue
141
+ if GdhRead.as_int(ProjectSettings.get_setting(setting)) == WARN:
142
+ ProjectSettings.set_setting(setting, ERROR_LEVEL)
143
+
144
+
145
+ ## The warnings of each script in [code]paths[/code], under the levels [method raise_warnings] left:
146
+ ## [code]scripts[/code] holds each that has any, with its warnings' lines and the engine's messages
147
+ ## as raised. The analyser runs before anything is compiled, so a script whose dependency failed on a
148
+ ## warning still reports its own, and loading on past a failure loses nothing; only what is raised at
149
+ ## the script's own path is its, since a dependency is read on its own entry as well.
150
+ func check_warnings(params: Dictionary) -> Dictionary:
151
+ var asked: Array = params.get("paths", [])
152
+ var caught: GdhCaught = GdhCaught.new()
153
+ OS.add_logger(caught)
154
+ var checked: Array[String] = []
155
+ var scripts: Array[Dictionary] = []
156
+ for entry: Variant in asked:
157
+ var path: String = str(entry)
158
+ checked.append(path)
159
+ caught.raised.clear()
160
+ var _loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
161
+ var warnings: Array[Dictionary] = []
162
+ for error: Dictionary in caught.raised:
163
+ var message: String = str(error["message"])
164
+ if str(error["path"]) != path or not message.ends_with(RAISED):
165
+ continue
166
+ var found: Dictionary = {"line": GdhRead.as_int(error["line"]), "message": message}
167
+ if not found in warnings:
168
+ warnings.append(found)
169
+ if not warnings.is_empty():
170
+ scripts.append({"path": path, "warnings": warnings})
171
+ OS.remove_logger(caught)
172
+ return {"checked": checked, "scripts": scripts}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.38",
3
+ "version": "1.3.39",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",