Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 1 addition & 139 deletions executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { exec, spawn } = require('child_process');
const path = require('path');
const fs = require('fs').promises;
const { v4: uuidv4 } = require('uuid');
const { parsePytestOutput } = require('./pytest-parser');

/**
* Ensures the datasets repo is cloned or updated in the unique directory.
Expand Down Expand Up @@ -237,145 +238,6 @@ ${testCode}
}
}

/**
* Parses pytest output to extract structured test results.
* @param {string} stdout - The stdout from pytest.
* @param {string} [stderr=''] - The stderr from pytest.
* @param {number|null} [exitCode=null] - The pytest process exit code.
* @returns {object} - The test summary.
*/
function buildRawOutput(stdout = '', stderr = '') {
if (stdout && stderr) {
return `${stdout}\n${stderr}`;
}
return stdout || stderr || '';
}

function extractPytestSummary(output) {
const lines = output
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);

const summaryLine = [...lines].reverse().find((line) => (
/^=+/.test(line) &&
/=+$/.test(line) &&
(/\bin [\d.]+s\b/.test(line) || /\bno tests ran\b/.test(line))
));

if (!summaryLine) {
return '';
}

return summaryLine.replace(/^=+\s*/, '').replace(/\s*=+$/, '');
}

function extractPytestCount(summary, labelPattern) {
const match = summary.match(new RegExp(`(\\d+) ${labelPattern}\\b`));
return match ? parseInt(match[1], 10) : 0;
}

function extractPytestShortSummaryTarget(output, prefix) {
const line = output
.split(/\r?\n/)
.map((entry) => entry.trim())
.find((entry) => entry.startsWith(`${prefix} `));

return line ? line.slice(prefix.length + 1).trim() : '';
}

function extractPytestErrorMessage(output, stderr = '') {
const combined = buildRawOutput(output, stderr);
const patterns = [
/^E\s+([A-Za-z_.]+(?:Error|Exception): .+)$/m,
/^([A-Za-z_.]+(?:Error|Exception): .+)$/m,
/^(ImportError while importing test module .+)$/m,
/^E\s+(.+)$/m,
];

for (const pattern of patterns) {
const match = combined.match(pattern);
if (match) {
return match[1].trim();
}
}

return 'Pytest error during collection or execution';
}

function parsePytestOutput(stdout = '', stderr = '', exitCode = null) {
const summary = extractPytestSummary(stdout);
const rawout = buildRawOutput(stdout, stderr);
const passed_tests = extractPytestCount(summary, 'passed');
const failed_tests = extractPytestCount(summary, 'failed');
const errors = extractPytestCount(summary, 'error(?:s)?');
const no_tests_collected = exitCode === 5 || /\bno tests ran\b/.test(summary);
const failures = [];

const failureBlocks = stdout.split(/={10,} FAILURES ={10,}/)[1]?.split(/={10,}/)[0] || '';
const matches = [...failureBlocks.matchAll(
/_{5,}\s*(.*?)\s*_{5,}[\s\S]*?>\s*assert\s+(.*?)\s*?\nE\s+assert\s+(.*?)\s*?(?:\nE\s+\+\s+where\s+(.*?)\s+=)?/g
)];

matches.forEach((match, index) => {
const test_case = match[1]?.trim() || `Test ${index + 1}`;
const assertionLine = match[2]?.trim();
const failedExpr = match[3]?.trim();
const evaluated = match[4]?.trim() || '';

failures.push({
test_case,
expected: failedExpr.split('==')[1]?.trim() || '',
received: evaluated || failedExpr.split('==')[0]?.trim(),
error_message: `Assertion failed: ${assertionLine}`,
rawout,
});
});

if (failed_tests > 0 && failures.length === 0) {
failures.push({
test_case: extractPytestShortSummaryTarget(stdout, 'FAILED') || 'pytest assertion failure',
expected: '',
received: '',
error_message: 'Pytest reported one or more failed assertions',
rawout,
});
}

let runtime_error = '';

if (errors > 0) {
runtime_error = extractPytestErrorMessage(stdout, stderr);
failures.push({
test_case: extractPytestShortSummaryTarget(stdout, 'ERROR') || 'pytest collection/execution',
expected: '',
received: '',
error_message: runtime_error,
rawout,
});
} else if (no_tests_collected) {
runtime_error = 'Pytest did not collect any tests';
failures.push({
test_case: 'pytest collection',
expected: 'at least 1 collected test',
received: '0 collected tests',
error_message: runtime_error,
rawout,
});
}

return {
tests_run: passed_tests + failed_tests,
passed: passed_tests,
failed: failed_tests,
errors,
no_tests_collected,
exit_code: exitCode,
failure_details: failures,
runtime_error,
};
}

function parseCppTestOutput(output, stdout = '', stderr = '') {
output = output.toString();
let total_tests = 0;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test"
},
"keywords": [],
"author": "",
Expand Down
Loading