Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.DS_Store
.cache/
.env

# Project-local temporary data (never use OS tmpdir)
.tmp/
.env.*
!.env.example*
.idea/
Expand Down
130 changes: 75 additions & 55 deletions client/src/lib/integration-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,20 +454,40 @@ export class IntegrationTestRunner {
*/
validateCodeQLQueryRunOutput(interpretedOutput, expectedOutputPath, toolName, testCase) {
try {
// Check if expected output exists
if (!fs.existsSync(expectedOutputPath)) {
// Expected output doesn't exist - validate non-empty content only
this.logger.log(
` Note: No expected output found at ${expectedOutputPath}, validating non-empty content only`
);
return this.validateNonEmptyOutput(interpretedOutput, toolName, testCase);
// Read both paths directly to avoid TOCTOU race (CWE-367).
// If a path is a directory, readFileSync throws EISDIR.
// If a path doesn't exist, readFileSync throws ENOENT.
let actualContent, expectedContent;
let actualIsDir = false,
expectedIsDir = false;

try {
actualContent = fs.readFileSync(interpretedOutput, "utf8");
} catch (readErr) {
if (readErr.code === "EISDIR") {
actualIsDir = true;
} else {
throw readErr;
}
}

// Compare actual output against expected output
const actualStats = fs.statSync(interpretedOutput);
const expectedStats = fs.statSync(expectedOutputPath);
try {
expectedContent = fs.readFileSync(expectedOutputPath, "utf8");
} catch (readErr) {
if (readErr.code === "EISDIR") {
expectedIsDir = true;
} else if (readErr.code === "ENOENT") {
// Expected output doesn't exist - validate non-empty content only
this.logger.log(
` Note: No expected output found at ${expectedOutputPath}, validating non-empty content only`
);
return this.validateNonEmptyOutput(interpretedOutput, toolName, testCase);
} else {
throw readErr;
}
}

if (actualStats.isDirectory() && expectedStats.isDirectory()) {
if (actualIsDir && expectedIsDir) {
// Compare directory structures
const comparisonResult = compareDirectories(interpretedOutput, expectedOutputPath);
if (!comparisonResult) {
Expand All @@ -479,11 +499,8 @@ export class IntegrationTestRunner {
this.logger.log(` ✓ Output files match expected output`);
return true;
}
} else if (actualStats.isFile() && expectedStats.isFile()) {
// Compare file contents
const actualContent = fs.readFileSync(interpretedOutput, "utf8");
const expectedContent = fs.readFileSync(expectedOutputPath, "utf8");

} else if (!actualIsDir && !expectedIsDir) {
// Compare file contents (already read above)
if (actualContent !== expectedContent) {
this.logger.log(
` Validation Failed: Output content does not match expected content for ${toolName}/${testCase}`
Expand Down Expand Up @@ -519,52 +536,55 @@ export class IntegrationTestRunner {
*/
validateNonEmptyOutput(outputPath, toolName, testCase) {
try {
const stats = fs.statSync(outputPath);

if (stats.isDirectory()) {
// Find all output files in the directory using getDirectoryFiles
const allFiles = getDirectoryFiles(outputPath);

// Filter for relevant output file extensions
const outputExtensions = [".txt", ".dgml", ".dot", ".sarif", ".csv", ".json"];
const outputFiles = allFiles.filter((file) =>
outputExtensions.some((ext) => file.endsWith(ext))
);

if (outputFiles.length === 0) {
this.logger.log(
` Validation Failed: No output files found in ${outputPath} for ${toolName}/${testCase}`
);
return false;
}

// Check that at least one file has non-empty content
let hasNonEmptyContent = false;
for (const file of outputFiles) {
const content = fs.readFileSync(file, "utf8");
if (content.trim().length > 0) {
hasNonEmptyContent = true;
break;
}
}

if (!hasNonEmptyContent) {
this.logger.log(
` Validation Failed: All output files are empty for ${toolName}/${testCase}`
);
return false;
}

return true;
} else {
// File - check if non-empty
// Try reading as a file first to avoid TOCTOU race (CWE-367).
// If the path is a directory, readFileSync throws EISDIR.
try {
const content = fs.readFileSync(outputPath, "utf8");
if (content.trim().length === 0) {
this.logger.log(` Validation Failed: Output file is empty for ${toolName}/${testCase}`);
return false;
}
return true;
} catch (readErr) {
if (readErr.code !== "EISDIR") {
throw readErr;
}
}

// Path is a directory - find and check output files
const allFiles = getDirectoryFiles(outputPath);

// Filter for relevant output file extensions
const outputExtensions = [".txt", ".dgml", ".dot", ".sarif", ".csv", ".json"];
const outputFiles = allFiles.filter((file) =>
outputExtensions.some((ext) => file.endsWith(ext))
);

if (outputFiles.length === 0) {
this.logger.log(
` Validation Failed: No output files found in ${outputPath} for ${toolName}/${testCase}`
);
return false;
}

// Check that at least one file has non-empty content
let hasNonEmptyContent = false;
for (const file of outputFiles) {
const content = fs.readFileSync(file, "utf8");
if (content.trim().length > 0) {
hasNonEmptyContent = true;
break;
}
}

if (!hasNonEmptyContent) {
this.logger.log(
` Validation Failed: All output files are empty for ${toolName}/${testCase}`
);
return false;
}

return true;
} catch (error) {
this.logger.log(
` Validation Error: Failed to check output at ${outputPath} for ${toolName}/${testCase}: ${error.message}`
Expand Down
Loading
Loading