mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 20:42:56 +08:00
Add native test-only workflow and durable full regression shards
This commit is contained in:
@@ -10,7 +10,7 @@ const { spawn } = require('node:child_process');
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const { startMarketplaceServer } = require('../../dist/plugins/marketplace');
|
||||
const repo = path.resolve(__dirname, '../..');
|
||||
const out = path.join(repo, '.qa/recovery/author-e2e');
|
||||
const out = path.resolve(process.env.AUTHOR_OUTPUT || path.join(repo, '.qa/recovery/author-e2e'));
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const lab = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-external-author-'));
|
||||
const author = path.join(lab, 'python-events');
|
||||
@@ -18,6 +18,7 @@ const project = path.join(lab, 'python-app');
|
||||
const commands = [];
|
||||
const env = { ...process.env, npm_config_cache: path.join(lab, 'npm-cache'), CODEGRAPH_TELEMETRY: '0', CODEGRAPH_PARSE_WORKERS: '2', CODEGRAPH_RESOLVE_WORKERS: '2', CODEGRAPH_PARALLEL_RESOLVE_MIN: '0' };
|
||||
async function run(command, args, cwd = lab) {
|
||||
if (command === 'npm') [command, args] = require('./platform-tools.cjs').npmCommand(args);
|
||||
const start = Date.now();
|
||||
// Keep the event loop responsive while the CLI downloads from our local registry.
|
||||
const result = await new Promise(resolve => {
|
||||
|
||||
@@ -73,7 +73,7 @@ async function cli(args, expected = 0) {
|
||||
assert.deepEqual(all.map(r => r.version), ['9.0.0', '1.10.0', '1.9.0', '0.5.0', '20.0.0-beta.1']);
|
||||
assert.equal(all[0].apiVersion, 1); checks.push('signed immutable versions; semantic catalog order independent of publication order');
|
||||
bridge = await startExtensionBridge(roots.slice(0, 2), registry);
|
||||
browser = await chromium.launch({ executablePath: '/opt/google/chrome/chrome', headless: true, args: ['--no-sandbox'] });
|
||||
browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || '/opt/google/chrome/chrome', headless: true, args: ['--no-sandbox'] });
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
page.setDefaultTimeout(60000);
|
||||
const errors = []; page.on('pageerror', e => errors.push(e.message));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Real SIGKILLs target ONLY children created by this disposable-project harness.
|
||||
// Forced termination targets ONLY children created by this disposable-project harness.
|
||||
// Node maps SIGKILL to TerminateProcess on Windows; retain the observed exit/signal.
|
||||
const fs = require('node:fs'), os = require('node:os'), path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawn } = require('node:child_process');
|
||||
@@ -75,8 +76,9 @@ if (process.argv[2] === '--child') {
|
||||
try { await Promise.race([c.reached, c.done.then(r => { throw new Error(`Child exited before ${boundary}: ${r.stderr}`); }), new Promise((_, reject) => timer = setTimeout(() => reject(new Error('Boundary timeout: ' + boundary)), 90000))]); }
|
||||
finally { clearTimeout(timer); }
|
||||
assert.ok(processes.has(c.processChild));
|
||||
c.processChild.kill('SIGKILL');
|
||||
const result = await c.done; assert.equal(result.signal, 'SIGKILL');
|
||||
assert.equal(c.processChild.kill('SIGKILL'), true);
|
||||
const result = await c.done;
|
||||
assert.ok(result.signal === 'SIGKILL' || (process.platform === 'win32' && result.exit !== 0), 'owned paused child must be forcibly terminated');
|
||||
return c.processChild.pid;
|
||||
}
|
||||
async function run(root, action = 'inspect', expected = 0, cli = false) {
|
||||
@@ -104,7 +106,7 @@ if (process.argv[2] === '--child') {
|
||||
for (const entry of entries) {
|
||||
const pkg = path.join(folder, 'packages', entry.integrity);
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'))).version, entry.version);
|
||||
assert.equal(trusted[pkg], packageDigest(pkg));
|
||||
assert.equal(trusted[fs.realpathSync(pkg)], packageDigest(pkg));
|
||||
}
|
||||
}
|
||||
function receipt(value) { rows.push(value); fs.writeFileSync(path.join(out, 'matrix.json'), JSON.stringify(rows, null, 2)); console.log(JSON.stringify(value)); }
|
||||
@@ -212,7 +214,7 @@ if (process.argv[2] === '--child') {
|
||||
assert.equal(reader.getNode(node.id).name, '/2.0.0');
|
||||
} finally { reader.close(); }
|
||||
receipt({ action: 'long-lived reader after fresh-process recovery', decision: 'old cached node invalidated by commit marker', passed: true });
|
||||
fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify({ platform: process.platform, node: process.version, rows, commands: commands.length }, null, 2));
|
||||
fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify({ platform: process.platform, arch: process.arch, node: process.version, termination: process.platform === 'win32' ? 'Node child.kill(SIGKILL) / TerminateProcess' : 'SIGKILL', rows, commands: commands.length }, null, 2));
|
||||
} finally {
|
||||
for (const c of processes) c.kill('SIGKILL');
|
||||
if (processes.size) await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
@@ -15,7 +15,7 @@ process.env.CODEGRAPH_PARALLEL_RESOLVE_MIN = '0';
|
||||
const roots = [];
|
||||
after(() => roots.forEach(root => fs.rmSync(root, { recursive: true, force: true })));
|
||||
function root() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-compiled-'));
|
||||
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cg-compiled-')));
|
||||
roots.push(dir);
|
||||
fs.writeFileSync(path.join(dir, 'app.ts'), 'export function entry() {}\nexport function handler() {}\n');
|
||||
return dir;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Real native filesystem/casing/separator checks, never a simulated OS switch.
|
||||
const fs = require('node:fs'), os = require('node:os'), path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const { CodeGraph, createExtensionProject, packExtension } = require('../../dist');
|
||||
const { ExtensionManager } = require('../../dist/plugins/manager');
|
||||
const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cg-native-paths-')));
|
||||
(async () => {
|
||||
try {
|
||||
const author = path.join(base, 'Mixed Case Author'), root = path.join(base, 'Mixed Case Python App');
|
||||
createExtensionProject(author, 'native-paths');
|
||||
fs.mkdirSync(path.join(root, 'Src'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'Checkout.events.yaml'), 'order.created: send_receipt\n');
|
||||
fs.writeFileSync(path.join(root, 'Src', 'Handlers.py'), 'def send_receipt():\n return "sent"\n');
|
||||
const manager = new ExtensionManager(root);
|
||||
await manager.install({ bytes: packExtension(author) });
|
||||
const alias = path.join(base, 'mixed case python app');
|
||||
const caseInsensitive = fs.existsSync(alias);
|
||||
const chosen = caseInsensitive ? alias : root;
|
||||
const nativeSeparated = path.normalize(chosen);
|
||||
let graph = await CodeGraph.open(nativeSeparated);
|
||||
try {
|
||||
const routes = graph.getNodesByKind('route'); assert.equal(routes.length, 1);
|
||||
const edges = graph.getOutgoingEdges(routes[0].id).filter(e => e.metadata?.synthesizedBy === 'native-paths');
|
||||
assert.equal(edges.length, 1); const handler = graph.getNode(edges[0].target);
|
||||
assert.equal(handler.name, 'send_receipt');
|
||||
assert.equal(path.basename(handler.filePath), 'Handlers.py');
|
||||
assert.ok(fs.existsSync(path.resolve(root, handler.filePath)));
|
||||
} finally { graph.close(); }
|
||||
const aliasManager = new ExtensionManager(nativeSeparated);
|
||||
await aliasManager.setEnabled('native-paths', false);
|
||||
graph = await CodeGraph.open(root); try { assert.deepEqual(graph.getNodesByKind('route'), []); } finally { graph.close(); }
|
||||
await aliasManager.setEnabled('native-paths', true);
|
||||
await aliasManager.remove('native-paths');
|
||||
graph = await CodeGraph.open(root); try { assert.deepEqual(graph.getNodesByKind('route'), []); } finally { graph.close(); }
|
||||
console.log(JSON.stringify({ platform: process.platform, arch: process.arch, separator: path.sep, caseInsensitive,
|
||||
aliasExercised: caseInsensitive, checks: ['mixed case and spaced project paths', 'nested native file separators', 'actual event-to-handler graph edge', 'case alias when supported by real filesystem', 'disable/enable/remove through selected path'] }));
|
||||
} finally { fs.rmSync(base, { recursive: true, force: true }); }
|
||||
})().catch(error => { console.error(error.stack); process.exitCode = 1; });
|
||||
@@ -0,0 +1,57 @@
|
||||
// Test-only native runner: durable receipts, real OS metadata, no release commands.
|
||||
const fs = require('node:fs'), path = require('node:path'), os = require('node:os');
|
||||
const { spawn, execFileSync } = require('node:child_process');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { npmCommand } = require('./platform-tools.cjs');
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const out = path.resolve(process.env.NATIVE_OUTPUT || `.qa/native/${process.platform}-${process.arch}`);
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const version = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim();
|
||||
const metadata = { revision: version, platform: process.platform, arch: process.arch, release: os.release(),
|
||||
node: process.version, runnerOS: process.env.RUNNER_OS, runnerArch: process.env.RUNNER_ARCH,
|
||||
imageOS: process.env.ImageOS, imageVersion: process.env.ImageVersion,
|
||||
githubRun: process.env.GITHUB_RUN_ID, githubAttempt: process.env.GITHUB_RUN_ATTEMPT,
|
||||
started: new Date().toISOString(), steps: [] };
|
||||
function save() { fs.writeFileSync(path.join(out, 'summary.json.tmp'), JSON.stringify(metadata, null, 2)); fs.renameSync(path.join(out, 'summary.json.tmp'), path.join(out, 'summary.json')); }
|
||||
save();
|
||||
const env = { ...process.env, CODEGRAPH_TELEMETRY: '0', CODEGRAPH_WASM_RELAUNCHED: '1', CODEGRAPH_PARSE_WORKERS: '2', CODEGRAPH_RESOLVE_WORKERS: '2', CODEGRAPH_PARALLEL_RESOLVE_MIN: '0',
|
||||
RECOVERY_OUTPUT: path.join(out, 'recovery'), AUTHOR_OUTPUT: path.join(out, 'author'), COMPAT_OUTPUT: path.join(out, 'compatibility'), BROWSER_OUTPUT: path.join(out, 'browser') };
|
||||
const playwright = process.env.PLAYWRIGHT_MODULE || path.join(root, '.qa/browser/node_modules/playwright');
|
||||
env.PLAYWRIGHT_MODULE = playwright;
|
||||
env.CHROME_PATH = process.env.CHROME_PATH || require(playwright).chromium.executablePath();
|
||||
async function run(name, command, args, timeout = 360000) {
|
||||
const step = { name, command: [command, ...args], cwd: root, revision: version, started: new Date().toISOString(), status: 'running' };
|
||||
metadata.steps.push(step); save(); console.log('START', name);
|
||||
const logFile = path.join(out, name + '.log'); const log = fs.openSync(logFile, 'w');
|
||||
const started = Date.now();
|
||||
const result = await new Promise(resolve => {
|
||||
const child = spawn(command, args, { cwd: root, env, stdio: ['ignore', log, log], timeout });
|
||||
let error;
|
||||
child.on('error', e => error = String(e));
|
||||
child.on('close', (exit, signal) => resolve({ exit, signal, error }));
|
||||
});
|
||||
fs.fsyncSync(log); fs.closeSync(log);
|
||||
Object.assign(step, result, { status: result.exit === 0 ? 'passed' : 'failed', seconds: (Date.now() - started) / 1000,
|
||||
finished: new Date().toISOString(), log: name + '.log', sha256: createHash('sha256').update(fs.readFileSync(logFile)).digest('hex') });
|
||||
save(); console.log('END', name, step.status, step.seconds);
|
||||
if (step.status === 'failed') console.log(fs.readFileSync(logFile, 'utf8').split('\n').slice(-65).join('\n'));
|
||||
return step.status === 'passed';
|
||||
}
|
||||
(async () => {
|
||||
const built = await run('build', ...npmCommand(['run', 'build']));
|
||||
if (built) {
|
||||
await run('marketplace-build', ...npmCommand(['run', 'build', '--prefix', 'marketplace']));
|
||||
await run('focused', process.execPath, ['node_modules/vitest/vitest.mjs', 'run',
|
||||
'__tests__/extension-releases.test.ts', '__tests__/extension-marketplace.test.ts', '__tests__/extension-author.test.ts', '__tests__/plugins.test.ts', '__tests__/extension-explore.test.ts',
|
||||
'__tests__/db-reopen-on-replace.test.ts', '__tests__/status-json.test.ts', '__tests__/sync.test.ts', '__tests__/concurrent-locking.test.ts',
|
||||
'--maxWorkers=2', '--minWorkers=1', '--reporter=default', '--reporter=json', `--outputFile.json=${path.join(out, 'focused.json')}`]);
|
||||
await run('compiled-workers', process.execPath, ['--test', 'scripts/validation/extensions-runtime.test.cjs']);
|
||||
await run('native-paths', process.execPath, ['scripts/validation/native-paths.cjs']);
|
||||
await run('process-recovery', process.execPath, ['scripts/validation/extensions-recovery.cjs'], 600000);
|
||||
await run('external-author', process.execPath, ['scripts/validation/extension-author.cjs'], 600000);
|
||||
await run('compatibility', process.execPath, ['scripts/validation/extensions-compatibility.cjs']);
|
||||
await run('browser', process.execPath, ['scripts/validation/marketplace-browser.cjs']);
|
||||
} else metadata.blocked = 'Build failed; dependent checks were not executed';
|
||||
metadata.finished = new Date().toISOString(); metadata.success = built && metadata.steps.every(s => s.status === 'passed'); save();
|
||||
process.exitCode = metadata.success ? 0 : 1;
|
||||
})().catch(error => { metadata.failure = String(error.stack); save(); console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,14 @@
|
||||
// Validation-only helpers. Invoke npm's JS CLI directly (npm.cmd is not an
|
||||
// executable on Windows), without shell interpolation of project paths.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
function npmCommand(args) {
|
||||
const bin = path.dirname(process.execPath);
|
||||
const candidates = [process.env.npm_execpath, process.env.NPM_CLI,
|
||||
path.join(bin, 'node_modules/npm/bin/npm-cli.js'),
|
||||
path.resolve(bin, '../lib/node_modules/npm/bin/npm-cli.js')];
|
||||
const cli = candidates.find(p => p && p.endsWith('.js') && fs.existsSync(p));
|
||||
if (!cli) throw new Error('Cannot locate npm-cli.js. Set NPM_CLI to the installed npm JavaScript entry point.');
|
||||
return [process.execPath, [cli, ...args]];
|
||||
}
|
||||
module.exports = { npmCommand };
|
||||
@@ -0,0 +1,46 @@
|
||||
// Four sequential full-suite shards, resumable only with matching receipts.
|
||||
const fs = require('node:fs'), path = require('node:path'), os = require('node:os');
|
||||
const { spawn, execFileSync } = require('node:child_process');
|
||||
const { createHash } = require('node:crypto');
|
||||
const out = path.resolve(process.env.REGRESSION_OUTPUT || '.qa/native-linux'); fs.mkdirSync(out, { recursive: true });
|
||||
const revision = () => execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
||||
const source = revision(), results = [];
|
||||
const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
function write(file, data) { fs.writeFileSync(file + '.tmp', JSON.stringify(data, null, 2)); fs.renameSync(file + '.tmp', file); }
|
||||
function expectedFiles(dir) { return fs.readdirSync(dir, { withFileTypes: true }).flatMap(d => d.isDirectory() ? expectedFiles(path.join(dir, d.name)) : d.name.endsWith('.test.ts') ? [path.resolve(dir, d.name)] : []); }
|
||||
(async () => {
|
||||
for (let shard = 1; shard <= 4; shard++) {
|
||||
if (revision() !== source) throw new Error('HEAD changed during shards; completed receipts retained; refuse a mixed-revision total');
|
||||
const stem = path.join(out, `shard-${shard}`), report = stem + '.tests.json', file = stem + '.json', logFile = stem + '.log';
|
||||
if (process.argv.includes('--resume') && fs.existsSync(file)) {
|
||||
const saved = JSON.parse(fs.readFileSync(file));
|
||||
if (saved.revision === source && saved.exit === 0 && digest(report) === saved.reportHash && digest(logFile) === saved.logHash) {
|
||||
results.push(saved); console.log('REUSE', shard); continue;
|
||||
}
|
||||
throw new Error('Existing receipt is not a verified successful shard of this revision; choose a new REGRESSION_OUTPUT');
|
||||
}
|
||||
if (fs.existsSync(file) || fs.existsSync(stem + '.started.json')) throw new Error('Prior attempt exists; inspect it and select a new output directory rather than overwrite evidence');
|
||||
const args = ['node_modules/vitest/vitest.mjs', 'run', `--shard=${shard}/4`, '--maxWorkers=2', '--minWorkers=1', '--reporter=default', '--reporter=json', `--outputFile.json=${report}`];
|
||||
const receipt = { revision: source, shard, command: [process.execPath, ...args], platform: process.platform, arch: process.arch, node: process.version, release: os.release(), started: new Date().toISOString() };
|
||||
write(stem + '.started.json', receipt); const started = Date.now(), fd = fs.openSync(logFile, 'w'); console.log('START', shard);
|
||||
const status = await new Promise(resolve => {
|
||||
const child = spawn(process.execPath, args, { stdio: ['ignore', fd, fd], timeout: 600000 });
|
||||
let error; child.on('error', e => error = String(e)); child.on('close', (exit, signal) => resolve({ exit, signal, error }));
|
||||
});
|
||||
fs.fsyncSync(fd); fs.closeSync(fd);
|
||||
Object.assign(receipt, status, { seconds: (Date.now() - started) / 1000, finished: new Date().toISOString(), logHash: digest(logFile) });
|
||||
if (fs.existsSync(report)) {
|
||||
const r = JSON.parse(fs.readFileSync(report));
|
||||
Object.assign(receipt, { reportHash: digest(report), passed: r.numPassedTests, failed: r.numFailedTests, skipped: r.numPendingTests, files: r.testResults.map(f => f.name) });
|
||||
} else receipt.incomplete = 'Vitest JSON result missing';
|
||||
write(file, receipt); results.push(receipt); write(path.join(out, 'checkpoint.json'), { revision: source, results });
|
||||
console.log('END', shard, JSON.stringify(receipt));
|
||||
}
|
||||
const expected = expectedFiles('__tests__').sort(), actual = results.flatMap(r => r.files || []).sort();
|
||||
const counts = actual.reduce((m, f) => (m[f] = (m[f] || 0) + 1, m), {});
|
||||
const missing = expected.filter(f => !counts[f]), duplicate = Object.keys(counts).filter(f => counts[f] !== 1), unexpected = actual.filter(f => !expected.includes(f));
|
||||
const aggregate = { revision: source, results, expectedFiles: expected.length, observedFiles: actual.length, missing, duplicate, unexpected,
|
||||
passed: results.reduce((n, r) => n + (r.passed || 0), 0), failed: results.reduce((n, r) => n + (r.failed || 0), 0), skipped: results.reduce((n, r) => n + (r.skipped || 0), 0) };
|
||||
aggregate.success = results.length === 4 && results.every(r => r.exit === 0 && !r.incomplete) && !missing.length && !duplicate.length && !unexpected.length;
|
||||
write(path.join(out, 'result.json'), aggregate); console.log(JSON.stringify(aggregate)); process.exitCode = aggregate.success ? 0 : 1;
|
||||
})().catch(error => { write(path.join(out, 'incomplete.json'), { revision: source, error: String(error.stack), results }); console.error(error); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user