@fabricorg/databricks-testkit 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +196 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +191 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1256,6 +1256,195 @@ function autoLoaderIngestionCheck(options = {}) {
|
|
|
1256
1256
|
}
|
|
1257
1257
|
};
|
|
1258
1258
|
}
|
|
1259
|
+
function safeName(prefix) {
|
|
1260
|
+
return `${prefix}_${crypto.randomUUID().replaceAll("-", "_")}`;
|
|
1261
|
+
}
|
|
1262
|
+
function performanceBudgetCheck() {
|
|
1263
|
+
return {
|
|
1264
|
+
id: "performance",
|
|
1265
|
+
description: "SQL Statement Execution p95 stays within the configured latency budget",
|
|
1266
|
+
configured: (env) => Boolean(env.DBX_TEST_PERFORMANCE_P95_MS),
|
|
1267
|
+
async run({ env, warehouse }) {
|
|
1268
|
+
const budgetMs = Number(env.DBX_TEST_PERFORMANCE_P95_MS);
|
|
1269
|
+
const runs = Number(env.DBX_TEST_PERFORMANCE_RUNS ?? 7);
|
|
1270
|
+
if (!Number.isFinite(budgetMs) || budgetMs <= 0) {
|
|
1271
|
+
throw new Error("DBX_TEST_PERFORMANCE_P95_MS must be a positive number");
|
|
1272
|
+
}
|
|
1273
|
+
if (!Number.isInteger(runs) || runs < 3 || runs > 100) {
|
|
1274
|
+
throw new Error("DBX_TEST_PERFORMANCE_RUNS must be an integer from 3 to 100");
|
|
1275
|
+
}
|
|
1276
|
+
await warehouse.query("SELECT 1 AS warmup");
|
|
1277
|
+
const durations = [];
|
|
1278
|
+
for (let index = 0; index < runs; index += 1) {
|
|
1279
|
+
const started = performance.now();
|
|
1280
|
+
await warehouse.query("SELECT 1 AS performance_probe");
|
|
1281
|
+
durations.push(performance.now() - started);
|
|
1282
|
+
}
|
|
1283
|
+
durations.sort((left, right) => left - right);
|
|
1284
|
+
const p95 = durations[Math.ceil(durations.length * 0.95) - 1] ?? 0;
|
|
1285
|
+
if (p95 > budgetMs) {
|
|
1286
|
+
throw new Error(`SQL p95 ${Math.round(p95)}ms exceeded ${budgetMs}ms budget`);
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
runs,
|
|
1290
|
+
p50Ms: Math.round(durations[Math.floor(durations.length * 0.5)] ?? 0),
|
|
1291
|
+
p95Ms: Math.round(p95),
|
|
1292
|
+
budgetMs
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function failureRecoveryCheck() {
|
|
1298
|
+
return {
|
|
1299
|
+
id: "failure-recovery",
|
|
1300
|
+
description: "A failed statement is isolated and a subsequent statement recovers",
|
|
1301
|
+
async run({ warehouse }) {
|
|
1302
|
+
let failedAsExpected = false;
|
|
1303
|
+
try {
|
|
1304
|
+
await warehouse.query("SELECT * FROM __fabric_experiments_expected_missing_table__");
|
|
1305
|
+
} catch {
|
|
1306
|
+
failedAsExpected = true;
|
|
1307
|
+
}
|
|
1308
|
+
if (!failedAsExpected) throw new Error("Injected failure unexpectedly succeeded");
|
|
1309
|
+
const rows = await warehouse.query("SELECT 1 AS recovered");
|
|
1310
|
+
if (Number(rows[0]?.recovered) !== 1) {
|
|
1311
|
+
throw new Error("Statement execution did not recover after the injected failure");
|
|
1312
|
+
}
|
|
1313
|
+
return { injectedFailure: true, recovered: true };
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
function secretRotationCheck(options = {}) {
|
|
1318
|
+
return {
|
|
1319
|
+
id: "secret-rotation",
|
|
1320
|
+
description: "Databricks secret put, overwrite, metadata verification, and cleanup",
|
|
1321
|
+
configured: (env) => Boolean(env.DBX_TEST_ROTATION_SECRET_SCOPE),
|
|
1322
|
+
async run({ env, client }) {
|
|
1323
|
+
const scope = env.DBX_TEST_ROTATION_SECRET_SCOPE;
|
|
1324
|
+
const key = options.randomId?.() ?? safeName("fx_rotation_probe");
|
|
1325
|
+
const put = async (value) => client.post("/api/2.0/secrets/put", { scope, key, string_value: value });
|
|
1326
|
+
try {
|
|
1327
|
+
await put(crypto.randomBytes(32).toString("base64url"));
|
|
1328
|
+
await put(crypto.randomBytes(32).toString("base64url"));
|
|
1329
|
+
const listed = await client.get("/api/2.0/secrets/list", {
|
|
1330
|
+
scope
|
|
1331
|
+
});
|
|
1332
|
+
if (!(listed.secrets ?? []).some((secret) => secret.key === key)) {
|
|
1333
|
+
throw new Error(`Rotated secret metadata was not visible in scope ${scope}`);
|
|
1334
|
+
}
|
|
1335
|
+
return { scope, rotated: true, cleanedUp: true };
|
|
1336
|
+
} finally {
|
|
1337
|
+
await client.post("/api/2.0/secrets/delete", { scope, key }).catch(() => void 0);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
function backupRestoreCheck() {
|
|
1343
|
+
return {
|
|
1344
|
+
id: "backup-restore",
|
|
1345
|
+
description: "Delta DEEP CLONE backup restores a dropped scratch table",
|
|
1346
|
+
async run({ warehouse }) {
|
|
1347
|
+
const source = warehouse.qual(safeName("backup_source"));
|
|
1348
|
+
const backup = warehouse.qual(safeName("backup_clone"));
|
|
1349
|
+
try {
|
|
1350
|
+
await warehouse.exec(`CREATE TABLE ${source} USING DELTA AS SELECT 7 AS value`);
|
|
1351
|
+
await warehouse.exec(`CREATE TABLE ${backup} DEEP CLONE ${source}`);
|
|
1352
|
+
await warehouse.exec(`DROP TABLE ${source}`);
|
|
1353
|
+
await warehouse.exec(`CREATE TABLE ${source} DEEP CLONE ${backup}`);
|
|
1354
|
+
const rows = await warehouse.query(`SELECT value FROM ${source}`);
|
|
1355
|
+
if (Number(rows[0]?.value) !== 7) throw new Error("Restored Delta table lost its data");
|
|
1356
|
+
return { restoredRows: rows.length, deepClone: true };
|
|
1357
|
+
} finally {
|
|
1358
|
+
await warehouse.exec(`DROP TABLE IF EXISTS ${source}`).catch(() => void 0);
|
|
1359
|
+
await warehouse.exec(`DROP TABLE IF EXISTS ${backup}`).catch(() => void 0);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function rollbackCheck() {
|
|
1365
|
+
return {
|
|
1366
|
+
id: "rollback",
|
|
1367
|
+
description: "Delta time-travel rollback restores a known-good table version",
|
|
1368
|
+
async run({ warehouse }) {
|
|
1369
|
+
const table = warehouse.qual(safeName("rollback_probe"));
|
|
1370
|
+
try {
|
|
1371
|
+
await warehouse.exec(`CREATE TABLE ${table} (value INT) USING DELTA`);
|
|
1372
|
+
await warehouse.exec(`INSERT INTO ${table} VALUES (1)`);
|
|
1373
|
+
const history = await warehouse.query(`DESCRIBE HISTORY ${table} LIMIT 1`);
|
|
1374
|
+
const knownGoodVersion = Number(history[0]?.version);
|
|
1375
|
+
if (!Number.isInteger(knownGoodVersion)) {
|
|
1376
|
+
throw new Error("Delta history did not return a known-good version");
|
|
1377
|
+
}
|
|
1378
|
+
await warehouse.exec(`INSERT INTO ${table} VALUES (999)`);
|
|
1379
|
+
await warehouse.exec(`RESTORE TABLE ${table} TO VERSION AS OF ${knownGoodVersion}`);
|
|
1380
|
+
const rows = await warehouse.query(`SELECT value FROM ${table} ORDER BY value`);
|
|
1381
|
+
if (rows.length !== 1 || Number(rows[0]?.value) !== 1) {
|
|
1382
|
+
throw new Error("Delta rollback did not restore the known-good contents");
|
|
1383
|
+
}
|
|
1384
|
+
return { knownGoodVersion, restoredRows: rows.length };
|
|
1385
|
+
} finally {
|
|
1386
|
+
await warehouse.exec(`DROP TABLE IF EXISTS ${table}`).catch(() => void 0);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function serverlessComputeCheck() {
|
|
1392
|
+
return {
|
|
1393
|
+
id: "azure-serverless",
|
|
1394
|
+
description: "Azure Databricks serverless SQL warehouse is enabled and executes SQL",
|
|
1395
|
+
configured: (env) => Boolean(env.DATABRICKS_WAREHOUSE_ID),
|
|
1396
|
+
async run({ env, client, warehouse }) {
|
|
1397
|
+
const id = env.DATABRICKS_WAREHOUSE_ID;
|
|
1398
|
+
const details = await client.get(`/api/2.0/sql/warehouses/${encodeURIComponent(id)}`);
|
|
1399
|
+
if (details.enable_serverless_compute !== true) {
|
|
1400
|
+
throw new Error(`SQL warehouse ${id} is not configured for serverless compute`);
|
|
1401
|
+
}
|
|
1402
|
+
const rows = await warehouse.query("SELECT current_catalog() AS catalog");
|
|
1403
|
+
return { warehouseId: id, name: details.name, state: details.state, rows: rows.length };
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
function classicComputeCheck() {
|
|
1408
|
+
return {
|
|
1409
|
+
id: "azure-classic",
|
|
1410
|
+
description: "Azure Databricks classic job cluster starts and completes a notebook workload",
|
|
1411
|
+
configured: (env) => Boolean(
|
|
1412
|
+
env.DBX_TEST_CLASSIC_SPARK_VERSION && env.DBX_TEST_CLASSIC_NODE_TYPE && env.DBX_TEST_NOTEBOOK_PATH
|
|
1413
|
+
),
|
|
1414
|
+
async run({ env, client }) {
|
|
1415
|
+
const submitted = await client.post("/api/2.1/jobs/runs/submit", {
|
|
1416
|
+
run_name: `fabric-experiments-classic-certification-${Date.now()}`,
|
|
1417
|
+
tasks: [
|
|
1418
|
+
{
|
|
1419
|
+
task_key: "classic_notebook",
|
|
1420
|
+
new_cluster: {
|
|
1421
|
+
spark_version: env.DBX_TEST_CLASSIC_SPARK_VERSION,
|
|
1422
|
+
node_type_id: env.DBX_TEST_CLASSIC_NODE_TYPE,
|
|
1423
|
+
num_workers: Number(env.DBX_TEST_CLASSIC_WORKERS ?? 1),
|
|
1424
|
+
autotermination_minutes: 10,
|
|
1425
|
+
data_security_mode: env.DBX_TEST_CLASSIC_SECURITY_MODE ?? "SINGLE_USER"
|
|
1426
|
+
},
|
|
1427
|
+
notebook_task: { notebook_path: env.DBX_TEST_NOTEBOOK_PATH }
|
|
1428
|
+
}
|
|
1429
|
+
]
|
|
1430
|
+
});
|
|
1431
|
+
if (!submitted.run_id) throw new Error("Classic compute submit returned no run_id");
|
|
1432
|
+
const run = await databricks.waitForDatabricksRun(client, submitted.run_id, {
|
|
1433
|
+
timeoutMs: Number(env.DBX_TEST_CLASSIC_TIMEOUT_MS ?? 18e5),
|
|
1434
|
+
pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 15e3)
|
|
1435
|
+
});
|
|
1436
|
+
const result = run.state?.result_state;
|
|
1437
|
+
if (result !== "SUCCESS") {
|
|
1438
|
+
throw new Error(`Classic compute run ended in ${result ?? "UNKNOWN"}`);
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
runId: submitted.run_id,
|
|
1442
|
+
sparkVersion: env.DBX_TEST_CLASSIC_SPARK_VERSION,
|
|
1443
|
+
nodeType: env.DBX_TEST_CLASSIC_NODE_TYPE
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1259
1448
|
|
|
1260
1449
|
Object.defineProperty(exports, "waitForDatabricksRun", {
|
|
1261
1450
|
enumerable: true,
|
|
@@ -1268,6 +1457,8 @@ Object.defineProperty(exports, "waitForDatabricksStatement", {
|
|
|
1268
1457
|
exports.appHealthCheck = appHealthCheck;
|
|
1269
1458
|
exports.assertDeltaContract = assertDeltaContract;
|
|
1270
1459
|
exports.autoLoaderIngestionCheck = autoLoaderIngestionCheck;
|
|
1460
|
+
exports.backupRestoreCheck = backupRestoreCheck;
|
|
1461
|
+
exports.classicComputeCheck = classicComputeCheck;
|
|
1271
1462
|
exports.compareToGolden = compareToGolden;
|
|
1272
1463
|
exports.createClientFromEnv = createClientFromEnv;
|
|
1273
1464
|
exports.createDuckDbContext = createDuckDbContext;
|
|
@@ -1285,6 +1476,7 @@ exports.deltaContractCheck = deltaContractCheck;
|
|
|
1285
1476
|
exports.ensureVolumeDirectory = ensureVolumeDirectory;
|
|
1286
1477
|
exports.expectQueryToMatchGolden = expectQueryToMatchGolden;
|
|
1287
1478
|
exports.expectTable = expectTable;
|
|
1479
|
+
exports.failureRecoveryCheck = failureRecoveryCheck;
|
|
1288
1480
|
exports.jobRunCheck = jobRunCheck;
|
|
1289
1481
|
exports.lakebaseCredentialCheck = lakebaseCredentialCheck;
|
|
1290
1482
|
exports.lakebaseRoundTripCheck = lakebaseRoundTripCheck;
|
|
@@ -1293,6 +1485,7 @@ exports.normalizeVolumeRoot = normalizeVolumeRoot;
|
|
|
1293
1485
|
exports.notebookSubmitCheck = notebookSubmitCheck;
|
|
1294
1486
|
exports.parseFixtureColumns = parseFixtureColumns;
|
|
1295
1487
|
exports.parseStatementRows = parseStatementRows;
|
|
1488
|
+
exports.performanceBudgetCheck = performanceBudgetCheck;
|
|
1296
1489
|
exports.pipelineRefreshCheck = pipelineRefreshCheck;
|
|
1297
1490
|
exports.renderJUnit = renderJUnit;
|
|
1298
1491
|
exports.resolveFixtureRows = resolveFixtureRows;
|
|
@@ -1300,8 +1493,11 @@ exports.resolveLakebaseProject = resolveLakebaseProject;
|
|
|
1300
1493
|
exports.resolveProfile = resolveProfile;
|
|
1301
1494
|
exports.resolveTokenProvider = resolveTokenProvider;
|
|
1302
1495
|
exports.restrictedPrincipalCheck = restrictedPrincipalCheck;
|
|
1496
|
+
exports.rollbackCheck = rollbackCheck;
|
|
1303
1497
|
exports.runLiveSuite = runLiveSuite;
|
|
1498
|
+
exports.secretRotationCheck = secretRotationCheck;
|
|
1304
1499
|
exports.secretScopeCheck = secretScopeCheck;
|
|
1500
|
+
exports.serverlessComputeCheck = serverlessComputeCheck;
|
|
1305
1501
|
exports.sqlRoundTripCheck = sqlRoundTripCheck;
|
|
1306
1502
|
exports.toLakebaseBranchId = toLakebaseBranchId;
|
|
1307
1503
|
exports.toNamedParameters = toNamedParameters;
|