@fabricorg/databricks-bdd 0.1.0 → 0.3.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 +17 -5
- package/dist/{chunk-EW5VNRZP.js → chunk-FNTI2VUP.js} +5 -2
- package/dist/chunk-FNTI2VUP.js.map +1 -0
- package/dist/chunk-THB7O2G6.js +112 -0
- package/dist/chunk-THB7O2G6.js.map +1 -0
- package/dist/{chunk-LFEU3ZHD.js → chunk-UQAKGVJS.js} +32 -6
- package/dist/chunk-UQAKGVJS.js.map +1 -0
- package/dist/evidence-formatter.cjs +31 -3
- package/dist/evidence-formatter.cjs.map +1 -1
- package/dist/evidence-formatter.d.cts +56 -2
- package/dist/evidence-formatter.d.ts +56 -2
- package/dist/evidence-formatter.js +1 -1
- package/dist/index.cjs +131 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/output-capture.cjs +114 -0
- package/dist/output-capture.cjs.map +1 -0
- package/dist/output-capture.d.cts +27 -0
- package/dist/output-capture.d.ts +27 -0
- package/dist/output-capture.js +3 -0
- package/dist/output-capture.js.map +1 -0
- package/dist/steps.cjs +125 -2
- package/dist/steps.cjs.map +1 -1
- package/dist/steps.js +19 -5
- package/dist/steps.js.map +1 -1
- package/package.json +14 -14
- package/LICENSE +0 -21
- package/dist/chunk-EW5VNRZP.js.map +0 -1
- package/dist/chunk-LFEU3ZHD.js.map +0 -1
package/README.md
CHANGED
|
@@ -45,6 +45,13 @@ import '@fabricorg/databricks-bdd/steps'; // registers the world, hooks, and ste
|
|
|
45
45
|
Run with the tsx loader: `NODE_OPTIONS="--import tsx" cucumber-js`. Or embed
|
|
46
46
|
in vitest via `runFeatures({ cwd })` from the package root export.
|
|
47
47
|
|
|
48
|
+
Local developer profiles:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm test:features:pretty # colored, source-annotated output
|
|
52
|
+
pnpm test:features:wip # @wip only, fail fast, summary output
|
|
53
|
+
```
|
|
54
|
+
|
|
48
55
|
## Step library
|
|
49
56
|
|
|
50
57
|
Given: `a Databricks workspace` · `catalog {string} and schema {string}` ·
|
|
@@ -69,12 +76,17 @@ Then: `table {string} has {int} rows` · `table {string} contains rows matching:
|
|
|
69
76
|
`the pipeline update completes within {duration}` · schema-contract assertions.
|
|
70
77
|
|
|
71
78
|
The World supplies Behave-style scenario cleanup, typed userdata, run-level
|
|
72
|
-
live preflight, redacted failure attachments,
|
|
73
|
-
Outlines, serial live execution, and parallel
|
|
74
|
-
|
|
79
|
+
live preflight, redacted failure attachments, per-step process-output capture,
|
|
80
|
+
custom parameter types, Scenario Outlines, serial live execution, and parallel
|
|
81
|
+
local execution. Evidence JSON schema v2 includes structured SQL, statement ID,
|
|
82
|
+
and captured-output failure records. Capture defaults to 32 KiB per step; use
|
|
83
|
+
`DBX_TEST_CAPTURE_OUTPUT=passthrough` to show output while capturing or `off` to
|
|
84
|
+
disable it. Generate the complete catalog, including unused definitions, with
|
|
85
|
+
`pnpm steps:catalog`.
|
|
75
86
|
|
|
76
|
-
Keep feature text engine-neutral
|
|
77
|
-
|
|
87
|
+
Keep feature text engine-neutral. The shipped framework is TypeScript end to
|
|
88
|
+
end; Python adapters are optional ecosystem bridges and are not required or
|
|
89
|
+
currently included.
|
|
78
90
|
|
|
79
91
|
Product-domain example (aggregates, variants, SRM guardrail):
|
|
80
92
|
`apps/api/features/` in this repo. Docs:
|
|
@@ -18,6 +18,7 @@ var DatabricksWorld = class extends World {
|
|
|
18
18
|
catalogOverride;
|
|
19
19
|
lastSql;
|
|
20
20
|
lastStatementId;
|
|
21
|
+
stepOutputCapture;
|
|
21
22
|
lakebasePool;
|
|
22
23
|
ctx;
|
|
23
24
|
activeCtx;
|
|
@@ -71,6 +72,8 @@ var DatabricksWorld = class extends World {
|
|
|
71
72
|
this.schemaOverride = this.profile === "live" ? process.env.DBX_TEST_SCHEMA ?? schema : schema;
|
|
72
73
|
}
|
|
73
74
|
async dispose() {
|
|
75
|
+
this.stepOutputCapture?.stop();
|
|
76
|
+
this.stepOutputCapture = void 0;
|
|
74
77
|
const errors = [];
|
|
75
78
|
for (const cleanup of this.cleanups.splice(0).reverse()) {
|
|
76
79
|
try {
|
|
@@ -195,5 +198,5 @@ async function startPipelineUpdate(client, pipelineId, options = {}) {
|
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
export { DatabricksWorld, LIVE_ONLY_TAGS, coerceMatchRows, inferFixture, liveOnlyReason, resolveJobId, resolvePipelineId, runJobToTerminal, startPipelineUpdate };
|
|
198
|
-
//# sourceMappingURL=chunk-
|
|
199
|
-
//# sourceMappingURL=chunk-
|
|
201
|
+
//# sourceMappingURL=chunk-FNTI2VUP.js.map
|
|
202
|
+
//# sourceMappingURL=chunk-FNTI2VUP.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-FNTI2VUP.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 { 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 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 /** 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,112 @@
|
|
|
1
|
+
import { format } from 'util';
|
|
2
|
+
|
|
3
|
+
// src/output-capture.ts
|
|
4
|
+
var DEFAULT_MAX_BYTES = 32 * 1024;
|
|
5
|
+
function startStepOutputCapture(options = {}) {
|
|
6
|
+
const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
7
|
+
const stdout = process.stdout;
|
|
8
|
+
const stderr = process.stderr;
|
|
9
|
+
const originalStdoutWrite = stdout.write;
|
|
10
|
+
const originalStderrWrite = stderr.write;
|
|
11
|
+
const originalConsole = Object.fromEntries(
|
|
12
|
+
["debug", "error", "info", "log", "warn"].map((method) => [method, console[method]])
|
|
13
|
+
);
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let retainedBytes = 0;
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
let stopped = false;
|
|
18
|
+
const capture = (stream, chunk) => {
|
|
19
|
+
const text = chunkToString(chunk);
|
|
20
|
+
const bytes = Buffer.byteLength(text);
|
|
21
|
+
totalBytes += bytes;
|
|
22
|
+
const remaining = maxBytes - retainedBytes;
|
|
23
|
+
if (remaining <= 0 || bytes === 0) return;
|
|
24
|
+
const retained = bytes <= remaining ? text : truncateUtf8(text, remaining);
|
|
25
|
+
retainedBytes += Buffer.byteLength(retained);
|
|
26
|
+
if (retained) chunks.push({ stream, text: retained });
|
|
27
|
+
};
|
|
28
|
+
stdout.write = interceptedWrite(
|
|
29
|
+
stdout,
|
|
30
|
+
"stdout",
|
|
31
|
+
originalStdoutWrite,
|
|
32
|
+
capture,
|
|
33
|
+
options.passthrough
|
|
34
|
+
);
|
|
35
|
+
stderr.write = interceptedWrite(
|
|
36
|
+
stderr,
|
|
37
|
+
"stderr",
|
|
38
|
+
originalStderrWrite,
|
|
39
|
+
capture,
|
|
40
|
+
options.passthrough
|
|
41
|
+
);
|
|
42
|
+
if (!options.passthrough) {
|
|
43
|
+
for (const method of ["debug", "info", "log"]) {
|
|
44
|
+
console[method] = (...args) => capture("stdout", `${format(...args)}
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
for (const method of ["error", "warn"]) {
|
|
48
|
+
console[method] = (...args) => capture("stderr", `${format(...args)}
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
stop() {
|
|
54
|
+
if (!stopped) {
|
|
55
|
+
stdout.write = originalStdoutWrite;
|
|
56
|
+
stderr.write = originalStderrWrite;
|
|
57
|
+
for (const method of Object.keys(originalConsole)) {
|
|
58
|
+
console[method] = originalConsole[method];
|
|
59
|
+
}
|
|
60
|
+
stopped = true;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
text: renderChunks(chunks),
|
|
64
|
+
truncated: totalBytes > retainedBytes,
|
|
65
|
+
totalBytes
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function interceptedWrite(destination, stream, original, capture, passthrough = false) {
|
|
71
|
+
return function write(chunk, encodingOrCallback, callback) {
|
|
72
|
+
capture(stream, chunk);
|
|
73
|
+
if (passthrough) {
|
|
74
|
+
return original.call(
|
|
75
|
+
destination,
|
|
76
|
+
chunk,
|
|
77
|
+
encodingOrCallback,
|
|
78
|
+
callback
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
82
|
+
if (typeof done === "function") queueMicrotask(() => done());
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function chunkToString(chunk) {
|
|
87
|
+
if (typeof chunk === "string") return chunk;
|
|
88
|
+
if (Buffer.isBuffer(chunk)) return chunk.toString("utf8");
|
|
89
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString("utf8");
|
|
90
|
+
return String(chunk);
|
|
91
|
+
}
|
|
92
|
+
function truncateUtf8(value, maxBytes) {
|
|
93
|
+
return Buffer.from(value).subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
|
|
94
|
+
}
|
|
95
|
+
function renderChunks(chunks) {
|
|
96
|
+
let rendered = "";
|
|
97
|
+
let previous;
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
if (chunk.stream !== previous) {
|
|
100
|
+
if (rendered && !rendered.endsWith("\n")) rendered += "\n";
|
|
101
|
+
rendered += `[${chunk.stream}]
|
|
102
|
+
`;
|
|
103
|
+
previous = chunk.stream;
|
|
104
|
+
}
|
|
105
|
+
rendered += chunk.text;
|
|
106
|
+
}
|
|
107
|
+
return rendered.trimEnd();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { startStepOutputCapture };
|
|
111
|
+
//# sourceMappingURL=chunk-THB7O2G6.js.map
|
|
112
|
+
//# sourceMappingURL=chunk-THB7O2G6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/output-capture.ts"],"names":[],"mappings":"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","file":"chunk-THB7O2G6.js","sourcesContent":["import type { Writable } from 'node:stream';\nimport { format } from 'node:util';\n\nexport type CapturedStream = 'stdout' | 'stderr';\n\nexport interface CapturedStepOutput {\n /** Ordered stdout/stderr output with stream labels. */\n text: string;\n /** True when output exceeded the configured byte budget. */\n truncated: boolean;\n /** Total bytes emitted before truncation. */\n totalBytes: number;\n}\n\nexport interface StepOutputCapture {\n stop(): CapturedStepOutput;\n}\n\nexport interface StepOutputCaptureOptions {\n /** Maximum UTF-8 bytes retained for one step. Defaults to 32 KiB. */\n maxBytes?: number;\n /** Forward output while capturing. Defaults to false, matching Behave capture semantics. */\n passthrough?: boolean;\n}\n\ninterface CapturedChunk {\n stream: CapturedStream;\n text: string;\n}\n\ntype Write = Writable['write'];\ntype ConsoleMethod = 'debug' | 'error' | 'info' | 'log' | 'warn';\n\nconst DEFAULT_MAX_BYTES = 32 * 1024;\n\n/**\n * Capture process stdout/stderr until stop() is called. Console output and\n * Pino's standard destination both flow through these streams. Child\n * processes that inherit the OS file descriptors directly are intentionally\n * outside this in-process capture boundary.\n */\nexport function startStepOutputCapture(options: StepOutputCaptureOptions = {}): StepOutputCapture {\n const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_MAX_BYTES);\n const stdout = process.stdout;\n const stderr = process.stderr;\n const originalStdoutWrite = stdout.write;\n const originalStderrWrite = stderr.write;\n const originalConsole = Object.fromEntries(\n (['debug', 'error', 'info', 'log', 'warn'] as const).map((method) => [method, console[method]]),\n ) as Record<ConsoleMethod, typeof console.log>;\n const chunks: CapturedChunk[] = [];\n let retainedBytes = 0;\n let totalBytes = 0;\n let stopped = false;\n\n const capture = (stream: CapturedStream, chunk: unknown): void => {\n const text = chunkToString(chunk);\n const bytes = Buffer.byteLength(text);\n totalBytes += bytes;\n const remaining = maxBytes - retainedBytes;\n if (remaining <= 0 || bytes === 0) return;\n const retained = bytes <= remaining ? text : truncateUtf8(text, remaining);\n retainedBytes += Buffer.byteLength(retained);\n if (retained) chunks.push({ stream, text: retained });\n };\n\n stdout.write = interceptedWrite(\n stdout,\n 'stdout',\n originalStdoutWrite,\n capture,\n options.passthrough,\n );\n stderr.write = interceptedWrite(\n stderr,\n 'stderr',\n originalStderrWrite,\n capture,\n options.passthrough,\n );\n if (!options.passthrough) {\n for (const method of ['debug', 'info', 'log'] as const) {\n console[method] = (...args: unknown[]) => capture('stdout', `${format(...args)}\\n`);\n }\n for (const method of ['error', 'warn'] as const) {\n console[method] = (...args: unknown[]) => capture('stderr', `${format(...args)}\\n`);\n }\n }\n\n return {\n stop(): CapturedStepOutput {\n if (!stopped) {\n stdout.write = originalStdoutWrite;\n stderr.write = originalStderrWrite;\n for (const method of Object.keys(originalConsole) as ConsoleMethod[]) {\n console[method] = originalConsole[method];\n }\n stopped = true;\n }\n return {\n text: renderChunks(chunks),\n truncated: totalBytes > retainedBytes,\n totalBytes,\n };\n },\n };\n}\n\nfunction interceptedWrite(\n destination: Writable,\n stream: CapturedStream,\n original: Write,\n capture: (stream: CapturedStream, chunk: unknown) => void,\n passthrough = false,\n): Write {\n return function write(chunk: unknown, encodingOrCallback?: unknown, callback?: unknown): boolean {\n capture(stream, chunk);\n if (passthrough) {\n return original.call(\n destination,\n chunk as never,\n encodingOrCallback as never,\n callback as never,\n );\n }\n const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;\n if (typeof done === 'function') queueMicrotask(() => done());\n return true;\n } as Write;\n}\n\nfunction chunkToString(chunk: unknown): string {\n if (typeof chunk === 'string') return chunk;\n if (Buffer.isBuffer(chunk)) return chunk.toString('utf8');\n if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString('utf8');\n return String(chunk);\n}\n\nfunction truncateUtf8(value: string, maxBytes: number): string {\n return Buffer.from(value)\n .subarray(0, maxBytes)\n .toString('utf8')\n .replace(/\\uFFFD$/u, '');\n}\n\nfunction renderChunks(chunks: readonly CapturedChunk[]): string {\n let rendered = '';\n let previous: CapturedStream | undefined;\n for (const chunk of chunks) {\n if (chunk.stream !== previous) {\n if (rendered && !rendered.endsWith('\\n')) rendered += '\\n';\n rendered += `[${chunk.stream}]\\n`;\n previous = chunk.stream;\n }\n rendered += chunk.text;\n }\n return rendered.trimEnd();\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Formatter } from '@cucumber/cucumber';
|
|
2
2
|
|
|
3
3
|
// src/evidence-formatter.ts
|
|
4
|
+
var FAILURE_EVIDENCE_MEDIA_TYPE = "application/vnd.fabric.databricks-bdd.failure+json";
|
|
4
5
|
var STATUS_RANK = ["UNKNOWN", "PASSED", "SKIPPED", "PENDING", "UNDEFINED", "AMBIGUOUS", "FAILED"];
|
|
5
6
|
var EvidenceFormatter = class extends Formatter {
|
|
6
7
|
pickles = /* @__PURE__ */ new Map();
|
|
@@ -18,9 +19,15 @@ var EvidenceFormatter = class extends Formatter {
|
|
|
18
19
|
this.attempts.set(envelope.testCaseStarted.id, {
|
|
19
20
|
pickleId: this.testCases.get(envelope.testCaseStarted.testCaseId) ?? "",
|
|
20
21
|
status: "UNKNOWN",
|
|
21
|
-
durationMs: 0
|
|
22
|
+
durationMs: 0,
|
|
23
|
+
failures: []
|
|
22
24
|
});
|
|
23
25
|
}
|
|
26
|
+
if (envelope.attachment?.testCaseStartedId) {
|
|
27
|
+
const attempt = this.attempts.get(envelope.attachment.testCaseStartedId);
|
|
28
|
+
const failure = parseFailureEvidenceAttachment(envelope.attachment);
|
|
29
|
+
if (attempt && failure) attempt.failures.push(failure);
|
|
30
|
+
}
|
|
24
31
|
if (envelope.testStepFinished) {
|
|
25
32
|
const attempt = this.attempts.get(envelope.testStepFinished.testCaseStartedId);
|
|
26
33
|
if (attempt) {
|
|
@@ -48,11 +55,12 @@ var EvidenceFormatter = class extends Formatter {
|
|
|
48
55
|
tags: (pickle?.tags ?? []).map((t) => t.name),
|
|
49
56
|
status,
|
|
50
57
|
durationMs: Math.round(attempt.durationMs),
|
|
51
|
-
...attempt.error ? { error: attempt.error } : {}
|
|
58
|
+
...attempt.error ? { error: attempt.error } : {},
|
|
59
|
+
...attempt.failures.length > 0 ? { failures: attempt.failures } : {}
|
|
52
60
|
});
|
|
53
61
|
}
|
|
54
62
|
const report = {
|
|
55
|
-
schemaVersion:
|
|
63
|
+
schemaVersion: 2,
|
|
56
64
|
kind: "databricks-bdd",
|
|
57
65
|
profile: process.env.DBX_TEST_PROFILE ?? "local",
|
|
58
66
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -64,6 +72,24 @@ var EvidenceFormatter = class extends Formatter {
|
|
|
64
72
|
`);
|
|
65
73
|
}
|
|
66
74
|
};
|
|
75
|
+
function parseFailureEvidenceAttachment(attachment) {
|
|
76
|
+
if (attachment.mediaType !== FAILURE_EVIDENCE_MEDIA_TYPE) return null;
|
|
77
|
+
try {
|
|
78
|
+
const body = attachment.contentEncoding === "BASE64" ? Buffer.from(attachment.body, "base64").toString("utf8") : attachment.body;
|
|
79
|
+
const parsed = JSON.parse(redactSecrets(body));
|
|
80
|
+
if (typeof parsed.step !== "string") return null;
|
|
81
|
+
return {
|
|
82
|
+
step: parsed.step,
|
|
83
|
+
...typeof parsed.statementId === "string" ? { statementId: parsed.statementId } : {},
|
|
84
|
+
...typeof parsed.sql === "string" ? { sql: parsed.sql } : {},
|
|
85
|
+
...typeof parsed.output === "string" ? { output: parsed.output } : {},
|
|
86
|
+
...typeof parsed.outputBytes === "number" ? { outputBytes: parsed.outputBytes } : {},
|
|
87
|
+
...typeof parsed.outputTruncated === "boolean" ? { outputTruncated: parsed.outputTruncated } : {}
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
67
93
|
var SECRET_ENV_RE = /(TOKEN|SECRET|PASSWORD|BEARER|KEY)/;
|
|
68
94
|
function redactSecrets(text, env = process.env) {
|
|
69
95
|
let out = text;
|
|
@@ -75,6 +101,6 @@ function redactSecrets(text, env = process.env) {
|
|
|
75
101
|
return out;
|
|
76
102
|
}
|
|
77
103
|
|
|
78
|
-
export { EvidenceFormatter, redactSecrets };
|
|
79
|
-
//# sourceMappingURL=chunk-
|
|
80
|
-
//# sourceMappingURL=chunk-
|
|
104
|
+
export { EvidenceFormatter, FAILURE_EVIDENCE_MEDIA_TYPE, parseFailureEvidenceAttachment, redactSecrets };
|
|
105
|
+
//# sourceMappingURL=chunk-UQAKGVJS.js.map
|
|
106
|
+
//# sourceMappingURL=chunk-UQAKGVJS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/evidence-formatter.ts"],"names":[],"mappings":"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","file":"chunk-UQAKGVJS.js","sourcesContent":["import { Formatter, type IFormatterOptions } from '@cucumber/cucumber';\n\n/**\n * Evidence formatter: schema-versioned, secret-redacted JSON summary of a\n * BDD run, shared in spirit with the live check runner's evidence output so\n * nightly runs double as audit artifacts. Wire it up in cucumber config:\n *\n * format: [['@fabricorg/databricks-bdd/evidence-formatter', 'reports/evidence.json']]\n */\nexport interface EvidenceScenario {\n name: string;\n uri: string;\n tags: string[];\n status: string;\n durationMs: number;\n error?: string;\n failures?: StepFailureEvidence[];\n}\n\nexport interface StepFailureEvidence {\n step: string;\n statementId?: string;\n sql?: string;\n output?: string;\n outputBytes?: number;\n outputTruncated?: boolean;\n}\n\nexport interface EvidenceReport {\n schemaVersion: 2;\n kind: 'databricks-bdd';\n profile: string;\n finishedAt: string;\n success: boolean;\n summary: Record<string, number>;\n scenarios: EvidenceScenario[];\n}\n\ninterface Envelope {\n attachment?: {\n body: string;\n contentEncoding: 'IDENTITY' | 'BASE64';\n mediaType: string;\n testCaseStartedId?: string;\n };\n pickle?: { id: string; name: string; uri: string; tags?: { name: string }[] };\n testCase?: { id: string; pickleId: string };\n testCaseStarted?: { id: string; testCaseId: string };\n testStepFinished?: {\n testCaseStartedId: string;\n testStepResult: {\n status: string;\n message?: string;\n duration?: { seconds: number; nanos: number };\n };\n };\n testRunFinished?: { success?: boolean; timestamp?: { seconds: number; nanos: number } };\n}\n\nexport const FAILURE_EVIDENCE_MEDIA_TYPE = 'application/vnd.fabric.databricks-bdd.failure+json';\n\nconst STATUS_RANK = ['UNKNOWN', 'PASSED', 'SKIPPED', 'PENDING', 'UNDEFINED', 'AMBIGUOUS', 'FAILED'];\n\nexport default class EvidenceFormatter extends Formatter {\n private readonly pickles = new Map<string, NonNullable<Envelope['pickle']>>();\n private readonly testCases = new Map<string, string>(); // testCaseId → pickleId\n private readonly attempts = new Map<\n string,\n {\n pickleId: string;\n status: string;\n durationMs: number;\n error?: string;\n failures: StepFailureEvidence[];\n }\n >();\n\n constructor(options: IFormatterOptions) {\n super(options);\n options.eventBroadcaster.on('envelope', (envelope: Envelope) => this.onEnvelope(envelope));\n }\n\n private onEnvelope(envelope: Envelope): void {\n if (envelope.pickle) this.pickles.set(envelope.pickle.id, envelope.pickle);\n if (envelope.testCase) this.testCases.set(envelope.testCase.id, envelope.testCase.pickleId);\n if (envelope.testCaseStarted) {\n this.attempts.set(envelope.testCaseStarted.id, {\n pickleId: this.testCases.get(envelope.testCaseStarted.testCaseId) ?? '',\n status: 'UNKNOWN',\n durationMs: 0,\n failures: [],\n });\n }\n if (envelope.attachment?.testCaseStartedId) {\n const attempt = this.attempts.get(envelope.attachment.testCaseStartedId);\n const failure = parseFailureEvidenceAttachment(envelope.attachment);\n if (attempt && failure) attempt.failures.push(failure);\n }\n if (envelope.testStepFinished) {\n const attempt = this.attempts.get(envelope.testStepFinished.testCaseStartedId);\n if (attempt) {\n const result = envelope.testStepFinished.testStepResult;\n if (STATUS_RANK.indexOf(result.status) > STATUS_RANK.indexOf(attempt.status)) {\n attempt.status = result.status;\n if (result.message) attempt.error = redactSecrets(result.message);\n }\n const d = result.duration;\n if (d) attempt.durationMs += d.seconds * 1000 + d.nanos / 1e6;\n }\n }\n if (envelope.testRunFinished) this.finish(envelope.testRunFinished);\n }\n\n private finish(finished: NonNullable<Envelope['testRunFinished']>): void {\n const scenarios: EvidenceScenario[] = [];\n const summary: Record<string, number> = {};\n for (const attempt of this.attempts.values()) {\n const pickle = this.pickles.get(attempt.pickleId);\n const status = attempt.status.toLowerCase();\n summary[status] = (summary[status] ?? 0) + 1;\n scenarios.push({\n name: pickle?.name ?? attempt.pickleId,\n uri: pickle?.uri ?? '',\n tags: (pickle?.tags ?? []).map((t) => t.name),\n status,\n durationMs: Math.round(attempt.durationMs),\n ...(attempt.error ? { error: attempt.error } : {}),\n ...(attempt.failures.length > 0 ? { failures: attempt.failures } : {}),\n });\n }\n const report: EvidenceReport = {\n schemaVersion: 2,\n kind: 'databricks-bdd',\n profile: process.env.DBX_TEST_PROFILE ?? 'local',\n finishedAt: new Date().toISOString(),\n success: finished.success ?? !scenarios.some((s) => s.status === 'failed'),\n summary,\n scenarios,\n };\n this.log(`${JSON.stringify(report, null, 2)}\\n`);\n }\n}\n\n/** Decode the structured attachment emitted by the failure hook. */\nexport function parseFailureEvidenceAttachment(\n attachment: NonNullable<Envelope['attachment']>,\n): StepFailureEvidence | null {\n if (attachment.mediaType !== FAILURE_EVIDENCE_MEDIA_TYPE) return null;\n try {\n const body =\n attachment.contentEncoding === 'BASE64'\n ? Buffer.from(attachment.body, 'base64').toString('utf8')\n : attachment.body;\n const parsed = JSON.parse(redactSecrets(body)) as Partial<StepFailureEvidence>;\n if (typeof parsed.step !== 'string') return null;\n return {\n step: parsed.step,\n ...(typeof parsed.statementId === 'string' ? { statementId: parsed.statementId } : {}),\n ...(typeof parsed.sql === 'string' ? { sql: parsed.sql } : {}),\n ...(typeof parsed.output === 'string' ? { output: parsed.output } : {}),\n ...(typeof parsed.outputBytes === 'number' ? { outputBytes: parsed.outputBytes } : {}),\n ...(typeof parsed.outputTruncated === 'boolean'\n ? { outputTruncated: parsed.outputTruncated }\n : {}),\n };\n } catch {\n return null;\n }\n}\n\nconst SECRET_ENV_RE = /(TOKEN|SECRET|PASSWORD|BEARER|KEY)/;\n\n/** Replace values of secret-looking env vars anywhere they appear in text. */\nexport function redactSecrets(\n text: string,\n env: Record<string, string | undefined> = process.env,\n): string {\n let out = text;\n for (const [name, value] of Object.entries(env)) {\n if (!value || value.length < 6) continue;\n if (!SECRET_ENV_RE.test(name)) continue;\n out = out.split(value).join('[REDACTED]');\n }\n return out;\n}\n"]}
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var cucumber = require('@cucumber/cucumber');
|
|
6
6
|
|
|
7
7
|
// src/evidence-formatter.ts
|
|
8
|
+
var FAILURE_EVIDENCE_MEDIA_TYPE = "application/vnd.fabric.databricks-bdd.failure+json";
|
|
8
9
|
var STATUS_RANK = ["UNKNOWN", "PASSED", "SKIPPED", "PENDING", "UNDEFINED", "AMBIGUOUS", "FAILED"];
|
|
9
10
|
var EvidenceFormatter = class extends cucumber.Formatter {
|
|
10
11
|
pickles = /* @__PURE__ */ new Map();
|
|
@@ -22,9 +23,15 @@ var EvidenceFormatter = class extends cucumber.Formatter {
|
|
|
22
23
|
this.attempts.set(envelope.testCaseStarted.id, {
|
|
23
24
|
pickleId: this.testCases.get(envelope.testCaseStarted.testCaseId) ?? "",
|
|
24
25
|
status: "UNKNOWN",
|
|
25
|
-
durationMs: 0
|
|
26
|
+
durationMs: 0,
|
|
27
|
+
failures: []
|
|
26
28
|
});
|
|
27
29
|
}
|
|
30
|
+
if (envelope.attachment?.testCaseStartedId) {
|
|
31
|
+
const attempt = this.attempts.get(envelope.attachment.testCaseStartedId);
|
|
32
|
+
const failure = parseFailureEvidenceAttachment(envelope.attachment);
|
|
33
|
+
if (attempt && failure) attempt.failures.push(failure);
|
|
34
|
+
}
|
|
28
35
|
if (envelope.testStepFinished) {
|
|
29
36
|
const attempt = this.attempts.get(envelope.testStepFinished.testCaseStartedId);
|
|
30
37
|
if (attempt) {
|
|
@@ -52,11 +59,12 @@ var EvidenceFormatter = class extends cucumber.Formatter {
|
|
|
52
59
|
tags: (pickle?.tags ?? []).map((t) => t.name),
|
|
53
60
|
status,
|
|
54
61
|
durationMs: Math.round(attempt.durationMs),
|
|
55
|
-
...attempt.error ? { error: attempt.error } : {}
|
|
62
|
+
...attempt.error ? { error: attempt.error } : {},
|
|
63
|
+
...attempt.failures.length > 0 ? { failures: attempt.failures } : {}
|
|
56
64
|
});
|
|
57
65
|
}
|
|
58
66
|
const report = {
|
|
59
|
-
schemaVersion:
|
|
67
|
+
schemaVersion: 2,
|
|
60
68
|
kind: "databricks-bdd",
|
|
61
69
|
profile: process.env.DBX_TEST_PROFILE ?? "local",
|
|
62
70
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -68,6 +76,24 @@ var EvidenceFormatter = class extends cucumber.Formatter {
|
|
|
68
76
|
`);
|
|
69
77
|
}
|
|
70
78
|
};
|
|
79
|
+
function parseFailureEvidenceAttachment(attachment) {
|
|
80
|
+
if (attachment.mediaType !== FAILURE_EVIDENCE_MEDIA_TYPE) return null;
|
|
81
|
+
try {
|
|
82
|
+
const body = attachment.contentEncoding === "BASE64" ? Buffer.from(attachment.body, "base64").toString("utf8") : attachment.body;
|
|
83
|
+
const parsed = JSON.parse(redactSecrets(body));
|
|
84
|
+
if (typeof parsed.step !== "string") return null;
|
|
85
|
+
return {
|
|
86
|
+
step: parsed.step,
|
|
87
|
+
...typeof parsed.statementId === "string" ? { statementId: parsed.statementId } : {},
|
|
88
|
+
...typeof parsed.sql === "string" ? { sql: parsed.sql } : {},
|
|
89
|
+
...typeof parsed.output === "string" ? { output: parsed.output } : {},
|
|
90
|
+
...typeof parsed.outputBytes === "number" ? { outputBytes: parsed.outputBytes } : {},
|
|
91
|
+
...typeof parsed.outputTruncated === "boolean" ? { outputTruncated: parsed.outputTruncated } : {}
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
71
97
|
var SECRET_ENV_RE = /(TOKEN|SECRET|PASSWORD|BEARER|KEY)/;
|
|
72
98
|
function redactSecrets(text, env = process.env) {
|
|
73
99
|
let out = text;
|
|
@@ -79,7 +105,9 @@ function redactSecrets(text, env = process.env) {
|
|
|
79
105
|
return out;
|
|
80
106
|
}
|
|
81
107
|
|
|
108
|
+
exports.FAILURE_EVIDENCE_MEDIA_TYPE = FAILURE_EVIDENCE_MEDIA_TYPE;
|
|
82
109
|
exports.default = EvidenceFormatter;
|
|
110
|
+
exports.parseFailureEvidenceAttachment = parseFailureEvidenceAttachment;
|
|
83
111
|
exports.redactSecrets = redactSecrets;
|
|
84
112
|
//# sourceMappingURL=evidence-formatter.cjs.map
|
|
85
113
|
//# sourceMappingURL=evidence-formatter.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/evidence-formatter.ts"],"names":["Formatter"],"mappings":"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","file":"evidence-formatter.cjs","sourcesContent":["import { Formatter, type IFormatterOptions } from '@cucumber/cucumber';\n\n/**\n * Evidence formatter: schema-versioned, secret-redacted JSON summary of a\n * BDD run, shared in spirit with the live check runner's evidence output so\n * nightly runs double as audit artifacts. Wire it up in cucumber config:\n *\n * format: [['@fabricorg/databricks-bdd/evidence-formatter', 'reports/evidence.json']]\n */\nexport interface EvidenceScenario {\n name: string;\n uri: string;\n tags: string[];\n status: string;\n durationMs: number;\n error?: string;\n}\n\nexport interface EvidenceReport {\n schemaVersion: 1;\n kind: 'databricks-bdd';\n profile: string;\n finishedAt: string;\n success: boolean;\n summary: Record<string, number>;\n scenarios: EvidenceScenario[];\n}\n\ninterface Envelope {\n pickle?: { id: string; name: string; uri: string; tags?: { name: string }[] };\n testCase?: { id: string; pickleId: string };\n testCaseStarted?: { id: string; testCaseId: string };\n testStepFinished?: {\n testCaseStartedId: string;\n testStepResult: {\n status: string;\n message?: string;\n duration?: { seconds: number; nanos: number };\n };\n };\n testRunFinished?: { success?: boolean; timestamp?: { seconds: number; nanos: number } };\n}\n\nconst STATUS_RANK = ['UNKNOWN', 'PASSED', 'SKIPPED', 'PENDING', 'UNDEFINED', 'AMBIGUOUS', 'FAILED'];\n\nexport default class EvidenceFormatter extends Formatter {\n private readonly pickles = new Map<string, NonNullable<Envelope['pickle']>>();\n private readonly testCases = new Map<string, string>(); // testCaseId → pickleId\n private readonly attempts = new Map<\n string,\n { pickleId: string; status: string; durationMs: number; error?: string }\n >();\n\n constructor(options: IFormatterOptions) {\n super(options);\n options.eventBroadcaster.on('envelope', (envelope: Envelope) => this.onEnvelope(envelope));\n }\n\n private onEnvelope(envelope: Envelope): void {\n if (envelope.pickle) this.pickles.set(envelope.pickle.id, envelope.pickle);\n if (envelope.testCase) this.testCases.set(envelope.testCase.id, envelope.testCase.pickleId);\n if (envelope.testCaseStarted) {\n this.attempts.set(envelope.testCaseStarted.id, {\n pickleId: this.testCases.get(envelope.testCaseStarted.testCaseId) ?? '',\n status: 'UNKNOWN',\n durationMs: 0,\n });\n }\n if (envelope.testStepFinished) {\n const attempt = this.attempts.get(envelope.testStepFinished.testCaseStartedId);\n if (attempt) {\n const result = envelope.testStepFinished.testStepResult;\n if (STATUS_RANK.indexOf(result.status) > STATUS_RANK.indexOf(attempt.status)) {\n attempt.status = result.status;\n if (result.message) attempt.error = redactSecrets(result.message);\n }\n const d = result.duration;\n if (d) attempt.durationMs += d.seconds * 1000 + d.nanos / 1e6;\n }\n }\n if (envelope.testRunFinished) this.finish(envelope.testRunFinished);\n }\n\n private finish(finished: NonNullable<Envelope['testRunFinished']>): void {\n const scenarios: EvidenceScenario[] = [];\n const summary: Record<string, number> = {};\n for (const attempt of this.attempts.values()) {\n const pickle = this.pickles.get(attempt.pickleId);\n const status = attempt.status.toLowerCase();\n summary[status] = (summary[status] ?? 0) + 1;\n scenarios.push({\n name: pickle?.name ?? attempt.pickleId,\n uri: pickle?.uri ?? '',\n tags: (pickle?.tags ?? []).map((t) => t.name),\n status,\n durationMs: Math.round(attempt.durationMs),\n ...(attempt.error ? { error: attempt.error } : {}),\n });\n }\n const report: EvidenceReport = {\n schemaVersion: 1,\n kind: 'databricks-bdd',\n profile: process.env.DBX_TEST_PROFILE ?? 'local',\n finishedAt: new Date().toISOString(),\n success: finished.success ?? !scenarios.some((s) => s.status === 'failed'),\n summary,\n scenarios,\n };\n this.log(`${JSON.stringify(report, null, 2)}\\n`);\n }\n}\n\nconst SECRET_ENV_RE = /(TOKEN|SECRET|PASSWORD|BEARER|KEY)/;\n\n/** Replace values of secret-looking env vars anywhere they appear in text. */\nexport function redactSecrets(\n text: string,\n env: Record<string, string | undefined> = process.env,\n): string {\n let out = text;\n for (const [name, value] of Object.entries(env)) {\n if (!value || value.length < 6) continue;\n if (!SECRET_ENV_RE.test(name)) continue;\n out = out.split(value).join('[REDACTED]');\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/evidence-formatter.ts"],"names":["Formatter"],"mappings":"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","file":"evidence-formatter.cjs","sourcesContent":["import { Formatter, type IFormatterOptions } from '@cucumber/cucumber';\n\n/**\n * Evidence formatter: schema-versioned, secret-redacted JSON summary of a\n * BDD run, shared in spirit with the live check runner's evidence output so\n * nightly runs double as audit artifacts. Wire it up in cucumber config:\n *\n * format: [['@fabricorg/databricks-bdd/evidence-formatter', 'reports/evidence.json']]\n */\nexport interface EvidenceScenario {\n name: string;\n uri: string;\n tags: string[];\n status: string;\n durationMs: number;\n error?: string;\n failures?: StepFailureEvidence[];\n}\n\nexport interface StepFailureEvidence {\n step: string;\n statementId?: string;\n sql?: string;\n output?: string;\n outputBytes?: number;\n outputTruncated?: boolean;\n}\n\nexport interface EvidenceReport {\n schemaVersion: 2;\n kind: 'databricks-bdd';\n profile: string;\n finishedAt: string;\n success: boolean;\n summary: Record<string, number>;\n scenarios: EvidenceScenario[];\n}\n\ninterface Envelope {\n attachment?: {\n body: string;\n contentEncoding: 'IDENTITY' | 'BASE64';\n mediaType: string;\n testCaseStartedId?: string;\n };\n pickle?: { id: string; name: string; uri: string; tags?: { name: string }[] };\n testCase?: { id: string; pickleId: string };\n testCaseStarted?: { id: string; testCaseId: string };\n testStepFinished?: {\n testCaseStartedId: string;\n testStepResult: {\n status: string;\n message?: string;\n duration?: { seconds: number; nanos: number };\n };\n };\n testRunFinished?: { success?: boolean; timestamp?: { seconds: number; nanos: number } };\n}\n\nexport const FAILURE_EVIDENCE_MEDIA_TYPE = 'application/vnd.fabric.databricks-bdd.failure+json';\n\nconst STATUS_RANK = ['UNKNOWN', 'PASSED', 'SKIPPED', 'PENDING', 'UNDEFINED', 'AMBIGUOUS', 'FAILED'];\n\nexport default class EvidenceFormatter extends Formatter {\n private readonly pickles = new Map<string, NonNullable<Envelope['pickle']>>();\n private readonly testCases = new Map<string, string>(); // testCaseId → pickleId\n private readonly attempts = new Map<\n string,\n {\n pickleId: string;\n status: string;\n durationMs: number;\n error?: string;\n failures: StepFailureEvidence[];\n }\n >();\n\n constructor(options: IFormatterOptions) {\n super(options);\n options.eventBroadcaster.on('envelope', (envelope: Envelope) => this.onEnvelope(envelope));\n }\n\n private onEnvelope(envelope: Envelope): void {\n if (envelope.pickle) this.pickles.set(envelope.pickle.id, envelope.pickle);\n if (envelope.testCase) this.testCases.set(envelope.testCase.id, envelope.testCase.pickleId);\n if (envelope.testCaseStarted) {\n this.attempts.set(envelope.testCaseStarted.id, {\n pickleId: this.testCases.get(envelope.testCaseStarted.testCaseId) ?? '',\n status: 'UNKNOWN',\n durationMs: 0,\n failures: [],\n });\n }\n if (envelope.attachment?.testCaseStartedId) {\n const attempt = this.attempts.get(envelope.attachment.testCaseStartedId);\n const failure = parseFailureEvidenceAttachment(envelope.attachment);\n if (attempt && failure) attempt.failures.push(failure);\n }\n if (envelope.testStepFinished) {\n const attempt = this.attempts.get(envelope.testStepFinished.testCaseStartedId);\n if (attempt) {\n const result = envelope.testStepFinished.testStepResult;\n if (STATUS_RANK.indexOf(result.status) > STATUS_RANK.indexOf(attempt.status)) {\n attempt.status = result.status;\n if (result.message) attempt.error = redactSecrets(result.message);\n }\n const d = result.duration;\n if (d) attempt.durationMs += d.seconds * 1000 + d.nanos / 1e6;\n }\n }\n if (envelope.testRunFinished) this.finish(envelope.testRunFinished);\n }\n\n private finish(finished: NonNullable<Envelope['testRunFinished']>): void {\n const scenarios: EvidenceScenario[] = [];\n const summary: Record<string, number> = {};\n for (const attempt of this.attempts.values()) {\n const pickle = this.pickles.get(attempt.pickleId);\n const status = attempt.status.toLowerCase();\n summary[status] = (summary[status] ?? 0) + 1;\n scenarios.push({\n name: pickle?.name ?? attempt.pickleId,\n uri: pickle?.uri ?? '',\n tags: (pickle?.tags ?? []).map((t) => t.name),\n status,\n durationMs: Math.round(attempt.durationMs),\n ...(attempt.error ? { error: attempt.error } : {}),\n ...(attempt.failures.length > 0 ? { failures: attempt.failures } : {}),\n });\n }\n const report: EvidenceReport = {\n schemaVersion: 2,\n kind: 'databricks-bdd',\n profile: process.env.DBX_TEST_PROFILE ?? 'local',\n finishedAt: new Date().toISOString(),\n success: finished.success ?? !scenarios.some((s) => s.status === 'failed'),\n summary,\n scenarios,\n };\n this.log(`${JSON.stringify(report, null, 2)}\\n`);\n }\n}\n\n/** Decode the structured attachment emitted by the failure hook. */\nexport function parseFailureEvidenceAttachment(\n attachment: NonNullable<Envelope['attachment']>,\n): StepFailureEvidence | null {\n if (attachment.mediaType !== FAILURE_EVIDENCE_MEDIA_TYPE) return null;\n try {\n const body =\n attachment.contentEncoding === 'BASE64'\n ? Buffer.from(attachment.body, 'base64').toString('utf8')\n : attachment.body;\n const parsed = JSON.parse(redactSecrets(body)) as Partial<StepFailureEvidence>;\n if (typeof parsed.step !== 'string') return null;\n return {\n step: parsed.step,\n ...(typeof parsed.statementId === 'string' ? { statementId: parsed.statementId } : {}),\n ...(typeof parsed.sql === 'string' ? { sql: parsed.sql } : {}),\n ...(typeof parsed.output === 'string' ? { output: parsed.output } : {}),\n ...(typeof parsed.outputBytes === 'number' ? { outputBytes: parsed.outputBytes } : {}),\n ...(typeof parsed.outputTruncated === 'boolean'\n ? { outputTruncated: parsed.outputTruncated }\n : {}),\n };\n } catch {\n return null;\n }\n}\n\nconst SECRET_ENV_RE = /(TOKEN|SECRET|PASSWORD|BEARER|KEY)/;\n\n/** Replace values of secret-looking env vars anywhere they appear in text. */\nexport function redactSecrets(\n text: string,\n env: Record<string, string | undefined> = process.env,\n): string {\n let out = text;\n for (const [name, value] of Object.entries(env)) {\n if (!value || value.length < 6) continue;\n if (!SECRET_ENV_RE.test(name)) continue;\n out = out.split(value).join('[REDACTED]');\n }\n return out;\n}\n"]}
|
|
@@ -14,9 +14,18 @@ interface EvidenceScenario {
|
|
|
14
14
|
status: string;
|
|
15
15
|
durationMs: number;
|
|
16
16
|
error?: string;
|
|
17
|
+
failures?: StepFailureEvidence[];
|
|
18
|
+
}
|
|
19
|
+
interface StepFailureEvidence {
|
|
20
|
+
step: string;
|
|
21
|
+
statementId?: string;
|
|
22
|
+
sql?: string;
|
|
23
|
+
output?: string;
|
|
24
|
+
outputBytes?: number;
|
|
25
|
+
outputTruncated?: boolean;
|
|
17
26
|
}
|
|
18
27
|
interface EvidenceReport {
|
|
19
|
-
schemaVersion:
|
|
28
|
+
schemaVersion: 2;
|
|
20
29
|
kind: 'databricks-bdd';
|
|
21
30
|
profile: string;
|
|
22
31
|
finishedAt: string;
|
|
@@ -24,6 +33,49 @@ interface EvidenceReport {
|
|
|
24
33
|
summary: Record<string, number>;
|
|
25
34
|
scenarios: EvidenceScenario[];
|
|
26
35
|
}
|
|
36
|
+
interface Envelope {
|
|
37
|
+
attachment?: {
|
|
38
|
+
body: string;
|
|
39
|
+
contentEncoding: 'IDENTITY' | 'BASE64';
|
|
40
|
+
mediaType: string;
|
|
41
|
+
testCaseStartedId?: string;
|
|
42
|
+
};
|
|
43
|
+
pickle?: {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
uri: string;
|
|
47
|
+
tags?: {
|
|
48
|
+
name: string;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
testCase?: {
|
|
52
|
+
id: string;
|
|
53
|
+
pickleId: string;
|
|
54
|
+
};
|
|
55
|
+
testCaseStarted?: {
|
|
56
|
+
id: string;
|
|
57
|
+
testCaseId: string;
|
|
58
|
+
};
|
|
59
|
+
testStepFinished?: {
|
|
60
|
+
testCaseStartedId: string;
|
|
61
|
+
testStepResult: {
|
|
62
|
+
status: string;
|
|
63
|
+
message?: string;
|
|
64
|
+
duration?: {
|
|
65
|
+
seconds: number;
|
|
66
|
+
nanos: number;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
testRunFinished?: {
|
|
71
|
+
success?: boolean;
|
|
72
|
+
timestamp?: {
|
|
73
|
+
seconds: number;
|
|
74
|
+
nanos: number;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
declare const FAILURE_EVIDENCE_MEDIA_TYPE = "application/vnd.fabric.databricks-bdd.failure+json";
|
|
27
79
|
declare class EvidenceFormatter extends Formatter {
|
|
28
80
|
private readonly pickles;
|
|
29
81
|
private readonly testCases;
|
|
@@ -32,7 +84,9 @@ declare class EvidenceFormatter extends Formatter {
|
|
|
32
84
|
private onEnvelope;
|
|
33
85
|
private finish;
|
|
34
86
|
}
|
|
87
|
+
/** Decode the structured attachment emitted by the failure hook. */
|
|
88
|
+
declare function parseFailureEvidenceAttachment(attachment: NonNullable<Envelope['attachment']>): StepFailureEvidence | null;
|
|
35
89
|
/** Replace values of secret-looking env vars anywhere they appear in text. */
|
|
36
90
|
declare function redactSecrets(text: string, env?: Record<string, string | undefined>): string;
|
|
37
91
|
|
|
38
|
-
export { type EvidenceReport, type EvidenceScenario, EvidenceFormatter as default, redactSecrets };
|
|
92
|
+
export { type EvidenceReport, type EvidenceScenario, FAILURE_EVIDENCE_MEDIA_TYPE, type StepFailureEvidence, EvidenceFormatter as default, parseFailureEvidenceAttachment, redactSecrets };
|
|
@@ -14,9 +14,18 @@ interface EvidenceScenario {
|
|
|
14
14
|
status: string;
|
|
15
15
|
durationMs: number;
|
|
16
16
|
error?: string;
|
|
17
|
+
failures?: StepFailureEvidence[];
|
|
18
|
+
}
|
|
19
|
+
interface StepFailureEvidence {
|
|
20
|
+
step: string;
|
|
21
|
+
statementId?: string;
|
|
22
|
+
sql?: string;
|
|
23
|
+
output?: string;
|
|
24
|
+
outputBytes?: number;
|
|
25
|
+
outputTruncated?: boolean;
|
|
17
26
|
}
|
|
18
27
|
interface EvidenceReport {
|
|
19
|
-
schemaVersion:
|
|
28
|
+
schemaVersion: 2;
|
|
20
29
|
kind: 'databricks-bdd';
|
|
21
30
|
profile: string;
|
|
22
31
|
finishedAt: string;
|
|
@@ -24,6 +33,49 @@ interface EvidenceReport {
|
|
|
24
33
|
summary: Record<string, number>;
|
|
25
34
|
scenarios: EvidenceScenario[];
|
|
26
35
|
}
|
|
36
|
+
interface Envelope {
|
|
37
|
+
attachment?: {
|
|
38
|
+
body: string;
|
|
39
|
+
contentEncoding: 'IDENTITY' | 'BASE64';
|
|
40
|
+
mediaType: string;
|
|
41
|
+
testCaseStartedId?: string;
|
|
42
|
+
};
|
|
43
|
+
pickle?: {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
uri: string;
|
|
47
|
+
tags?: {
|
|
48
|
+
name: string;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
testCase?: {
|
|
52
|
+
id: string;
|
|
53
|
+
pickleId: string;
|
|
54
|
+
};
|
|
55
|
+
testCaseStarted?: {
|
|
56
|
+
id: string;
|
|
57
|
+
testCaseId: string;
|
|
58
|
+
};
|
|
59
|
+
testStepFinished?: {
|
|
60
|
+
testCaseStartedId: string;
|
|
61
|
+
testStepResult: {
|
|
62
|
+
status: string;
|
|
63
|
+
message?: string;
|
|
64
|
+
duration?: {
|
|
65
|
+
seconds: number;
|
|
66
|
+
nanos: number;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
testRunFinished?: {
|
|
71
|
+
success?: boolean;
|
|
72
|
+
timestamp?: {
|
|
73
|
+
seconds: number;
|
|
74
|
+
nanos: number;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
declare const FAILURE_EVIDENCE_MEDIA_TYPE = "application/vnd.fabric.databricks-bdd.failure+json";
|
|
27
79
|
declare class EvidenceFormatter extends Formatter {
|
|
28
80
|
private readonly pickles;
|
|
29
81
|
private readonly testCases;
|
|
@@ -32,7 +84,9 @@ declare class EvidenceFormatter extends Formatter {
|
|
|
32
84
|
private onEnvelope;
|
|
33
85
|
private finish;
|
|
34
86
|
}
|
|
87
|
+
/** Decode the structured attachment emitted by the failure hook. */
|
|
88
|
+
declare function parseFailureEvidenceAttachment(attachment: NonNullable<Envelope['attachment']>): StepFailureEvidence | null;
|
|
35
89
|
/** Replace values of secret-looking env vars anywhere they appear in text. */
|
|
36
90
|
declare function redactSecrets(text: string, env?: Record<string, string | undefined>): string;
|
|
37
91
|
|
|
38
|
-
export { type EvidenceReport, type EvidenceScenario, EvidenceFormatter as default, redactSecrets };
|
|
92
|
+
export { type EvidenceReport, type EvidenceScenario, FAILURE_EVIDENCE_MEDIA_TYPE, type StepFailureEvidence, EvidenceFormatter as default, parseFailureEvidenceAttachment, redactSecrets };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { EvidenceFormatter as default, redactSecrets } from './chunk-
|
|
1
|
+
export { FAILURE_EVIDENCE_MEDIA_TYPE, EvidenceFormatter as default, parseFailureEvidenceAttachment, redactSecrets } from './chunk-UQAKGVJS.js';
|
|
2
2
|
//# sourceMappingURL=evidence-formatter.js.map
|
|
3
3
|
//# sourceMappingURL=evidence-formatter.js.map
|