gdharness 1.3.18 → 1.3.20

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
@@ -11803,8 +11803,42 @@ function dictionary(entries) {
11803
11803
 
11804
11804
  // src/call-signal.ts
11805
11805
  import { AsyncLocalStorage } from "node:async_hooks";
11806
- function withCallSignal(signal, answer, progress) {
11807
- return signal === undefined && progress === undefined ? answer() : current.run({ signal, progress: progress === undefined ? undefined : risingOnly(progress) }, answer);
11806
+ function stillGoingSaid(tool, op, elapsedMs) {
11807
+ const seconds = Math.floor(elapsedMs / 1000);
11808
+ const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
11809
+ return `${op === null || op === "" ? tool : `${tool} ${op}`} is still going; ${took} in.`;
11810
+ }
11811
+ async function withCallSignal(signal, answer, progress, still) {
11812
+ if (progress === undefined) {
11813
+ return signal === undefined ? answer() : current.run({ signal, progress: undefined }, answer);
11814
+ }
11815
+ const began = Date.now();
11816
+ const rising = risingOnly(progress);
11817
+ let going = still;
11818
+ let quiet;
11819
+ const rearm = () => {
11820
+ clearTimeout(quiet);
11821
+ if (going === undefined) {
11822
+ return;
11823
+ }
11824
+ const { everyMs, said } = going;
11825
+ quiet = setTimeout(() => {
11826
+ const elapsed = Date.now() - began;
11827
+ told(Math.floor(elapsed / 1000), said(elapsed));
11828
+ }, everyMs);
11829
+ quiet.unref();
11830
+ };
11831
+ const told = (number, message) => {
11832
+ rising(number, message);
11833
+ rearm();
11834
+ };
11835
+ rearm();
11836
+ try {
11837
+ return await current.run({ signal, progress: told }, answer);
11838
+ } finally {
11839
+ going = undefined;
11840
+ clearTimeout(quiet);
11841
+ }
11808
11842
  }
11809
11843
  function risingOnly(progress) {
11810
11844
  let last = Number.NEGATIVE_INFINITY;
@@ -11819,7 +11853,7 @@ function callSignal() {
11819
11853
  function callProgress() {
11820
11854
  return current.getStore()?.progress;
11821
11855
  }
11822
- var current;
11856
+ var current, CALL_HEARTBEAT_MS = 20000;
11823
11857
  var init_call_signal = __esm(() => {
11824
11858
  current = new AsyncLocalStorage;
11825
11859
  });
@@ -48344,14 +48378,14 @@ var init_suites_scene = __esm(() => {
48344
48378
  });
48345
48379
 
48346
48380
  // src/test-selection.ts
48347
- import { existsSync as existsSync33, readFileSync as readFileSync36 } from "node:fs";
48381
+ import { existsSync as existsSync33 } from "node:fs";
48348
48382
  import { basename as basename12, join as join42 } from "node:path";
48349
- function casesIn(projectPath, suite) {
48350
- const text = readFileSync36(join42(projectPath, suite.slice("res://".length)), "utf8");
48351
- return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
48383
+ function casesIn(scripts, suite) {
48384
+ return scripts.suite(suite)?.cases ?? scripts.script(suite)?.funcs.filter((name) => name.startsWith("test_")) ?? [];
48352
48385
  }
48353
48386
  function scriptsWithoutCases(projectPath, asked) {
48354
- return asked.filter((path) => path.endsWith(".gd") && existsSync33(join42(projectPath, path.slice("res://".length))) && casesIn(projectPath, path).length === 0);
48387
+ const scripts = new ProjectScripts(projectPath);
48388
+ return asked.filter((path) => path.endsWith(".gd") && existsSync33(join42(projectPath, path.slice("res://".length))) && casesIn(scripts, path).length === 0);
48355
48389
  }
48356
48390
  function suitesUnder(projectPath, asked) {
48357
48391
  const scripts = projectFiles(projectPath, [".gd"]).map((path) => `res://${path}`);
@@ -48367,7 +48401,8 @@ function selectTests(projectPath, asked, given) {
48367
48401
  if (entries.length === 0) {
48368
48402
  return { ok: false, reason: "tests is an empty list: name a case, or leave tests out to run every one." };
48369
48403
  }
48370
- const declared = suitesUnder(projectPath, asked).flatMap((suite) => casesIn(projectPath, suite).map((test) => ({ suite, test })));
48404
+ const scripts = new ProjectScripts(projectPath);
48405
+ const declared = suitesUnder(projectPath, asked).flatMap((suite) => casesIn(scripts, suite).map((test) => ({ suite, test })));
48371
48406
  const nameOf = (suite) => basename12(suite, ".gd");
48372
48407
  const selected = [];
48373
48408
  for (const [index, entry] of entries.entries()) {
@@ -48408,6 +48443,7 @@ function selectTests(projectPath, asked, given) {
48408
48443
  }
48409
48444
  var init_test_selection = __esm(() => {
48410
48445
  init_project_scan();
48446
+ init_suite_plan();
48411
48447
  });
48412
48448
 
48413
48449
  // src/server.ts
@@ -48422,7 +48458,7 @@ import {
48422
48458
  mkdirSync as mkdirSync23,
48423
48459
  openSync as openSync6,
48424
48460
  readdirSync as readdirSync19,
48425
- readFileSync as readFileSync37,
48461
+ readFileSync as readFileSync36,
48426
48462
  readSync as readSync3,
48427
48463
  realpathSync as realpathSync5,
48428
48464
  rmdirSync as rmdirSync2,
@@ -48443,9 +48479,9 @@ function greetingWaitMs() {
48443
48479
  return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
48444
48480
  }
48445
48481
  function pictureToSend(pngPath, jpegPath) {
48446
- const png = readFileSync37(pngPath);
48482
+ const png = readFileSync36(pngPath);
48447
48483
  const beside = pngPath.replace(/\.png$/, ".jpg");
48448
- const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync34(beside) ? readFileSync37(beside) : null;
48484
+ const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync34(beside) ? readFileSync36(beside) : null;
48449
48485
  const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
48450
48486
  return {
48451
48487
  data: sent.toString("base64"),
@@ -48924,7 +48960,7 @@ function endedToStartThis(ended) {
48924
48960
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
48925
48961
  }
48926
48962
  function lineNothingStopsOn(absolutePath, relativePath, line) {
48927
- const text = readFileSync37(absolutePath, "utf8");
48963
+ const text = readFileSync36(absolutePath, "utf8");
48928
48964
  const lines = text === "" ? 0 : text.split(`
48929
48965
  `).length - (text.endsWith(`
48930
48966
  `) ? 1 : 0);
@@ -49135,7 +49171,7 @@ function notRunNote(notRun, failFast) {
49135
49171
  function presetsIn(projectPath) {
49136
49172
  let text;
49137
49173
  try {
49138
- text = readFileSync37(join43(projectPath, "export_presets.cfg"), "utf8");
49174
+ text = readFileSync36(join43(projectPath, "export_presets.cfg"), "utf8");
49139
49175
  } catch {
49140
49176
  return null;
49141
49177
  }
@@ -49285,7 +49321,7 @@ function rewriteBusNamed(projectPath, from, to) {
49285
49321
  const rewritten = [];
49286
49322
  for (const file of projectFiles(projectPath, [".tscn", ".tres"])) {
49287
49323
  const absolute = join43(projectPath, file);
49288
- const lines = readFileSync37(absolute, "utf8").split(`
49324
+ const lines = readFileSync36(absolute, "utf8").split(`
49289
49325
  `);
49290
49326
  const at = [];
49291
49327
  const after = lines.map((line, index) => {
@@ -49304,7 +49340,7 @@ function rewriteBusNamed(projectPath, from, to) {
49304
49340
  }
49305
49341
  const namedInScripts = [];
49306
49342
  for (const file of projectFiles(projectPath, [".gd"])) {
49307
- readFileSync37(join43(projectPath, file), "utf8").split(`
49343
+ readFileSync36(join43(projectPath, file), "utf8").split(`
49308
49344
  `).forEach((line, index) => {
49309
49345
  if (line.includes(`"${from}"`)) {
49310
49346
  namedInScripts.push({ file: `res://${file}`, line: index + 1, text: line.trim() });
@@ -49579,7 +49615,7 @@ function profileFrameFunctions(projectPath) {
49579
49615
  let projectGodot = null;
49580
49616
  if (projectPath !== null) {
49581
49617
  try {
49582
- projectGodot = readFileSync37(join43(projectPath, "project.godot"), "utf8");
49618
+ projectGodot = readFileSync36(join43(projectPath, "project.godot"), "utf8");
49583
49619
  } catch {}
49584
49620
  }
49585
49621
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -49603,7 +49639,7 @@ function projectSourceOf(projectPath) {
49603
49639
  let source = null;
49604
49640
  if (inside.ok) {
49605
49641
  try {
49606
- source = readFileSync37(inside.absolutePath, "utf8");
49642
+ source = readFileSync36(inside.absolutePath, "utf8");
49607
49643
  } catch {}
49608
49644
  }
49609
49645
  read.set(script, source);
@@ -49762,7 +49798,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
49762
49798
  }
49763
49799
  function gdUnitFailsOnScriptErrors(projectPath) {
49764
49800
  try {
49765
- return settingKeys(readFileSync37(join43(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
49801
+ return settingKeys(readFileSync36(join43(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
49766
49802
  } catch {
49767
49803
  return true;
49768
49804
  }
@@ -49890,11 +49926,11 @@ function isDirectory(path) {
49890
49926
  }
49891
49927
  }
49892
49928
  function mainSceneOf(projectFile) {
49893
- const scene = parseProjectGodot(readFileSync37(projectFile, "utf8"))["application"]?.["run/main_scene"];
49929
+ const scene = parseProjectGodot(readFileSync36(projectFile, "utf8"))["application"]?.["run/main_scene"];
49894
49930
  return typeof scene === "string" && scene !== "" ? scene : null;
49895
49931
  }
49896
49932
  function editorPlaysHeadless(projectFile) {
49897
- const runArgs = parseProjectGodot(readFileSync37(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
49933
+ const runArgs = parseProjectGodot(readFileSync36(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
49898
49934
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
49899
49935
  }
49900
49936
  function ownArguments(params) {
@@ -50347,7 +50383,10 @@ class GodotServer {
50347
50383
  const call = this.callsTaken++;
50348
50384
  this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
50349
50385
  try {
50350
- const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
50386
+ const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress, {
50387
+ everyMs: CALL_HEARTBEAT_MS,
50388
+ said: (elapsedMs) => stillGoingSaid(spec.name, checked.op, elapsedMs)
50389
+ });
50351
50390
  return this.withinTheClientsLimit(spec.name, this.withFeedbackNotice(this.withUpdateNotice(answer)));
50352
50391
  } finally {
50353
50392
  this.inFlight.delete(call);
@@ -51786,7 +51825,7 @@ class GodotServer {
51786
51825
  if (!engine.ok) {
51787
51826
  return engine.response;
51788
51827
  }
51789
- const application = parseProjectGodot(readFileSync37(project.value.file, "utf8"))["application"] ?? {};
51828
+ const application = parseProjectGodot(readFileSync36(project.value.file, "utf8"))["application"] ?? {};
51790
51829
  const name = application["config/name"];
51791
51830
  const mainScene = application["run/main_scene"];
51792
51831
  const info = {
@@ -52306,13 +52345,13 @@ ${platform}`;
52306
52345
  const reportsDir = join43(project.value.path, ".godot", ...ours.split("/"));
52307
52346
  let report = null;
52308
52347
  let reportProblem = null;
52309
- const casePrinted = withNestedErrors(readRun(existsSync34(engineLog) ? readFileSync37(engineLog, "utf8") : ""), run.log.all);
52348
+ const casePrinted = withNestedErrors(readRun(existsSync34(engineLog) ? readFileSync36(engineLog, "utf8") : ""), run.log.all);
52310
52349
  const declaredErrors = readExpected(expectedFile);
52311
52350
  try {
52312
52351
  const written = existsSync34(reportsDir) && notCompiled === null ? readdirSync19(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
52313
52352
  const newest = written.at(-1);
52314
52353
  if (newest !== undefined) {
52315
- report = parseJUnit(readFileSync37(join43(reportsDir, newest, "results.xml"), "utf8"));
52354
+ report = parseJUnit(readFileSync36(join43(reportsDir, newest, "results.xml"), "utf8"));
52316
52355
  }
52317
52356
  } catch (error) {
52318
52357
  reportProblem = errorMessage(error);
@@ -52791,7 +52830,7 @@ ${error.text}`;
52791
52830
  return null;
52792
52831
  }
52793
52832
  try {
52794
- return readFileSync37(contained.absolutePath, "utf8");
52833
+ return readFileSync36(contained.absolutePath, "utf8");
52795
52834
  } catch {
52796
52835
  return null;
52797
52836
  }
@@ -52885,7 +52924,7 @@ ${error.text}`;
52885
52924
  return null;
52886
52925
  }
52887
52926
  try {
52888
- return readFileSync37(contained.absolutePath, "utf8");
52927
+ return readFileSync36(contained.absolutePath, "utf8");
52889
52928
  } catch {
52890
52929
  return null;
52891
52930
  }
@@ -53388,7 +53427,7 @@ ${error.text}`;
53388
53427
  return null;
53389
53428
  }
53390
53429
  try {
53391
- return settingKeys(readFileSync37(join43(projectPath, "project.godot"), "utf8"));
53430
+ return settingKeys(readFileSync36(join43(projectPath, "project.godot"), "utf8"));
53392
53431
  } catch {
53393
53432
  return null;
53394
53433
  }
@@ -54168,7 +54207,7 @@ ${error.text}`;
54168
54207
  return 0;
54169
54208
  }
54170
54209
  try {
54171
- return reportAlreadyInTranscript(readFileSync37(game.transcript), readFileSync37(report));
54210
+ return reportAlreadyInTranscript(readFileSync36(game.transcript), readFileSync36(report));
54172
54211
  } catch (error) {
54173
54212
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
54174
54213
  return 0;
@@ -56501,7 +56540,7 @@ ${error.text}`;
56501
56540
  }
56502
56541
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56503
56542
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
56504
- if (declared.test(readFileSync37(scriptPath, "utf8"))) {
56543
+ if (declared.test(readFileSync36(scriptPath, "utf8"))) {
56505
56544
  return null;
56506
56545
  }
56507
56546
  const checked = await this.classesTheEditorCannotSee(args);
@@ -80,6 +80,11 @@ class GdhRunner:
80
80
  var together: GDScript = null
81
81
  ## The cases dealt by their rows, by `res://suite.gd:test_case`.
82
82
  var row_shares: Dictionary[String, GdhRowShare] = {}
83
+ ## The cases to leave out, by `res://suite.gd:test_case` of the suite running them. gdUnit4's own
84
+ ## list is matched against the script declaring a case, which for a case a suite inherits is its
85
+ ## base, so an inherited case left out under its suite ran in every engine the suite was dealt to,
86
+ ## and a case of `res://tests/a_test.gd` was also left out of `res://more/tests/a_test.gd`.
87
+ var left_out: Dictionary[String, bool] = {}
83
88
  ## The cases gdUnit4 said it finished, which [GdhSessionCheck] holds the session's lookups against.
84
89
  var cases_ended: int = 0
85
90
  ## The marker of the shutdown walk's oldest end, made as the run starts.
@@ -106,10 +111,40 @@ class GdhRunner:
106
111
  return options
107
112
  return options_of(OS.get_cmdline_args(), own_path)
108
113
 
114
+ ## gdUnit4's own discovery, with each case filed under the suite it was found in before it is
115
+ ## asked whether to leave the case out. gdUnit4 files a case under the script declaring it, which
116
+ ## for a case a suite inherits is its base: its reports added such a case to no suite, so
117
+ ## project_test counted none of them, and a parameterised one ran as a suite of the base's own,
118
+ ## outside the suite inheriting it and its hooks, and was judged a case that suite never ran.
119
+ func discover_tests() -> Array[GdUnitTestCase]:
120
+ var added: Signal = GdUnitSignals.instance().gdunit_test_discover_added
121
+ _test_cases = _runner_config.test_cases()
122
+ var scanner: GdUnitTestSuiteScanner = GdUnitTestSuiteScanner.new()
123
+ for path: String in _included_tests:
124
+ for script: Script in scanner.scan(path):
125
+ var suite: String = script.resource_path
126
+ var found: Callable = func(test: GdUnitTestCase) -> void:
127
+ filed_under(test, suite)
128
+ if not is_skipped(test):
129
+ _test_cases.append(test)
130
+ added.emit(test)
131
+ GdUnitTestDiscoverer.discover_tests(script, found)
132
+ return _test_cases
133
+
134
+ ## [param test] as a case of the suite at [param suite], under which gdUnit4 groups, reports and
135
+ ## leaves out its cases.
136
+ static func filed_under(test: GdUnitTestCase, suite: String) -> void:
137
+ if test.suite_resource_path == suite and test.source_file == suite:
138
+ return
139
+ test.suite_resource_path = suite
140
+ test.source_file = suite
141
+ test.suite_name = suite.get_file().get_basename()
142
+ test._build_fully_qualified_name(suite)
143
+
109
144
  ## gdUnit4 asks this of every case it finds, a parameterised case once a row, each row carrying
110
145
  ## its index.
111
146
  func is_skipped(test: GdUnitTestCase) -> bool:
112
- if super(test):
147
+ if left_out.has("%s:%s" % [test.suite_resource_path, test.test_name]) or super(test):
113
148
  return true
114
149
  var share: GdhRowShare = row_shares.get("%s:%s" % [test.suite_resource_path, test.test_name], null)
115
150
  if share == null or not share.leaves_out(test.attribute_index):
@@ -348,7 +383,11 @@ static func started(given: PackedStringArray) -> GdhRunner:
348
383
  if not expected_out.is_empty():
349
384
  write_expected(expected_out, suites)
350
385
  runner.add_test_suites(suites)
351
- runner.skip_test_suites(listed(asked, SKIPPING))
386
+ for entry: String in listed(asked, SKIPPING):
387
+ if is_one_case(entry):
388
+ runner.left_out[entry] = true
389
+ else:
390
+ runner.skip_test_suite(entry)
352
391
  runner.row_shares = row_shares_of(listed(asked, ROWS))
353
392
  # Added to the list itself rather than through `register`, which saves every hook it holds into
354
393
  # `project.godot`.
@@ -426,6 +465,19 @@ static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
426
465
  return named
427
466
 
428
467
 
468
+ ## Whether [param entry] of a [constant SKIPPING] file names one case of one suite file, as
469
+ ## `res://suite.gd:test_case`, rather than a suite or a case by gdUnit4's looser names, which go to
470
+ ## gdUnit4 as they are.
471
+ static func is_one_case(entry: String) -> bool:
472
+ var at: int = entry.rfind(":")
473
+ return (
474
+ entry.begins_with("res://")
475
+ and at > "res://".length()
476
+ and entry.substr(0, at).ends_with(".gd")
477
+ and entry.substr(at + 1).is_valid_ascii_identifier()
478
+ )
479
+
480
+
429
481
  ## The row shares [param lines] of a [constant ROWS] file give, by case.
430
482
  static func row_shares_of(lines: PackedStringArray) -> Dictionary[String, GdhRowShare]:
431
483
  var shares: Dictionary[String, GdhRowShare] = {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.18",
3
+ "version": "1.3.20",
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",