@fabricorg/databricks-bdd 0.3.1 → 0.4.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.
Files changed (49) hide show
  1. package/README.md +56 -5
  2. package/dist/actions-CuXXKUqe.d.ts +92 -0
  3. package/dist/actions-CxfxhBZh.d.cts +92 -0
  4. package/dist/actions.cjs +14 -0
  5. package/dist/actions.cjs.map +1 -0
  6. package/dist/actions.d.cts +6 -0
  7. package/dist/actions.d.ts +6 -0
  8. package/dist/actions.js +3 -0
  9. package/dist/actions.js.map +1 -0
  10. package/dist/cardinality.cjs +24 -0
  11. package/dist/cardinality.cjs.map +1 -0
  12. package/dist/cardinality.d.cts +19 -0
  13. package/dist/cardinality.d.ts +19 -0
  14. package/dist/cardinality.js +3 -0
  15. package/dist/cardinality.js.map +1 -0
  16. package/dist/{chunk-FNTI2VUP.js → chunk-3XDMBHFP.js} +117 -4
  17. package/dist/chunk-3XDMBHFP.js.map +1 -0
  18. package/dist/chunk-6JC65RH2.js +11 -0
  19. package/dist/chunk-6JC65RH2.js.map +1 -0
  20. package/dist/chunk-HTKRJLVX.js +70 -0
  21. package/dist/chunk-HTKRJLVX.js.map +1 -0
  22. package/dist/chunk-K5LJ7WSW.js +22 -0
  23. package/dist/chunk-K5LJ7WSW.js.map +1 -0
  24. package/dist/chunk-SZL3KWW7.js +62 -0
  25. package/dist/chunk-SZL3KWW7.js.map +1 -0
  26. package/dist/fixtures.cjs +65 -0
  27. package/dist/fixtures.cjs.map +1 -0
  28. package/dist/fixtures.d.cts +34 -0
  29. package/dist/fixtures.d.ts +34 -0
  30. package/dist/fixtures.js +3 -0
  31. package/dist/fixtures.js.map +1 -0
  32. package/dist/index.cjs +277 -0
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +24 -55
  35. package/dist/index.d.ts +24 -55
  36. package/dist/index.js +5 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/scoped-state.cjs +74 -0
  39. package/dist/scoped-state.cjs.map +1 -0
  40. package/dist/scoped-state.d.cts +30 -0
  41. package/dist/scoped-state.d.ts +30 -0
  42. package/dist/scoped-state.js +3 -0
  43. package/dist/scoped-state.js.map +1 -0
  44. package/dist/steps.cjs +254 -2
  45. package/dist/steps.cjs.map +1 -1
  46. package/dist/steps.js +15 -4
  47. package/dist/steps.js.map +1 -1
  48. package/package.json +41 -1
  49. package/dist/chunk-FNTI2VUP.js.map +0 -1
package/dist/steps.cjs CHANGED
@@ -21,6 +21,65 @@ function redactSecrets(text, env = process.env) {
21
21
  return out;
22
22
  }
23
23
 
24
+ // src/fixtures.ts
25
+ function isFixtureResult(value) {
26
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "value") && typeof value.cleanup === "function";
27
+ }
28
+ var SharedFixtureRegistry = class {
29
+ runEntries = /* @__PURE__ */ new Map();
30
+ featureEntries = /* @__PURE__ */ new Map();
31
+ cleanupOrder = [];
32
+ runFixture(key, setup) {
33
+ return this.resolve(this.runEntries, key, setup);
34
+ }
35
+ featureFixture(featureUri, key, setup) {
36
+ if (!featureUri) throw new Error("featureFixture requires a feature URI");
37
+ let feature = this.featureEntries.get(featureUri);
38
+ if (!feature) {
39
+ feature = /* @__PURE__ */ new Map();
40
+ this.featureEntries.set(featureUri, feature);
41
+ }
42
+ return this.resolve(feature, key, setup);
43
+ }
44
+ resolve(entries, key, setup) {
45
+ if (!key.trim()) throw new Error("Fixture key must not be empty");
46
+ const existing = entries.get(key);
47
+ if (existing) return existing.value;
48
+ const entry = {
49
+ value: Promise.resolve().then(setup).then((result) => {
50
+ if (isFixtureResult(result)) {
51
+ entry.cleanup = result.cleanup;
52
+ return result.value;
53
+ }
54
+ return result;
55
+ }).catch((error) => {
56
+ entries.delete(key);
57
+ const index = this.cleanupOrder.indexOf(entry);
58
+ if (index >= 0) this.cleanupOrder.splice(index, 1);
59
+ throw error;
60
+ })
61
+ };
62
+ entries.set(key, entry);
63
+ this.cleanupOrder.push(entry);
64
+ return entry.value;
65
+ }
66
+ async dispose() {
67
+ const errors = [];
68
+ for (const entry of this.cleanupOrder.splice(0).reverse()) {
69
+ try {
70
+ await entry.value.catch(() => void 0);
71
+ await entry.cleanup?.();
72
+ } catch (error) {
73
+ errors.push(error);
74
+ }
75
+ }
76
+ this.runEntries.clear();
77
+ this.featureEntries.clear();
78
+ if (errors.length > 0) throw new AggregateError(errors, "Shared fixture cleanup failed");
79
+ }
80
+ };
81
+ var sharedFixtures = new SharedFixtureRegistry();
82
+
24
83
  // src/infer.ts
25
84
  var INT_RE = /^-?\d+$/;
26
85
  var FLOAT_RE = /^-?\d+\.\d+$/;
@@ -264,6 +323,142 @@ function liveOnlyReason(tags, profile) {
264
323
  if (blocking.length === 0) return null;
265
324
  return `requires a live workspace (${blocking.join(", ")}); running under DBX_TEST_PROFILE=local`;
266
325
  }
326
+ function capabilitiesFromEnv(profile, env = process.env) {
327
+ const capabilities = { profile };
328
+ const builtins = [
329
+ ["cloud", env.DBX_TEST_CLOUD],
330
+ ["compute", env.DBX_TEST_COMPUTE_MODE ?? env.DBX_TEST_COMPUTE],
331
+ ["stage", env.DBX_TEST_STAGE]
332
+ ];
333
+ for (const [key, value] of builtins) {
334
+ if (value !== void 0 && value !== "") capabilities[key] = value;
335
+ }
336
+ for (const [name, value] of Object.entries(env)) {
337
+ if (!name.startsWith("DBX_TEST_CAPABILITY_") || value === void 0) continue;
338
+ capabilities[normalizeCapabilityKey(name.slice("DBX_TEST_CAPABILITY_".length))] = value;
339
+ }
340
+ return capabilities;
341
+ }
342
+ function capabilityTagReason(tags, capabilities) {
343
+ const normalized = new Map(
344
+ Object.entries(capabilities).map(([key, value]) => [
345
+ normalizeCapabilityKey(key),
346
+ String(value).toLowerCase()
347
+ ])
348
+ );
349
+ for (const tag of tags) {
350
+ const predicate = parseCapabilityTag(tag);
351
+ if (!predicate) continue;
352
+ const actual = normalized.get(normalizeCapabilityKey(predicate.key));
353
+ const matches = predicate.expected ? actual === predicate.expected.toLowerCase() : actual !== void 0 && !["", "0", "false", "no", "off"].includes(actual);
354
+ if (predicate.kind === "requires" && !matches) {
355
+ return `${tag} requires capability ${predicate.key}${predicate.expected ? `=${predicate.expected}` : ""}; actual ${actual ?? "unset"}`;
356
+ }
357
+ if (predicate.kind === "excludes" && matches) {
358
+ return `${tag} excludes capability ${predicate.key}${predicate.expected ? `=${predicate.expected}` : ""}`;
359
+ }
360
+ }
361
+ return null;
362
+ }
363
+ function scenarioSkipReason(tags, profile, env = process.env) {
364
+ return liveOnlyReason(tags, profile) ?? capabilityTagReason(tags, capabilitiesFromEnv(profile, env));
365
+ }
366
+ function parseCapabilityTag(tag) {
367
+ const direct = tag.match(/^@(requires|excludes)\.([^=]+)(?:=(.+))?$/i);
368
+ if (direct?.[1] && direct[2]) {
369
+ return {
370
+ kind: direct[1].toLowerCase(),
371
+ key: direct[2],
372
+ ...direct[3] ? { expected: direct[3] } : {}
373
+ };
374
+ }
375
+ const behave = tag.match(/^@(use|not)\.with_([^=]+)=(.+)$/i);
376
+ if (behave?.[1] && behave[2] && behave[3]) {
377
+ return {
378
+ kind: behave[1].toLowerCase() === "use" ? "requires" : "excludes",
379
+ key: behave[2],
380
+ expected: behave[3]
381
+ };
382
+ }
383
+ return void 0;
384
+ }
385
+ function normalizeCapabilityKey(key) {
386
+ return key.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_");
387
+ }
388
+
389
+ // src/scoped-state.ts
390
+ var SharedStateRegistry = class {
391
+ runValues = /* @__PURE__ */ new Map();
392
+ featureValues = /* @__PURE__ */ new Map();
393
+ run() {
394
+ return this.runValues;
395
+ }
396
+ feature(featureUri) {
397
+ if (!featureUri) throw new Error("Feature state requires a feature URI");
398
+ let values = this.featureValues.get(featureUri);
399
+ if (!values) {
400
+ values = /* @__PURE__ */ new Map();
401
+ this.featureValues.set(featureUri, values);
402
+ }
403
+ return values;
404
+ }
405
+ clear() {
406
+ this.runValues.clear();
407
+ this.featureValues.clear();
408
+ }
409
+ };
410
+ var ScopedState = class {
411
+ constructor(runValues, featureValues) {
412
+ this.runValues = runValues;
413
+ this.featureValues = featureValues;
414
+ }
415
+ runValues;
416
+ featureValues;
417
+ scenarioValues = /* @__PURE__ */ new Map();
418
+ set(scope, key, value) {
419
+ this.values(scope).set(validateKey(key), value);
420
+ return value;
421
+ }
422
+ get(key) {
423
+ const validKey = validateKey(key);
424
+ if (this.scenarioValues.has(validKey)) return this.scenarioValues.get(validKey);
425
+ if (this.featureValues.has(validKey)) return this.featureValues.get(validKey);
426
+ return this.runValues.get(validKey);
427
+ }
428
+ require(key) {
429
+ const value = this.get(key);
430
+ if (value === void 0) throw new Error(`Required BDD state '${key}' is not set`);
431
+ return value;
432
+ }
433
+ has(key) {
434
+ const validKey = validateKey(key);
435
+ return this.scenarioValues.has(validKey) || this.featureValues.has(validKey) || this.runValues.has(validKey);
436
+ }
437
+ delete(scope, key) {
438
+ return this.values(scope).delete(validateKey(key));
439
+ }
440
+ clearScenario() {
441
+ this.scenarioValues.clear();
442
+ }
443
+ values(scope) {
444
+ if (scope === "run") return this.runValues;
445
+ if (scope === "feature") return this.featureValues;
446
+ return this.scenarioValues;
447
+ }
448
+ };
449
+ function validateKey(key) {
450
+ const trimmed = key.trim();
451
+ if (!trimmed) throw new Error("BDD state key must not be empty");
452
+ return trimmed;
453
+ }
454
+ var sharedState = new SharedStateRegistry();
455
+
456
+ // src/actions.ts
457
+ async function runAction(world, action, ...args) {
458
+ return await action(world, ...args);
459
+ }
460
+
461
+ // src/world.ts
267
462
  var DatabricksWorld = class extends cucumber.World {
268
463
  profile;
269
464
  /** Rows from the last `When I run the SQL` / `When I query table` step. */
@@ -278,10 +473,14 @@ var DatabricksWorld = class extends cucumber.World {
278
473
  catalogOverride;
279
474
  lastSql;
280
475
  lastStatementId;
476
+ featureUri;
477
+ scenarioName;
478
+ effectiveTags = [];
281
479
  stepOutputCapture;
282
480
  lakebasePool;
283
481
  ctx;
284
482
  activeCtx;
483
+ scopedState;
285
484
  cleanups = [];
286
485
  constructor(options) {
287
486
  super(options);
@@ -316,6 +515,43 @@ var DatabricksWorld = class extends cucumber.World {
316
515
  addCleanup(cleanup) {
317
516
  this.cleanups.push(cleanup);
318
517
  }
518
+ /** Initialize Behave-style feature/scenario metadata and layered state. */
519
+ initializeScenario(featureUri, scenarioName, tags) {
520
+ this.featureUri = featureUri;
521
+ this.scenarioName = scenarioName;
522
+ this.effectiveTags = [...tags];
523
+ this.scopedState = new ScopedState(sharedState.run(), sharedState.feature(featureUri));
524
+ }
525
+ /** Set typed state at run, feature, or scenario scope. */
526
+ setState(scope, key, value) {
527
+ return this.state().set(scope, key, value);
528
+ }
529
+ /** Resolve state using scenario → feature → run precedence. */
530
+ getState(key) {
531
+ return this.state().get(key);
532
+ }
533
+ /** Resolve required layered state or fail with a useful message. */
534
+ requireState(key) {
535
+ return this.state().require(key);
536
+ }
537
+ /** Compose a reusable typed action without reparsing textual Gherkin. */
538
+ runAction(action, ...args) {
539
+ return runAction(this, action, ...args);
540
+ }
541
+ /** Setup a resource once for this Cucumber run and clean it up in AfterAll. */
542
+ runFixture(key, setup) {
543
+ return sharedFixtures.runFixture(key, setup);
544
+ }
545
+ /**
546
+ * Setup a resource once per feature file. It is isolated by feature URI and
547
+ * cleaned in AfterAll because cucumber-js has no reliable after-feature hook.
548
+ */
549
+ featureFixture(key, setup) {
550
+ if (!this.featureUri) {
551
+ throw new Error("featureFixture is only available after the scenario Before hook");
552
+ }
553
+ return sharedFixtures.featureFixture(this.featureUri, key, setup);
554
+ }
319
555
  /** Resolve runner userdata from worldParameters first, then DBX_TEST_USERDATA_* env. */
320
556
  userdata(name) {
321
557
  const configured = this.parameters.userdata?.[name];
@@ -348,13 +584,21 @@ var DatabricksWorld = class extends cucumber.World {
348
584
  errors.push(error);
349
585
  }
350
586
  this.ctx = void 0;
587
+ this.scopedState?.clearScenario();
588
+ this.scopedState = void 0;
351
589
  if (errors.length > 0) throw new AggregateError(errors, "Scenario cleanup failed");
352
590
  }
591
+ state() {
592
+ if (!this.scopedState) {
593
+ throw new Error("BDD scoped state is only available after the scenario Before hook");
594
+ }
595
+ return this.scopedState;
596
+ }
353
597
  };
354
598
 
355
599
  // src/steps.ts
356
600
  cucumber.setWorldConstructor(DatabricksWorld);
357
- cucumber.setDefaultTimeout(6e4);
601
+ cucumber.setDefaultTimeout(databricksTestkit.resolveProfile() === "live" ? 20 * 6e4 : 6e4);
358
602
  registerParameterTypes();
359
603
  cucumber.BeforeAll(async () => {
360
604
  if (databricksTestkit.resolveProfile() !== "live") return;
@@ -370,10 +614,18 @@ cucumber.BeforeAll(async () => {
370
614
  });
371
615
  cucumber.Before(function({ pickle }) {
372
616
  const tags = pickle.tags.map((t) => t.name);
373
- const reason = liveOnlyReason(tags, this.profile);
617
+ this.initializeScenario(pickle.uri, pickle.name, tags);
618
+ const reason = scenarioSkipReason(tags, this.profile);
374
619
  if (reason) return "skipped";
375
620
  return void 0;
376
621
  });
622
+ cucumber.AfterAll(async () => {
623
+ try {
624
+ await sharedFixtures.dispose();
625
+ } finally {
626
+ sharedState.clear();
627
+ }
628
+ });
377
629
  cucumber.BeforeStep(function() {
378
630
  const captureMode = process.env.DBX_TEST_CAPTURE_OUTPUT?.toLowerCase();
379
631
  if (captureMode === "off") return;