@schwabyio/gta 0.9.0 → 0.11.0

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/dist/gta.js CHANGED
@@ -20,13 +20,15 @@ import {
20
20
  parseMarkdown,
21
21
  projectEnvironments,
22
22
  readProject,
23
+ referenceProblems,
23
24
  relativePosix,
24
25
  require_dist,
25
26
  resolveFlags,
27
+ resultName,
26
28
  samePath,
27
29
  stepsForTags,
28
30
  unattempted
29
- } from "./chunks/chunk-VRAFYQNE.js";
31
+ } from "./chunks/chunk-OM3ZR56O.js";
30
32
 
31
33
  // src/args.ts
32
34
  var UsageError = class extends Error {
@@ -194,7 +196,7 @@ function tally(target, outcome, durationMs) {
194
196
  error: null,
195
197
  // Named for the row they ran with, when a data file drove the collection.
196
198
  problems: summary2.results.flatMap(
197
- (r, i) => r.status === "fail" || r.status === "error" ? [{ ...r, item: { ...r.item, name: iterationName(r.item.name, outcome.steps[i]) } }] : []
199
+ (r, i) => r.status === "fail" || r.status === "error" ? [{ ...r, item: { ...r.item, name: iterationName(resultName(r), outcome.steps[i]) } }] : []
198
200
  )
199
201
  };
200
202
  }
@@ -491,13 +493,13 @@ ${tests || '<p class="dim">No tests were run.</p>'}
491
493
  var statusOf = (r) => r.status === "pass" ? "passed" : r.status === "fail" ? "failed" : r.status === "error" ? "errored" : "skipped";
492
494
  function testSection(r, index, ref) {
493
495
  const status = statusOf(r);
494
- const named = escape(iterationName(r.item.name, ref));
496
+ const named = escape(iterationName(resultName(r), ref));
495
497
  const name = r.use ? `${named} <span class="dim">(${escape(r.use.set)} ${r.use.child + 1}/${r.use.of})</span>` : named;
496
498
  if (r.skipped) {
497
- return `<details class="acc skipped skipped" data-search="${escape(iterationName(r.item.name, ref).toLowerCase())}">
499
+ return `<details class="acc skipped skipped" data-search="${escape(iterationName(resultName(r), ref).toLowerCase())}">
498
500
  <summary><span>Test ${index + 1}: ${name}</span>${icon("skipped")}</summary>
499
501
  <div class="acc-body">
500
- ${card("Skipped", field2("Feature Flag", `<span class="c-skipped">${escape(r.skipped.reason)} \u2014 this test was not run</span>`, "wide"))}
502
+ ${card("Skipped", field2("Reason", `<span class="c-skipped">${escape(r.skipped.reason)} \u2014 this test was not run</span>`, "wide"))}
501
503
  </div>
502
504
  </details>`;
503
505
  }
@@ -509,7 +511,7 @@ ${card("Skipped", field2("Feature Flag", `<span class="c-skipped">${escape(r.ski
509
511
  r.logs?.length ? consoleCard(r) : ""
510
512
  ].filter(Boolean);
511
513
  const group = status === "passed" ? "passed" : status === "skipped" ? "skipped" : "failed";
512
- const search = iterationName(r.item.name, ref).toLowerCase();
514
+ const search = iterationName(resultName(r), ref).toLowerCase();
513
515
  return `<details class="acc ${group} ${status}" data-search="${escape(search)}"${status === "passed" ? "" : " open"}>
514
516
  <summary><span>Test ${index + 1}: ${name}</span>${icon(status)}</summary>
515
517
  <div class="acc-body">
@@ -950,7 +952,7 @@ function testcase(classname, name, durationMs, body) {
950
952
  if (body.length === 0) return [`${open}/>`];
951
953
  return [`${open}>`, ...body.map((line) => ` ${line}`), " </testcase>"];
952
954
  }
953
- var caseName = (result) => result.use ? `${result.item.name} [${result.use.set} ${result.use.child + 1}/${result.use.of}]` : result.item.name;
955
+ var caseName = (result) => result.use ? `${resultName(result)} [${result.use.set} ${result.use.child + 1}/${result.use.of}]` : result.item.name;
954
956
  var summarize = (a) => a.message ? `${a.name}: ${a.message}` : a.name;
955
957
  function describe(a) {
956
958
  const lines = [summarize(a)];
@@ -1070,6 +1072,7 @@ function jsonListing(options) {
1070
1072
  project: options.project,
1071
1073
  environments: options.environments,
1072
1074
  environmentType: options.environmentType,
1075
+ settings: { values: options.settings.values, sources: options.settings.sources },
1073
1076
  flags: { values: options.flags.values, sources: options.flags.sources },
1074
1077
  tags: options.tags,
1075
1078
  notTags: options.notTags ?? [],
@@ -1086,7 +1089,8 @@ function jsonListing(options) {
1086
1089
  tags: target.collection.doc.tags ?? [],
1087
1090
  broken: target.broken
1088
1091
  })),
1089
- leftOut: { excluded: options.excluded, untagged: options.untagged }
1092
+ leftOut: { excluded: options.excluded, untagged: options.untagged },
1093
+ problems: options.problems
1090
1094
  };
1091
1095
  }
1092
1096
  var fileOf = (collection) => `collections/${collection.relativePath}`;
@@ -1165,13 +1169,11 @@ var SettingsError = class extends Error {
1165
1169
  name = "SettingsError";
1166
1170
  };
1167
1171
  var envNameOf = (key) => `GTA_${key.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase()}`;
1172
+ var DEFAULT_SOURCE = "default";
1168
1173
  async function loadSettings(options) {
1169
- const { root, overrides = {}, env = process.env } = options;
1170
- const file = path.join(root, SETTINGS_FILE);
1171
- let source;
1172
- try {
1173
- source = await fs.readFile(file, "utf8");
1174
- } catch {
1174
+ const { root, global = null, overrides = {}, env = process.env } = options;
1175
+ const own = await readSettingsFile(path.join(root, SETTINGS_FILE), SETTINGS_FILE);
1176
+ if (own === null) {
1175
1177
  throw new SettingsError(
1176
1178
  `There is no ${SETTINGS_FILE} in ${root}.
1177
1179
  gta runs from a project folder: the one holding collections/ and ${SETTINGS_FILE}.
@@ -1182,23 +1184,20 @@ A minimal ${SETTINGS_FILE}:
1182
1184
  `
1183
1185
  );
1184
1186
  }
1185
- let fromFile;
1186
- try {
1187
- fromFile = (0, import_yaml.parse)(source) ?? {};
1188
- } catch (cause) {
1189
- throw new SettingsError(`${SETTINGS_FILE} will not parse: ${cause.message}`);
1190
- }
1191
- if (typeof fromFile !== "object" || fromFile === null || Array.isArray(fromFile)) {
1192
- throw new SettingsError(
1193
- `${SETTINGS_FILE} must be a map of settings, such as limitConcurrency: 4`
1194
- );
1195
- }
1187
+ const sharedLabel = global ? path.posix.join(global.uses, SETTINGS_FILE) : "";
1188
+ const shared = global ? await readSettingsFile(path.join(global.root, SETTINGS_FILE), sharedLabel) : null;
1196
1189
  const origin = /* @__PURE__ */ new Map();
1197
1190
  const merged = {};
1198
- for (const [key, value] of Object.entries(fromFile)) {
1199
- checkKey(key, SETTINGS_FILE);
1200
- merged[key] = value;
1201
- origin.set(key, SETTINGS_FILE);
1191
+ const files = [
1192
+ [sharedLabel, shared],
1193
+ [SETTINGS_FILE, own]
1194
+ ];
1195
+ for (const [label, fromFile] of files) {
1196
+ for (const [key, value] of Object.entries(fromFile ?? {})) {
1197
+ checkKey(key, label);
1198
+ merged[key] = value;
1199
+ origin.set(key, label);
1200
+ }
1202
1201
  }
1203
1202
  for (const key of KEYS) {
1204
1203
  const value = env[envNameOf(key)];
@@ -1220,7 +1219,29 @@ A minimal ${SETTINGS_FILE}:
1220
1219
  throw new SettingsError(`Settings are not valid:
1221
1220
  ${lines.join("\n")}`);
1222
1221
  }
1223
- return result.data;
1222
+ const sources = Object.fromEntries(
1223
+ KEYS.map((key) => [key, origin.get(key) ?? DEFAULT_SOURCE])
1224
+ );
1225
+ return { settings: result.data, sources };
1226
+ }
1227
+ async function readSettingsFile(file, label) {
1228
+ let source;
1229
+ try {
1230
+ source = await fs.readFile(file, "utf8");
1231
+ } catch (cause) {
1232
+ if (cause.code === "ENOENT") return null;
1233
+ throw new SettingsError(`${label} cannot be read: ${cause.message}`);
1234
+ }
1235
+ let fromFile;
1236
+ try {
1237
+ fromFile = (0, import_yaml.parse)(source) ?? {};
1238
+ } catch (cause) {
1239
+ throw new SettingsError(`${label} will not parse: ${cause.message}`);
1240
+ }
1241
+ if (typeof fromFile !== "object" || fromFile === null || Array.isArray(fromFile)) {
1242
+ throw new SettingsError(`${label} must be a map of settings, such as limitConcurrency: 4`);
1243
+ }
1244
+ return fromFile;
1224
1245
  }
1225
1246
  function checkKey(key, where) {
1226
1247
  if (!Object.hasOwn(SETTING_KEYS, key)) {
@@ -1248,7 +1269,7 @@ function coerce(key, value, where) {
1248
1269
  }
1249
1270
 
1250
1271
  // src/main.ts
1251
- var VERSION = true ? "0.9.0" : "0.0.0-dev";
1272
+ var VERSION = true ? "0.11.0" : "0.0.0-dev";
1252
1273
  var JUNIT_DIR = "junit";
1253
1274
  var JUNIT_FILE = "junit.xml";
1254
1275
  var HTML_DIR = "html";
@@ -1283,23 +1304,28 @@ async function main(argv, cli) {
1283
1304
  const command = args.command;
1284
1305
  if (command === "g" || command === "get") {
1285
1306
  const selection = selectFor(project, { kind: "all" });
1307
+ const problems = await listingProblems(project, selection);
1308
+ const code = problems.length > 0 || selection.targets.some((target) => target.broken) ? EXIT.failed : EXIT.passed;
1286
1309
  if (emit) {
1287
1310
  emit(
1288
1311
  jsonListing({
1289
1312
  project: { name: project.name, root: project.root },
1290
1313
  environments: project.environments,
1291
1314
  environmentType: project.settings.environmentType,
1315
+ settings: { values: project.settings, sources: project.settingSources },
1292
1316
  flags: project.flags,
1293
1317
  tags: project.settings.tags,
1294
1318
  notTags: project.settings.notTags,
1295
1319
  targets: selection.targets,
1296
1320
  excluded: selection.excluded,
1297
- untagged: selection.untagged
1321
+ untagged: selection.untagged,
1322
+ problems
1298
1323
  })
1299
1324
  );
1300
- return EXIT.passed;
1325
+ return code;
1301
1326
  }
1302
- return list(project, selection, ctx, p);
1327
+ list(project, selection, problems, ctx, p);
1328
+ return code;
1303
1329
  }
1304
1330
  const request = command === "a" || command === "all" ? { kind: "all" } : { kind: "list", selectors: command.split(",").filter((s) => s.trim() !== "") };
1305
1331
  return await run(project, selectFor(project, request), ctx, p, emit);
@@ -1321,14 +1347,19 @@ async function openProject(ctx, overrides, flagArgs = []) {
1321
1347
  gta runs from a project folder: the one holding ${COLLECTIONS_DIR}/ and ${SETTINGS_FILE}.`
1322
1348
  );
1323
1349
  }
1324
- const settings = await loadSettings({ root, overrides, env: ctx.env });
1325
1350
  const info = await readProject(root);
1351
+ const { settings, sources: settingSources } = await loadSettings({
1352
+ root,
1353
+ global: info.global,
1354
+ overrides,
1355
+ env: ctx.env
1356
+ });
1326
1357
  const layout = await discoverProject(root);
1327
1358
  const collections = await loadProjectCollections(root, layout.files);
1328
1359
  const environments = environmentNames(await projectEnvironments(root, info.global));
1329
1360
  if (settings.environmentType && !environments.includes(settings.environmentType)) {
1330
1361
  throw new ProjectError(
1331
- `environmentType: there is no environment called "${settings.environmentType}". ` + (environments.length > 0 ? `This project has: ${environments.join(", ")}` : `This project has no environments/.`)
1362
+ `environmentType (from ${settingSources.environmentType}): there is no environment called "${settings.environmentType}". ` + (environments.length > 0 ? `This project has: ${environments.join(", ")}` : `This project has no environments/.`)
1332
1363
  );
1333
1364
  }
1334
1365
  let flags;
@@ -1353,6 +1384,7 @@ gta runs from a project folder: the one holding ${COLLECTIONS_DIR}/ and ${SETTIN
1353
1384
  root,
1354
1385
  name: info.doc?.name ?? path2.basename(root),
1355
1386
  settings,
1387
+ settingSources,
1356
1388
  flags,
1357
1389
  collections,
1358
1390
  environments,
@@ -1368,7 +1400,24 @@ var selectFor = (project, request) => selectCollections(project.collections, req
1368
1400
  tags: project.settings.tags,
1369
1401
  notTags: project.settings.notTags
1370
1402
  });
1371
- function list(project, selection, ctx, p) {
1403
+ async function listingProblems(project, selection) {
1404
+ const listed = new Set(selection.targets.map((target) => target.id));
1405
+ const problems = [];
1406
+ for (const collection of project.collections) {
1407
+ const id = idOf(collection);
1408
+ const leftOut = listed.has(id) ? null : collection.doc.exclude === true ? "excluded" : "tags";
1409
+ if (collection.problems.length > 0) {
1410
+ const message = collection.problems.map((problem) => problem.message).join("; ");
1411
+ if (leftOut) problems.push({ id, leftOut, step: null, message });
1412
+ continue;
1413
+ }
1414
+ for (const problem of await referenceProblems(collection.doc, collection.path)) {
1415
+ problems.push({ id, leftOut, ...problem });
1416
+ }
1417
+ }
1418
+ return problems;
1419
+ }
1420
+ function list(project, selection, problems, ctx, p) {
1372
1421
  const { settings } = project;
1373
1422
  ctx.out(field("Project", `${project.name} ${p.dim(project.root)}`));
1374
1423
  ctx.out(
@@ -1379,8 +1428,7 @@ function list(project, selection, ctx, p) {
1379
1428
  );
1380
1429
  ctx.out(field("Flags", flagLine(project.flags, p)));
1381
1430
  if (project.caFiles.length > 0) ctx.out(field("CA files", project.caFiles.join(", ")));
1382
- if (settings.tags.length > 0) ctx.out(field("Tags", settings.tags.join(", ")));
1383
- if (settings.notTags.length > 0) ctx.out(field("Not tags", settings.notTags.join(", ")));
1431
+ for (const line of settingLines(project, p)) ctx.out(line);
1384
1432
  ctx.out("");
1385
1433
  const width = nameWidthFor(selection.targets.map((t) => t.id));
1386
1434
  const dirWidth = Math.max(
@@ -1406,6 +1454,12 @@ function list(project, selection, ctx, p) {
1406
1454
  for (const target of selection.targets.filter((t) => t.broken)) {
1407
1455
  ctx.out(`${p.red("broken")} ${target.id}: ${target.broken}`);
1408
1456
  }
1457
+ const leftOut = { excluded: " (excluded)", tags: " (left out by tags)" };
1458
+ for (const problem of problems) {
1459
+ const step = problem.step === null ? "" : `, ${problem.stage ? `${problem.stage} ` : ""}step ${problem.step + 1}`;
1460
+ const where = `${problem.id}${problem.leftOut ? leftOut[problem.leftOut] : ""}${step}`;
1461
+ ctx.out(`${p.red("broken")} ${where}: ${problem.message}`);
1462
+ }
1409
1463
  if (selection.excluded.length > 0) {
1410
1464
  ctx.out(field("Excluded", `${selection.excluded.join(", ")} ${p.dim("(exclude: true)")}`));
1411
1465
  }
@@ -1414,8 +1468,20 @@ function list(project, selection, ctx, p) {
1414
1468
  }
1415
1469
  const steps = selection.targets.reduce((n, t) => n + stepCountOf(t), 0);
1416
1470
  ctx.out(field("Total", `${selection.targets.length} collections, ${steps} steps`));
1417
- return EXIT.passed;
1418
1471
  }
1472
+ function settingLines(project, p) {
1473
+ const rows = Object.keys(SETTING_KEYS).filter((key) => project.settingSources[key] !== DEFAULT_SOURCE).map((key) => ({
1474
+ cell: `${key}: ${shownSetting(project.settings[key])}`,
1475
+ source: project.settingSources[key]
1476
+ }));
1477
+ if (rows.length === 0) return [field("Settings", p.dim("all defaults"))];
1478
+ const width = Math.max(...rows.map((row) => row.cell.length));
1479
+ const indent = " ".repeat(field("Settings", "").length);
1480
+ return rows.map(
1481
+ (row, i) => `${i === 0 ? field("Settings", "") : indent}${row.cell.padEnd(width)} ${p.dim(row.source)}`
1482
+ );
1483
+ }
1484
+ var shownSetting = (value) => Array.isArray(value) ? value.length > 0 ? value.join(", ") : "[]" : String(value ?? "none");
1419
1485
  async function run(project, selection, ctx, p, emit = null) {
1420
1486
  const { settings } = project;
1421
1487
  const width = nameWidthFor(selection.targets.map((t) => t.id));
@@ -1603,6 +1669,8 @@ function usage(p) {
1603
1669
  "",
1604
1670
  ` ${p.bold("Commands:")}`,
1605
1671
  " g, get List the collections `gta all` would run, without running them.",
1672
+ " Exits 1 on a broken collection, or a use:, extends: or body",
1673
+ " file that would stop a run, in any collection.",
1606
1674
  " a, all Run every collection, less those with exclude: true.",
1607
1675
  " <list> Run the collections and directories named, comma separated,",
1608
1676
  " in that order: smoke,checkout/sessions,payments",
@@ -1617,7 +1685,8 @@ function usage(p) {
1617
1685
  " Override a feature flag for this run; any number of them.",
1618
1686
  " GTA_FLAG_<name>=value does the same from the environment.",
1619
1687
  "",
1620
- ` ${p.bold("Settings")} (settings.yml, then GTA_* environment variables, then --flags):`,
1688
+ ` ${p.bold("Settings")} (the global project's settings.yml, then this project's, then`,
1689
+ " GTA_* environment variables, then --flags; gta get shows where each came from):",
1621
1690
  ...settings,
1622
1691
  "",
1623
1692
  ` ${p.bold("Exit code:")} 0 passed, 1 failed, 2 could not run (or write a report).`
package/dist/worker.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire as __gtaCreateRequire } from 'node:module';
3
3
  const require = __gtaCreateRequire(import.meta.url);
4
4
  import {
5
5
  runJob
6
- } from "./chunks/chunk-VRAFYQNE.js";
6
+ } from "./chunks/chunk-OM3ZR56O.js";
7
7
 
8
8
  // src/worker.ts
9
9
  import { parentPort, workerData } from "node:worker_threads";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schwabyio/gta",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "gta, the Gravity Test Automation command-line runner: HTTP API tests written as YAML files, run from a terminal or CI.",
6
6
  "keywords": [