@wgtechlabs/log-engine
Version:
A lightweight, security-first logging utility with automatic data redaction for Node.js applications - the first logging library with built-in PII protection.
231 lines • 7.71 kB
JavaScript
/**
* Test utilities for handling async file operations properly
* Replaces arbitrary timeouts with proper Promise-based waiting
* Optimized for CI environments with faster polling and shorter timeouts
*/
import * as fs from 'fs';
import * as path from 'path';
// Optimized defaults for CI environments
const DEFAULT_TIMEOUT = 3000; // Reduced from 5000ms
const POLL_INTERVAL = 5; // Reduced from 10ms for faster detection
/**
* Wait for a file to exist with optimized polling
*/
export async function waitForFile(filePath, timeoutMs = DEFAULT_TIMEOUT) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return; // File exists
}
catch (error) {
// File doesn't exist yet, wait a bit
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
}
}
throw new Error(`File ${filePath} did not appear within ${timeoutMs}ms`);
}
/**
* Wait for multiple files to exist with parallel checking
*/
export async function waitForFiles(filePaths, timeoutMs = DEFAULT_TIMEOUT) {
await Promise.all(filePaths.map(filePath => waitForFile(filePath, timeoutMs)));
}
/**
* Wait for a file to have specific content with optimized polling
*/
export async function waitForFileContent(filePath, expectedContent, timeoutMs = DEFAULT_TIMEOUT) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
try {
const content = await fs.promises.readFile(filePath, 'utf8');
if (typeof expectedContent === 'string') {
if (content.includes(expectedContent)) {
return;
}
}
else {
if (expectedContent.test(content)) {
return;
}
}
}
catch (error) {
// File might not exist yet or be readable
}
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
}
throw new Error(`File ${filePath} did not contain expected content within ${timeoutMs}ms`);
}
/**
* Wait for a directory to be empty with faster polling
*/
export async function waitForDirectoryEmpty(dirPath, timeoutMs = DEFAULT_TIMEOUT) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
try {
const files = await fs.promises.readdir(dirPath);
if (files.length === 0) {
return;
}
}
catch (error) {
// Directory might not exist, which is also "empty"
return;
}
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
}
throw new Error(`Directory ${dirPath} was not empty within ${timeoutMs}ms`);
}
/**
* Safely remove a file with optimized retry logic
*/
export async function safeRemoveFile(filePath, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await fs.promises.unlink(filePath);
return;
}
catch (error) {
if (i === maxRetries - 1) {
// Only throw on last retry if it's not a "file not found" error
if (error.code !== 'ENOENT') {
throw error;
}
}
else {
// Wait a bit before retrying (reduced wait time)
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
}
/**
* Safely clean up a directory with retry logic
*/
export async function safeCleanupDirectory(dirPath) {
try {
const files = await fs.promises.readdir(dirPath);
// Remove all files with retry logic
await Promise.all(files.map(file => safeRemoveFile(path.join(dirPath, file))));
// Remove the directory itself
await fs.promises.rmdir(dirPath);
}
catch (error) {
// Directory might not exist, which is fine
if (error.code !== 'ENOENT') {
// Try force removal as fallback
try {
await fs.promises.rm(dirPath, { recursive: true, force: true });
}
catch (fallbackError) {
// Ignore cleanup errors in tests
}
}
}
}
/**
* Enhanced mock HTTP handler with faster timeouts and better error handling
*/
export class MockHttpHandler {
constructor() {
this.requests = [];
this.pendingPromises = [];
this.timeoutIds = new Set();
}
addRequest(url, options) {
let resolveRequest;
const promise = new Promise(resolve => {
resolveRequest = resolve;
});
this.requests.push({
url,
options,
resolve: resolveRequest
});
this.pendingPromises.push(promise);
}
getRequests() {
return this.requests.map(({ url, options }) => ({ url, options }));
}
async waitForRequests(count = 1, timeoutMs = DEFAULT_TIMEOUT) {
const startTime = Date.now();
// Set up a timeout that will be cleaned up
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`Expected ${count} requests but got ${this.requests.length} within ${timeoutMs}ms`));
}, timeoutMs);
this.timeoutIds.add(timeoutId);
});
try {
while (this.requests.length < count && Date.now() - startTime < timeoutMs) {
await new Promise(resolve => {
const id = setTimeout(resolve, POLL_INTERVAL);
this.timeoutIds.add(id);
});
}
if (this.requests.length < count) {
throw new Error(`Expected ${count} requests but got ${this.requests.length} within ${timeoutMs}ms`);
}
// Mark all requests as processed
this.requests.forEach(req => req.resolve());
// Wait for all pending promises to resolve
await Promise.all(this.pendingPromises);
}
finally {
// Clean up the timeout
if (timeoutId) {
clearTimeout(timeoutId);
this.timeoutIds.delete(timeoutId);
}
}
}
clear() {
// Clean up any remaining timeouts
for (const timeoutId of this.timeoutIds) {
clearTimeout(timeoutId);
}
this.timeoutIds.clear();
this.requests = [];
this.pendingPromises = [];
}
}
/**
* Create a test timeout that fails fast instead of hanging
*/
export function createTestTimeout(timeoutMs = DEFAULT_TIMEOUT) {
let timeoutId;
const promise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`Test timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
const cancel = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
return { promise, cancel };
}
/**
* Race a promise against a timeout for fail-fast behavior
*/
export async function withTimeout(promise, timeoutMs = DEFAULT_TIMEOUT) {
const timeout = createTestTimeout(timeoutMs);
try {
const result = await Promise.race([
promise,
timeout.promise
]);
// Cancel the timeout since we got a result
timeout.cancel();
return result;
}
catch (error) {
// Cancel the timeout in case of error
timeout.cancel();
throw error;
}
}
//# sourceMappingURL=async-test-utils.js.map