chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Report generation for RTK integration test results.
|
||||
*/
|
||||
|
||||
import type { TestResult } from "./test";
|
||||
import { getCounts, getResults } from "./test";
|
||||
|
||||
interface BuildInfo {
|
||||
buildTime: number;
|
||||
binarySize: number;
|
||||
version: string;
|
||||
branch: string;
|
||||
commit: string;
|
||||
}
|
||||
|
||||
export function generateReport(buildInfo: BuildInfo): string {
|
||||
const { total, passed, failed, skipped } = getCounts();
|
||||
const results = getResults();
|
||||
const passRate = total > 0 ? Math.round((passed * 100) / total) : 0;
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("======================================================");
|
||||
lines.push(" RTK INTEGRATION TEST REPORT");
|
||||
lines.push("======================================================");
|
||||
lines.push("");
|
||||
lines.push(`Date: ${new Date().toISOString()}`);
|
||||
lines.push(`Branch: ${buildInfo.branch}`);
|
||||
lines.push(`Commit: ${buildInfo.commit}`);
|
||||
lines.push(`Version: ${buildInfo.version}`);
|
||||
lines.push(`Binary: ${buildInfo.binarySize} bytes`);
|
||||
lines.push(`Build: ${buildInfo.buildTime}s`);
|
||||
lines.push("");
|
||||
|
||||
// Summary
|
||||
lines.push("--- Summary ---");
|
||||
lines.push(`Total: ${total}`);
|
||||
lines.push(`Passed: ${passed} (${passRate}%)`);
|
||||
lines.push(`Failed: ${failed}`);
|
||||
lines.push(`Skipped: ${skipped}`);
|
||||
lines.push("");
|
||||
|
||||
// Group results by phase (name prefix before ":")
|
||||
const phases = new Map<string, TestResult[]>();
|
||||
for (const r of results) {
|
||||
const colonIdx = r.name.indexOf(":");
|
||||
const phase = colonIdx > 0 ? r.name.slice(0, colonIdx) : "misc";
|
||||
if (!phases.has(phase)) phases.set(phase, []);
|
||||
phases.get(phase)!.push(r);
|
||||
}
|
||||
|
||||
for (const [phase, phaseResults] of phases) {
|
||||
const pPassed = phaseResults.filter((r) => r.status === "PASS").length;
|
||||
const pTotal = phaseResults.length;
|
||||
lines.push(`--- ${phase} (${pPassed}/${pTotal}) ---`);
|
||||
|
||||
for (const r of phaseResults) {
|
||||
const shortName = r.name.includes(":") ? r.name.split(":")[1] : r.name;
|
||||
lines.push(` ${r.status.padEnd(4)} | ${shortName} | ${r.detail}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// Failures detail
|
||||
const failures = results.filter((r) => r.status === "FAIL");
|
||||
if (failures.length > 0) {
|
||||
lines.push("--- Failures ---");
|
||||
for (const f of failures) {
|
||||
lines.push(` ${f.name}: ${f.detail}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// Token savings summary
|
||||
const savingsResults = results.filter((r) => r.savings !== undefined);
|
||||
if (savingsResults.length > 0) {
|
||||
const avgSavings = Math.round(
|
||||
savingsResults.reduce((sum, r) => sum + (r.savings ?? 0), 0) /
|
||||
savingsResults.length
|
||||
);
|
||||
const minSavings = Math.min(
|
||||
...savingsResults.map((r) => r.savings ?? 100)
|
||||
);
|
||||
const maxSavings = Math.max(...savingsResults.map((r) => r.savings ?? 0));
|
||||
lines.push("--- Token Savings ---");
|
||||
lines.push(`Average: ${avgSavings}%`);
|
||||
lines.push(`Min: ${minSavings}%`);
|
||||
lines.push(`Max: ${maxSavings}%`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// Verdict
|
||||
lines.push("======================================================");
|
||||
if (failed === 0) {
|
||||
lines.push(" Verdict: READY FOR RELEASE");
|
||||
} else {
|
||||
lines.push(` Verdict: NOT READY (${failed} failures)`);
|
||||
}
|
||||
lines.push("======================================================");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Save report to file */
|
||||
export async function saveReport(
|
||||
buildInfo: BuildInfo,
|
||||
outPath: string
|
||||
): Promise<string> {
|
||||
const report = generateReport(buildInfo);
|
||||
await Bun.write(outPath, report);
|
||||
console.log(`\nReport saved to: ${outPath}`);
|
||||
return report;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Test helpers for RTK integration testing.
|
||||
*/
|
||||
|
||||
import { vmExec, RTK_BIN } from "./vm";
|
||||
|
||||
export type TestStatus = "PASS" | "FAIL" | "SKIP";
|
||||
|
||||
export interface TestResult {
|
||||
name: string;
|
||||
status: TestStatus;
|
||||
detail: string;
|
||||
exitCode?: number;
|
||||
outputSize?: number;
|
||||
savings?: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
const results: TestResult[] = [];
|
||||
|
||||
export function getResults(): TestResult[] {
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getCounts() {
|
||||
const total = results.length;
|
||||
const passed = results.filter((r) => r.status === "PASS").length;
|
||||
const failed = results.filter((r) => r.status === "FAIL").length;
|
||||
const skipped = results.filter((r) => r.status === "SKIP").length;
|
||||
return { total, passed, failed, skipped };
|
||||
}
|
||||
|
||||
function record(result: TestResult) {
|
||||
results.push(result);
|
||||
const icon =
|
||||
result.status === "PASS"
|
||||
? "\x1b[32mPASS\x1b[0m"
|
||||
: result.status === "FAIL"
|
||||
? "\x1b[31mFAIL\x1b[0m"
|
||||
: "\x1b[33mSKIP\x1b[0m";
|
||||
console.log(` ${icon} | ${result.name} | ${result.detail}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a command exits with expected code and doesn't crash.
|
||||
* expectedExit: number or "any" (just checks no signal death)
|
||||
*/
|
||||
export async function testCmd(
|
||||
name: string,
|
||||
cmd: string,
|
||||
expectedExit: number | "any" = 0
|
||||
): Promise<TestResult> {
|
||||
const start = Date.now();
|
||||
const { stdout, stderr, exitCode } = await vmExec(cmd);
|
||||
const duration = Date.now() - start;
|
||||
const outputSize = stdout.length + stderr.length;
|
||||
|
||||
let status: TestStatus;
|
||||
let detail: string;
|
||||
|
||||
if (expectedExit === "any") {
|
||||
// Just check it didn't die from signal (exit >= 128)
|
||||
if (exitCode < 128) {
|
||||
status = "PASS";
|
||||
detail = `exit=${exitCode} | ${outputSize}b | ${duration}ms`;
|
||||
} else {
|
||||
status = "FAIL";
|
||||
detail = `SIGNAL exit=${exitCode} | ${outputSize}b`;
|
||||
}
|
||||
} else if (exitCode === expectedExit) {
|
||||
status = "PASS";
|
||||
detail = `exit=${exitCode} | ${outputSize}b | ${duration}ms`;
|
||||
} else {
|
||||
status = "FAIL";
|
||||
detail = `expected exit=${expectedExit}, got ${exitCode} | ${outputSize}b`;
|
||||
}
|
||||
|
||||
const result: TestResult = {
|
||||
name,
|
||||
status,
|
||||
detail,
|
||||
exitCode,
|
||||
outputSize,
|
||||
duration,
|
||||
};
|
||||
record(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test token savings: compare raw command output vs RTK filtered output.
|
||||
*/
|
||||
export async function testSavings(
|
||||
name: string,
|
||||
rawCmd: string,
|
||||
rtkCmd: string,
|
||||
targetPct: number
|
||||
): Promise<TestResult> {
|
||||
const raw = await vmExec(rawCmd);
|
||||
const rtk = await vmExec(rtkCmd);
|
||||
|
||||
const rawSize = raw.stdout.length;
|
||||
const rtkSize = rtk.stdout.length;
|
||||
|
||||
if (rawSize === 0) {
|
||||
const result: TestResult = {
|
||||
name,
|
||||
status: "SKIP",
|
||||
detail: "raw output empty",
|
||||
};
|
||||
record(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
const savings = Math.round(100 - (rtkSize * 100) / rawSize);
|
||||
|
||||
let status: TestStatus;
|
||||
let detail: string;
|
||||
|
||||
if (savings >= targetPct) {
|
||||
status = "PASS";
|
||||
detail = `raw=${rawSize}b filtered=${rtkSize}b savings=${savings}% (target: >=${targetPct}%)`;
|
||||
} else {
|
||||
status = "FAIL";
|
||||
detail = `savings=${savings}% < target ${targetPct}% (raw=${rawSize}b filtered=${rtkSize}b)`;
|
||||
}
|
||||
|
||||
const result: TestResult = { name, status, detail, savings };
|
||||
record(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test rewrite engine: input -> expected output.
|
||||
*/
|
||||
export async function testRewrite(
|
||||
input: string,
|
||||
expected: string
|
||||
): Promise<TestResult> {
|
||||
const escaped = input.replace(/'/g, "'\\''");
|
||||
const { stdout } = await vmExec(`${RTK_BIN} rewrite '${escaped}'`);
|
||||
const actual = stdout.trim();
|
||||
|
||||
let status: TestStatus;
|
||||
let detail: string;
|
||||
|
||||
if (actual === expected) {
|
||||
status = "PASS";
|
||||
detail = `'${input}' -> '${actual}'`;
|
||||
} else {
|
||||
status = "FAIL";
|
||||
detail = `'${input}' -> expected '${expected}', got '${actual}'`;
|
||||
}
|
||||
|
||||
const result: TestResult = { name: `rewrite: ${input}`, status, detail };
|
||||
record(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a test with a reason.
|
||||
*/
|
||||
export function skipTest(name: string, reason: string): TestResult {
|
||||
const result: TestResult = { name, status: "SKIP", detail: reason };
|
||||
record(result);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Multipass VM management for RTK integration testing.
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
|
||||
const VM_NAME = "rtk-test";
|
||||
const CLOUD_INIT = "scripts/benchmark/cloud-init.yaml";
|
||||
|
||||
export interface VmInfo {
|
||||
name: string;
|
||||
state: string;
|
||||
ipv4: string;
|
||||
}
|
||||
|
||||
/** Check if VM exists and is running */
|
||||
export async function vmExists(): Promise<boolean> {
|
||||
const result = await $`multipass list --format json`.quiet();
|
||||
const data = JSON.parse(result.stdout.toString());
|
||||
return data.list?.some((vm: VmInfo) => vm.name === VM_NAME) ?? false;
|
||||
}
|
||||
|
||||
/** Check if VM is running */
|
||||
export async function vmRunning(): Promise<boolean> {
|
||||
const result = await $`multipass list --format json`.quiet();
|
||||
const data = JSON.parse(result.stdout.toString());
|
||||
const vm = data.list?.find((v: VmInfo) => v.name === VM_NAME);
|
||||
return vm?.state === "Running";
|
||||
}
|
||||
|
||||
/** Create a new VM with cloud-init (20 min timeout for full provisioning) */
|
||||
export async function vmCreate(): Promise<void> {
|
||||
console.log(`[vm] Creating ${VM_NAME} with cloud-init (this takes ~10-15 min)...`);
|
||||
// --timeout 1200 = 20 min for cloud-init to finish installing Rust, Go, Node, .NET, etc.
|
||||
await $`multipass launch --name ${VM_NAME} --cpus 2 --memory 4G --disk 20G --timeout 1200 --cloud-init ${CLOUD_INIT} 24.04`;
|
||||
}
|
||||
|
||||
/** Start existing VM */
|
||||
export async function vmStart(): Promise<void> {
|
||||
console.log(`[vm] Starting ${VM_NAME}...`);
|
||||
await $`multipass start ${VM_NAME}`;
|
||||
}
|
||||
|
||||
/** Execute a command in the VM, returns stdout (60s timeout per test by default) */
|
||||
export async function vmExec(
|
||||
cmd: string,
|
||||
timeoutMs = 60_000
|
||||
): Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}> {
|
||||
const exec = $`multipass exec ${VM_NAME} -- bash -c ${cmd}`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.then((r) => ({
|
||||
stdout: r.stdout.toString(),
|
||||
stderr: r.stderr.toString(),
|
||||
exitCode: r.exitCode,
|
||||
}));
|
||||
|
||||
const timeout = new Promise<{ stdout: string; stderr: string; exitCode: number }>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`vmExec timed out after ${timeoutMs}ms: ${cmd}`)), timeoutMs)
|
||||
);
|
||||
|
||||
return Promise.race([exec, timeout]);
|
||||
}
|
||||
|
||||
/** Transfer a file to the VM */
|
||||
export async function vmTransfer(
|
||||
localPath: string,
|
||||
remotePath: string
|
||||
): Promise<void> {
|
||||
await $`multipass transfer ${localPath} ${VM_NAME}:${remotePath}`;
|
||||
}
|
||||
|
||||
/** Wait for cloud-init to complete (max 40 min — installs Rust, Go, Node, .NET, etc.) */
|
||||
export async function vmWaitReady(maxWaitSec = 2400): Promise<boolean> {
|
||||
console.log("[vm] Waiting for cloud-init...");
|
||||
const start = Date.now();
|
||||
while ((Date.now() - start) / 1000 < maxWaitSec) {
|
||||
const { exitCode } = await vmExec(
|
||||
"test -f /home/ubuntu/.cloud-init-complete"
|
||||
);
|
||||
if (exitCode === 0) {
|
||||
const elapsed = Math.round((Date.now() - start) / 1000);
|
||||
console.log(`[vm] Cloud-init complete after ${elapsed}s`);
|
||||
return true;
|
||||
}
|
||||
await Bun.sleep(10_000);
|
||||
}
|
||||
console.error("[vm] Cloud-init timed out!");
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Transfer RTK source and build in release mode */
|
||||
export async function vmBuildRtk(projectRoot: string): Promise<{
|
||||
buildTime: number;
|
||||
binarySize: number;
|
||||
version: string;
|
||||
}> {
|
||||
console.log("[vm] Transferring RTK source...");
|
||||
|
||||
// Create tarball excluding heavy dirs and macOS resource forks (._*)
|
||||
await $`COPYFILE_DISABLE=1 tar czf /tmp/rtk-src.tar.gz --exclude target --exclude .git --exclude node_modules --exclude "index.html*" --exclude "._*" -C ${projectRoot} .`;
|
||||
await vmTransfer("/tmp/rtk-src.tar.gz", "/tmp/rtk-src.tar.gz");
|
||||
await vmExec(
|
||||
"mkdir -p /home/ubuntu/rtk && cd /home/ubuntu/rtk && tar xzf /tmp/rtk-src.tar.gz"
|
||||
);
|
||||
|
||||
console.log("[vm] Building RTK (release)...");
|
||||
const start = Date.now();
|
||||
const { stdout, exitCode } = await vmExec(
|
||||
"export PATH=$HOME/.cargo/bin:$PATH && cd /home/ubuntu/rtk && cargo build --release 2>&1 | tail -5"
|
||||
);
|
||||
const buildTime = Math.round((Date.now() - start) / 1000);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`Build failed:\n${stdout}`);
|
||||
}
|
||||
|
||||
const { stdout: sizeStr } = await vmExec(
|
||||
"stat -c%s /home/ubuntu/rtk/target/release/rtk"
|
||||
);
|
||||
const binarySize = parseInt(sizeStr.trim(), 10);
|
||||
|
||||
const { stdout: version } = await vmExec(
|
||||
"/home/ubuntu/rtk/target/release/rtk --version"
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[vm] Build OK in ${buildTime}s — ${binarySize} bytes — ${version.trim()}`
|
||||
);
|
||||
|
||||
return { buildTime, binarySize, version: version.trim() };
|
||||
}
|
||||
|
||||
/** Delete the VM */
|
||||
export async function vmDelete(): Promise<void> {
|
||||
console.log(`[vm] Deleting ${VM_NAME}...`);
|
||||
await $`multipass delete ${VM_NAME} --purge`.nothrow();
|
||||
}
|
||||
|
||||
/** Ensure VM is ready (create or reuse) */
|
||||
export async function vmEnsureReady(): Promise<void> {
|
||||
if (await vmExists()) {
|
||||
if (!(await vmRunning())) {
|
||||
await vmStart();
|
||||
}
|
||||
console.log(`[vm] Reusing existing VM ${VM_NAME}`);
|
||||
// Check if cloud-init is still running
|
||||
const { exitCode } = await vmExec(
|
||||
"test -f /home/ubuntu/.cloud-init-complete"
|
||||
);
|
||||
if (exitCode !== 0) {
|
||||
console.log("[vm] Cloud-init still running, waiting...");
|
||||
const ready = await vmWaitReady();
|
||||
if (!ready) {
|
||||
throw new Error(
|
||||
"Cloud-init timed out. Check: multipass exec rtk-test -- cat /var/log/cloud-init-output.log"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await vmCreate();
|
||||
// multipass launch --timeout should wait, but double-check
|
||||
const { exitCode } = await vmExec(
|
||||
"test -f /home/ubuntu/.cloud-init-complete"
|
||||
);
|
||||
if (exitCode !== 0) {
|
||||
const ready = await vmWaitReady();
|
||||
if (!ready) {
|
||||
throw new Error(
|
||||
"Cloud-init timed out. Check: multipass exec rtk-test -- cat /var/log/cloud-init-output.log"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const RTK_BIN = "/home/ubuntu/rtk/target/release/rtk";
|
||||
Reference in New Issue
Block a user