gdharness 1.3.19 → 1.3.21

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
@@ -48378,14 +48378,14 @@ var init_suites_scene = __esm(() => {
48378
48378
  });
48379
48379
 
48380
48380
  // src/test-selection.ts
48381
- import { existsSync as existsSync33, readFileSync as readFileSync36 } from "node:fs";
48381
+ import { existsSync as existsSync33 } from "node:fs";
48382
48382
  import { basename as basename12, join as join42 } from "node:path";
48383
- function casesIn(projectPath, suite) {
48384
- const text = readFileSync36(join42(projectPath, suite.slice("res://".length)), "utf8");
48385
- 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_")) ?? [];
48386
48385
  }
48387
48386
  function scriptsWithoutCases(projectPath, asked) {
48388
- 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);
48389
48389
  }
48390
48390
  function suitesUnder(projectPath, asked) {
48391
48391
  const scripts = projectFiles(projectPath, [".gd"]).map((path) => `res://${path}`);
@@ -48401,7 +48401,8 @@ function selectTests(projectPath, asked, given) {
48401
48401
  if (entries.length === 0) {
48402
48402
  return { ok: false, reason: "tests is an empty list: name a case, or leave tests out to run every one." };
48403
48403
  }
48404
- 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 })));
48405
48406
  const nameOf = (suite) => basename12(suite, ".gd");
48406
48407
  const selected = [];
48407
48408
  for (const [index, entry] of entries.entries()) {
@@ -48442,6 +48443,7 @@ function selectTests(projectPath, asked, given) {
48442
48443
  }
48443
48444
  var init_test_selection = __esm(() => {
48444
48445
  init_project_scan();
48446
+ init_suite_plan();
48445
48447
  });
48446
48448
 
48447
48449
  // src/server.ts
@@ -48456,7 +48458,7 @@ import {
48456
48458
  mkdirSync as mkdirSync23,
48457
48459
  openSync as openSync6,
48458
48460
  readdirSync as readdirSync19,
48459
- readFileSync as readFileSync37,
48461
+ readFileSync as readFileSync36,
48460
48462
  readSync as readSync3,
48461
48463
  realpathSync as realpathSync5,
48462
48464
  rmdirSync as rmdirSync2,
@@ -48477,9 +48479,9 @@ function greetingWaitMs() {
48477
48479
  return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
48478
48480
  }
48479
48481
  function pictureToSend(pngPath, jpegPath) {
48480
- const png = readFileSync37(pngPath);
48482
+ const png = readFileSync36(pngPath);
48481
48483
  const beside = pngPath.replace(/\.png$/, ".jpg");
48482
- 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;
48483
48485
  const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
48484
48486
  return {
48485
48487
  data: sent.toString("base64"),
@@ -48958,7 +48960,7 @@ function endedToStartThis(ended) {
48958
48960
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
48959
48961
  }
48960
48962
  function lineNothingStopsOn(absolutePath, relativePath, line) {
48961
- const text = readFileSync37(absolutePath, "utf8");
48963
+ const text = readFileSync36(absolutePath, "utf8");
48962
48964
  const lines = text === "" ? 0 : text.split(`
48963
48965
  `).length - (text.endsWith(`
48964
48966
  `) ? 1 : 0);
@@ -49169,7 +49171,7 @@ function notRunNote(notRun, failFast) {
49169
49171
  function presetsIn(projectPath) {
49170
49172
  let text;
49171
49173
  try {
49172
- text = readFileSync37(join43(projectPath, "export_presets.cfg"), "utf8");
49174
+ text = readFileSync36(join43(projectPath, "export_presets.cfg"), "utf8");
49173
49175
  } catch {
49174
49176
  return null;
49175
49177
  }
@@ -49319,7 +49321,7 @@ function rewriteBusNamed(projectPath, from, to) {
49319
49321
  const rewritten = [];
49320
49322
  for (const file of projectFiles(projectPath, [".tscn", ".tres"])) {
49321
49323
  const absolute = join43(projectPath, file);
49322
- const lines = readFileSync37(absolute, "utf8").split(`
49324
+ const lines = readFileSync36(absolute, "utf8").split(`
49323
49325
  `);
49324
49326
  const at = [];
49325
49327
  const after = lines.map((line, index) => {
@@ -49338,7 +49340,7 @@ function rewriteBusNamed(projectPath, from, to) {
49338
49340
  }
49339
49341
  const namedInScripts = [];
49340
49342
  for (const file of projectFiles(projectPath, [".gd"])) {
49341
- readFileSync37(join43(projectPath, file), "utf8").split(`
49343
+ readFileSync36(join43(projectPath, file), "utf8").split(`
49342
49344
  `).forEach((line, index) => {
49343
49345
  if (line.includes(`"${from}"`)) {
49344
49346
  namedInScripts.push({ file: `res://${file}`, line: index + 1, text: line.trim() });
@@ -49613,7 +49615,7 @@ function profileFrameFunctions(projectPath) {
49613
49615
  let projectGodot = null;
49614
49616
  if (projectPath !== null) {
49615
49617
  try {
49616
- projectGodot = readFileSync37(join43(projectPath, "project.godot"), "utf8");
49618
+ projectGodot = readFileSync36(join43(projectPath, "project.godot"), "utf8");
49617
49619
  } catch {}
49618
49620
  }
49619
49621
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -49637,7 +49639,7 @@ function projectSourceOf(projectPath) {
49637
49639
  let source = null;
49638
49640
  if (inside.ok) {
49639
49641
  try {
49640
- source = readFileSync37(inside.absolutePath, "utf8");
49642
+ source = readFileSync36(inside.absolutePath, "utf8");
49641
49643
  } catch {}
49642
49644
  }
49643
49645
  read.set(script, source);
@@ -49796,7 +49798,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
49796
49798
  }
49797
49799
  function gdUnitFailsOnScriptErrors(projectPath) {
49798
49800
  try {
49799
- 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;
49800
49802
  } catch {
49801
49803
  return true;
49802
49804
  }
@@ -49924,11 +49926,11 @@ function isDirectory(path) {
49924
49926
  }
49925
49927
  }
49926
49928
  function mainSceneOf(projectFile) {
49927
- const scene = parseProjectGodot(readFileSync37(projectFile, "utf8"))["application"]?.["run/main_scene"];
49929
+ const scene = parseProjectGodot(readFileSync36(projectFile, "utf8"))["application"]?.["run/main_scene"];
49928
49930
  return typeof scene === "string" && scene !== "" ? scene : null;
49929
49931
  }
49930
49932
  function editorPlaysHeadless(projectFile) {
49931
- const runArgs = parseProjectGodot(readFileSync37(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
49933
+ const runArgs = parseProjectGodot(readFileSync36(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
49932
49934
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
49933
49935
  }
49934
49936
  function ownArguments(params) {
@@ -51823,7 +51825,7 @@ class GodotServer {
51823
51825
  if (!engine.ok) {
51824
51826
  return engine.response;
51825
51827
  }
51826
- const application = parseProjectGodot(readFileSync37(project.value.file, "utf8"))["application"] ?? {};
51828
+ const application = parseProjectGodot(readFileSync36(project.value.file, "utf8"))["application"] ?? {};
51827
51829
  const name = application["config/name"];
51828
51830
  const mainScene = application["run/main_scene"];
51829
51831
  const info = {
@@ -52343,13 +52345,13 @@ ${platform}`;
52343
52345
  const reportsDir = join43(project.value.path, ".godot", ...ours.split("/"));
52344
52346
  let report = null;
52345
52347
  let reportProblem = null;
52346
- 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);
52347
52349
  const declaredErrors = readExpected(expectedFile);
52348
52350
  try {
52349
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))) : [];
52350
52352
  const newest = written.at(-1);
52351
52353
  if (newest !== undefined) {
52352
- report = parseJUnit(readFileSync37(join43(reportsDir, newest, "results.xml"), "utf8"));
52354
+ report = parseJUnit(readFileSync36(join43(reportsDir, newest, "results.xml"), "utf8"));
52353
52355
  }
52354
52356
  } catch (error) {
52355
52357
  reportProblem = errorMessage(error);
@@ -52828,7 +52830,7 @@ ${error.text}`;
52828
52830
  return null;
52829
52831
  }
52830
52832
  try {
52831
- return readFileSync37(contained.absolutePath, "utf8");
52833
+ return readFileSync36(contained.absolutePath, "utf8");
52832
52834
  } catch {
52833
52835
  return null;
52834
52836
  }
@@ -52922,7 +52924,7 @@ ${error.text}`;
52922
52924
  return null;
52923
52925
  }
52924
52926
  try {
52925
- return readFileSync37(contained.absolutePath, "utf8");
52927
+ return readFileSync36(contained.absolutePath, "utf8");
52926
52928
  } catch {
52927
52929
  return null;
52928
52930
  }
@@ -53425,7 +53427,7 @@ ${error.text}`;
53425
53427
  return null;
53426
53428
  }
53427
53429
  try {
53428
- return settingKeys(readFileSync37(join43(projectPath, "project.godot"), "utf8"));
53430
+ return settingKeys(readFileSync36(join43(projectPath, "project.godot"), "utf8"));
53429
53431
  } catch {
53430
53432
  return null;
53431
53433
  }
@@ -54205,7 +54207,7 @@ ${error.text}`;
54205
54207
  return 0;
54206
54208
  }
54207
54209
  try {
54208
- return reportAlreadyInTranscript(readFileSync37(game.transcript), readFileSync37(report));
54210
+ return reportAlreadyInTranscript(readFileSync36(game.transcript), readFileSync36(report));
54209
54211
  } catch (error) {
54210
54212
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
54211
54213
  return 0;
@@ -56538,7 +56540,7 @@ ${error.text}`;
56538
56540
  }
56539
56541
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56540
56542
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
56541
- if (declared.test(readFileSync37(scriptPath, "utf8"))) {
56543
+ if (declared.test(readFileSync36(scriptPath, "utf8"))) {
56542
56544
  return null;
56543
56545
  }
56544
56546
  const checked = await this.classesTheEditorCannotSee(args);
@@ -57454,7 +57456,7 @@ metadata:
57454
57456
 
57455
57457
  # gdharness
57456
57458
 
57457
- An MCP server and three Godot addons. Together they answer about the editor that is open, the game
57459
+ An MCP server and four Godot addons. Together they answer about the editor that is open, the game
57458
57460
  that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`domain_verb\`.
57459
57461
 
57460
57462
  \`references/tools.md\` is every tool, op and argument.
@@ -59124,6 +59126,9 @@ async function upgrade() {
59124
59126
  const digestBefore = installedEditorDigest(projectPath);
59125
59127
  for (const addon of installAddons(projectPath)) {
59126
59128
  console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
59129
+ if (!addon.replaced) {
59130
+ console.log(` addons/${addon.name}/ is new to this project: a .gitignore entry or export filter naming gdharness's addons one by one has to name it too`);
59131
+ }
59127
59132
  }
59128
59133
  const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
59129
59134
  for (const [index, outcome] of enabled.entries()) {
@@ -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.19",
3
+ "version": "1.3.21",
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",