@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.
- package/README.md +56 -5
- package/dist/actions-CuXXKUqe.d.ts +92 -0
- package/dist/actions-CxfxhBZh.d.cts +92 -0
- package/dist/actions.cjs +14 -0
- package/dist/actions.cjs.map +1 -0
- package/dist/actions.d.cts +6 -0
- package/dist/actions.d.ts +6 -0
- package/dist/actions.js +3 -0
- package/dist/actions.js.map +1 -0
- package/dist/cardinality.cjs +24 -0
- package/dist/cardinality.cjs.map +1 -0
- package/dist/cardinality.d.cts +19 -0
- package/dist/cardinality.d.ts +19 -0
- package/dist/cardinality.js +3 -0
- package/dist/cardinality.js.map +1 -0
- package/dist/{chunk-FNTI2VUP.js → chunk-3XDMBHFP.js} +117 -4
- package/dist/chunk-3XDMBHFP.js.map +1 -0
- package/dist/chunk-6JC65RH2.js +11 -0
- package/dist/chunk-6JC65RH2.js.map +1 -0
- package/dist/chunk-HTKRJLVX.js +70 -0
- package/dist/chunk-HTKRJLVX.js.map +1 -0
- package/dist/chunk-K5LJ7WSW.js +22 -0
- package/dist/chunk-K5LJ7WSW.js.map +1 -0
- package/dist/chunk-SZL3KWW7.js +62 -0
- package/dist/chunk-SZL3KWW7.js.map +1 -0
- package/dist/fixtures.cjs +65 -0
- package/dist/fixtures.cjs.map +1 -0
- package/dist/fixtures.d.cts +34 -0
- package/dist/fixtures.d.ts +34 -0
- package/dist/fixtures.js +3 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/index.cjs +277 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -55
- package/dist/index.d.ts +24 -55
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/scoped-state.cjs +74 -0
- package/dist/scoped-state.cjs.map +1 -0
- package/dist/scoped-state.d.cts +30 -0
- package/dist/scoped-state.d.ts +30 -0
- package/dist/scoped-state.js +3 -0
- package/dist/scoped-state.js.map +1 -0
- package/dist/steps.cjs +254 -2
- package/dist/steps.cjs.map +1 -1
- package/dist/steps.js +15 -4
- package/dist/steps.js.map +1 -1
- package/package.json +41 -1
- 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
|
-
|
|
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;
|