mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-16 08:25:14 +02:00
test: verify managed runtime in public package smoke
This commit is contained in:
parent
f5e6698075
commit
ed00c9a0a3
2 changed files with 115 additions and 54 deletions
|
|
@ -4,7 +4,7 @@ import { createHash } from 'node:crypto';
|
|||
import { execFile } from 'node:child_process';
|
||||
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import {
|
||||
|
|
@ -591,6 +591,7 @@ if (typeof cli.runKtxCli !== 'function') {
|
|||
export function npmRuntimeSmokeSource() {
|
||||
return `
|
||||
import assert from 'node:assert/strict';
|
||||
import Database from 'better-sqlite3';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
|
@ -626,6 +627,15 @@ function requireSuccess(label, result) {
|
|||
assert.equal(result.stderr, '', label + ' wrote unexpected stderr');
|
||||
}
|
||||
|
||||
function requireSuccessWithStderr(label, result, stderrPattern) {
|
||||
assert.equal(
|
||||
result.code,
|
||||
0,
|
||||
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
||||
);
|
||||
assert.match(result.stderr, stderrPattern, label + ' stderr did not match ' + stderrPattern);
|
||||
}
|
||||
|
||||
function requireOutput(label, result, text) {
|
||||
assert.match(result.stdout, text, label + ' output did not match ' + text);
|
||||
}
|
||||
|
|
@ -653,30 +663,26 @@ function getRunId(stdout) {
|
|||
}
|
||||
|
||||
async function writeSqliteWarehouse(projectDir) {
|
||||
const createDb = await run('python', [
|
||||
'-c',
|
||||
[
|
||||
'import sqlite3',
|
||||
'import sys',
|
||||
'db_path = sys.argv[1]',
|
||||
'conn = sqlite3.connect(db_path)',
|
||||
'conn.executescript("""',
|
||||
'DROP TABLE IF EXISTS orders;',
|
||||
'CREATE TABLE orders (',
|
||||
' id INTEGER PRIMARY KEY,',
|
||||
' status TEXT NOT NULL,',
|
||||
' amount INTEGER NOT NULL',
|
||||
');',
|
||||
"INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10);",
|
||||
'""")',
|
||||
'conn.close()',
|
||||
].join('\\n'),
|
||||
join(projectDir, 'warehouse.db'),
|
||||
]);
|
||||
requireSuccess('create sqlite warehouse', createDb);
|
||||
const database = new Database(join(projectDir, 'warehouse.db'));
|
||||
try {
|
||||
database.exec(\`
|
||||
DROP TABLE IF EXISTS orders;
|
||||
CREATE TABLE orders (
|
||||
id INTEGER PRIMARY KEY,
|
||||
status TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL
|
||||
);
|
||||
INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10);
|
||||
\`);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-'));
|
||||
const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
|
||||
process.env.KTX_RUNTIME_ROOT = join(root, 'managed-runtime');
|
||||
let daemonStarted = false;
|
||||
try {
|
||||
const projectDir = join(root, 'project');
|
||||
const sourceDir = join(root, 'source');
|
||||
|
|
@ -685,6 +691,14 @@ try {
|
|||
requireSuccess('ktx public package version', version);
|
||||
requireOutput('ktx public package version', version, /@kaelio\\/ktx 0\\.0\\.0-private/);
|
||||
|
||||
const runtimeStatusBefore = parseJsonResult(
|
||||
'ktx runtime status missing',
|
||||
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
|
||||
);
|
||||
assert.equal(runtimeStatusBefore.kind, 'missing');
|
||||
assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
|
||||
process.stdout.write('ktx managed runtime starts missing in isolated root\\n');
|
||||
|
||||
const missingProjectDir = join(root, 'missing-project');
|
||||
await mkdir(missingProjectDir, { recursive: true });
|
||||
const missingProjectSearch = await run('pnpm', [
|
||||
|
|
@ -910,9 +924,22 @@ try {
|
|||
'--project-dir',
|
||||
projectDir,
|
||||
]);
|
||||
requireSuccess('ktx sl query', slQuery);
|
||||
requireOutput('ktx sl query', slQuery, /"mode": "compile_only"/);
|
||||
requireOutput('ktx sl query', slQuery, /orders/);
|
||||
requireSuccessWithStderr(
|
||||
'ktx sl query first managed runtime install',
|
||||
slQuery,
|
||||
/Installing KTX Python runtime \(core\) with uv[\\s\\S]*KTX Python runtime ready:/,
|
||||
);
|
||||
requireOutput('ktx sl query first managed runtime install', slQuery, /"mode": "compile_only"/);
|
||||
requireOutput('ktx sl query first managed runtime install', slQuery, /orders/);
|
||||
|
||||
const runtimeStatusAfter = parseJsonResult(
|
||||
'ktx runtime status ready',
|
||||
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
|
||||
);
|
||||
assert.equal(runtimeStatusAfter.kind, 'ready');
|
||||
assert.deepEqual(runtimeStatusAfter.manifest.features, ['core']);
|
||||
assert.equal(runtimeStatusAfter.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
|
||||
process.stdout.write('ktx managed runtime lazy install verified\\n');
|
||||
|
||||
const sqliteSlQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
|
||||
'--connection-id',
|
||||
|
|
@ -935,6 +962,31 @@ try {
|
|||
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"rows": \\[\\s*\\[\\s*3\\s*\\]\\s*\\]/);
|
||||
process.stdout.write('ktx sl query sqlite execute verified\\n');
|
||||
|
||||
const runtimeDoctor = await run('pnpm', ['exec', 'ktx', 'runtime', 'doctor']);
|
||||
requireSuccess('ktx runtime doctor', runtimeDoctor);
|
||||
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS uv/);
|
||||
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Bundled Python wheel/);
|
||||
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Managed Python runtime/);
|
||||
process.stdout.write('ktx runtime doctor verified\\n');
|
||||
|
||||
const runtimeStart = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
|
||||
requireSuccess('ktx runtime start', runtimeStart);
|
||||
daemonStarted = true;
|
||||
requireOutput('ktx runtime start', runtimeStart, /Started KTX Python daemon/);
|
||||
requireOutput('ktx runtime start', runtimeStart, /url: http:\\/\\/127\\.0\\.0\\.1:\\d+/);
|
||||
requireOutput('ktx runtime start', runtimeStart, /features: core/);
|
||||
|
||||
const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
|
||||
requireSuccess('ktx runtime start reuse', runtimeStartReuse);
|
||||
requireOutput('ktx runtime start reuse', runtimeStartReuse, /Using existing KTX Python daemon/);
|
||||
requireOutput('ktx runtime start reuse', runtimeStartReuse, /features: core/);
|
||||
|
||||
const runtimeStop = await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
|
||||
requireSuccess('ktx runtime stop', runtimeStop);
|
||||
daemonStarted = false;
|
||||
requireOutput('ktx runtime stop', runtimeStop, /Stopped KTX Python daemon/);
|
||||
process.stdout.write('ktx runtime daemon lifecycle verified\\n');
|
||||
|
||||
const structuralScan = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'warehouse',
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
|
|
@ -1016,6 +1068,14 @@ try {
|
|||
await access(join(projectDir, '.ktx', 'db.sqlite'));
|
||||
process.stdout.write('ktx dev ingest provider guard verified\\n');
|
||||
} finally {
|
||||
if (daemonStarted) {
|
||||
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
|
||||
}
|
||||
if (previousRuntimeRoot === undefined) {
|
||||
delete process.env.KTX_RUNTIME_ROOT;
|
||||
} else {
|
||||
process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
`;
|
||||
|
|
@ -1161,15 +1221,6 @@ function pythonExecutable(projectDir) {
|
|||
return join(projectDir, '.venv', 'bin', 'python');
|
||||
}
|
||||
|
||||
export function npmSmokePythonEnv(projectDir, baseEnv = process.env) {
|
||||
const binDir = process.platform === 'win32' ? join(projectDir, '.venv', 'Scripts') : join(projectDir, '.venv', 'bin');
|
||||
const existingPath = baseEnv.PATH ?? '';
|
||||
|
||||
return Object.assign({}, baseEnv, {
|
||||
PATH: existingPath ? `${binDir}${delimiter}${existingPath}` : binDir,
|
||||
});
|
||||
}
|
||||
|
||||
async function buildArtifacts(layout) {
|
||||
await rm(layout.artifactDir, { recursive: true, force: true });
|
||||
await mkdir(layout.npmDir, { recursive: true });
|
||||
|
|
@ -1202,10 +1253,8 @@ async function verifyNpmArtifacts(layout, tmpRoot) {
|
|||
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
|
||||
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
|
||||
}
|
||||
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
|
||||
|
||||
const projectDir = join(tmpRoot, 'npm-clean-install');
|
||||
const python = pythonExecutable(projectDir);
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(projectDir, 'package.json'),
|
||||
|
|
@ -1217,20 +1266,10 @@ async function verifyNpmArtifacts(layout, tmpRoot) {
|
|||
|
||||
await runCommand('pnpm', ['install'], { cwd: projectDir });
|
||||
await runCommand('pnpm', ['rebuild', 'better-sqlite3'], { cwd: projectDir });
|
||||
await runCommand('uv', ['venv', '.venv'], { cwd: projectDir });
|
||||
await runCommand('uv', pythonArtifactInstallArgs(python, pythonArtifacts), {
|
||||
cwd: projectDir,
|
||||
});
|
||||
await runCommand('node', ['verify-npm.mjs'], { cwd: projectDir });
|
||||
await runCommand('pnpm', ['exec', 'ktx', '--version'], { cwd: projectDir });
|
||||
await runCommand('node', ['verify-installed-cli.mjs'], {
|
||||
cwd: projectDir,
|
||||
env: npmSmokePythonEnv(projectDir),
|
||||
});
|
||||
await runCommand('node', ['verify-installed-demo.mjs'], {
|
||||
cwd: projectDir,
|
||||
env: npmSmokePythonEnv(projectDir),
|
||||
});
|
||||
await runCommand('node', ['verify-installed-cli.mjs'], { cwd: projectDir });
|
||||
await runCommand('node', ['verify-installed-demo.mjs'], { cwd: projectDir });
|
||||
}
|
||||
|
||||
async function verifyNpmDemoArtifacts(layout, tmpRoot) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import {
|
|||
npmDemoSmokeSource,
|
||||
npmRuntimeSmokeSource,
|
||||
npmSmokePackageJson,
|
||||
npmSmokePythonEnv,
|
||||
npmVerifySource,
|
||||
packageArtifactLayout,
|
||||
packageReleaseMetadata,
|
||||
|
|
@ -495,12 +494,18 @@ describe('pythonArtifactInstallArgs', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('npmSmokePythonEnv', () => {
|
||||
it('prepends the npm smoke virtualenv bin directory to PATH', () => {
|
||||
const env = npmSmokePythonEnv('/tmp/ktx-npm-smoke', { PATH: '/usr/bin' });
|
||||
describe('verifyNpmArtifacts', () => {
|
||||
it('does not prepare an external Python environment for the npm smoke', async () => {
|
||||
const source = await readFile(new URL('./package-artifacts.mjs', import.meta.url), 'utf8');
|
||||
const start = source.indexOf('async function verifyNpmArtifacts');
|
||||
const end = source.indexOf('async function verifyNpmDemoArtifacts');
|
||||
assert.ok(start > 0, 'verifyNpmArtifacts function must exist');
|
||||
assert.ok(end > start, 'verifyNpmDemoArtifacts must follow verifyNpmArtifacts');
|
||||
|
||||
assert.match(env.PATH, /^\/tmp\/ktx-npm-smoke\/\.venv\/(bin|Scripts)/);
|
||||
assert.match(env.PATH, /\/usr\/bin$/);
|
||||
const body = source.slice(start, end);
|
||||
assert.doesNotMatch(body, /uv', \['venv', '\.venv'\]/);
|
||||
assert.doesNotMatch(body, /pythonArtifactInstallArgs/);
|
||||
assert.doesNotMatch(body, /npmSmokePythonEnv/);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -557,6 +562,23 @@ describe('verification snippets', () => {
|
|||
assert.match(source, /"mode": "compile_only"/);
|
||||
assert.match(source, /"mode": "executed"/);
|
||||
assert.match(source, /ktx sl query sqlite execute/);
|
||||
assert.match(source, /import Database from 'better-sqlite3'/);
|
||||
assert.doesNotMatch(source, /run\('python'/);
|
||||
assert.match(source, /KTX_RUNTIME_ROOT/);
|
||||
assert.match(source, /managed-runtime/);
|
||||
assert.match(source, /ktx runtime status missing/);
|
||||
assert.match(source, /runtimeStatusBefore\.kind, 'missing'/);
|
||||
assert.match(source, /Installing KTX Python runtime \(core\) with uv/);
|
||||
assert.match(source, /KTX Python runtime ready:/);
|
||||
assert.match(source, /ktx runtime status ready/);
|
||||
assert.match(source, /runtimeStatusAfter\.kind, 'ready'/);
|
||||
assert.match(source, /runtimeStatusAfter\.manifest\.features/);
|
||||
assert.match(source, /ktx runtime doctor/);
|
||||
assert.match(source, /PASS Managed Python runtime/);
|
||||
assert.match(source, /ktx runtime start/);
|
||||
assert.match(source, /ktx runtime start reuse/);
|
||||
assert.match(source, /Using existing KTX Python daemon/);
|
||||
assert.match(source, /ktx runtime stop/);
|
||||
assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'dev',\s*'scan',\s*'warehouse'/);
|
||||
assert.match(source, /'--mode',\s*'enriched'/);
|
||||
assert.doesNotMatch(source, /'--enrich'/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue