agentic-qe
Version:
Agentic Quality Engineering Fleet System - AI-driven quality management platform
566 lines • 22.4 kB
JavaScript
;
/**
* TestFrameworkExecutor - Real test execution via child_process
*
* Replaces mock test execution with actual framework integration.
* Supports Jest, Mocha, Playwright with proper output parsing.
*
* @version 1.0.0
* @priority P0
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestFrameworkExecutor = void 0;
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const path = __importStar(require("path"));
/**
* Real test framework executor using child_process
*/
class TestFrameworkExecutor {
/**
* Execute tests with a specific framework
*/
async execute(config) {
const startTime = Date.now();
const command = TestFrameworkExecutor.FRAMEWORK_COMMANDS[config.framework];
const args = TestFrameworkExecutor.FRAMEWORK_ARGS[config.framework](config);
console.log(`[TestExecutor] Running: ${command} ${args.join(' ')}`);
console.log(`[TestExecutor] Working directory: ${config.workingDir}`);
let stdout = '';
let stderr = '';
let exitCode = null;
let timedOut = false;
try {
// Verify working directory exists
await this.verifyWorkingDirectory(config.workingDir);
// Check if framework is available
await this.checkFrameworkAvailable(config.framework, config.workingDir);
const child = (0, child_process_1.spawn)(command, args, {
cwd: config.workingDir,
env: {
...process.env,
NODE_ENV: config.environment || 'test',
CI: 'true', // Disable interactive prompts
FORCE_COLOR: '0' // Disable color codes
},
shell: false
});
// Set timeout
const timeout = config.timeout || 300000; // 5 minutes default
const timeoutHandle = setTimeout(() => {
timedOut = true;
child.kill('SIGTERM');
setTimeout(() => child.kill('SIGKILL'), 5000); // Force kill after 5s
}, timeout);
// Capture output
child.stdout.on('data', (data) => {
const text = data.toString();
stdout += text;
console.log(`[TestExecutor] stdout: ${text.slice(0, 200)}`);
});
child.stderr.on('data', (data) => {
const text = data.toString();
stderr += text;
console.error(`[TestExecutor] stderr: ${text.slice(0, 200)}`);
});
// Wait for process to complete
exitCode = await new Promise((resolve, reject) => {
child.on('close', (code) => {
clearTimeout(timeoutHandle);
resolve(code ?? 1);
});
child.on('error', (error) => {
clearTimeout(timeoutHandle);
reject(error);
});
});
const duration = Date.now() - startTime;
if (timedOut) {
return this.createTimeoutResult(config, stdout, stderr, duration);
}
// Parse results based on framework
const result = await this.parseResults(config.framework, stdout, stderr, exitCode, duration, config);
return result;
}
catch (error) {
const duration = Date.now() - startTime;
return this.createErrorResult(config, error, stdout, stderr, duration);
}
}
/**
* Detect test framework from package.json
*/
async detectFramework(workingDir) {
try {
const packageJsonPath = path.join(workingDir, 'package.json');
const packageJson = JSON.parse(await fs_1.promises.readFile(packageJsonPath, 'utf-8'));
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
// Check in priority order
if (allDeps['jest'] || allDeps['@jest/core'])
return 'jest';
if (allDeps['mocha'])
return 'mocha';
if (allDeps['playwright'] || allDeps['@playwright/test'])
return 'playwright';
if (allDeps['cypress'])
return 'cypress';
return null;
}
catch (error) {
console.warn('[TestExecutor] Could not detect framework:', error);
return null;
}
}
/**
* Verify working directory exists and is accessible
*/
async verifyWorkingDirectory(workingDir) {
try {
const stat = await fs_1.promises.stat(workingDir);
if (!stat.isDirectory()) {
throw new Error(`Working directory is not a directory: ${workingDir}`);
}
}
catch (error) {
throw new Error(`Working directory does not exist or is not accessible: ${workingDir}`);
}
}
/**
* Check if test framework is available
*/
async checkFrameworkAvailable(framework, workingDir) {
try {
const packageJsonPath = path.join(workingDir, 'package.json');
const packageJson = JSON.parse(await fs_1.promises.readFile(packageJsonPath, 'utf-8'));
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
const frameworkMappings = {
jest: ['jest', '@jest/core'],
mocha: ['mocha'],
playwright: ['playwright', '@playwright/test'],
cypress: ['cypress']
};
const requiredPackages = frameworkMappings[framework] || [framework];
const hasFramework = requiredPackages.some(pkg => allDeps[pkg]);
if (!hasFramework) {
throw new Error(`Framework '${framework}' not found in package.json dependencies. ` +
`Please install: ${requiredPackages.join(' or ')}`);
}
}
catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`package.json not found in ${workingDir}`);
}
throw error;
}
}
/**
* Parse test results based on framework
*/
async parseResults(framework, stdout, stderr, exitCode, duration, config) {
const parser = this.getParser(framework);
return parser(stdout, stderr, exitCode, duration, config);
}
/**
* Get parser function for framework
*/
getParser(framework) {
switch (framework) {
case 'jest':
return this.parseJestResults.bind(this);
case 'mocha':
return this.parseMochaResults.bind(this);
case 'playwright':
return this.parsePlaywrightResults.bind(this);
case 'cypress':
return this.parseCypressResults.bind(this);
default:
return this.parseGenericResults.bind(this);
}
}
/**
* Parse Jest JSON output
*/
async parseJestResults(stdout, stderr, exitCode, duration, config) {
try {
// Jest outputs JSON to stdout
const jsonOutput = this.extractJSON(stdout);
if (!jsonOutput) {
throw new Error('Could not find JSON output in Jest results');
}
const jestResult = JSON.parse(jsonOutput);
const tests = [];
// Parse test results
jestResult.testResults?.forEach((suiteResult) => {
suiteResult.assertionResults?.forEach((testResult) => {
tests.push({
name: testResult.title,
fullName: testResult.fullName || testResult.title,
status: testResult.status,
duration: testResult.duration || 0,
failureMessages: testResult.failureMessages,
ancestorTitles: testResult.ancestorTitles
});
});
});
// Parse coverage if available
let coverage;
if (config.coverage && jestResult.coverageMap) {
coverage = this.parseJestCoverage(jestResult.coverageMap);
}
return {
framework: 'jest',
status: exitCode === 0 ? 'passed' : 'failed',
totalTests: jestResult.numTotalTests || tests.length,
passedTests: jestResult.numPassedTests || tests.filter(t => t.status === 'passed').length,
failedTests: jestResult.numFailedTests || tests.filter(t => t.status === 'failed').length,
skippedTests: jestResult.numPendingTests || tests.filter(t => t.status === 'skipped' || t.status === 'pending').length,
duration,
tests,
coverage,
stdout,
stderr,
exitCode
};
}
catch (error) {
console.error('[TestExecutor] Failed to parse Jest results:', error);
return this.parseGenericResults(stdout, stderr, exitCode, duration, config);
}
}
/**
* Parse Mocha JSON output
*/
async parseMochaResults(stdout, stderr, exitCode, duration, config) {
try {
const jsonOutput = this.extractJSON(stdout);
if (!jsonOutput) {
throw new Error('Could not find JSON output in Mocha results');
}
const mochaResult = JSON.parse(jsonOutput);
const tests = [];
// Parse test results
mochaResult.tests?.forEach((testResult) => {
tests.push({
name: testResult.title,
fullName: testResult.fullTitle || testResult.title,
status: testResult.pass ? 'passed' : testResult.pending ? 'pending' : 'failed',
duration: testResult.duration || 0,
failureMessages: testResult.err ? [testResult.err.message] : undefined
});
});
const stats = mochaResult.stats || {};
return {
framework: 'mocha',
status: exitCode === 0 ? 'passed' : 'failed',
totalTests: stats.tests || tests.length,
passedTests: stats.passes || tests.filter(t => t.status === 'passed').length,
failedTests: stats.failures || tests.filter(t => t.status === 'failed').length,
skippedTests: stats.pending || tests.filter(t => t.status === 'pending').length,
duration: stats.duration || duration,
tests,
stdout,
stderr,
exitCode
};
}
catch (error) {
console.error('[TestExecutor] Failed to parse Mocha results:', error);
return this.parseGenericResults(stdout, stderr, exitCode, duration, config);
}
}
/**
* Parse Playwright JSON output
*/
async parsePlaywrightResults(stdout, stderr, exitCode, duration, config) {
try {
const jsonOutput = this.extractJSON(stdout);
if (!jsonOutput) {
throw new Error('Could not find JSON output in Playwright results');
}
const playwrightResult = JSON.parse(jsonOutput);
const tests = [];
// Parse test results
playwrightResult.suites?.forEach((suite) => {
suite.specs?.forEach((spec) => {
const test = spec.tests?.[0];
if (test) {
tests.push({
name: spec.title,
fullName: `${suite.title} > ${spec.title}`,
status: test.status === 'expected' ? 'passed' :
test.status === 'skipped' ? 'skipped' : 'failed',
duration: test.results?.[0]?.duration || 0,
failureMessages: test.results?.[0]?.error ? [test.results[0].error.message] : undefined
});
}
});
});
return {
framework: 'playwright',
status: exitCode === 0 ? 'passed' : 'failed',
totalTests: tests.length,
passedTests: tests.filter(t => t.status === 'passed').length,
failedTests: tests.filter(t => t.status === 'failed').length,
skippedTests: tests.filter(t => t.status === 'skipped').length,
duration,
tests,
stdout,
stderr,
exitCode
};
}
catch (error) {
console.error('[TestExecutor] Failed to parse Playwright results:', error);
return this.parseGenericResults(stdout, stderr, exitCode, duration, config);
}
}
/**
* Parse Cypress JSON output
*/
async parseCypressResults(stdout, stderr, exitCode, duration, config) {
try {
const jsonOutput = this.extractJSON(stdout);
if (!jsonOutput) {
throw new Error('Could not find JSON output in Cypress results');
}
const cypressResult = JSON.parse(jsonOutput);
const tests = [];
// Parse test results
cypressResult.runs?.forEach((run) => {
run.tests?.forEach((test) => {
tests.push({
name: test.title.join(' '),
fullName: test.title.join(' > '),
status: test.state,
duration: test.duration || 0,
failureMessages: test.err ? [test.err.message] : undefined
});
});
});
const stats = cypressResult.totalTests ? {
total: cypressResult.totalTests,
passed: cypressResult.totalPassed,
failed: cypressResult.totalFailed,
skipped: cypressResult.totalSkipped
} : null;
return {
framework: 'cypress',
status: exitCode === 0 ? 'passed' : 'failed',
totalTests: stats?.total || tests.length,
passedTests: stats?.passed || tests.filter(t => t.status === 'passed').length,
failedTests: stats?.failed || tests.filter(t => t.status === 'failed').length,
skippedTests: stats?.skipped || tests.filter(t => t.status === 'skipped' || t.status === 'pending').length,
duration: cypressResult.totalDuration || duration,
tests,
stdout,
stderr,
exitCode
};
}
catch (error) {
console.error('[TestExecutor] Failed to parse Cypress results:', error);
return this.parseGenericResults(stdout, stderr, exitCode, duration, config);
}
}
/**
* Generic results parser (fallback)
*/
async parseGenericResults(stdout, stderr, exitCode, duration, config) {
// Attempt to extract basic test counts from output
const passedMatch = stdout.match(/(\d+)\s+passing/);
const failedMatch = stdout.match(/(\d+)\s+failing/);
const skippedMatch = stdout.match(/(\d+)\s+pending/);
const passed = passedMatch ? parseInt(passedMatch[1]) : 0;
const failed = failedMatch ? parseInt(failedMatch[1]) : 0;
const skipped = skippedMatch ? parseInt(skippedMatch[1]) : 0;
return {
framework: config.framework,
status: exitCode === 0 ? 'passed' : 'failed',
totalTests: passed + failed + skipped,
passedTests: passed,
failedTests: failed,
skippedTests: skipped,
duration,
tests: [],
stdout,
stderr,
exitCode
};
}
/**
* Extract JSON from output (handles noise before/after JSON)
*/
extractJSON(output) {
// Find JSON start and end
const jsonStart = output.indexOf('{');
const jsonEnd = output.lastIndexOf('}');
if (jsonStart === -1 || jsonEnd === -1 || jsonStart >= jsonEnd) {
return null;
}
return output.substring(jsonStart, jsonEnd + 1);
}
/**
* Parse Jest coverage data
*/
parseJestCoverage(coverageMap) {
const files = {};
let totalLines = 0, coveredLines = 0;
let totalStatements = 0, coveredStatements = 0;
let totalFunctions = 0, coveredFunctions = 0;
let totalBranches = 0, coveredBranches = 0;
Object.keys(coverageMap).forEach(filePath => {
const fileCoverage = coverageMap[filePath];
files[filePath] = {
lines: { pct: fileCoverage.l?.pct || 0 },
statements: { pct: fileCoverage.s?.pct || 0 },
functions: { pct: fileCoverage.f?.pct || 0 },
branches: { pct: fileCoverage.b?.pct || 0 }
};
totalLines += fileCoverage.l?.total || 0;
coveredLines += fileCoverage.l?.covered || 0;
totalStatements += fileCoverage.s?.total || 0;
coveredStatements += fileCoverage.s?.covered || 0;
totalFunctions += fileCoverage.f?.total || 0;
coveredFunctions += fileCoverage.f?.covered || 0;
totalBranches += fileCoverage.b?.total || 0;
coveredBranches += fileCoverage.b?.covered || 0;
});
return {
lines: {
total: totalLines,
covered: coveredLines,
pct: totalLines > 0 ? (coveredLines / totalLines) * 100 : 0
},
statements: {
total: totalStatements,
covered: coveredStatements,
pct: totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0
},
functions: {
total: totalFunctions,
covered: coveredFunctions,
pct: totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0
},
branches: {
total: totalBranches,
covered: coveredBranches,
pct: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0
},
files
};
}
/**
* Create timeout result
*/
createTimeoutResult(config, stdout, stderr, duration) {
return {
framework: config.framework,
status: 'timeout',
totalTests: 0,
passedTests: 0,
failedTests: 0,
skippedTests: 0,
duration,
tests: [],
stdout,
stderr,
exitCode: null,
error: `Test execution timed out after ${config.timeout || 300000}ms`
};
}
/**
* Create error result
*/
createErrorResult(config, error, stdout, stderr, duration) {
return {
framework: config.framework,
status: 'error',
totalTests: 0,
passedTests: 0,
failedTests: 0,
skippedTests: 0,
duration,
tests: [],
stdout,
stderr,
exitCode: null,
error: error.message
};
}
}
exports.TestFrameworkExecutor = TestFrameworkExecutor;
TestFrameworkExecutor.FRAMEWORK_COMMANDS = {
jest: 'npx',
mocha: 'npx',
playwright: 'npx',
cypress: 'npx'
};
TestFrameworkExecutor.FRAMEWORK_ARGS = {
jest: (config) => {
const args = ['jest', '--json', '--testLocationInResults'];
if (config.coverage)
args.push('--coverage', '--coverageReporters=json');
if (config.testPattern)
args.push(config.testPattern);
if (config.config)
args.push('--config', config.config);
args.push('--no-cache', '--runInBand'); // Deterministic execution
return args;
},
mocha: (config) => {
const args = ['mocha', '--reporter', 'json'];
if (config.testPattern)
args.push(config.testPattern);
else
args.push('**/*.test.js', '**/*.spec.js');
if (config.config)
args.push('--config', config.config);
return args;
},
playwright: (config) => {
const args = ['playwright', 'test', '--reporter=json'];
if (config.testPattern)
args.push(config.testPattern);
if (config.config)
args.push('--config', config.config);
return args;
},
cypress: (config) => {
const args = ['cypress', 'run', '--reporter', 'json'];
if (config.testPattern)
args.push('--spec', config.testPattern);
if (config.config)
args.push('--config-file', config.config);
return args;
}
};
//# sourceMappingURL=TestFrameworkExecutor.js.map