gdharness 1.3.10 → 1.3.11

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
@@ -15127,6 +15127,13 @@ function funcOf(name) {
15127
15127
  const colon = name.indexOf(":");
15128
15128
  return colon < 0 ? name : name.slice(0, colon);
15129
15129
  }
15130
+ function rowOf(name) {
15131
+ const row = /^[^:]+:(\d+)(?: |$)/.exec(name)?.[1];
15132
+ return row === undefined ? null : Number(row);
15133
+ }
15134
+ function rowRuns(share, row) {
15135
+ return "only" in share ? row !== null && share.only.includes(row) : row === null || !share.besides.includes(row);
15136
+ }
15130
15137
  function readRun(printed) {
15131
15138
  const lines = stripVTControlCharacters(printed).replaceAll(`\r
15132
15139
  `, `
@@ -15146,7 +15153,15 @@ function readRun(printed) {
15146
15153
  }
15147
15154
  afterCase = [];
15148
15155
  };
15156
+ const leftOut = new Map;
15149
15157
  for (const [at, line] of lines.entries()) {
15158
+ const left = LEFT_OUT.exec(line);
15159
+ if (left !== null) {
15160
+ const key = `${left[1] ?? ""} > ${left[2] ?? ""}`;
15161
+ const row = Number(left[3]);
15162
+ leftOut.set(key, [...leftOut.get(key) ?? [], row < 0 ? null : row]);
15163
+ continue;
15164
+ }
15150
15165
  const opened = SUITE_OPENED.exec(line);
15151
15166
  if (opened?.[1] !== undefined) {
15152
15167
  settleAfterCase("after");
@@ -15166,6 +15181,7 @@ function readRun(printed) {
15166
15181
  open.cases.push({
15167
15182
  name,
15168
15183
  func: funcOf(name),
15184
+ row: rowOf(name),
15169
15185
  verdict: caseLine[3] ?? "",
15170
15186
  seconds: secondsOf(caseLine[4] ?? ""),
15171
15187
  at
@@ -15225,7 +15241,7 @@ function readRun(printed) {
15225
15241
  `);
15226
15242
  const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
15227
15243
  const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
15228
- return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
15244
+ return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn, leftOut };
15229
15245
  }
15230
15246
  function withNestedErrors(printed, piped) {
15231
15247
  const nested = piped.flatMap((entry, at) => {
@@ -15253,6 +15269,56 @@ function exitNamed(code) {
15253
15269
  const gdunit = GDUNIT_EXITS.get(code);
15254
15270
  return gdunit === undefined ? String(code) : `${String(code)} (${gdunit})`;
15255
15271
  }
15272
+ function rowsAcross(engines) {
15273
+ const dealtOut = new Map;
15274
+ for (const { handed, printed } of engines) {
15275
+ for (const suite of handed) {
15276
+ for (const func of Object.keys(suite.rows ?? {})) {
15277
+ const key = `${suite.path} > ${func}`;
15278
+ const entry = dealtOut.get(key) ?? { path: suite.path, func, holders: [] };
15279
+ entry.holders.push(printed);
15280
+ dealtOut.set(key, entry);
15281
+ }
15282
+ }
15283
+ }
15284
+ const faults = [];
15285
+ for (const [key, { path, func, holders }] of dealtOut) {
15286
+ const suites = holders.map((printed) => printed.suites.get(path));
15287
+ if (suites.some((suite) => suite?.closed === null || suite?.closed === undefined)) {
15288
+ continue;
15289
+ }
15290
+ const counts = new Map;
15291
+ for (const ran of suites.flatMap((suite) => suite?.cases ?? []).filter((ran) => ran.func === func)) {
15292
+ counts.set(ran.row, (counts.get(ran.row) ?? 0) + 1);
15293
+ }
15294
+ const said = (row) => row === null ? "the case whole" : `row ${String(row)}`;
15295
+ if (counts.size === 0) {
15296
+ faults.push({
15297
+ suite: path,
15298
+ fault: `${key} ran in none of the ${String(holders.length)} engines its rows were dealt to`
15299
+ });
15300
+ continue;
15301
+ }
15302
+ const twice = [...counts].filter(([, times]) => times > 1).map(([row]) => said(row));
15303
+ if (twice.length > 0) {
15304
+ faults.push({ suite: path, fault: `${key} ran ${named(twice)} in more than one engine` });
15305
+ }
15306
+ const rows = [...counts.keys()].filter((row) => row !== null);
15307
+ if (rows.length > 0 && counts.has(null)) {
15308
+ faults.push({ suite: path, fault: `${key} ran both whole and by its rows` });
15309
+ }
15310
+ const left = holders.flatMap((printed) => printed.leftOut.get(key) ?? []).filter((row) => row !== null);
15311
+ const top = Math.max(-1, ...rows, ...left);
15312
+ const gaps = Array.from({ length: top + 1 }, (_, row) => row).filter((row) => !counts.has(row));
15313
+ if (gaps.length > 0) {
15314
+ faults.push({
15315
+ suite: path,
15316
+ fault: `${key} did not run ${named(gaps.map(said))} in any of the ${String(holders.length)} engines its rows were dealt to`
15317
+ });
15318
+ }
15319
+ }
15320
+ return faults;
15321
+ }
15256
15322
  function named(items) {
15257
15323
  const shown = items.slice(0, MOST_NAMED).join(", ");
15258
15324
  return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
@@ -15273,7 +15339,7 @@ function judgeEngine(printed, handed, end) {
15273
15339
  if (printed.executed === null || printed.executed === 0) {
15274
15340
  faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
15275
15341
  }
15276
- for (const { path, cases } of handed.suites) {
15342
+ for (const { path, cases, rows = {} } of handed.suites) {
15277
15343
  const suite = printed.suites.get(path);
15278
15344
  if (suite === undefined) {
15279
15345
  faults.push(`${path} never started`);
@@ -15290,11 +15356,18 @@ function judgeEngine(printed, handed, end) {
15290
15356
  failing.add(path);
15291
15357
  }
15292
15358
  const finished = new Set(suite.cases.map((ran) => ran.func));
15293
- const missing = cases.filter((func) => !finished.has(func));
15359
+ const missing = cases.filter((func) => !finished.has(func) && rows[func] === undefined);
15294
15360
  if (missing.length > 0 && suite.closed !== null) {
15295
15361
  faults.push(`${path} closed without running ${named(missing)}`);
15296
15362
  failing.add(path);
15297
15363
  }
15364
+ for (const [func, share] of Object.entries(rows)) {
15365
+ const strays = suite.cases.filter((ran) => ran.func === func && !rowRuns(share, ran.row)).map((ran) => ran.row === null ? "the case whole" : `row ${String(ran.row)}`);
15366
+ if (strays.length > 0) {
15367
+ faults.push(`${path} > ${func} ran ${named(strays)}, which ${strays.length === 1 ? "was" : "were"} dealt to another engine`);
15368
+ failing.add(path);
15369
+ }
15370
+ }
15298
15371
  const wanted = new Set(cases);
15299
15372
  const unasked = [...finished].filter((func) => !wanted.has(func));
15300
15373
  if (unasked.length > 0) {
@@ -15416,7 +15489,7 @@ ${String(error.warning)}`);
15416
15489
  const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
15417
15490
  return { printed: unexpected, neverPrinted };
15418
15491
  }
15419
- var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, DRIVER_FALLBACK, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
15492
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, LEFT_OUT, DRIVER_FALLBACK, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
15420
15493
  var init_suite_judge = __esm(() => {
15421
15494
  init_engine_leaks();
15422
15495
  init_junit();
@@ -15427,6 +15500,7 @@ var init_suite_judge = __esm(() => {
15427
15500
  ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
15428
15501
  WARNING_LINE = /^\s*(?:SCRIPT |USER )?WARNING: (.*?)\s*$/;
15429
15502
  EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
15503
+ LEFT_OUT = /^gdharness left out (res:\/\/.+?\.gd) > ([^:\s]+):(-?\d+)\s*$/;
15430
15504
  DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
15431
15505
  ORPHANS_REPORTED = /^\s*WARNING: Detected \d+ (?:possible )?orphan nodes\.\s*$/;
15432
15506
  STARVED = [
@@ -15587,7 +15661,7 @@ function writeJson(file, value) {
15587
15661
  writeWhole(file, `${JSON.stringify(value, null, 1)}
15588
15662
  `);
15589
15663
  }
15590
- function timesAfter(times, suites, runs, now) {
15664
+ function timesAfter(times, suites, runs, now, cutShort = new Set) {
15591
15665
  const next = {};
15592
15666
  for (const [path, time] of Object.entries(times)) {
15593
15667
  const suite = now(path);
@@ -15596,7 +15670,12 @@ function timesAfter(times, suites, runs, now) {
15596
15670
  }
15597
15671
  const cases = Object.fromEntries(Object.entries(time.cases).filter(([func]) => suite.cases.includes(func)));
15598
15672
  const dropped = Object.entries(time.cases).filter(([func]) => !(func in cases)).reduce((sum, [, seconds]) => sum + seconds, 0);
15599
- next[path] = { seconds: round(Math.max(0, time.seconds - dropped)), cases };
15673
+ const rows = Object.fromEntries(Object.entries(time.rows ?? {}).filter(([func]) => suite.cases.includes(func)));
15674
+ next[path] = {
15675
+ seconds: round(Math.max(0, time.seconds - dropped)),
15676
+ cases,
15677
+ ...Object.keys(rows).length > 0 ? { rows } : {}
15678
+ };
15600
15679
  }
15601
15680
  const declared = new Map(suites.map((suite) => [suite.path, suite]));
15602
15681
  const measured = new Map;
@@ -15605,15 +15684,24 @@ function timesAfter(times, suites, runs, now) {
15605
15684
  if (printed.closed === null || !declared.has(printed.path)) {
15606
15685
  continue;
15607
15686
  }
15608
- const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
15687
+ const entry = measured.get(printed.path) ?? {
15688
+ cases: new Map,
15689
+ rows: new Map,
15690
+ around: 0
15691
+ };
15609
15692
  const own = new Map;
15610
15693
  for (const ran of printed.cases) {
15611
15694
  own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
15695
+ if (ran.row !== null) {
15696
+ const rows = entry.rows.get(ran.func) ?? new Map;
15697
+ rows.set(ran.row, ran.seconds);
15698
+ entry.rows.set(ran.func, rows);
15699
+ }
15612
15700
  }
15613
15701
  const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
15614
15702
  entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
15615
15703
  for (const [func, seconds] of own) {
15616
- entry.cases.set(func, seconds);
15704
+ entry.cases.set(func, (entry.cases.get(func) ?? 0) + seconds);
15617
15705
  }
15618
15706
  measured.set(printed.path, entry);
15619
15707
  }
@@ -15621,17 +15709,33 @@ function timesAfter(times, suites, runs, now) {
15621
15709
  for (const [path, entry] of measured) {
15622
15710
  const suite = declared.get(path);
15623
15711
  const cases = {};
15712
+ const rows = {};
15624
15713
  for (const func of suite?.cases ?? []) {
15625
- const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
15714
+ const measuredRows = entry.rows.get(func);
15715
+ const keptRows = measuredRows === undefined ? times[path]?.rows?.[func] : rowsAfter(times[path]?.rows?.[func], measuredRows, cutShort.has(`${path}:${func}`));
15716
+ if (keptRows !== undefined) {
15717
+ rows[func] = keptRows.map(round);
15718
+ }
15719
+ const seconds = measuredRows === undefined || keptRows === undefined ? entry.cases.get(func) ?? times[path]?.cases[func] : keptRows.reduce((sum, row) => sum + row, 0);
15626
15720
  if (seconds !== undefined) {
15627
15721
  cases[func] = round(seconds);
15628
15722
  }
15629
15723
  }
15630
15724
  const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
15631
- next[path] = { seconds: round(entry.around + inCases), cases };
15725
+ next[path] = {
15726
+ seconds: round(entry.around + inCases),
15727
+ cases,
15728
+ ...Object.keys(rows).length > 0 ? { rows } : {}
15729
+ };
15632
15730
  }
15633
15731
  return next;
15634
15732
  }
15733
+ function rowsAfter(before, measured, cutShort) {
15734
+ const count = Math.max(...measured.keys()) + 1;
15735
+ const whole = !cutShort && measured.size === count;
15736
+ const length = whole ? count : Math.max(count, before?.length ?? 0);
15737
+ return Array.from({ length }, (_, row) => measured.get(row) ?? before?.[row] ?? 0);
15738
+ }
15635
15739
  function round(seconds) {
15636
15740
  return Math.round(seconds * 1000) / 1000;
15637
15741
  }
@@ -15652,24 +15756,39 @@ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
15652
15756
  const costOf = (func) => time?.cases[func] ?? caseCost;
15653
15757
  const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
15654
15758
  const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
15655
- if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
15656
- return [
15657
- {
15658
- suite: suite.path,
15659
- cases: suite.cases,
15660
- cost: whole,
15661
- windowed: suite.windowed,
15662
- headless: suite.headless
15663
- }
15664
- ];
15665
- }
15666
- return suite.cases.map((func) => ({
15759
+ const wholly = {
15667
15760
  suite: suite.path,
15668
- cases: [func],
15669
- cost: around + costOf(func),
15670
- windowed: false,
15761
+ cases: suite.cases,
15762
+ cost: whole,
15763
+ windowed: suite.windowed,
15671
15764
  headless: suite.headless
15672
- }));
15765
+ };
15766
+ if (suite.windowed || whole <= splitPast) {
15767
+ return [wholly];
15768
+ }
15769
+ const units = suite.cases.flatMap((func) => {
15770
+ const rows = time?.rows?.[func] ?? [];
15771
+ if (rows.length < 2 || around + costOf(func) <= splitPast) {
15772
+ return [
15773
+ {
15774
+ suite: suite.path,
15775
+ cases: [func],
15776
+ cost: around + costOf(func),
15777
+ windowed: false,
15778
+ headless: suite.headless
15779
+ }
15780
+ ];
15781
+ }
15782
+ return rows.map((seconds, index) => ({
15783
+ suite: suite.path,
15784
+ cases: [func],
15785
+ row: { index, rest: index === rows.length - 1 },
15786
+ cost: around + seconds,
15787
+ windowed: false,
15788
+ headless: suite.headless
15789
+ }));
15790
+ });
15791
+ return units.length < 2 ? [wholly] : units;
15673
15792
  });
15674
15793
  }
15675
15794
  function dealt(units, declared, most) {
@@ -15677,7 +15796,7 @@ function dealt(units, declared, most) {
15677
15796
  return [];
15678
15797
  }
15679
15798
  const windowed = units.filter((unit) => unit.windowed);
15680
- const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
15799
+ const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? "") || (a.row?.index ?? 0) - (b.row?.index ?? 0));
15681
15800
  const together = windowed.reduce((sum, unit) => sum + unit.cost, 0);
15682
15801
  const restart = windowed.length > 0 ? ENGINE_START_SECONDS : 0;
15683
15802
  const total = units.reduce((sum, unit) => sum + unit.cost, 0);
@@ -15691,8 +15810,8 @@ function dealt(units, declared, most) {
15691
15810
  loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
15692
15811
  }
15693
15812
  return [
15694
- ...windowed.length > 0 ? [planOf(windowed, declared, true, 0)] : [],
15695
- ...bins.flatMap((bin, lane) => bin.length === 0 ? [] : [planOf(bin, declared, false, lane)])
15813
+ ...windowed.length > 0 ? [planOf(windowed, declared, true, 0, units)] : [],
15814
+ ...bins.flatMap((bin, lane) => bin.length === 0 ? [] : [planOf(bin, declared, false, lane, units)])
15696
15815
  ];
15697
15816
  }
15698
15817
  function lanesOf(plans) {
@@ -15713,16 +15832,34 @@ function headlessInAWindow(plans, units) {
15713
15832
  return plans.filter((plan) => plan.windowed).flatMap((plan) => plan.suites.map((suite) => suite.path)).filter((path) => said.has(path));
15714
15833
  }
15715
15834
  function inOneEngine(units, declared) {
15716
- return planOf(units, declared, units.some((unit) => unit.windowed), 0);
15835
+ return planOf(units, declared, units.some((unit) => unit.windowed), 0, units);
15717
15836
  }
15718
- function planOf(bin, declared, windowed, lane) {
15837
+ function planOf(bin, declared, windowed, lane, all) {
15719
15838
  const held = new Map;
15720
15839
  for (const unit of bin) {
15721
- held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
15840
+ const cases = held.get(unit.suite) ?? [];
15841
+ held.set(unit.suite, [...cases, ...unit.cases.filter((func) => !cases.includes(func))]);
15722
15842
  }
15723
15843
  const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
15844
+ const rowsOf = (units, path, func) => units.flatMap((unit) => unit.suite === path && unit.cases[0] === func && unit.row !== undefined ? [unit.row.index] : []);
15845
+ const sharesOf = (path, cases) => {
15846
+ const shares = {};
15847
+ for (const func of cases) {
15848
+ const mine = rowsOf(bin, path, func);
15849
+ const dealtOut = rowsOf(all, path, func);
15850
+ if (mine.length === 0 || mine.length === dealtOut.length) {
15851
+ continue;
15852
+ }
15853
+ const rest = bin.some((unit) => unit.suite === path && unit.cases[0] === func && unit.row?.rest === true);
15854
+ shares[func] = rest ? { besides: dealtOut.filter((row) => !mine.includes(row)) } : { only: mine.sort((a, b) => a - b) };
15855
+ }
15856
+ return shares;
15857
+ };
15724
15858
  return {
15725
- suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
15859
+ suites: [...held].map(([path, cases]) => {
15860
+ const rows = sharesOf(path, cases);
15861
+ return { path, cases: [...cases].sort(), ...Object.keys(rows).length > 0 ? { rows } : {} };
15862
+ }),
15726
15863
  skipping,
15727
15864
  cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
15728
15865
  windowed,
@@ -57155,10 +57292,16 @@ ${existsSync19(path) ? readFileSync20(path, "latin1") : "missing"}`)
57155
57292
  const one = windowedAnyway.length === 1;
57156
57293
  console.log(`${named(windowedAnyway)} ${one ? "says it runs" : "say they run"} headless and ${one ? "runs" : "run"} with a window here, since an ordered run is one engine and a suite in it needs a window. Run ${one ? "it" : "them"} without the windowed suites to run ${one ? "it" : "them"} headless.`);
57157
57294
  }
57158
- const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path).length > 1);
57295
+ const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
57159
57296
  if (plans.length > 1 && split.length > 0) {
57160
57297
  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)}.`);
57161
57298
  }
57299
+ const byRows = [
57300
+ ...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
57301
+ ];
57302
+ if (plans.length > 1 && byRows.length > 0) {
57303
+ 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)}.`);
57304
+ }
57162
57305
  const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
57163
57306
  let ran = (await inTurns(lanes, room.engines, (lane) => runLane(launch, lane, units))).flat().sort((a, b) => a.index - b.index);
57164
57307
  if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
@@ -57169,9 +57312,10 @@ ${existsSync19(path) ? readFileSync20(path, "latin1") : "missing"}`)
57169
57312
  }
57170
57313
  ran = again;
57171
57314
  }
57172
- writeJson(timesFile, timesAfter(readTimes(timesFile), wholeSuites, ran.map((engine) => engine.printed), (path) => scripts.suite(path)));
57173
- recordPassed(passedFile, ran, printsOf);
57174
- return report(ran, startedAt, runDirectory, options.printOutput);
57315
+ writeJson(timesFile, timesAfter(readTimes(timesFile), wholeSuites, ran.map((engine) => engine.printed), (path) => scripts.suite(path), new Set(ran.flatMap((engine) => engine.plan.suites.filter((suite) => (engine.printed.suites.get(suite.path)?.closed ?? null) === null).flatMap((suite) => suite.cases.map((func) => `${suite.path}:${func}`))))));
57316
+ const across = rowsAcross(ran.map((engine) => ({ handed: engine.plan.suites, printed: engine.printed })));
57317
+ recordPassed(passedFile, ran, printsOf, new Set(across.map((fault) => fault.suite)));
57318
+ return report(ran, across.map((fault) => fault.fault), startedAt, runDirectory, options.printOutput);
57175
57319
  } finally {
57176
57320
  process12.off("SIGINT", interrupted);
57177
57321
  process12.off("SIGTERM", interrupted);
@@ -57207,9 +57351,9 @@ function readRecord(file) {
57207
57351
  return {};
57208
57352
  }
57209
57353
  }
57210
- function recordPassed(file, ran, printsOf) {
57354
+ function recordPassed(file, ran, printsOf, failedAcross) {
57211
57355
  const passed = readRecord(file);
57212
- const failedSomewhere = new Set;
57356
+ const failedSomewhere = new Set(failedAcross);
57213
57357
  const passedSomewhere = new Set;
57214
57358
  for (const engine of ran) {
57215
57359
  const passing = new Set(engine.verdict.passedSuites);
@@ -57270,12 +57414,19 @@ async function runEngine2(launch, plan, index, units, again = "", slot = null) {
57270
57414
  mkdirSync8(join22(directory, "user"), { recursive: true });
57271
57415
  const suitesFile = join22(directory, "suites.txt");
57272
57416
  const skippingFile = join22(directory, "skipping.txt");
57417
+ const rowsFile = join22(directory, "rows.txt");
57273
57418
  const expectedFile = join22(directory, "expected.json");
57274
57419
  writeFileSync10(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
57275
57420
  `)}
57276
57421
  `);
57277
57422
  writeFileSync10(skippingFile, `${plan.skipping.join(`
57278
57423
  `)}
57424
+ `);
57425
+ writeFileSync10(rowsFile, `${plan.suites.flatMap((suite) => Object.entries(suite.rows ?? {}).map(([func, share]) => [
57426
+ `${suite.path}:${func}`,
57427
+ ..."only" in share ? ["only", ...share.only] : ["besides", ...share.besides]
57428
+ ].join("\t"))).join(`
57429
+ `)}
57279
57430
  `);
57280
57431
  const viewport = viewportOf(launch.projectPath);
57281
57432
  const display = plan.windowed ? viewport === null ? [] : ["--resolution", `${String(viewport.width)}x${String(viewport.height)}`] : ["--headless"];
@@ -57298,6 +57449,8 @@ async function runEngine2(launch, plan, index, units, again = "", slot = null) {
57298
57449
  suitesFile,
57299
57450
  "--skipping-in",
57300
57451
  skippingFile,
57452
+ "--rows-in",
57453
+ rowsFile,
57301
57454
  "--html-report-off",
57302
57455
  "--junit-report-off",
57303
57456
  "--compile-together",
@@ -57443,7 +57596,7 @@ function endTree(child) {
57443
57596
  process12.kill(-child.pid, "SIGKILL");
57444
57597
  } catch {}
57445
57598
  }
57446
- function report(ran, startedAt, runDirectory, printOutput) {
57599
+ function report(ran, across, startedAt, runDirectory, printOutput) {
57447
57600
  if (printOutput) {
57448
57601
  for (const engine of ran) {
57449
57602
  console.log("");
@@ -57466,18 +57619,25 @@ function report(ran, startedAt, runDirectory, printOutput) {
57466
57619
  }
57467
57620
  console.log(`everything it printed: ${engine.log}`);
57468
57621
  }
57622
+ if (across.length > 0) {
57623
+ console.log("");
57624
+ console.log("--- the cases dealt by their rows, over every engine holding them:");
57625
+ for (const fault of across) {
57626
+ console.log(` ${fault}`);
57627
+ }
57628
+ }
57469
57629
  const cases = ran.reduce((sum, engine) => sum + [...engine.printed.suites.values()].reduce((n, s) => n + s.cases.length, 0), 0);
57470
57630
  const suites = new Set(ran.flatMap((engine) => engine.plan.suites.map((suite) => suite.path))).size;
57471
57631
  const longest = Math.max(0, ...ran.map((engine) => engine.seconds));
57472
57632
  const took = (Date.now() - startedAt) / 1000;
57473
57633
  const totals = `${plural(cases, "case")} in ${plural(suites, "suite")} across ${plural(ran.length, "engine")}, ${took.toFixed(1)} s; the longest engine took ${longest.toFixed(1)} s.`;
57474
57634
  console.log("");
57475
- if (failed.length === 0) {
57635
+ if (failed.length === 0 && across.length === 0) {
57476
57636
  rmSync10(runDirectory, { recursive: true, force: true });
57477
57637
  console.log(`Passed: ${totals}`);
57478
57638
  return 0;
57479
57639
  }
57480
- const problems = failed.reduce((sum, engine) => sum + engine.verdict.faults.length, 0);
57640
+ const problems = failed.reduce((sum, engine) => sum + engine.verdict.faults.length, across.length);
57481
57641
  console.log(`Failed: ${plural(problems, "problem")}, listed above. Ran ${totals}`);
57482
57642
  console.log(`Every engine's output is kept under ${runDirectory} until the next run.`);
57483
57643
  return 1;
@@ -15,6 +15,16 @@ extends SceneTree
15
15
 
16
16
  const LISTED: String = "--suites-in"
17
17
  const SKIPPING: String = "--skipping-in"
18
+ ## The file of the parameterised cases dealt by their rows, one a line and tab-separated:
19
+ ## `res://suite.gd:test_case`, then `only` and the rows this engine runs, or `besides` and the rows
20
+ ## it leaves to the others, for the engine that takes every row dealt nowhere else. Apart from
21
+ ## [constant SKIPPING], since gdUnit4 reads `suite:case:row` there as the case and leaves out every
22
+ ## row of it.
23
+ const ROWS: String = "--rows-in"
24
+ ## Printed before each row an engine leaves out, as `res://suite.gd > test_case:row`, -1 for a case
25
+ ## that is not parameterised. gdUnit4 prints nothing for a case it skips, and the caller needs every
26
+ ## row an engine found to tell one that no engine ran.
27
+ const LEFT_OUT: String = "gdharness left out"
18
28
  ## The flags turning off a report, with the gdUnit4 hook that writes it. A gate reads what a run
19
29
  ## prints, and each report finds every case it hears of in the session to write them all out as the
20
30
  ## run ends; project_test reads the JUnit one and turns off only the HTML one.
@@ -54,10 +64,23 @@ class GdhRunner:
54
64
  extends GdUnitTestCIRunner
55
65
 
56
66
  var own_path: String = ""
67
+ ## The cases dealt by their rows, by `res://suite.gd:test_case`.
68
+ var row_shares: Dictionary[String, GdhRowShare] = {}
57
69
 
58
70
  func get_cmdline_args() -> PackedStringArray:
59
71
  return options_of(OS.get_cmdline_args(), own_path)
60
72
 
73
+ ## gdUnit4 asks this of every case it finds, a parameterised case once a row, each row carrying
74
+ ## its index.
75
+ func is_skipped(test: GdUnitTestCase) -> bool:
76
+ if super(test):
77
+ return true
78
+ var share: GdhRowShare = row_shares.get("%s:%s" % [test.suite_resource_path, test.test_name], null)
79
+ if share == null or not share.leaves_out(test.attribute_index):
80
+ return false
81
+ print("%s %s > %s:%d" % [LEFT_OUT, test.suite_resource_path, test.test_name, test.attribute_index])
82
+ return true
83
+
61
84
  ## The session gdUnit4 has just made, swapped for a [GdhIndexedSession] of the same cases as it
62
85
  ## is announced: everything that reads the session, the hooks, the cases and the console printing
63
86
  ## every case, is handed it after this.
@@ -87,6 +110,19 @@ class GdhRunner:
87
110
  return forward.to_lower() if OS.get_name() == "Windows" else forward
88
111
 
89
112
 
113
+ ## The rows of one parameterised case an engine runs: those named, or every row but those.
114
+ class GdhRowShare:
115
+ extends RefCounted
116
+
117
+ var only: bool = true
118
+ var rows: PackedInt32Array = []
119
+
120
+ ## Whether the engine leaves out [param row], which is -1 for a case that is not parameterised
121
+ ## and so runs only in the engine taking the rows besides.
122
+ func leaves_out(row: int) -> bool:
123
+ return rows.has(row) != only
124
+
125
+
90
126
  ## gdUnit4's session finding a case by its id in a dictionary, where its own walks every case of the
91
127
  ## session, `GdUnitTestSession.find_test_by_id`: the console asks it twice a case and each report
92
128
  ## twice more, so a run's time in it grew with the square of its cases (17.9 s of processor time
@@ -132,6 +168,7 @@ func _initialize() -> void:
132
168
  _runner.own_path = get_script().resource_path
133
169
  _runner.add_test_suites(suites)
134
170
  _runner.skip_test_suites(listed(asked, SKIPPING))
171
+ _runner.row_shares = row_shares_of(listed(asked, ROWS))
135
172
  root.add_child(_runner)
136
173
 
137
174
 
@@ -213,6 +250,22 @@ static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
213
250
  return named
214
251
 
215
252
 
253
+ ## The row shares [param lines] of a [constant ROWS] file give, by case.
254
+ static func row_shares_of(lines: PackedStringArray) -> Dictionary[String, GdhRowShare]:
255
+ var shares: Dictionary[String, GdhRowShare] = {}
256
+ for line: String in lines:
257
+ var parts: PackedStringArray = line.split("\t")
258
+ if parts.size() < 2:
259
+ continue
260
+ var share: GdhRowShare = GdhRowShare.new()
261
+ share.only = parts[1] == "only"
262
+ for row: String in parts.slice(2):
263
+ if row.is_valid_int():
264
+ var _added: bool = share.rows.append(row.to_int())
265
+ shares[parts[0]] = share
266
+ return shares
267
+
268
+
216
269
  ## The argument after [param flag] in [param args], or "" when there is none.
217
270
  static func argument_after(args: PackedStringArray, flag: String) -> String:
218
271
  var at: int = args.find(flag)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
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",