@fabricorg/databricks-bdd 0.3.0 → 0.3.2

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 CHANGED
@@ -75,8 +75,9 @@ Then: `table {string} has {int} rows` · `table {string} contains rows matching:
75
75
  `row count of {string} is within {int}% of {int}` · `the job run reaches {runState}` ·
76
76
  `the pipeline update completes within {duration}` · schema-contract assertions.
77
77
 
78
- The World supplies Behave-style scenario cleanup, typed userdata, run-level
79
- live preflight, redacted failure attachments, per-step process-output capture,
78
+ The World supplies Behave-style scenario cleanup, retry-safe run/feature
79
+ fixtures with reverse-order `AfterAll` cleanup, typed userdata, run-level live
80
+ preflight, redacted failure attachments, per-step process-output capture,
80
81
  custom parameter types, Scenario Outlines, serial live execution, and parallel
81
82
  local execution. Evidence JSON schema v2 includes structured SQL, statement ID,
82
83
  and captured-output failure records. Capture defaults to 32 KiB per step; use
@@ -84,6 +85,18 @@ and captured-output failure records. Capture defaults to 32 KiB per step; use
84
85
  disable it. Generate the complete catalog, including unused definitions, with
85
86
  `pnpm steps:catalog`.
86
87
 
88
+ ```ts
89
+ const seeded = await this.featureFixture('reference-data', async () => ({
90
+ value: await seedReferenceData(),
91
+ cleanup: deleteReferenceData,
92
+ }))
93
+ ```
94
+
95
+ Feature fixtures are keyed by feature URI and cleaned at run end because
96
+ cucumber-js has no safe `after_feature` event when filters, retries, and
97
+ parallel workers are involved. Use `this.addCleanup()` for scenario scope and
98
+ `sharedFixtures.runFixture()` for run scope.
99
+
87
100
  Keep feature text engine-neutral. The shipped framework is TypeScript end to
88
101
  end; Python adapters are optional ecosystem bridges and are not required or
89
102
  currently included.
@@ -1,9 +1,9 @@
1
+ import { sharedFixtures } from './chunk-SZL3KWW7.js';
1
2
  import { World } from '@cucumber/cucumber';
2
3
  import { resolveProfile, createExecutionContext } from '@fabricorg/databricks-testkit';
3
4
  export { waitForPipelineUpdate } from '@fabricorg/databricks-testkit';
4
5
  import { waitForDatabricksRun } from '@fabric-harness/databricks';
5
6
 
6
- // src/world.ts
7
7
  var DatabricksWorld = class extends World {
8
8
  profile;
9
9
  /** Rows from the last `When I run the SQL` / `When I query table` step. */
@@ -18,6 +18,7 @@ var DatabricksWorld = class extends World {
18
18
  catalogOverride;
19
19
  lastSql;
20
20
  lastStatementId;
21
+ featureUri;
21
22
  stepOutputCapture;
22
23
  lakebasePool;
23
24
  ctx;
@@ -56,6 +57,20 @@ var DatabricksWorld = class extends World {
56
57
  addCleanup(cleanup) {
57
58
  this.cleanups.push(cleanup);
58
59
  }
60
+ /** Setup a resource once for this Cucumber run and clean it up in AfterAll. */
61
+ runFixture(key, setup) {
62
+ return sharedFixtures.runFixture(key, setup);
63
+ }
64
+ /**
65
+ * Setup a resource once per feature file. It is isolated by feature URI and
66
+ * cleaned in AfterAll because cucumber-js has no reliable after-feature hook.
67
+ */
68
+ featureFixture(key, setup) {
69
+ if (!this.featureUri) {
70
+ throw new Error("featureFixture is only available after the scenario Before hook");
71
+ }
72
+ return sharedFixtures.featureFixture(this.featureUri, key, setup);
73
+ }
59
74
  /** Resolve runner userdata from worldParameters first, then DBX_TEST_USERDATA_* env. */
60
75
  userdata(name) {
61
76
  const configured = this.parameters.userdata?.[name];
@@ -198,5 +213,5 @@ async function startPipelineUpdate(client, pipelineId, options = {}) {
198
213
  }
199
214
 
200
215
  export { DatabricksWorld, LIVE_ONLY_TAGS, coerceMatchRows, inferFixture, liveOnlyReason, resolveJobId, resolvePipelineId, runJobToTerminal, startPipelineUpdate };
201
- //# sourceMappingURL=chunk-FNTI2VUP.js.map
202
- //# sourceMappingURL=chunk-FNTI2VUP.js.map
216
+ //# sourceMappingURL=chunk-355WDWP3.js.map
217
+ //# sourceMappingURL=chunk-355WDWP3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/world.ts","../src/profile.ts","../src/infer.ts","../src/jobs.ts","../src/pipelines.ts"],"names":[],"mappings":"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","file":"chunk-355WDWP3.js","sourcesContent":["import { type IWorldOptions, World } from '@cucumber/cucumber';\nimport {\n type ExecutionContext,\n type TestProfile,\n createExecutionContext,\n resolveProfile,\n} from '@fabricorg/databricks-testkit';\nimport { type FixtureSetup, sharedFixtures } from './fixtures.js';\nimport type { StepOutputCapture } from './output-capture.js';\n\nexport interface DatabricksWorldParameters {\n /** Default schema for the execution context; overridable per scenario via the catalog/schema step. */\n schema?: string;\n /** Arbitrary typed runner configuration, comparable to behave `-D` userdata. */\n userdata?: Record<string, string | number | boolean>;\n}\n\ntype Cleanup = () => void | Promise<void>;\n\n/**\n * Cucumber World holding one ExecutionContext per scenario. The context is\n * created lazily on first use so `Given catalog ... and schema ...` can run\n * first; it is closed by the After hook registered in ./steps.ts.\n */\nexport class DatabricksWorld extends World<DatabricksWorldParameters> {\n readonly profile: TestProfile;\n /** Rows from the last `When I run the SQL` / `When I query table` step. */\n lastRows: Record<string, unknown>[] | null = null;\n /** Error captured from the last statement, for `Then the statement fails ...`. */\n lastError: Error | null = null;\n /** Terminal run record from the last `When I run job ...` step. */\n lastRun: Record<string, unknown> | null = null;\n /** Pipeline update handle from `When I start a full refresh of pipeline ...`. */\n lastPipelineUpdate: { pipelineId: string; updateId: string } | null = null;\n schemaOverride: string | undefined;\n catalogOverride: string | undefined;\n lastSql: string | undefined;\n lastStatementId: string | undefined;\n featureUri: string | undefined;\n stepOutputCapture: StepOutputCapture | undefined;\n lakebasePool:\n | { query<T extends Record<string, unknown>>(sql: string): Promise<{ rows: T[] }> }\n | undefined;\n private ctx: ExecutionContext | undefined;\n private activeCtx: ExecutionContext | undefined;\n private readonly cleanups: Cleanup[] = [];\n\n constructor(options: IWorldOptions<DatabricksWorldParameters>) {\n super(options);\n this.profile = resolveProfile();\n }\n\n async context(): Promise<ExecutionContext> {\n if (this.activeCtx) return this.activeCtx;\n if (!this.ctx) {\n const env = this.catalogOverride\n ? { ...process.env, DBX_TEST_CATALOG: this.catalogOverride }\n : process.env;\n this.ctx = await createExecutionContext({\n profile: this.profile,\n env,\n schema: this.schemaOverride ?? this.parameters.schema,\n onStatement: ({ sql, statementId }) => {\n this.lastSql = sql;\n this.lastStatementId = statementId;\n },\n });\n }\n return this.ctx;\n }\n\n /** Route subsequent steps through a scenario-owned secondary identity. */\n useExecutionContext(context: ExecutionContext): void {\n if (this.activeCtx) throw new Error('A secondary execution context is already active');\n this.activeCtx = context;\n this.addCleanup(async () => {\n await context.close();\n this.activeCtx = undefined;\n });\n }\n\n /** Register scenario-scoped cleanup. Cleanups run in reverse order. */\n addCleanup(cleanup: Cleanup): void {\n this.cleanups.push(cleanup);\n }\n\n /** Setup a resource once for this Cucumber run and clean it up in AfterAll. */\n runFixture<T>(key: string, setup: FixtureSetup<T>): Promise<T> {\n return sharedFixtures.runFixture(key, setup);\n }\n\n /**\n * Setup a resource once per feature file. It is isolated by feature URI and\n * cleaned in AfterAll because cucumber-js has no reliable after-feature hook.\n */\n featureFixture<T>(key: string, setup: FixtureSetup<T>): Promise<T> {\n if (!this.featureUri) {\n throw new Error('featureFixture is only available after the scenario Before hook');\n }\n return sharedFixtures.featureFixture(this.featureUri, key, setup);\n }\n\n /** Resolve runner userdata from worldParameters first, then DBX_TEST_USERDATA_* env. */\n userdata(name: string): string | number | boolean | undefined {\n const configured = this.parameters.userdata?.[name];\n if (configured !== undefined) return configured;\n return process.env[`DBX_TEST_USERDATA_${name.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}`];\n }\n\n scopeTo(catalog: string, schema: string): void {\n if (this.ctx) {\n throw new Error(\n 'Given catalog/schema must run before any step that touches the warehouse in the scenario',\n );\n }\n // Feature files stay portable and readable with illustrative names. A live\n // runner may redirect every scenario into its provisioned scratch scope.\n this.catalogOverride =\n this.profile === 'live' ? (process.env.DBX_TEST_CATALOG ?? catalog) : catalog;\n this.schemaOverride =\n this.profile === 'live' ? (process.env.DBX_TEST_SCHEMA ?? schema) : schema;\n }\n\n async dispose(): Promise<void> {\n this.stepOutputCapture?.stop();\n this.stepOutputCapture = undefined;\n const errors: unknown[] = [];\n for (const cleanup of this.cleanups.splice(0).reverse()) {\n try {\n await cleanup();\n } catch (error) {\n errors.push(error);\n }\n }\n try {\n await this.ctx?.close();\n } catch (error) {\n errors.push(error);\n }\n this.ctx = undefined;\n if (errors.length > 0) throw new AggregateError(errors, 'Scenario cleanup failed');\n }\n}\n","import type { TestProfile } from '@fabricorg/databricks-testkit';\n\n/**\n * Tags that require a live workspace (or a long budget). Scenarios carrying\n * any of them are skipped under the `local` profile — see ADR-0006.\n */\nexport const LIVE_ONLY_TAGS = [\n '@live',\n '@jobs',\n '@dlt',\n '@pipeline',\n '@autoloader',\n '@lakebase',\n '@slow',\n] as const;\n\n/** Returns a skip reason when the scenario cannot run under the given profile, else null. */\nexport function liveOnlyReason(tags: readonly string[], profile: TestProfile): string | null {\n if (profile === 'live') return null;\n const blocking = tags.filter((t) => (LIVE_ONLY_TAGS as readonly string[]).includes(t));\n if (blocking.length === 0) return null;\n return `requires a live workspace (${blocking.join(', ')}); running under DBX_TEST_PROFILE=local`;\n}\n","import type { TableFixture } from '@fabricorg/databricks-testkit';\n\nconst INT_RE = /^-?\\d+$/;\nconst FLOAT_RE = /^-?\\d+\\.\\d+$/;\nconst TIMESTAMP_RE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2})?/;\nconst BOOL_RE = /^(true|false)$/i;\n\n/**\n * Turn a Gherkin data table (header row + string cells) into a TableFixture,\n * inferring portable column types from the values: BIGINT, DOUBLE,\n * TIMESTAMP, BOOLEAN, else STRING. Empty cells become NULL.\n */\nexport function inferFixture(table: string, raw: readonly (readonly string[])[]): TableFixture {\n if (raw.length < 2) {\n throw new Error(`Data table for ${table} needs a header row and at least one data row`);\n }\n const [header, ...body] = raw as [readonly string[], ...(readonly string[])[]];\n const types = header.map((_, col) => inferColumnType(body.map((row) => row[col] ?? '')));\n const schema = header.map((name, i) => `\"${name}\" ${types[i]}`).join(', ');\n const rows = body.map((row) => row.map((cell, i) => coerceCell(cell, types[i] ?? 'STRING')));\n return { table, schema, rows };\n}\n\n/** Coerce data-table string cells to typed values for row matching (no header row). */\nexport function coerceMatchRows(raw: readonly (readonly string[])[]): Record<string, unknown>[] {\n if (raw.length < 2) {\n throw new Error('Match table needs a header row and at least one data row');\n }\n const [header, ...body] = raw as [readonly string[], ...(readonly string[])[]];\n return body.map((row) => {\n const out: Record<string, unknown> = {};\n header.forEach((name, i) => {\n out[name] = coerceCell(row[i] ?? '', inferColumnType([row[i] ?? '']));\n });\n return out;\n });\n}\n\nfunction inferColumnType(cells: readonly string[]): string {\n const present = cells.filter((c) => c !== '');\n if (present.length === 0) return 'STRING';\n if (present.every((c) => INT_RE.test(c))) return 'BIGINT';\n if (present.every((c) => FLOAT_RE.test(c) || INT_RE.test(c))) return 'DOUBLE';\n if (present.every((c) => TIMESTAMP_RE.test(c))) return 'TIMESTAMP';\n if (present.every((c) => BOOL_RE.test(c))) return 'BOOLEAN';\n return 'STRING';\n}\n\nfunction coerceCell(cell: string, type: string): unknown {\n if (cell === '') return null;\n if (type === 'BIGINT' || type === 'DOUBLE') return Number(cell);\n if (type === 'BOOLEAN') return /^true$/i.test(cell);\n return cell;\n}\n","import { type DatabricksRestClient, waitForDatabricksRun } from '@fabric-harness/databricks';\nimport type { WorkspaceClientLike } from '@fabricorg/databricks-testkit';\n\nexport interface RunJobOptions {\n pollIntervalMs?: number;\n timeoutMs?: number;\n}\n\nexport interface JobRunResult {\n runId: number;\n lifeCycleState: string;\n resultState: string;\n run: Record<string, unknown>;\n}\n\n/** Resolve a job by numeric id or by exact name via /api/2.1/jobs/list. */\nexport async function resolveJobId(client: WorkspaceClientLike, nameOrId: string): Promise<number> {\n if (/^\\d+$/.test(nameOrId)) return Number(nameOrId);\n const response = await client.get<{ jobs?: { job_id: number; settings?: { name?: string } }[] }>(\n '/api/2.1/jobs/list',\n { name: nameOrId },\n );\n const match = (response.jobs ?? []).find((j) => j.settings?.name === nameOrId);\n if (!match) throw new Error(`No Databricks job named '${nameOrId}' found`);\n return match.job_id;\n}\n\n/** run-now a job and poll its run to a terminal state. */\nexport async function runJobToTerminal(\n client: WorkspaceClientLike,\n nameOrId: string,\n options: RunJobOptions = {},\n): Promise<JobRunResult> {\n const jobId = await resolveJobId(client, nameOrId);\n const submitted = await client.post<{ run_id: number }>('/api/2.1/jobs/run-now', {\n job_id: jobId,\n });\n const run = await waitForDatabricksRun(client as DatabricksRestClient, submitted.run_id, {\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n });\n const state = (run as { state?: { life_cycle_state?: string; result_state?: string } }).state;\n return {\n runId: submitted.run_id,\n lifeCycleState: state?.life_cycle_state ?? 'UNKNOWN',\n resultState: state?.result_state ?? 'UNKNOWN',\n run,\n };\n}\n","import {\n type PollOptions,\n type WorkspaceClientLike,\n waitForPipelineUpdate,\n} from '@fabricorg/databricks-testkit';\n\nexport type PipelineWaitOptions = PollOptions;\n\n/** Resolve a DLT/Lakeflow pipeline by id or exact name via /api/2.0/pipelines. */\nexport async function resolvePipelineId(\n client: WorkspaceClientLike,\n nameOrId: string,\n): Promise<string> {\n if (/^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(nameOrId)) return nameOrId;\n const response = await client.get<{ statuses?: { pipeline_id: string; name?: string }[] }>(\n '/api/2.0/pipelines',\n { filter: `name LIKE '${nameOrId}'` },\n );\n const match = (response.statuses ?? []).find((p) => p.name === nameOrId);\n if (!match) throw new Error(`No Databricks pipeline named '${nameOrId}' found`);\n return match.pipeline_id;\n}\n\n/** Start a pipeline update; returns the update id to poll. */\nexport async function startPipelineUpdate(\n client: WorkspaceClientLike,\n pipelineId: string,\n options: { fullRefresh?: boolean } = {},\n): Promise<string> {\n const response = await client.post<{ update_id: string }>(\n `/api/2.0/pipelines/${encodeURIComponent(pipelineId)}/updates`,\n { full_refresh: options.fullRefresh ?? false },\n );\n return response.update_id;\n}\n\n/** Poll an update to a terminal state; throws on timeout or non-COMPLETED terminal states. */\nexport { waitForPipelineUpdate };\n"]}
@@ -0,0 +1,62 @@
1
+ // src/fixtures.ts
2
+ function isFixtureResult(value) {
3
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "value") && typeof value.cleanup === "function";
4
+ }
5
+ var SharedFixtureRegistry = class {
6
+ runEntries = /* @__PURE__ */ new Map();
7
+ featureEntries = /* @__PURE__ */ new Map();
8
+ cleanupOrder = [];
9
+ runFixture(key, setup) {
10
+ return this.resolve(this.runEntries, key, setup);
11
+ }
12
+ featureFixture(featureUri, key, setup) {
13
+ if (!featureUri) throw new Error("featureFixture requires a feature URI");
14
+ let feature = this.featureEntries.get(featureUri);
15
+ if (!feature) {
16
+ feature = /* @__PURE__ */ new Map();
17
+ this.featureEntries.set(featureUri, feature);
18
+ }
19
+ return this.resolve(feature, key, setup);
20
+ }
21
+ resolve(entries, key, setup) {
22
+ if (!key.trim()) throw new Error("Fixture key must not be empty");
23
+ const existing = entries.get(key);
24
+ if (existing) return existing.value;
25
+ const entry = {
26
+ value: Promise.resolve().then(setup).then((result) => {
27
+ if (isFixtureResult(result)) {
28
+ entry.cleanup = result.cleanup;
29
+ return result.value;
30
+ }
31
+ return result;
32
+ }).catch((error) => {
33
+ entries.delete(key);
34
+ const index = this.cleanupOrder.indexOf(entry);
35
+ if (index >= 0) this.cleanupOrder.splice(index, 1);
36
+ throw error;
37
+ })
38
+ };
39
+ entries.set(key, entry);
40
+ this.cleanupOrder.push(entry);
41
+ return entry.value;
42
+ }
43
+ async dispose() {
44
+ const errors = [];
45
+ for (const entry of this.cleanupOrder.splice(0).reverse()) {
46
+ try {
47
+ await entry.value.catch(() => void 0);
48
+ await entry.cleanup?.();
49
+ } catch (error) {
50
+ errors.push(error);
51
+ }
52
+ }
53
+ this.runEntries.clear();
54
+ this.featureEntries.clear();
55
+ if (errors.length > 0) throw new AggregateError(errors, "Shared fixture cleanup failed");
56
+ }
57
+ };
58
+ var sharedFixtures = new SharedFixtureRegistry();
59
+
60
+ export { SharedFixtureRegistry, sharedFixtures };
61
+ //# sourceMappingURL=chunk-SZL3KWW7.js.map
62
+ //# sourceMappingURL=chunk-SZL3KWW7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fixtures.ts"],"names":[],"mappings":"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","file":"chunk-SZL3KWW7.js","sourcesContent":["/**\n * Behave-style shared fixtures for expensive Databricks resources.\n *\n * Cucumber Worlds are scenario-scoped. These registries provide the two\n * broader scopes users commonly need when migrating Behave suites:\n *\n * - runFixture: setup once for the entire Cucumber run;\n * - featureFixture: setup once per feature URI.\n *\n * Both scopes are drained by the package AfterAll hook. Deferring feature\n * cleanup until AfterAll is intentional: cucumber-js may interleave scenarios\n * and does not expose an after-feature hook. The resource is still isolated by\n * feature URI and is never reused by another feature.\n */\n\nexport type FixtureCleanup = () => void | Promise<void>;\nexport type FixtureSetup<T> = () =>\n | T\n | Promise<T>\n | { value: T; cleanup: FixtureCleanup }\n | Promise<{ value: T; cleanup: FixtureCleanup }>;\n\ninterface FixtureEntry<T = unknown> {\n value: Promise<T>;\n cleanup?: FixtureCleanup;\n}\n\nfunction isFixtureResult<T>(value: T | { value: T; cleanup: FixtureCleanup }): value is {\n value: T;\n cleanup: FixtureCleanup;\n} {\n return (\n typeof value === 'object' &&\n value !== null &&\n Object.prototype.hasOwnProperty.call(value, 'value') &&\n typeof (value as { cleanup?: unknown }).cleanup === 'function'\n );\n}\n\nexport class SharedFixtureRegistry {\n private readonly runEntries = new Map<string, FixtureEntry>();\n private readonly featureEntries = new Map<string, Map<string, FixtureEntry>>();\n private readonly cleanupOrder: FixtureEntry[] = [];\n\n runFixture<T>(key: string, setup: FixtureSetup<T>): Promise<T> {\n return this.resolve(this.runEntries, key, setup);\n }\n\n featureFixture<T>(featureUri: string, key: string, setup: FixtureSetup<T>): Promise<T> {\n if (!featureUri) throw new Error('featureFixture requires a feature URI');\n let feature = this.featureEntries.get(featureUri);\n if (!feature) {\n feature = new Map();\n this.featureEntries.set(featureUri, feature);\n }\n return this.resolve(feature, key, setup);\n }\n\n private resolve<T>(entries: Map<string, FixtureEntry>, key: string, setup: FixtureSetup<T>) {\n if (!key.trim()) throw new Error('Fixture key must not be empty');\n const existing = entries.get(key);\n if (existing) return existing.value as Promise<T>;\n\n const entry: FixtureEntry<T> = {\n value: Promise.resolve()\n .then(setup)\n .then((result) => {\n if (isFixtureResult(result)) {\n entry.cleanup = result.cleanup;\n return result.value;\n }\n return result;\n })\n .catch((error) => {\n entries.delete(key);\n const index = this.cleanupOrder.indexOf(entry);\n if (index >= 0) this.cleanupOrder.splice(index, 1);\n throw error;\n }),\n };\n entries.set(key, entry);\n this.cleanupOrder.push(entry);\n return entry.value;\n }\n\n async dispose(): Promise<void> {\n const errors: unknown[] = [];\n for (const entry of this.cleanupOrder.splice(0).reverse()) {\n try {\n // Ensure setup has settled before attempting its cleanup.\n await entry.value.catch(() => undefined);\n await entry.cleanup?.();\n } catch (error) {\n errors.push(error);\n }\n }\n this.runEntries.clear();\n this.featureEntries.clear();\n if (errors.length > 0) throw new AggregateError(errors, 'Shared fixture cleanup failed');\n }\n}\n\nexport const sharedFixtures = new SharedFixtureRegistry();\n"]}
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ // src/fixtures.ts
4
+ function isFixtureResult(value) {
5
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "value") && typeof value.cleanup === "function";
6
+ }
7
+ var SharedFixtureRegistry = class {
8
+ runEntries = /* @__PURE__ */ new Map();
9
+ featureEntries = /* @__PURE__ */ new Map();
10
+ cleanupOrder = [];
11
+ runFixture(key, setup) {
12
+ return this.resolve(this.runEntries, key, setup);
13
+ }
14
+ featureFixture(featureUri, key, setup) {
15
+ if (!featureUri) throw new Error("featureFixture requires a feature URI");
16
+ let feature = this.featureEntries.get(featureUri);
17
+ if (!feature) {
18
+ feature = /* @__PURE__ */ new Map();
19
+ this.featureEntries.set(featureUri, feature);
20
+ }
21
+ return this.resolve(feature, key, setup);
22
+ }
23
+ resolve(entries, key, setup) {
24
+ if (!key.trim()) throw new Error("Fixture key must not be empty");
25
+ const existing = entries.get(key);
26
+ if (existing) return existing.value;
27
+ const entry = {
28
+ value: Promise.resolve().then(setup).then((result) => {
29
+ if (isFixtureResult(result)) {
30
+ entry.cleanup = result.cleanup;
31
+ return result.value;
32
+ }
33
+ return result;
34
+ }).catch((error) => {
35
+ entries.delete(key);
36
+ const index = this.cleanupOrder.indexOf(entry);
37
+ if (index >= 0) this.cleanupOrder.splice(index, 1);
38
+ throw error;
39
+ })
40
+ };
41
+ entries.set(key, entry);
42
+ this.cleanupOrder.push(entry);
43
+ return entry.value;
44
+ }
45
+ async dispose() {
46
+ const errors = [];
47
+ for (const entry of this.cleanupOrder.splice(0).reverse()) {
48
+ try {
49
+ await entry.value.catch(() => void 0);
50
+ await entry.cleanup?.();
51
+ } catch (error) {
52
+ errors.push(error);
53
+ }
54
+ }
55
+ this.runEntries.clear();
56
+ this.featureEntries.clear();
57
+ if (errors.length > 0) throw new AggregateError(errors, "Shared fixture cleanup failed");
58
+ }
59
+ };
60
+ var sharedFixtures = new SharedFixtureRegistry();
61
+
62
+ exports.SharedFixtureRegistry = SharedFixtureRegistry;
63
+ exports.sharedFixtures = sharedFixtures;
64
+ //# sourceMappingURL=fixtures.cjs.map
65
+ //# sourceMappingURL=fixtures.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fixtures.ts"],"names":[],"mappings":"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","file":"fixtures.cjs","sourcesContent":["/**\n * Behave-style shared fixtures for expensive Databricks resources.\n *\n * Cucumber Worlds are scenario-scoped. These registries provide the two\n * broader scopes users commonly need when migrating Behave suites:\n *\n * - runFixture: setup once for the entire Cucumber run;\n * - featureFixture: setup once per feature URI.\n *\n * Both scopes are drained by the package AfterAll hook. Deferring feature\n * cleanup until AfterAll is intentional: cucumber-js may interleave scenarios\n * and does not expose an after-feature hook. The resource is still isolated by\n * feature URI and is never reused by another feature.\n */\n\nexport type FixtureCleanup = () => void | Promise<void>;\nexport type FixtureSetup<T> = () =>\n | T\n | Promise<T>\n | { value: T; cleanup: FixtureCleanup }\n | Promise<{ value: T; cleanup: FixtureCleanup }>;\n\ninterface FixtureEntry<T = unknown> {\n value: Promise<T>;\n cleanup?: FixtureCleanup;\n}\n\nfunction isFixtureResult<T>(value: T | { value: T; cleanup: FixtureCleanup }): value is {\n value: T;\n cleanup: FixtureCleanup;\n} {\n return (\n typeof value === 'object' &&\n value !== null &&\n Object.prototype.hasOwnProperty.call(value, 'value') &&\n typeof (value as { cleanup?: unknown }).cleanup === 'function'\n );\n}\n\nexport class SharedFixtureRegistry {\n private readonly runEntries = new Map<string, FixtureEntry>();\n private readonly featureEntries = new Map<string, Map<string, FixtureEntry>>();\n private readonly cleanupOrder: FixtureEntry[] = [];\n\n runFixture<T>(key: string, setup: FixtureSetup<T>): Promise<T> {\n return this.resolve(this.runEntries, key, setup);\n }\n\n featureFixture<T>(featureUri: string, key: string, setup: FixtureSetup<T>): Promise<T> {\n if (!featureUri) throw new Error('featureFixture requires a feature URI');\n let feature = this.featureEntries.get(featureUri);\n if (!feature) {\n feature = new Map();\n this.featureEntries.set(featureUri, feature);\n }\n return this.resolve(feature, key, setup);\n }\n\n private resolve<T>(entries: Map<string, FixtureEntry>, key: string, setup: FixtureSetup<T>) {\n if (!key.trim()) throw new Error('Fixture key must not be empty');\n const existing = entries.get(key);\n if (existing) return existing.value as Promise<T>;\n\n const entry: FixtureEntry<T> = {\n value: Promise.resolve()\n .then(setup)\n .then((result) => {\n if (isFixtureResult(result)) {\n entry.cleanup = result.cleanup;\n return result.value;\n }\n return result;\n })\n .catch((error) => {\n entries.delete(key);\n const index = this.cleanupOrder.indexOf(entry);\n if (index >= 0) this.cleanupOrder.splice(index, 1);\n throw error;\n }),\n };\n entries.set(key, entry);\n this.cleanupOrder.push(entry);\n return entry.value;\n }\n\n async dispose(): Promise<void> {\n const errors: unknown[] = [];\n for (const entry of this.cleanupOrder.splice(0).reverse()) {\n try {\n // Ensure setup has settled before attempting its cleanup.\n await entry.value.catch(() => undefined);\n await entry.cleanup?.();\n } catch (error) {\n errors.push(error);\n }\n }\n this.runEntries.clear();\n this.featureEntries.clear();\n if (errors.length > 0) throw new AggregateError(errors, 'Shared fixture cleanup failed');\n }\n}\n\nexport const sharedFixtures = new SharedFixtureRegistry();\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Behave-style shared fixtures for expensive Databricks resources.
3
+ *
4
+ * Cucumber Worlds are scenario-scoped. These registries provide the two
5
+ * broader scopes users commonly need when migrating Behave suites:
6
+ *
7
+ * - runFixture: setup once for the entire Cucumber run;
8
+ * - featureFixture: setup once per feature URI.
9
+ *
10
+ * Both scopes are drained by the package AfterAll hook. Deferring feature
11
+ * cleanup until AfterAll is intentional: cucumber-js may interleave scenarios
12
+ * and does not expose an after-feature hook. The resource is still isolated by
13
+ * feature URI and is never reused by another feature.
14
+ */
15
+ type FixtureCleanup = () => void | Promise<void>;
16
+ type FixtureSetup<T> = () => T | Promise<T> | {
17
+ value: T;
18
+ cleanup: FixtureCleanup;
19
+ } | Promise<{
20
+ value: T;
21
+ cleanup: FixtureCleanup;
22
+ }>;
23
+ declare class SharedFixtureRegistry {
24
+ private readonly runEntries;
25
+ private readonly featureEntries;
26
+ private readonly cleanupOrder;
27
+ runFixture<T>(key: string, setup: FixtureSetup<T>): Promise<T>;
28
+ featureFixture<T>(featureUri: string, key: string, setup: FixtureSetup<T>): Promise<T>;
29
+ private resolve;
30
+ dispose(): Promise<void>;
31
+ }
32
+ declare const sharedFixtures: SharedFixtureRegistry;
33
+
34
+ export { type FixtureCleanup, type FixtureSetup, SharedFixtureRegistry, sharedFixtures };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Behave-style shared fixtures for expensive Databricks resources.
3
+ *
4
+ * Cucumber Worlds are scenario-scoped. These registries provide the two
5
+ * broader scopes users commonly need when migrating Behave suites:
6
+ *
7
+ * - runFixture: setup once for the entire Cucumber run;
8
+ * - featureFixture: setup once per feature URI.
9
+ *
10
+ * Both scopes are drained by the package AfterAll hook. Deferring feature
11
+ * cleanup until AfterAll is intentional: cucumber-js may interleave scenarios
12
+ * and does not expose an after-feature hook. The resource is still isolated by
13
+ * feature URI and is never reused by another feature.
14
+ */
15
+ type FixtureCleanup = () => void | Promise<void>;
16
+ type FixtureSetup<T> = () => T | Promise<T> | {
17
+ value: T;
18
+ cleanup: FixtureCleanup;
19
+ } | Promise<{
20
+ value: T;
21
+ cleanup: FixtureCleanup;
22
+ }>;
23
+ declare class SharedFixtureRegistry {
24
+ private readonly runEntries;
25
+ private readonly featureEntries;
26
+ private readonly cleanupOrder;
27
+ runFixture<T>(key: string, setup: FixtureSetup<T>): Promise<T>;
28
+ featureFixture<T>(featureUri: string, key: string, setup: FixtureSetup<T>): Promise<T>;
29
+ private resolve;
30
+ dispose(): Promise<void>;
31
+ }
32
+ declare const sharedFixtures: SharedFixtureRegistry;
33
+
34
+ export { type FixtureCleanup, type FixtureSetup, SharedFixtureRegistry, sharedFixtures };
@@ -0,0 +1,3 @@
1
+ export { SharedFixtureRegistry, sharedFixtures } from './chunk-SZL3KWW7.js';
2
+ //# sourceMappingURL=fixtures.js.map
3
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"fixtures.js"}
package/dist/index.cjs CHANGED
@@ -9,6 +9,67 @@ var path = require('path');
9
9
  var util = require('util');
10
10
 
11
11
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
12
+ // src/world.ts
13
+
14
+ // src/fixtures.ts
15
+ function isFixtureResult(value) {
16
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "value") && typeof value.cleanup === "function";
17
+ }
18
+ var SharedFixtureRegistry = class {
19
+ runEntries = /* @__PURE__ */ new Map();
20
+ featureEntries = /* @__PURE__ */ new Map();
21
+ cleanupOrder = [];
22
+ runFixture(key, setup) {
23
+ return this.resolve(this.runEntries, key, setup);
24
+ }
25
+ featureFixture(featureUri, key, setup) {
26
+ if (!featureUri) throw new Error("featureFixture requires a feature URI");
27
+ let feature = this.featureEntries.get(featureUri);
28
+ if (!feature) {
29
+ feature = /* @__PURE__ */ new Map();
30
+ this.featureEntries.set(featureUri, feature);
31
+ }
32
+ return this.resolve(feature, key, setup);
33
+ }
34
+ resolve(entries, key, setup) {
35
+ if (!key.trim()) throw new Error("Fixture key must not be empty");
36
+ const existing = entries.get(key);
37
+ if (existing) return existing.value;
38
+ const entry = {
39
+ value: Promise.resolve().then(setup).then((result) => {
40
+ if (isFixtureResult(result)) {
41
+ entry.cleanup = result.cleanup;
42
+ return result.value;
43
+ }
44
+ return result;
45
+ }).catch((error) => {
46
+ entries.delete(key);
47
+ const index = this.cleanupOrder.indexOf(entry);
48
+ if (index >= 0) this.cleanupOrder.splice(index, 1);
49
+ throw error;
50
+ })
51
+ };
52
+ entries.set(key, entry);
53
+ this.cleanupOrder.push(entry);
54
+ return entry.value;
55
+ }
56
+ async dispose() {
57
+ const errors = [];
58
+ for (const entry of this.cleanupOrder.splice(0).reverse()) {
59
+ try {
60
+ await entry.value.catch(() => void 0);
61
+ await entry.cleanup?.();
62
+ } catch (error) {
63
+ errors.push(error);
64
+ }
65
+ }
66
+ this.runEntries.clear();
67
+ this.featureEntries.clear();
68
+ if (errors.length > 0) throw new AggregateError(errors, "Shared fixture cleanup failed");
69
+ }
70
+ };
71
+ var sharedFixtures = new SharedFixtureRegistry();
72
+
12
73
  // src/world.ts
13
74
  var DatabricksWorld = class extends cucumber.World {
14
75
  profile;
@@ -24,6 +85,7 @@ var DatabricksWorld = class extends cucumber.World {
24
85
  catalogOverride;
25
86
  lastSql;
26
87
  lastStatementId;
88
+ featureUri;
27
89
  stepOutputCapture;
28
90
  lakebasePool;
29
91
  ctx;
@@ -62,6 +124,20 @@ var DatabricksWorld = class extends cucumber.World {
62
124
  addCleanup(cleanup) {
63
125
  this.cleanups.push(cleanup);
64
126
  }
127
+ /** Setup a resource once for this Cucumber run and clean it up in AfterAll. */
128
+ runFixture(key, setup) {
129
+ return sharedFixtures.runFixture(key, setup);
130
+ }
131
+ /**
132
+ * Setup a resource once per feature file. It is isolated by feature URI and
133
+ * cleaned in AfterAll because cucumber-js has no reliable after-feature hook.
134
+ */
135
+ featureFixture(key, setup) {
136
+ if (!this.featureUri) {
137
+ throw new Error("featureFixture is only available after the scenario Before hook");
138
+ }
139
+ return sharedFixtures.featureFixture(this.featureUri, key, setup);
140
+ }
65
141
  /** Resolve runner userdata from worldParameters first, then DBX_TEST_USERDATA_* env. */
66
142
  userdata(name) {
67
143
  const configured = this.parameters.userdata?.[name];
@@ -386,6 +462,7 @@ exports.DatabricksWorld = DatabricksWorld;
386
462
  exports.FAILURE_EVIDENCE_MEDIA_TYPE = FAILURE_EVIDENCE_MEDIA_TYPE;
387
463
  exports.LIVE_ONLY_TAGS = LIVE_ONLY_TAGS;
388
464
  exports.RUN_STATES = RUN_STATES;
465
+ exports.SharedFixtureRegistry = SharedFixtureRegistry;
389
466
  exports.coerceMatchRows = coerceMatchRows;
390
467
  exports.inferFixture = inferFixture;
391
468
  exports.liveOnlyReason = liveOnlyReason;
@@ -397,6 +474,7 @@ exports.resolveJobId = resolveJobId;
397
474
  exports.resolvePipelineId = resolvePipelineId;
398
475
  exports.runFeatures = runFeatures;
399
476
  exports.runJobToTerminal = runJobToTerminal;
477
+ exports.sharedFixtures = sharedFixtures;
400
478
  exports.startPipelineUpdate = startPipelineUpdate;
401
479
  exports.startStepOutputCapture = startStepOutputCapture;
402
480
  //# sourceMappingURL=index.cjs.map