@fabricorg/databricks-testkit 0.2.0 → 0.2.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/dist/index.cjs +214 -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 +209 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1256,6 +1256,213 @@ 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 numWorkers = Number(env.DBX_TEST_CLASSIC_WORKERS ?? 1);
|
|
1416
|
+
const alternateNodeTypes = (env.DBX_TEST_CLASSIC_ALTERNATE_NODE_TYPES ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
1417
|
+
const singleNode = numWorkers === 0;
|
|
1418
|
+
const submitted = await client.post("/api/2.1/jobs/runs/submit", {
|
|
1419
|
+
run_name: `fabric-experiments-classic-certification-${Date.now()}`,
|
|
1420
|
+
tasks: [
|
|
1421
|
+
{
|
|
1422
|
+
task_key: "classic_notebook",
|
|
1423
|
+
new_cluster: {
|
|
1424
|
+
spark_version: env.DBX_TEST_CLASSIC_SPARK_VERSION,
|
|
1425
|
+
node_type_id: env.DBX_TEST_CLASSIC_NODE_TYPE,
|
|
1426
|
+
num_workers: numWorkers,
|
|
1427
|
+
data_security_mode: env.DBX_TEST_CLASSIC_SECURITY_MODE ?? "SINGLE_USER",
|
|
1428
|
+
...env.DBX_TEST_CLASSIC_SINGLE_USER_NAME ? { single_user_name: env.DBX_TEST_CLASSIC_SINGLE_USER_NAME } : {},
|
|
1429
|
+
...singleNode ? {
|
|
1430
|
+
spark_conf: {
|
|
1431
|
+
"spark.databricks.cluster.profile": "singleNode",
|
|
1432
|
+
"spark.master": "local[*]"
|
|
1433
|
+
},
|
|
1434
|
+
custom_tags: { ResourceClass: "SingleNode" }
|
|
1435
|
+
} : {},
|
|
1436
|
+
...alternateNodeTypes.length ? {
|
|
1437
|
+
worker_node_type_flexibility: {
|
|
1438
|
+
alternate_node_type_ids: alternateNodeTypes
|
|
1439
|
+
},
|
|
1440
|
+
driver_node_type_flexibility: {
|
|
1441
|
+
alternate_node_type_ids: alternateNodeTypes
|
|
1442
|
+
}
|
|
1443
|
+
} : {}
|
|
1444
|
+
},
|
|
1445
|
+
notebook_task: { notebook_path: env.DBX_TEST_NOTEBOOK_PATH }
|
|
1446
|
+
}
|
|
1447
|
+
]
|
|
1448
|
+
});
|
|
1449
|
+
if (!submitted.run_id) throw new Error("Classic compute submit returned no run_id");
|
|
1450
|
+
const run = await databricks.waitForDatabricksRun(client, submitted.run_id, {
|
|
1451
|
+
timeoutMs: Number(env.DBX_TEST_CLASSIC_TIMEOUT_MS ?? 18e5),
|
|
1452
|
+
pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 15e3)
|
|
1453
|
+
});
|
|
1454
|
+
const result = run.state?.result_state;
|
|
1455
|
+
if (result !== "SUCCESS") {
|
|
1456
|
+
throw new Error(`Classic compute run ended in ${result ?? "UNKNOWN"}`);
|
|
1457
|
+
}
|
|
1458
|
+
return {
|
|
1459
|
+
runId: submitted.run_id,
|
|
1460
|
+
sparkVersion: env.DBX_TEST_CLASSIC_SPARK_VERSION,
|
|
1461
|
+
nodeType: env.DBX_TEST_CLASSIC_NODE_TYPE
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1259
1466
|
|
|
1260
1467
|
Object.defineProperty(exports, "waitForDatabricksRun", {
|
|
1261
1468
|
enumerable: true,
|
|
@@ -1268,6 +1475,8 @@ Object.defineProperty(exports, "waitForDatabricksStatement", {
|
|
|
1268
1475
|
exports.appHealthCheck = appHealthCheck;
|
|
1269
1476
|
exports.assertDeltaContract = assertDeltaContract;
|
|
1270
1477
|
exports.autoLoaderIngestionCheck = autoLoaderIngestionCheck;
|
|
1478
|
+
exports.backupRestoreCheck = backupRestoreCheck;
|
|
1479
|
+
exports.classicComputeCheck = classicComputeCheck;
|
|
1271
1480
|
exports.compareToGolden = compareToGolden;
|
|
1272
1481
|
exports.createClientFromEnv = createClientFromEnv;
|
|
1273
1482
|
exports.createDuckDbContext = createDuckDbContext;
|
|
@@ -1285,6 +1494,7 @@ exports.deltaContractCheck = deltaContractCheck;
|
|
|
1285
1494
|
exports.ensureVolumeDirectory = ensureVolumeDirectory;
|
|
1286
1495
|
exports.expectQueryToMatchGolden = expectQueryToMatchGolden;
|
|
1287
1496
|
exports.expectTable = expectTable;
|
|
1497
|
+
exports.failureRecoveryCheck = failureRecoveryCheck;
|
|
1288
1498
|
exports.jobRunCheck = jobRunCheck;
|
|
1289
1499
|
exports.lakebaseCredentialCheck = lakebaseCredentialCheck;
|
|
1290
1500
|
exports.lakebaseRoundTripCheck = lakebaseRoundTripCheck;
|
|
@@ -1293,6 +1503,7 @@ exports.normalizeVolumeRoot = normalizeVolumeRoot;
|
|
|
1293
1503
|
exports.notebookSubmitCheck = notebookSubmitCheck;
|
|
1294
1504
|
exports.parseFixtureColumns = parseFixtureColumns;
|
|
1295
1505
|
exports.parseStatementRows = parseStatementRows;
|
|
1506
|
+
exports.performanceBudgetCheck = performanceBudgetCheck;
|
|
1296
1507
|
exports.pipelineRefreshCheck = pipelineRefreshCheck;
|
|
1297
1508
|
exports.renderJUnit = renderJUnit;
|
|
1298
1509
|
exports.resolveFixtureRows = resolveFixtureRows;
|
|
@@ -1300,8 +1511,11 @@ exports.resolveLakebaseProject = resolveLakebaseProject;
|
|
|
1300
1511
|
exports.resolveProfile = resolveProfile;
|
|
1301
1512
|
exports.resolveTokenProvider = resolveTokenProvider;
|
|
1302
1513
|
exports.restrictedPrincipalCheck = restrictedPrincipalCheck;
|
|
1514
|
+
exports.rollbackCheck = rollbackCheck;
|
|
1303
1515
|
exports.runLiveSuite = runLiveSuite;
|
|
1516
|
+
exports.secretRotationCheck = secretRotationCheck;
|
|
1304
1517
|
exports.secretScopeCheck = secretScopeCheck;
|
|
1518
|
+
exports.serverlessComputeCheck = serverlessComputeCheck;
|
|
1305
1519
|
exports.sqlRoundTripCheck = sqlRoundTripCheck;
|
|
1306
1520
|
exports.toLakebaseBranchId = toLakebaseBranchId;
|
|
1307
1521
|
exports.toNamedParameters = toNamedParameters;
|