From 01cc58aa7a7014b4c59eff92d54c6cd95e31351f Mon Sep 17 00:00:00 2001 From: IanKulin Date: Thu, 25 Sep 2025 21:26:31 +0800 Subject: [PATCH] fmt --- README.md | 93 +++++----- demo.js | 124 +++++++------ lib/logger.ts | 257 +++++++++++++++----------- test/helpers/logger-test-helpers.js | 4 +- test/logger.caller-level.test.js | 124 ++++++------- test/logger.constructor.test.js | 166 ++++++++++------- test/logger.internal-errors.test.js | 46 ++--- test/logger.internal-fallback.test.js | 169 ++++++++--------- test/logger.json-formatter.test.js | 122 ++++++------ test/logger.level-management.test.js | 150 ++++++++------- test/logger.robustness.test.js | 52 +++--- test/logger.simple-formatter.test.js | 152 +++++++-------- test/logger.time-formatting.test.js | 134 +++++++------- test/logger.util-format.test.js | 155 ++++++++-------- 14 files changed, 924 insertions(+), 824 deletions(-) diff --git a/README.md b/README.md index 75a0609..db2d097 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ - **Multiple log levels**: silent, error, warn, info, debug - **Flexible output formats**: JSON or simple text -- **Caller detection**: Automatically identifies source file and line number based on log level +- **Caller detection**: Automatically identifies source file and line number + based on log level - **Color support**: Automatic TTY detection with colored output ## Install @@ -27,8 +28,8 @@ $ deno run --allow-env --allow-sys your-script.js import Logger from "@iankulin/logger"; const logger = new Logger(); -logger.info('Hello from logger'); -logger.error('Something went wrong'); +logger.info("Hello from logger"); +logger.error("Something went wrong"); ``` ## Usage Examples @@ -37,12 +38,12 @@ logger.error('Something went wrong'); ```typescript import Logger from "@iankulin/logger"; -const logger = new Logger({ level: 'info' }); +const logger = new Logger({ level: "info" }); -logger.error('Critical error occurred'); -logger.warn('This is a warning'); -logger.info('Informational message'); -logger.debug('Debug info'); // Won't be shown (level is 'info') +logger.error("Critical error occurred"); +logger.warn("This is a warning"); +logger.info("Informational message"); +logger.debug("Debug info"); // Won't be shown (level is 'info') ``` ### Log Levels @@ -56,17 +57,17 @@ The logger supports five log levels (from least to most verbose): - `debug` - All messages ```typescript -const logger = new Logger({ level: 'debug' }); +const logger = new Logger({ level: "debug" }); // All of these will be logged -logger.error('Error message'); -logger.warn('Warning message'); -logger.info('Info message'); -logger.debug('Debug message'); +logger.error("Error message"); +logger.warn("Warning message"); +logger.info("Info message"); +logger.debug("Debug message"); // Change level dynamically -logger.level('error'); -logger.info('This will not be logged'); +logger.level("error"); +logger.info("This will not be logged"); // Get current level console.log(logger.level()); // 'error' @@ -77,8 +78,8 @@ console.log(logger.level()); // 'error' #### JSON Format (Default) ```typescript -const logger = new Logger({ format: 'json' }); -logger.info('Hello world'); +const logger = new Logger({ format: "json" }); +logger.info("Hello world"); ``` ```json @@ -97,8 +98,8 @@ logger.info('Hello world'); #### Simple Format ```typescript -const logger = new Logger({ format: 'simple' }); -logger.error('Something failed'); +const logger = new Logger({ format: "simple" }); +logger.error("Something failed"); ``` ``` @@ -107,22 +108,23 @@ logger.error('Something failed'); ### Message Formatting -The logger supports `util.format()` style message formatting with placeholders like `%s`, `%d`, `%j`. +The logger supports `util.format()` style message formatting with placeholders +like `%s`, `%d`, `%j`. ```typescript -const logger = new Logger({ format: 'json' }); +const logger = new Logger({ format: "json" }); // String formatting -logger.info('User %s has %d points', 'john', 100); +logger.info("User %s has %d points", "john", 100); // Output: {"level":"info","msg":"User john has 100 points",...} // JSON formatting -logger.info('Config: %j', { debug: true, port: 3000 }); +logger.info("Config: %j", { debug: true, port: 3000 }); // Output: {"level":"info","msg":"Config: {\"debug\":true,\"port\":3000}",...} // Simple format example -const simpleLogger = new Logger({ format: 'simple' }); -simpleLogger.warn('Processing file %s (%d bytes)', 'data.txt', 1024); +const simpleLogger = new Logger({ format: "simple" }); +simpleLogger.warn("Processing file %s (%d bytes)", "data.txt", 1024); // Output: [2025-07-05 10:30] [WARN ] [app.js:15] Processing file data.txt (1024 bytes) ``` @@ -131,26 +133,28 @@ simpleLogger.warn('Processing file %s (%d bytes)', 'data.txt', 1024); ```typescript const logger = new Logger({ colours: { - error: '\x1b[31m', // Red - warn: '\x1b[93m', // Bright yellow - info: '\x1b[36m', // Cyan - debug: '\x1b[90m', // Dark gray + error: "\x1b[31m", // Red + warn: "\x1b[93m", // Bright yellow + info: "\x1b[36m", // Cyan + debug: "\x1b[90m", // Dark gray }, }); ``` ### Caller Level Control -Control when caller information (file and line number) is included in log messages. This is useful for performance optimization since caller detection can be expensive. +Control when caller information (file and line number) is included in log +messages. This is useful for performance optimization since caller detection can +be expensive. ```typescript // Default: only include caller info for warnings and errors -const logger = new Logger({ callerLevel: 'warn' }); +const logger = new Logger({ callerLevel: "warn" }); -logger.error('Critical error'); // Includes caller info -logger.warn('Warning message'); // Includes caller info -logger.info('Info message'); // No caller info -logger.debug('Debug message'); // No caller info +logger.error("Critical error"); // Includes caller info +logger.warn("Warning message"); // Includes caller info +logger.info("Info message"); // No caller info +logger.debug("Debug message"); // No caller info ``` **JSON Format Output:** @@ -175,7 +179,9 @@ logger.debug('Debug message'); // No caller info - `'info'` - Include caller info for info, warnings, and errors - `'debug'` - Always include caller info -**Performance Tip:** For production applications that primarily log info/debug messages, setting `callerLevel: 'error'` can significantly improve performance by avoiding expensive stack trace analysis for routine logging. +**Performance Tip:** For production applications that primarily log info/debug +messages, setting `callerLevel: 'error'` can significantly improve performance +by avoiding expensive stack trace analysis for routine logging. ## Constructor Options @@ -194,19 +200,19 @@ import Logger from "@iankulin/logger"; // Production: JSON format with environment-based level const prodLogger = new Logger({ - level: Deno.env.get('LOG_LEVEL') || 'info', - format: 'json', - callerLevel: 'error', // Performance optimization + level: Deno.env.get("LOG_LEVEL") || "info", + format: "json", + callerLevel: "error", // Performance optimization }); // Development: Simple format with debug level const devLogger = new Logger({ - level: 'debug', - format: 'simple', + level: "debug", + format: "simple", }); // Testing: Silent mode -const testLogger = new Logger({ level: 'silent' }); +const testLogger = new Logger({ level: "silent" }); ``` ## Requirements @@ -220,7 +226,8 @@ const testLogger = new Logger({ level: 'silent' }); ## Versions - **1.0.0** - JSR release with full Deno support - - Migrated from [npm version](https://www.npmjs.com/package/@iankulin/logger) to JSR (JavaScript Registry) + - Migrated from [npm version](https://www.npmjs.com/package/@iankulin/logger) + to JSR (JavaScript Registry) - Full TypeScript support - Native Deno compatibility diff --git a/demo.js b/demo.js index 59d3ac9..cff7f03 100644 --- a/demo.js +++ b/demo.js @@ -1,94 +1,102 @@ -import Logger from './lib/logger.ts'; -const logger = new Logger({ level: 'debug' }); +import Logger from "./lib/logger.ts"; +const logger = new Logger({ level: "debug" }); -logger.error('Unable to fetch student'); -logger.info('Hello from logger'); -logger.warn('This is a warning'); -logger.debug('This is a debug message'); // This won't be logged if level is set to 'info' -logger.level('error'); -logger.debug('This is a debug message'); // This won't be logged if level is set to 'info' or higher +logger.error("Unable to fetch student"); +logger.info("Hello from logger"); +logger.warn("This is a warning"); +logger.debug("This is a debug message"); // This won't be logged if level is set to 'info' +logger.level("error"); +logger.debug("This is a debug message"); // This won't be logged if level is set to 'info' or higher -const simple_logger = new Logger({ level: 'debug', format: 'simple' }); +const simple_logger = new Logger({ level: "debug", format: "simple" }); -simple_logger.error('Unable to fetch student'); -simple_logger.info('Hello from logger'); -simple_logger.warn('This is a warning'); -simple_logger.debug('This is a debug message'); // This won't be logged if level is set to 'info' -simple_logger.level('error'); -simple_logger.debug('This is a debug message'); // This won't be logged if level is set to 'info' or higher +simple_logger.error("Unable to fetch student"); +simple_logger.info("Hello from logger"); +simple_logger.warn("This is a warning"); +simple_logger.debug("This is a debug message"); // This won't be logged if level is set to 'info' +simple_logger.level("error"); +simple_logger.debug("This is a debug message"); // This won't be logged if level is set to 'info' or higher -const longLogger = new Logger({ time: 'long', format: 'simple' }); -const shortLogger = new Logger({ time: 'short', format: 'simple' }); +const longLogger = new Logger({ time: "long", format: "simple" }); +const shortLogger = new Logger({ time: "short", format: "simple" }); -longLogger.info('This uses long time format'); -shortLogger.info('This uses short time format'); +longLogger.info("This uses long time format"); +shortLogger.info("This uses short time format"); // Demonstrate callerLevel functionality -console.log('\n=== Caller Level Demo ==='); +console.log("\n=== Caller Level Demo ==="); // Default callerLevel is 'warn' - only errors and warnings include caller info -const defaultCallerLogger = new Logger({ format: 'simple' }); +const defaultCallerLogger = new Logger({ format: "simple" }); console.log( - 'Default callerLevel (warn) - only errors and warnings show caller info:' + "Default callerLevel (warn) - only errors and warnings show caller info:", ); -defaultCallerLogger.error('Error with caller info'); -defaultCallerLogger.warn('Warning with caller info'); -defaultCallerLogger.info('Info without caller info'); -defaultCallerLogger.debug('Debug without caller info'); +defaultCallerLogger.error("Error with caller info"); +defaultCallerLogger.warn("Warning with caller info"); +defaultCallerLogger.info("Info without caller info"); +defaultCallerLogger.debug("Debug without caller info"); // Set callerLevel to 'error' - only errors include caller info -const errorOnlyLogger = new Logger({ format: 'simple', callerLevel: 'error' }); -console.log('\nCallerLevel set to error - only errors show caller info:'); -errorOnlyLogger.error('Error with caller info'); -errorOnlyLogger.warn('Warning without caller info'); -errorOnlyLogger.info('Info without caller info'); +const errorOnlyLogger = new Logger({ format: "simple", callerLevel: "error" }); +console.log("\nCallerLevel set to error - only errors show caller info:"); +errorOnlyLogger.error("Error with caller info"); +errorOnlyLogger.warn("Warning without caller info"); +errorOnlyLogger.info("Info without caller info"); // Set callerLevel to 'debug' - all levels include caller info -const allLevelsLogger = new Logger({ format: 'simple', callerLevel: 'debug' }); -console.log('\nCallerLevel set to debug - all levels show caller info:'); -allLevelsLogger.error('Error with caller info'); -allLevelsLogger.warn('Warning with caller info'); -allLevelsLogger.info('Info with caller info'); -allLevelsLogger.debug('Debug with caller info'); +const allLevelsLogger = new Logger({ format: "simple", callerLevel: "debug" }); +console.log("\nCallerLevel set to debug - all levels show caller info:"); +allLevelsLogger.error("Error with caller info"); +allLevelsLogger.warn("Warning with caller info"); +allLevelsLogger.info("Info with caller info"); +allLevelsLogger.debug("Debug with caller info"); // Set callerLevel to 'silent' - no levels include caller info -const noneLogger = new Logger({ format: 'simple', callerLevel: 'silent' }); -console.log('\nCallerLevel set to silent - no levels show caller info:'); -noneLogger.error('Error without caller info'); -noneLogger.warn('Warning without caller info'); -noneLogger.info('Info without caller info'); +const noneLogger = new Logger({ format: "simple", callerLevel: "silent" }); +console.log("\nCallerLevel set to silent - no levels show caller info:"); +noneLogger.error("Error without caller info"); +noneLogger.warn("Warning without caller info"); +noneLogger.info("Info without caller info"); // Demonstrate format string functionality (util.format style) -console.log('\n=== Format String Demo ==='); +console.log("\n=== Format String Demo ==="); -const formatLogger = new Logger({ format: 'simple', level: 'debug' }); -console.log('Format strings with various specifiers:'); +const formatLogger = new Logger({ format: "simple", level: "debug" }); +console.log("Format strings with various specifiers:"); // String formatting (%s) -formatLogger.info('User %s logged in successfully', 'john_doe'); +formatLogger.info("User %s logged in successfully", "john_doe"); // Number formatting (%d, %i, %f) -formatLogger.warn('Database has %d connections, CPU usage: %f%%', 25, 84.3); -formatLogger.debug('Processing item %i of %d', 42, 100); +formatLogger.warn("Database has %d connections, CPU usage: %f%%", 25, 84.3); +formatLogger.debug("Processing item %i of %d", 42, 100); // JSON formatting (%j) -const user = { name: 'Alice', role: 'admin', active: true }; +const user = { name: "Alice", role: "admin", active: true }; const config = { timeout: 5000, retries: 3 }; -formatLogger.info('User data: %j, Config: %j', user, config); +formatLogger.info("User data: %j, Config: %j", user, config); // Mixed formatting -formatLogger.error('API call failed for user %s (ID: %d) with config %j', 'bob', 1234, config); +formatLogger.error( + "API call failed for user %s (ID: %d) with config %j", + "bob", + 1234, + config, +); // Multiple arguments without format specifiers -formatLogger.warn('System alert:', 'High memory usage detected', { usage: '89%', threshold: '80%' }); +formatLogger.warn("System alert:", "High memory usage detected", { + usage: "89%", + threshold: "80%", +}); // Literal percentage with %% -formatLogger.info('Upload progress: 50%% complete'); +formatLogger.info("Upload progress: 50%% complete"); // Edge cases -formatLogger.debug('Values: %s, %s, %d', null, undefined, null); +formatLogger.debug("Values: %s, %s, %d", null, undefined, null); -console.log('\nJSON format with same messages:'); -const jsonFormatLogger = new Logger({ format: 'json' }); -jsonFormatLogger.info('User %s logged in with %d failed attempts', 'alice', 2); -jsonFormatLogger.warn('Config loaded: %j', { env: 'production', debug: false }); +console.log("\nJSON format with same messages:"); +const jsonFormatLogger = new Logger({ format: "json" }); +jsonFormatLogger.info("User %s logged in with %d failed attempts", "alice", 2); +jsonFormatLogger.warn("Config loaded: %j", { env: "production", debug: false }); diff --git a/lib/logger.ts b/lib/logger.ts index 5b715d4..46ad518 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -1,29 +1,29 @@ // Native implementation of util.format functionality function format(f: unknown, ...args: unknown[]): string { - if (typeof f !== 'string') { + if (typeof f !== "string") { // If first argument is not a string, just join all args with spaces - return [f, ...args].map(arg => { - if (arg === null) return 'null'; - if (arg === undefined) return 'undefined'; - if (typeof arg === 'object') { + return [f, ...args].map((arg) => { + if (arg === null) return "null"; + if (arg === undefined) return "undefined"; + if (typeof arg === "object") { try { return JSON.stringify(arg, null, 0); } catch { // Handle circular references and other JSON errors - return '[object Object]'; + return "[object Object]"; } } return String(arg); - }).join(' '); + }).join(" "); } let i = 0; // First, handle %% replacement - if there are no args, %% stays as %% // If there are args, %% becomes % - const handlePercentPercent = args.length === 0 ? '%%' : '%'; + const handlePercentPercent = args.length === 0 ? "%%" : "%"; const str = f.replace(/%[sdifj%]/g, (match: string) => { - if (match === '%%') { + if (match === "%%") { return handlePercentPercent; } @@ -32,48 +32,50 @@ function format(f: unknown, ...args: unknown[]): string { const arg = args[i++]; switch (match) { - case '%s': - if (arg === null) return 'null'; - if (arg === undefined) return 'undefined'; - if (typeof arg === 'object') { + case "%s": + if (arg === null) return "null"; + if (arg === undefined) return "undefined"; + if (typeof arg === "object") { try { // For objects without %j, use a simplified string representation if (Array.isArray(arg)) { - return `[ ${arg.join(', ')} ]`; + return `[ ${arg.join(", ")} ]`; } // For plain objects, show key-value pairs - const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`); - return `{ ${entries.join(', ')} }`; + const entries = Object.entries(arg).map(([k, v]) => + `${k}: ${typeof v === "string" ? `'${v}'` : v}` + ); + return `{ ${entries.join(", ")} }`; } catch { - return '[object Object]'; + return "[object Object]"; } } try { return String(arg); } catch { - return '[object Object]'; + return "[object Object]"; } - case '%d': - if (arg === null) return '0'; - if (arg === undefined) return 'NaN'; + case "%d": + if (arg === null) return "0"; + if (arg === undefined) return "NaN"; return String(Number(arg)); - case '%i': - if (arg === null) return '0'; - if (arg === undefined) return 'NaN'; + case "%i": + if (arg === null) return "0"; + if (arg === undefined) return "NaN"; return String(parseInt(String(Number(arg)), 10)); - case '%f': - if (arg === null) return '0'; - if (arg === undefined) return 'NaN'; + case "%f": + if (arg === null) return "0"; + if (arg === undefined) return "NaN"; return String(parseFloat(String(Number(arg)))); - case '%j': + case "%j": try { return JSON.stringify(arg); } catch { - return '[Circular]'; + return "[Circular]"; } default: @@ -84,28 +86,30 @@ function format(f: unknown, ...args: unknown[]): string { // Append any remaining arguments const remainingArgs = args.slice(i); if (remainingArgs.length > 0) { - return str + ' ' + remainingArgs.map(arg => { - if (arg === null) return 'null'; - if (arg === undefined) return 'undefined'; - if (typeof arg === 'object') { + return str + " " + remainingArgs.map((arg) => { + if (arg === null) return "null"; + if (arg === undefined) return "undefined"; + if (typeof arg === "object") { try { if (Array.isArray(arg)) { - return `[ ${arg.join(', ')} ]`; + return `[ ${arg.join(", ")} ]`; } - const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`); - return `{ ${entries.join(', ')} }`; + const entries = Object.entries(arg).map(([k, v]) => + `${k}: ${typeof v === "string" ? `'${v}'` : v}` + ); + return `{ ${entries.join(", ")} }`; } catch { - return '[object Object]'; + return "[object Object]"; } } return String(arg); - }).join(' '); + }).join(" "); } return str; } -export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug'; +export type LogLevel = "silent" | "error" | "warn" | "info" | "debug"; export interface LogLevels { [level: string]: number; @@ -142,8 +146,8 @@ export type Formatter = (logEntry: LogEntry) => string; export interface LoggerOptions { level?: LogLevel; levels?: Partial; - format?: 'json' | 'simple'; - time?: 'long' | 'short'; + format?: "json" | "simple"; + time?: "long" | "short"; callerLevel?: LogLevel; colours?: Partial; } @@ -152,8 +156,8 @@ class Logger { options: { level: LogLevel; levels: LogLevels; - format: 'json' | 'simple'; - time: 'long' | 'short'; + format: "json" | "simple"; + time: "long" | "short"; callerLevel: LogLevel; colours: Colours; }; @@ -174,19 +178,19 @@ class Logger { }; const defaultColours: Colours = { - error: '\x1b[91m', - warn: '\x1b[33m', - info: '\x1b[94m', - debug: '\x1b[37m', - reset: '\x1b[0m', + error: "\x1b[91m", + warn: "\x1b[33m", + info: "\x1b[94m", + debug: "\x1b[37m", + reset: "\x1b[0m", }; this.options = { - level: options.level || 'info', + level: options.level || "info", levels: Object.assign({}, defaultLevels, options.levels), - format: options.format || 'json', - time: options.time || 'short', - callerLevel: options.callerLevel || 'warn', + format: options.format || "json", + time: options.time || "short", + callerLevel: options.callerLevel || "warn", colours: Object.assign({}, defaultColours, options.colours), }; @@ -207,71 +211,83 @@ class Logger { validateOptions(options: LoggerOptions): void { // Validate level if provided if (options.level !== undefined) { - const validLevels: LogLevel[] = ['silent', 'error', 'warn', 'info', 'debug']; + const validLevels: LogLevel[] = [ + "silent", + "error", + "warn", + "info", + "debug", + ]; if (!validLevels.includes(options.level)) { throw new Error( - `Invalid log level: ${ - options.level - }. Valid levels are: ${validLevels.join(', ')}` + `Invalid log level: ${options.level}. Valid levels are: ${ + validLevels.join(", ") + }`, ); } } // Validate format if provided if (options.format !== undefined) { - const validFormats = ['json', 'simple']; + const validFormats = ["json", "simple"]; if (!validFormats.includes(options.format)) { throw new Error( - `Invalid format: ${ - options.format - }. Valid formats are: ${validFormats.join(', ')}` + `Invalid format: ${options.format}. Valid formats are: ${ + validFormats.join(", ") + }`, ); } } // Validate time if provided if (options.time !== undefined) { - const validTimes = ['long', 'short']; + const validTimes = ["long", "short"]; if (!validTimes.includes(options.time)) { throw new Error( - `Invalid time: ${ - options.time - }. Valid times are: ${validTimes.join(', ')}` + `Invalid time: ${options.time}. Valid times are: ${ + validTimes.join(", ") + }`, ); } } // Validate callerLevel if provided if (options.callerLevel !== undefined) { - const validLevels: LogLevel[] = ['silent', 'error', 'warn', 'info', 'debug']; + const validLevels: LogLevel[] = [ + "silent", + "error", + "warn", + "info", + "debug", + ]; if (!validLevels.includes(options.callerLevel)) { throw new Error( - `Invalid callerLevel: ${ - options.callerLevel - }. Valid levels are: ${validLevels.join(', ')}` + `Invalid callerLevel: ${options.callerLevel}. Valid levels are: ${ + validLevels.join(", ") + }`, ); } } // Validate colours if provided (should be an object) - if (options.colours !== undefined && typeof options.colours !== 'object') { - throw new Error('colours option must be an object'); + if (options.colours !== undefined && typeof options.colours !== "object") { + throw new Error("colours option must be an object"); } // Validate levels if provided (should be an object with numeric values) if (options.levels !== undefined) { - if (typeof options.levels !== 'object') { - throw new Error('levels option must be an object'); + if (typeof options.levels !== "object") { + throw new Error("levels option must be an object"); } for (const [level, value] of Object.entries(options.levels)) { if ( - typeof value !== 'number' || + typeof value !== "number" || value < 0 || !Number.isInteger(value) ) { throw new Error( - `Level value for '${level}' must be a non-negative integer` + `Level value for '${level}' must be a non-negative integer`, ); } } @@ -295,15 +311,19 @@ class Logger { msg: String(logEntry.msg), // Convert to string safely callerFile: logEntry.callerFile, callerLine: logEntry.callerLine, - jsonError: `JSON stringify failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + jsonError: `JSON stringify failed: ${ + error instanceof Error ? error.message : "Unknown error" + }`, }; return JSON.stringify(safeEntry); } catch { // Last resort - return a plain string - return `{"level":"${logEntry.level}","msg":"${String( - logEntry.msg - // eslint-disable-next-line no-useless-escape - ).replace(/"/g, '\"')}","jsonError":"Multiple JSON errors occurred"}`; + return `{"level":"${logEntry.level}","msg":"${ + String( + logEntry.msg, + // eslint-disable-next-line no-useless-escape + ).replace(/"/g, '"') + }","jsonError":"Multiple JSON errors occurred"}`; } } } @@ -312,7 +332,7 @@ class Logger { simpleFormatter(logEntry: LogEntry): string { const levelPadded = logEntry.level.toUpperCase().padEnd(5); const caller = logEntry.callerFile - ? `${logEntry.callerFile.split('/').pop()}:${logEntry.callerLine}` + ? `${logEntry.callerFile.split("/").pop()}:${logEntry.callerLine}` : null; return caller @@ -321,17 +341,22 @@ class Logger { } getCallerInfo(): { callerFile: string; callerLine: number } { - const originalFunc = (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace; - let callerFile = 'unknown'; + const originalFunc = + (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace; + let callerFile = "unknown"; let callerLine = 0; try { const err = new Error(); // deno-lint-ignore prefer-const let currentFile: string | undefined; - (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = function (_err: unknown, stack: unknown) { - return stack; + (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = + function (_err: unknown, stack: unknown) { + return stack; + }; + const stack = err.stack as unknown as { + shift: () => { getFileName: () => string; getLineNumber: () => number }; + length: number; }; - const stack = err.stack as unknown as { shift: () => { getFileName: () => string; getLineNumber: () => number }; length: number }; currentFile = stack.shift().getFileName(); while (stack.length) { const stackFrame = stack.shift(); @@ -345,17 +370,18 @@ class Logger { } catch (e) { this.callerErrorCount++; if (this.callerErrorCount <= this.maxCallerErrors) { - console.error('Error retrieving caller info:', e); + console.error("Error retrieving caller info:", e); if (this.callerErrorCount === this.maxCallerErrors) { // loop detected console.error( - `Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.` + `Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`, ); } } // callerFile and callerLine already set to defaults above } finally { - (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = originalFunc; + (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = + originalFunc; } return { callerFile, callerLine }; } @@ -366,18 +392,16 @@ class Logger { } // Only get caller info if current level is at or above callerLevel threshold - const shouldIncludeCaller = - this.options.levels[level] <= + const shouldIncludeCaller = this.options.levels[level] <= this.options.levels[this.options.callerLevel]; const { callerFile, callerLine } = shouldIncludeCaller ? this.getCallerInfo() : { callerFile: undefined, callerLine: undefined }; const now = new Date(); - const time = - this.options.time === 'long' - ? now.toISOString() - : now.toISOString().slice(0, 16).replace('T', ' '); + const time = this.options.time === "long" + ? now.toISOString() + : now.toISOString().slice(0, 16).replace("T", " "); const logEntry: LogEntry = { level, @@ -389,7 +413,10 @@ class Logger { }; // Only include caller info if it was requested - if (shouldIncludeCaller && callerFile !== undefined && callerLine !== undefined) { + if ( + shouldIncludeCaller && callerFile !== undefined && + callerLine !== undefined + ) { logEntry.callerFile = callerFile; logEntry.callerLine = callerLine; } @@ -398,17 +425,17 @@ class Logger { const resetColour = this.options.colours.reset; // Select the appropriate formatter - const formatter = - this.formatters[this.options.format] || this.formatters.json; + const formatter = this.formatters[this.options.format] || + this.formatters.json; let formattedLog: string; try { formattedLog = formatter(logEntry); // Ensure formatter returned a string - if (typeof formattedLog !== 'string') { + if (typeof formattedLog !== "string") { throw new Error( - `Formatter returned ${typeof formattedLog} instead of string` + `Formatter returned ${typeof formattedLog} instead of string`, ); } } catch (error) { @@ -416,18 +443,24 @@ class Logger { try { const safeEntry = { ...logEntry, - formatterError: `Formatter failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + formatterError: `Formatter failed: ${ + error instanceof Error ? error.message : "Unknown error" + }`, }; formattedLog = this.formatters.json(safeEntry); } catch { // Even JSON formatter failed, create minimal safe output - formattedLog = `{"level":"${logEntry.level}","msg":"${String( - logEntry.msg - ).replace( - /"/g, - // eslint-disable-next-line no-useless-escape - '\"' - )}","formatterError":"Formatter failed: ${error instanceof Error ? error.message : 'Unknown error'}"}`; + formattedLog = `{"level":"${logEntry.level}","msg":"${ + String( + logEntry.msg, + ).replace( + /"/g, + // eslint-disable-next-line no-useless-escape + '"', + ) + }","formatterError":"Formatter failed: ${ + error instanceof Error ? error.message : "Unknown error" + }"}`; } } @@ -440,19 +473,19 @@ class Logger { } error(message: unknown, ...args: unknown[]): void { - this.log('error', message, ...args); + this.log("error", message, ...args); } warn(message: unknown, ...args: unknown[]): void { - this.log('warn', message, ...args); + this.log("warn", message, ...args); } info(message: unknown, ...args: unknown[]): void { - this.log('info', message, ...args); + this.log("info", message, ...args); } debug(message: unknown, ...args: unknown[]): void { - this.log('debug', message, ...args); + this.log("debug", message, ...args); } level(): LogLevel; @@ -480,4 +513,4 @@ class Logger { } } -export default Logger; \ No newline at end of file +export default Logger; diff --git a/test/helpers/logger-test-helpers.js b/test/helpers/logger-test-helpers.js index d175081..662ed02 100644 --- a/test/helpers/logger-test-helpers.js +++ b/test/helpers/logger-test-helpers.js @@ -9,7 +9,7 @@ const originalIsTTY = Deno.stdout.isTerminal(); export function mockConsole() { console.log = (...args) => { - capturedLogs.push(args.join(' ')); + capturedLogs.push(args.join(" ")); }; } @@ -74,7 +74,7 @@ export function restoreMocks() { // Helper to get parsed JSON from first captured log export function getFirstLogAsJSON() { if (capturedLogs.length === 0) { - throw new Error('No logs captured'); + throw new Error("No logs captured"); } return JSON.parse(capturedLogs[0]); } diff --git a/test/logger.caller-level.test.js b/test/logger.caller-level.test.js index a2d3ea0..536ee47 100644 --- a/test/logger.caller-level.test.js +++ b/test/logger.caller-level.test.js @@ -1,28 +1,28 @@ -import { assertEquals, assert } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import { assert, assertEquals } from "@std/assert"; +import Logger from "../lib/logger.ts"; Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for error when callerLevel is warn", () => { - const logger = new Logger({ callerLevel: 'warn' }); + const logger = new Logger({ callerLevel: "warn" }); // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { - logger.error('test message'); + logger.error("test message"); // Parse JSON output and check for caller info const logEntry = JSON.parse(capturedOutput); assert( logEntry.callerFile, - 'Should include callerFile for error level' + "Should include callerFile for error level", ); assert( - typeof logEntry.callerLine === 'number', - 'Should include callerLine for error level' + typeof logEntry.callerLine === "number", + "Should include callerLine for error level", ); } finally { console.log = originalLog; @@ -30,27 +30,27 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should include ca }); Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for warn when callerLevel is warn", () => { - const logger = new Logger({ callerLevel: 'warn' }); + const logger = new Logger({ callerLevel: "warn" }); // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { - logger.warn('test message'); + logger.warn("test message"); // Parse JSON output and check for caller info const logEntry = JSON.parse(capturedOutput); assert( logEntry.callerFile, - 'Should include callerFile for warn level' + "Should include callerFile for warn level", ); assert( - typeof logEntry.callerLine === 'number', - 'Should include callerLine for warn level' + typeof logEntry.callerLine === "number", + "Should include callerLine for warn level", ); } finally { console.log = originalLog; @@ -58,29 +58,29 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should include ca }); Deno.test("Logger callerLevel - Caller Information Filtering - should NOT include caller info for info when callerLevel is warn", () => { - const logger = new Logger({ callerLevel: 'warn' }); + const logger = new Logger({ callerLevel: "warn" }); // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { - logger.info('test message'); + logger.info("test message"); // Parse JSON output and check for absence of caller info const logEntry = JSON.parse(capturedOutput); assertEquals( logEntry.callerFile, undefined, - 'Should NOT include callerFile for info level' + "Should NOT include callerFile for info level", ); assertEquals( logEntry.callerLine, undefined, - 'Should NOT include callerLine for info level' + "Should NOT include callerLine for info level", ); } finally { console.log = originalLog; @@ -88,29 +88,29 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should NOT includ }); Deno.test("Logger callerLevel - Caller Information Filtering - should NOT include caller info for debug when callerLevel is warn", () => { - const logger = new Logger({ callerLevel: 'warn', level: 'debug' }); // Set level to debug to ensure debug messages are logged + const logger = new Logger({ callerLevel: "warn", level: "debug" }); // Set level to debug to ensure debug messages are logged // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { - logger.debug('test message'); + logger.debug("test message"); // Parse JSON output and check for absence of caller info const logEntry = JSON.parse(capturedOutput); assertEquals( logEntry.callerFile, undefined, - 'Should NOT include callerFile for debug level' + "Should NOT include callerFile for debug level", ); assertEquals( logEntry.callerLine, undefined, - 'Should NOT include callerLine for debug level' + "Should NOT include callerLine for debug level", ); } finally { console.log = originalLog; @@ -118,29 +118,29 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should NOT includ }); Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for all levels when callerLevel is debug", () => { - const logger = new Logger({ callerLevel: 'debug', level: 'debug' }); + const logger = new Logger({ callerLevel: "debug", level: "debug" }); // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { // Test each level - const levels = ['error', 'warn', 'info', 'debug']; + const levels = ["error", "warn", "info", "debug"]; for (const level of levels) { - logger[level]('test message'); + logger[level]("test message"); const logEntry = JSON.parse(capturedOutput); assert( logEntry.callerFile, - `Should include callerFile for ${level} level` + `Should include callerFile for ${level} level`, ); assert( - typeof logEntry.callerLine === 'number', - `Should include callerLine for ${level} level` + typeof logEntry.callerLine === "number", + `Should include callerLine for ${level} level`, ); } } finally { @@ -149,31 +149,31 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should include ca }); Deno.test("Logger callerLevel - Caller Information Filtering - should NOT include caller info for any level when callerLevel is silent", () => { - const logger = new Logger({ callerLevel: 'silent', level: 'debug' }); + const logger = new Logger({ callerLevel: "silent", level: "debug" }); // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { // Test each level - const levels = ['error', 'warn', 'info', 'debug']; + const levels = ["error", "warn", "info", "debug"]; for (const level of levels) { - logger[level]('test message'); + logger[level]("test message"); const logEntry = JSON.parse(capturedOutput); assertEquals( logEntry.callerFile, undefined, - `Should NOT include callerFile for ${level} level` + `Should NOT include callerFile for ${level} level`, ); assertEquals( logEntry.callerLine, undefined, - `Should NOT include callerLine for ${level} level` + `Should NOT include callerLine for ${level} level`, ); } } finally { @@ -182,33 +182,33 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should NOT includ }); Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should format correctly without caller info when excluded", () => { - const logger = new Logger({ format: 'simple', callerLevel: 'error' }); + const logger = new Logger({ format: "simple", callerLevel: "error" }); // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { - logger.info('test message'); + logger.info("test message"); // Should not contain caller info pattern assert( - !capturedOutput.includes('unknown'), - 'Should not include caller placeholder' + !capturedOutput.includes("unknown"), + "Should not include caller placeholder", ); assert( - !capturedOutput.includes('.js:'), - 'Should not include file:line pattern' + !capturedOutput.includes(".js:"), + "Should not include file:line pattern", ); // Should still contain other parts - assert(capturedOutput.includes('INFO'), 'Should include log level'); + assert(capturedOutput.includes("INFO"), "Should include log level"); assert( - capturedOutput.includes('test message'), - 'Should include message' + capturedOutput.includes("test message"), + "Should include message", ); } finally { console.log = originalLog; @@ -216,27 +216,27 @@ Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should forma }); Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should format correctly with caller info when included", () => { - const logger = new Logger({ format: 'simple', callerLevel: 'info' }); + const logger = new Logger({ format: "simple", callerLevel: "info" }); // Mock console.log to capture output const originalLog = console.log; - let capturedOutput = ''; + let capturedOutput = ""; console.log = (message) => { capturedOutput = message; }; try { - logger.info('test message'); + logger.info("test message"); // Should contain caller info pattern assert( - capturedOutput.includes('.js:'), - 'Should include file:line pattern' + capturedOutput.includes(".js:"), + "Should include file:line pattern", ); - assert(capturedOutput.includes('INFO'), 'Should include log level'); + assert(capturedOutput.includes("INFO"), "Should include log level"); assert( - capturedOutput.includes('test message'), - 'Should include message' + capturedOutput.includes("test message"), + "Should include message", ); } finally { console.log = originalLog; @@ -244,7 +244,7 @@ Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should forma }); Deno.test("Logger callerLevel - Performance Considerations - should not call getCallerInfo when caller info is not needed", () => { - const logger = new Logger({ callerLevel: 'error' }); + const logger = new Logger({ callerLevel: "error" }); // Spy on getCallerInfo method let getCallerInfoCalled = false; @@ -259,11 +259,11 @@ Deno.test("Logger callerLevel - Performance Considerations - should not call get console.log = () => {}; try { - logger.info('test message'); + logger.info("test message"); assertEquals( getCallerInfoCalled, false, - 'getCallerInfo should not be called for info level when callerLevel is error' + "getCallerInfo should not be called for info level when callerLevel is error", ); } finally { console.log = originalLog; @@ -271,7 +271,7 @@ Deno.test("Logger callerLevel - Performance Considerations - should not call get }); Deno.test("Logger callerLevel - Performance Considerations - should call getCallerInfo when caller info is needed", () => { - const logger = new Logger({ callerLevel: 'warn' }); + const logger = new Logger({ callerLevel: "warn" }); // Spy on getCallerInfo method let getCallerInfoCalled = false; @@ -286,13 +286,13 @@ Deno.test("Logger callerLevel - Performance Considerations - should call getCall console.log = () => {}; try { - logger.error('test message'); + logger.error("test message"); assertEquals( getCallerInfoCalled, true, - 'getCallerInfo should be called for error level when callerLevel is warn' + "getCallerInfo should be called for error level when callerLevel is warn", ); } finally { console.log = originalLog; } -}); \ No newline at end of file +}); diff --git a/test/logger.constructor.test.js b/test/logger.constructor.test.js index deb23aa..4456227 100644 --- a/test/logger.constructor.test.js +++ b/test/logger.constructor.test.js @@ -1,74 +1,110 @@ import { assertEquals, assertThrows } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import Logger from "../lib/logger.ts"; Deno.test("Logger Constructor - should throw error for invalid log level", () => { - assertThrows(() => { - new Logger({ level: 'invalid' }); - }, Error, "Invalid log level: invalid. Valid levels are: silent, error, warn, info, debug"); + assertThrows( + () => { + new Logger({ level: "invalid" }); + }, + Error, + "Invalid log level: invalid. Valid levels are: silent, error, warn, info, debug", + ); }); Deno.test("Logger Constructor - should throw error for invalid format", () => { - assertThrows(() => { - new Logger({ format: 'invalid' }); - }, Error, "Invalid format: invalid. Valid formats are: json, simple"); + assertThrows( + () => { + new Logger({ format: "invalid" }); + }, + Error, + "Invalid format: invalid. Valid formats are: json, simple", + ); }); Deno.test("Logger Constructor - should throw error for invalid time option", () => { - assertThrows(() => { - new Logger({ time: 'invalid' }); - }, Error, "Invalid time: invalid. Valid times are: long, short"); + assertThrows( + () => { + new Logger({ time: "invalid" }); + }, + Error, + "Invalid time: invalid. Valid times are: long, short", + ); }); Deno.test("Logger Constructor - should throw error for invalid callerLevel", () => { - assertThrows(() => { - new Logger({ callerLevel: 'invalid' }); - }, Error, "Invalid callerLevel: invalid. Valid levels are: silent, error, warn, info, debug"); + assertThrows( + () => { + new Logger({ callerLevel: "invalid" }); + }, + Error, + "Invalid callerLevel: invalid. Valid levels are: silent, error, warn, info, debug", + ); }); Deno.test("Logger Constructor - should throw error for non-object colours", () => { - assertThrows(() => { - new Logger({ colours: 'not an object' }); - }, Error, "colours option must be an object"); + assertThrows( + () => { + new Logger({ colours: "not an object" }); + }, + Error, + "colours option must be an object", + ); }); Deno.test("Logger Constructor - should throw error for non-object levels", () => { - assertThrows(() => { - new Logger({ levels: 'not an object' }); - }, Error, "levels option must be an object"); + assertThrows( + () => { + new Logger({ levels: "not an object" }); + }, + Error, + "levels option must be an object", + ); }); Deno.test("Logger Constructor - should throw error for invalid level values", () => { - assertThrows(() => { - new Logger({ levels: { error: -1 } }); - }, Error, "Level value for 'error' must be a non-negative integer"); + assertThrows( + () => { + new Logger({ levels: { error: -1 } }); + }, + Error, + "Level value for 'error' must be a non-negative integer", + ); - assertThrows(() => { - new Logger({ levels: { error: 'not a number' } }); - }, Error, "Level value for 'error' must be a non-negative integer"); + assertThrows( + () => { + new Logger({ levels: { error: "not a number" } }); + }, + Error, + "Level value for 'error' must be a non-negative integer", + ); - assertThrows(() => { - new Logger({ levels: { error: 1.5 } }); - }, Error, "Level value for 'error' must be a non-negative integer"); + assertThrows( + () => { + new Logger({ levels: { error: 1.5 } }); + }, + Error, + "Level value for 'error' must be a non-negative integer", + ); }); Deno.test("Logger Constructor - should accept valid options without throwing", () => { // This should not throw new Logger({ - level: 'debug', - format: 'simple', - time: 'long', - callerLevel: 'error', - colours: { error: '\x1b[31m' }, + level: "debug", + format: "simple", + time: "long", + callerLevel: "error", + colours: { error: "\x1b[31m" }, levels: { custom: 4 }, }); }); Deno.test("Logger Constructor - should instantiate with default options", () => { const logger = new Logger(); - assertEquals(logger.options.level, 'info'); - assertEquals(logger.options.format, 'json'); - assertEquals(logger.options.time, 'short'); - assertEquals(logger.options.callerLevel, 'warn'); + assertEquals(logger.options.level, "info"); + assertEquals(logger.options.format, "json"); + assertEquals(logger.options.time, "short"); + assertEquals(logger.options.callerLevel, "warn"); assertEquals(logger.options.levels, { silent: -1, error: 0, @@ -80,49 +116,49 @@ Deno.test("Logger Constructor - should instantiate with default options", () => Deno.test("Logger Constructor - should instantiate with custom options", () => { const logger = new Logger({ - level: 'debug', - format: 'simple', - time: 'long', - callerLevel: 'error', + level: "debug", + format: "simple", + time: "long", + callerLevel: "error", }); - assertEquals(logger.options.level, 'debug'); - assertEquals(logger.options.format, 'simple'); - assertEquals(logger.options.time, 'long'); - assertEquals(logger.options.callerLevel, 'error'); + assertEquals(logger.options.level, "debug"); + assertEquals(logger.options.format, "simple"); + assertEquals(logger.options.time, "long"); + assertEquals(logger.options.callerLevel, "error"); }); Deno.test("Logger Constructor - should merge options correctly", () => { const customOptions = { - level: 'debug', - format: 'simple', - time: 'long', + level: "debug", + format: "simple", + time: "long", colours: { - error: '\x1b[31m', // different red + error: "\x1b[31m", // different red }, }; const logger = new Logger(customOptions); - assertEquals(logger.options.level, 'debug'); - assertEquals(logger.options.format, 'simple'); - assertEquals(logger.options.time, 'long'); - assertEquals(logger.options.colours.error, '\x1b[31m'); + assertEquals(logger.options.level, "debug"); + assertEquals(logger.options.format, "simple"); + assertEquals(logger.options.time, "long"); + assertEquals(logger.options.colours.error, "\x1b[31m"); // Should still have other default colors - assertEquals(logger.options.colours.warn, '\x1b[33m'); + assertEquals(logger.options.colours.warn, "\x1b[33m"); }); Deno.test("Logger Constructor - should have all log level methods", () => { const logger = new Logger(); - assertEquals(typeof logger.error, 'function'); - assertEquals(typeof logger.warn, 'function'); - assertEquals(typeof logger.info, 'function'); - assertEquals(typeof logger.debug, 'function'); + assertEquals(typeof logger.error, "function"); + assertEquals(typeof logger.warn, "function"); + assertEquals(typeof logger.info, "function"); + assertEquals(typeof logger.debug, "function"); }); Deno.test("Logger Constructor - should have level management methods", () => { const logger = new Logger(); - assertEquals(typeof logger.level, 'function'); - assertEquals(typeof logger.setLevel, 'function'); + assertEquals(typeof logger.level, "function"); + assertEquals(typeof logger.setLevel, "function"); }); Deno.test("Logger Constructor - should detect TTY correctly", () => { @@ -146,13 +182,13 @@ Deno.test("Logger Constructor - should work with all existing constructor patter new Logger(); // Partial options - should not throw - new Logger({ level: 'debug' }); + new Logger({ level: "debug" }); // Full options (without time) - should not throw new Logger({ - level: 'warn', - format: 'simple', - colours: { error: '\x1b[31m' }, + level: "warn", + format: "simple", + colours: { error: "\x1b[31m" }, levels: { custom: 5 }, }); -}); \ No newline at end of file +}); diff --git a/test/logger.internal-errors.test.js b/test/logger.internal-errors.test.js index 97b2a54..53efd08 100644 --- a/test/logger.internal-errors.test.js +++ b/test/logger.internal-errors.test.js @@ -1,24 +1,24 @@ -import { assertEquals, assert } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import { assert, assertEquals } from "@std/assert"; +import Logger from "../lib/logger.ts"; import { - setupMocks, - getCapturedLogs, clearCapturedLogs, -} from './helpers/logger-test-helpers.js'; + getCapturedLogs, + setupMocks, +} from "./helpers/logger-test-helpers.js"; // Setup and teardown for all tests setupMocks(); Deno.test("Logger Internal Error Handling - Formatter Error Handling - should fall back to JSON formatter when custom formatter throws", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); + const logger = new Logger({ format: "simple" }); // Replace the simple formatter with one that throws logger.formatters.simple = function () { - throw new Error('Custom formatter error'); + throw new Error("Custom formatter error"); }; - logger.info('test message'); + logger.info("test message"); // Should still produce output using JSON formatter fallback assertEquals(getCapturedLogs().length, 1); @@ -26,24 +26,24 @@ Deno.test("Logger Internal Error Handling - Formatter Error Handling - should fa // Should be valid JSON (fallback to JSON formatter) const logOutput = getCapturedLogs()[0]; const parsed = JSON.parse(logOutput); - assertEquals(parsed.msg, 'test message'); + assertEquals(parsed.msg, "test message"); assert( parsed.formatterError.includes( - 'Formatter failed: Custom formatter error' - ) + "Formatter failed: Custom formatter error", + ), ); }); Deno.test("Logger Internal Error Handling - Formatter Error Handling - should not crash when formatter returns non-string", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); + const logger = new Logger({ format: "simple" }); // Replace formatter with one that returns an object instead of string logger.formatters.simple = function () { return { notAString: true }; }; - logger.info('test message'); + logger.info("test message"); // Should still produce output (fallback should handle this) assertEquals(getCapturedLogs().length, 1); @@ -51,25 +51,25 @@ Deno.test("Logger Internal Error Handling - Formatter Error Handling - should no // Should be valid JSON from fallback const logOutput = getCapturedLogs()[0]; const parsed = JSON.parse(logOutput); - assertEquals(parsed.msg, 'test message'); + assertEquals(parsed.msg, "test message"); }); Deno.test("Logger Internal Error Handling - Formatter Error Handling - should preserve original formatters after error", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); + const logger = new Logger({ format: "simple" }); // Temporarily break the formatter const originalSimple = logger.formatters.simple; logger.formatters.simple = function () { - throw new Error('Temporary error'); + throw new Error("Temporary error"); }; - logger.info('first message'); + logger.info("first message"); // Restore the formatter logger.formatters.simple = originalSimple; - logger.info('second message'); + logger.info("second message"); // First message should have used fallback, second should work normally assertEquals(getCapturedLogs().length, 2); @@ -78,17 +78,17 @@ Deno.test("Logger Internal Error Handling - Formatter Error Handling - should pr JSON.parse(getCapturedLogs()[0]); // This should not throw // Second log should be simple format - assert(getCapturedLogs()[1].includes('[INFO ]')); - assert(getCapturedLogs()[1].includes('second message')); + assert(getCapturedLogs()[1].includes("[INFO ]")); + assert(getCapturedLogs()[1].includes("second message")); }); Deno.test("Logger Internal Error Handling - Formatter Error Handling - should handle repeated formatter failures without memory leaks", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); + const logger = new Logger({ format: "simple" }); // Break the formatter logger.formatters.simple = function () { - throw new Error('Always fails'); + throw new Error("Always fails"); }; // Log many times @@ -103,4 +103,4 @@ Deno.test("Logger Internal Error Handling - Formatter Error Handling - should ha getCapturedLogs().forEach((log) => { JSON.parse(log); // This should not throw }); -}); \ No newline at end of file +}); diff --git a/test/logger.internal-fallback.test.js b/test/logger.internal-fallback.test.js index 5122e8c..93902c5 100644 --- a/test/logger.internal-fallback.test.js +++ b/test/logger.internal-fallback.test.js @@ -1,89 +1,89 @@ -import { assertEquals, assertThrows, assert } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import { assert, assertEquals, assertThrows } from "@std/assert"; +import Logger from "../lib/logger.ts"; import { - setupMocks, - getCapturedLogs, + clearCapturedErrors, clearCapturedLogs, getCapturedErrors, - clearCapturedErrors, -} from './helpers/logger-test-helpers.js'; + getCapturedLogs, + setupMocks, +} from "./helpers/logger-test-helpers.js"; // Setup and teardown for all tests setupMocks(); Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle circular references in log data", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Create circular reference - const obj = { name: 'test' }; + const obj = { name: "test" }; obj.self = obj; - logger.info('Message with circular ref: %j', obj); + logger.info("Message with circular ref: %j", obj); assertEquals(getCapturedLogs().length, 1); const logOutput = getCapturedLogs()[0]; // Should be valid JSON despite circular reference const parsed = JSON.parse(logOutput); - assertEquals(parsed.level, 'info'); + assertEquals(parsed.level, "info"); // Check for either jsonError or that the message was logged successfully assert( - parsed.jsonError?.includes('JSON stringify failed') || - parsed.msg.includes('Message with circular ref') + parsed.jsonError?.includes("JSON stringify failed") || + parsed.msg.includes("Message with circular ref"), ); }); Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle objects with non-serializable properties", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Create object with function (non-serializable) const objWithFunction = { - name: 'test', + name: "test", func: function () { - return 'hello'; + return "hello"; }, - symbol: Symbol('test'), + symbol: Symbol("test"), undefined: undefined, }; - logger.info('Object: %j', objWithFunction); + logger.info("Object: %j", objWithFunction); assertEquals(getCapturedLogs().length, 1); const logOutput = getCapturedLogs()[0]; // Should produce valid JSON const parsed = JSON.parse(logOutput); - assertEquals(parsed.level, 'info'); + assertEquals(parsed.level, "info"); // Should have the message in some form - assert(parsed.msg.includes('Object:')); + assert(parsed.msg.includes("Object:")); }); Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle when JSON formatter itself is broken", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Break the JSON formatter logger.formatters.json = function () { - throw new Error('JSON formatter is broken'); + throw new Error("JSON formatter is broken"); }; - logger.info('test message'); + logger.info("test message"); // Should still produce some output (last resort fallback) assertEquals(getCapturedLogs().length, 1); const logOutput = getCapturedLogs()[0]; // Should contain the message even if not perfectly formatted - assert(logOutput.includes('test message')); + assert(logOutput.includes("test message")); }); Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - should handle stack manipulation errors", () => { clearCapturedLogs(); clearCapturedErrors(); - const logger = new Logger({ format: 'json', callerLevel: 'info' }); + const logger = new Logger({ format: "json", callerLevel: "info" }); // Override getCallerInfo to simulate an error const originalGetCallerInfo = logger.getCallerInfo; @@ -91,26 +91,26 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - this.callerErrorCount++; if (this.callerErrorCount <= this.maxCallerErrors) { console.error( - 'Error retrieving caller info:', - new Error('Simulated caller error') + "Error retrieving caller info:", + new Error("Simulated caller error"), ); if (this.callerErrorCount === this.maxCallerErrors) { console.error( - `Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.` + `Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`, ); } } - return { callerFile: 'unknown', callerLine: 0 }; + return { callerFile: "unknown", callerLine: 0 }; }; try { - logger.info('test with simulated caller error'); + logger.info("test with simulated caller error"); // Should still log the message assertEquals(getCapturedLogs().length, 1); const parsed = JSON.parse(getCapturedLogs()[0]); - assertEquals(parsed.msg, 'test with simulated caller error'); - assertEquals(parsed.callerFile, 'unknown'); + assertEquals(parsed.msg, "test with simulated caller error"); + assertEquals(parsed.callerFile, "unknown"); assertEquals(parsed.callerLine, 0); // Should have logged an error about caller detection @@ -125,7 +125,7 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - clearCapturedLogs(); clearCapturedErrors(); - const logger = new Logger({ format: 'json', callerLevel: 'info' }); + const logger = new Logger({ format: "json", callerLevel: "info" }); // Override getCallerInfo to always simulate errors const originalGetCallerInfo = logger.getCallerInfo; @@ -133,16 +133,16 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - this.callerErrorCount++; if (this.callerErrorCount <= this.maxCallerErrors) { console.error( - 'Error retrieving caller info:', - new Error('Always fails') + "Error retrieving caller info:", + new Error("Always fails"), ); if (this.callerErrorCount === this.maxCallerErrors) { console.error( - `Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.` + `Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`, ); } } - return { callerFile: 'unknown', callerLine: 0 }; + return { callerFile: "unknown", callerLine: 0 }; }; try { @@ -163,8 +163,8 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - const suppressionFound = errorLogs.some((errorArgs) => errorArgs.some( (arg) => - typeof arg === 'string' && - arg.includes('Suppressing further caller error messages') + typeof arg === "string" && + arg.includes("Suppressing further caller error messages"), ) ); assert(suppressionFound); @@ -178,50 +178,50 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - clearCapturedLogs(); clearCapturedErrors(); - const logger = new Logger({ format: 'json', callerLevel: 'info' }); + const logger = new Logger({ format: "json", callerLevel: "info" }); // Override getCallerInfo to simulate different phases const originalGetCallerInfo = logger.getCallerInfo; - let phase = 'error1'; + let phase = "error1"; logger.getCallerInfo = function () { - if (phase === 'error1') { + if (phase === "error1") { this.callerErrorCount++; if (this.callerErrorCount <= this.maxCallerErrors) { console.error( - 'Error retrieving caller info:', - new Error('Phase 1 error') + "Error retrieving caller info:", + new Error("Phase 1 error"), ); } - return { callerFile: 'unknown', callerLine: 0 }; - } else if (phase === 'working') { + return { callerFile: "unknown", callerLine: 0 }; + } else if (phase === "working") { // Reset error count on successful call this.callerErrorCount = 0; return originalGetCallerInfo.call(this); - } else if (phase === 'error2') { + } else if (phase === "error2") { this.callerErrorCount++; if (this.callerErrorCount <= this.maxCallerErrors) { console.error( - 'Error retrieving caller info:', - new Error('Phase 2 error') + "Error retrieving caller info:", + new Error("Phase 2 error"), ); } - return { callerFile: 'unknown', callerLine: 0 }; + return { callerFile: "unknown", callerLine: 0 }; } }; try { // Cause some errors - logger.info('test 1'); - logger.info('test 2'); + logger.info("test 1"); + logger.info("test 2"); // Switch to working mode - phase = 'working'; - logger.info('test 3'); + phase = "working"; + logger.info("test 3"); // Break it again - phase = 'error2'; - logger.info('test 4'); + phase = "error2"; + logger.info("test 4"); const errorLogs = getCapturedErrors(); // Should have errors from both phases @@ -234,20 +234,24 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when console.log itself throws", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Break console.log const originalLog = console.log; console.log = function () { - throw new Error('Console is broken'); + throw new Error("Console is broken"); }; try { // This should not crash the process, but the error will bubble up // since there's no try-catch around console.log in the logger - assertThrows(() => { - logger.info('test message'); - }, Error, "Console is broken"); + assertThrows( + () => { + logger.info("test message"); + }, + Error, + "Console is broken", + ); } finally { console.log = originalLog; } @@ -255,20 +259,20 @@ Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when util.format has issues with complex objects", () => { setupMocks(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Create an object that will cause issues with string conversion const problematicObject = { toString: function () { - throw new Error('toString failed'); + throw new Error("toString failed"); }, valueOf: function () { - throw new Error('valueOf failed'); + throw new Error("valueOf failed"); }, }; // The logger should handle this gracefully and not throw - logger.info('Message: %s', problematicObject); + logger.info("Message: %s", problematicObject); const logs = getCapturedLogs(); assertEquals(logs.length, 1); @@ -276,15 +280,14 @@ Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should const output = JSON.parse(logs[0]); // The logger should handle the problematic object gracefully // Either by showing [object Object] or the actual object structure - assertEquals(typeof output.msg, 'string'); - assertEquals(output.msg.startsWith('Message: '), true); - + assertEquals(typeof output.msg, "string"); + assertEquals(output.msg.startsWith("Message: "), true); }); Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when hostname fails", () => { setupMocks(); clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Mock hostname retrieval to throw (this would need to be mocked at the source) // Since we can't directly mock os.hostname in Deno, this test shows the concept @@ -292,9 +295,8 @@ Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should // For Deno, we could mock the hostname call if it were extracted to a mockable function // For now, this is more of a documentation test - logger.info('test message'); + logger.info("test message"); assertEquals(getCapturedLogs().length, 1); - }); Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should handle formatter failures gracefully", () => { @@ -302,11 +304,11 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha clearCapturedLogs(); clearCapturedErrors(); - const logger = new Logger({ format: 'simple', callerLevel: 'info' }); + const logger = new Logger({ format: "simple", callerLevel: "info" }); // Break the simple formatter logger.formatters.simple = function () { - throw new Error('Simple formatter broken'); + throw new Error("Simple formatter broken"); }; // Also simulate caller detection failure @@ -315,16 +317,16 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha this.callerErrorCount++; if (this.callerErrorCount <= this.maxCallerErrors) { console.error( - 'Error retrieving caller info:', - new Error('Caller detection failed') + "Error retrieving caller info:", + new Error("Caller detection failed"), ); } - return { callerFile: 'unknown', callerLine: 0 }; + return { callerFile: "unknown", callerLine: 0 }; }; try { // Should still produce some output despite multiple failures - logger.info('test message'); + logger.info("test message"); // Should produce some kind of output (fallback to JSON formatter) assertEquals(getCapturedLogs().length, 1); @@ -332,13 +334,13 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha // Should be valid JSON (fallback formatter) const parsed = JSON.parse(output); - assertEquals(parsed.msg, 'test message'); - assert(parsed.formatterError.includes('Simple formatter broken')); - assertEquals(parsed.callerFile, 'unknown'); + assertEquals(parsed.msg, "test message"); + assert(parsed.formatterError.includes("Simple formatter broken")); + assertEquals(parsed.callerFile, "unknown"); } finally { logger.getCallerInfo = originalGetCallerInfo; logger.callerErrorCount = 0; - } + } }); Deno.test("Logger Additional Fallback Tests - Resource Cleanup - should not leak memory during repeated errors", () => { @@ -346,11 +348,11 @@ Deno.test("Logger Additional Fallback Tests - Resource Cleanup - should not leak clearCapturedLogs(); clearCapturedErrors(); - const logger = new Logger({ format: 'simple' }); + const logger = new Logger({ format: "simple" }); // Break the formatter logger.formatters.simple = function () { - throw new Error('Always fails'); + throw new Error("Always fails"); }; // Log many times to check for memory leaks @@ -364,5 +366,4 @@ Deno.test("Logger Additional Fallback Tests - Resource Cleanup - should not leak // Check that we're not accumulating error state // (This is more of a smoke test - real memory leak detection would need different tools) assert(true); // If we get here without crashing, that's good - -}); \ No newline at end of file +}); diff --git a/test/logger.json-formatter.test.js b/test/logger.json-formatter.test.js index 2be0958..3d240ad 100644 --- a/test/logger.json-formatter.test.js +++ b/test/logger.json-formatter.test.js @@ -1,19 +1,19 @@ -import { assertEquals, assert } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import { assert, assertEquals } from "@std/assert"; +import Logger from "../lib/logger.ts"; import { - setupMocks, - getCapturedLogs, clearCapturedLogs, + getCapturedLogs, getFirstLogAsJSON, -} from './helpers/logger-test-helpers.js'; + setupMocks, +} from "./helpers/logger-test-helpers.js"; // Setup and teardown for all tests setupMocks(); Deno.test("Logger JSON Formatter - Basic JSON Output - should produce valid JSON output", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - logger.info('test message'); + const logger = new Logger({ format: "json" }); + logger.info("test message"); assertEquals(getCapturedLogs().length, 1); const logOutput = getCapturedLogs()[0]; @@ -24,36 +24,36 @@ Deno.test("Logger JSON Formatter - Basic JSON Output - should produce valid JSON Deno.test("Logger JSON Formatter - Basic JSON Output - should include all required fields in JSON output", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json', callerLevel: 'info' }); - logger.info('test message'); + const logger = new Logger({ format: "json", callerLevel: "info" }); + logger.info("test message"); const parsed = getFirstLogAsJSON(); - assertEquals(parsed.level, 'info'); + assertEquals(parsed.level, "info"); assertEquals(parsed.levelNumber, 2); - assertEquals(parsed.msg, 'test message'); - assertEquals(typeof parsed.time, 'string'); - assertEquals(typeof parsed.pid, 'number'); - assertEquals(typeof parsed.hostname, 'string'); + assertEquals(parsed.msg, "test message"); + assertEquals(typeof parsed.time, "string"); + assertEquals(typeof parsed.pid, "number"); + assertEquals(typeof parsed.hostname, "string"); assert(parsed.callerFile); - assertEquals(typeof parsed.callerLine, 'number'); + assertEquals(typeof parsed.callerLine, "number"); }); Deno.test("Logger JSON Formatter - Basic JSON Output - should format timestamp correctly based on time option", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json', time: 'short' }); - logger.info('test message'); + const logger = new Logger({ format: "json", time: "short" }); + logger.info("test message"); const parsed = getFirstLogAsJSON(); // Should be short format, not ISO assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)); - assert(!parsed.time.includes('T')); - assert(!parsed.time.includes('Z')); + assert(!parsed.time.includes("T")); + assert(!parsed.time.includes("Z")); }); Deno.test("Logger JSON Formatter - JSON Error Handling - should handle circular references in log entry", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Create a circular reference by modifying the logger's formatters const originalJsonFormatter = logger.formatters.json; @@ -67,33 +67,33 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle circular return originalJsonFormatter.call(this, logEntry); }; - logger.info('test with circular reference'); + logger.info("test with circular reference"); const logOutput = getCapturedLogs()[0]; // Should be valid JSON despite circular reference const parsed = JSON.parse(logOutput); // Should contain error information - assert(parsed.jsonError.includes('JSON stringify failed')); - assertEquals(parsed.msg, 'test with circular reference'); + assert(parsed.jsonError.includes("JSON stringify failed")); + assertEquals(parsed.msg, "test with circular reference"); }); Deno.test("Logger JSON Formatter - JSON Error Handling - should handle JSON stringify errors with fallback", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Create a problematic object that will cause JSON.stringify to fail const problematic = {}; - Object.defineProperty(problematic, 'badProp', { + Object.defineProperty(problematic, "badProp", { get() { - throw new Error('Property access error'); + throw new Error("Property access error"); }, enumerable: true, }); // Test the formatter directly with a problematic object const problematicLogEntry = { - level: 'info', - msg: 'test message', + level: "info", + msg: "test message", problematic: problematic, }; @@ -101,12 +101,12 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle JSON stri // Should produce valid JSON with error info const parsed = JSON.parse(result); - assert(parsed.jsonError.includes('JSON stringify failed')); + assert(parsed.jsonError.includes("JSON stringify failed")); }); Deno.test("Logger JSON Formatter - JSON Error Handling - should handle extreme JSON stringify failures", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // Create an object that will fail even the safe fallback // by mocking JSON.stringify to always throw @@ -116,22 +116,22 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle extreme J JSON.stringify = function (...args) { callCount++; if (callCount <= 2) { - throw new Error('Mock JSON error'); + throw new Error("Mock JSON error"); } return originalStringify.apply(this, args); }; try { const result = logger.formatters.json({ - level: 'error', - msg: 'test message', + level: "error", + msg: "test message", }); // Should still produce valid JSON string even after multiple failures const parsed = JSON.parse(result); - assertEquals(parsed.level, 'error'); - assertEquals(parsed.msg, 'test message'); - assert(parsed.jsonError.includes('Multiple JSON errors occurred')); + assertEquals(parsed.level, "error"); + assertEquals(parsed.msg, "test message"); + assert(parsed.jsonError.includes("Multiple JSON errors occurred")); } finally { JSON.stringify = originalStringify; } @@ -139,7 +139,7 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle extreme J Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle special characters", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); logger.info('Special chars: "quotes", \\backslash, \nnewline'); // Should produce valid JSON despite special characters @@ -148,26 +148,26 @@ Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should ha Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle empty messages", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - logger.info(''); + const logger = new Logger({ format: "json" }); + logger.info(""); const parsed = getFirstLogAsJSON(); - assertEquals(parsed.msg, ''); + assertEquals(parsed.msg, ""); }); Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle null and undefined arguments", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - logger.info('Value: %s', null); + const logger = new Logger({ format: "json" }); + logger.info("Value: %s", null); const parsed = getFirstLogAsJSON(); - assertEquals(parsed.msg, 'Value: null'); + assertEquals(parsed.msg, "Value: null"); }); Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle very long messages", () => { clearCapturedLogs(); - const longMessage = 'x'.repeat(10000); - const logger = new Logger({ format: 'json' }); + const longMessage = "x".repeat(10000); + const logger = new Logger({ format: "json" }); logger.info(longMessage); const parsed = getFirstLogAsJSON(); @@ -176,9 +176,9 @@ Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should ha Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle objects in messages", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - const obj = { key: 'value', nested: { prop: 123 } }; - logger.info('Object: %j', obj); + const logger = new Logger({ format: "json" }); + const obj = { key: "value", nested: { prop: 123 } }; + logger.info("Object: %j", obj); const parsed = getFirstLogAsJSON(); assert(parsed.msg.includes('{"key":"value","nested":{"prop":123}}')); @@ -186,40 +186,40 @@ Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should ha Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log error messages with correct level", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - logger.error('error message'); + const logger = new Logger({ format: "json" }); + logger.error("error message"); const parsed = getFirstLogAsJSON(); - assertEquals(parsed.level, 'error'); + assertEquals(parsed.level, "error"); assertEquals(parsed.levelNumber, 0); }); Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log warn messages with correct level", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - logger.warn('warn message'); + const logger = new Logger({ format: "json" }); + logger.warn("warn message"); const parsed = getFirstLogAsJSON(); - assertEquals(parsed.level, 'warn'); + assertEquals(parsed.level, "warn"); assertEquals(parsed.levelNumber, 1); }); Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log info messages with correct level", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - logger.info('info message'); + const logger = new Logger({ format: "json" }); + logger.info("info message"); const parsed = getFirstLogAsJSON(); - assertEquals(parsed.level, 'info'); + assertEquals(parsed.level, "info"); assertEquals(parsed.levelNumber, 2); }); Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log debug messages with correct level", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'debug', format: 'json' }); - logger.debug('debug message'); + const logger = new Logger({ level: "debug", format: "json" }); + logger.debug("debug message"); const parsed = getFirstLogAsJSON(); - assertEquals(parsed.level, 'debug'); + assertEquals(parsed.level, "debug"); assertEquals(parsed.levelNumber, 3); -}); \ No newline at end of file +}); diff --git a/test/logger.level-management.test.js b/test/logger.level-management.test.js index e7b635c..b5a61d0 100644 --- a/test/logger.level-management.test.js +++ b/test/logger.level-management.test.js @@ -1,151 +1,159 @@ import { assertEquals, assertThrows } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import Logger from "../lib/logger.ts"; import { - setupMocks, - getCapturedLogs, clearCapturedLogs, -} from './helpers/logger-test-helpers.js'; + getCapturedLogs, + setupMocks, +} from "./helpers/logger-test-helpers.js"; // Setup and teardown for all tests setupMocks(); Deno.test("Logger Level Management - Level Setting and Getting - should change log level with level() method", () => { const logger = new Logger(); - logger.level('debug'); - assertEquals(logger.options.level, 'debug'); + logger.level("debug"); + assertEquals(logger.options.level, "debug"); }); Deno.test("Logger Level Management - Level Setting and Getting - should return current level when called without arguments", () => { - const logger = new Logger({ level: 'debug' }); - assertEquals(logger.level(), 'debug'); + const logger = new Logger({ level: "debug" }); + assertEquals(logger.level(), "debug"); }); Deno.test("Logger Level Management - Level Setting and Getting - should return new level when setting level", () => { const logger = new Logger(); - const result = logger.level('error'); - assertEquals(result, 'error'); - assertEquals(logger.options.level, 'error'); + const result = logger.level("error"); + assertEquals(result, "error"); + assertEquals(logger.options.level, "error"); }); Deno.test("Logger Level Management - Level Setting and Getting - should throw error for invalid log level", () => { const logger = new Logger(); - assertThrows(() => { - logger.level('invalid'); - }, Error, "Invalid log level: invalid"); + assertThrows( + () => { + logger.level("invalid"); + }, + Error, + "Invalid log level: invalid", + ); }); Deno.test("Logger Level Management - Level Setting and Getting - should allow method chaining after setting level", () => { const logger = new Logger(); // This should not throw and should return a level - const result = logger.level('warn'); - assertEquals(result, 'warn'); - assertEquals(typeof result, 'string'); + const result = logger.level("warn"); + assertEquals(result, "warn"); + assertEquals(typeof result, "string"); }); Deno.test("Logger Level Management - setLevel Method - should have setLevel method as alias", () => { const logger = new Logger(); - assertEquals(typeof logger.setLevel, 'function'); + assertEquals(typeof logger.setLevel, "function"); }); Deno.test("Logger Level Management - setLevel Method - should set level correctly with setLevel method", () => { const logger = new Logger(); - const result = logger.setLevel('debug'); - assertEquals(result, 'debug'); - assertEquals(logger.options.level, 'debug'); + const result = logger.setLevel("debug"); + assertEquals(result, "debug"); + assertEquals(logger.options.level, "debug"); }); Deno.test("Logger Level Management - setLevel Method - should return current level with setLevel when no args", () => { - const logger = new Logger({ level: 'warn' }); + const logger = new Logger({ level: "warn" }); const result = logger.setLevel(); - assertEquals(result, 'warn'); + assertEquals(result, "warn"); }); Deno.test("Logger Level Management - setLevel Method - should throw error for invalid level in setLevel", () => { const logger = new Logger(); - assertThrows(() => { - logger.setLevel('invalid'); - }, Error, "Invalid log level: invalid"); + assertThrows( + () => { + logger.setLevel("invalid"); + }, + Error, + "Invalid log level: invalid", + ); }); Deno.test("Logger Level Management - setLevel Method - should maintain consistency between level() and setLevel()", () => { const logger = new Logger(); - logger.level('error'); - assertEquals(logger.setLevel(), 'error'); + logger.level("error"); + assertEquals(logger.setLevel(), "error"); - logger.setLevel('debug'); - assertEquals(logger.level(), 'debug'); + logger.setLevel("debug"); + assertEquals(logger.level(), "debug"); }); Deno.test("Logger Level Management - setLevel Method - should support fluent interface pattern", () => { const logger = new Logger(); // This demonstrates the fluent interface working - const currentLevel = logger.level('warn'); - assertEquals(currentLevel, 'warn'); + const currentLevel = logger.level("warn"); + assertEquals(currentLevel, "warn"); // Both methods should return the current level for chaining - assertEquals(logger.level('info'), 'info'); - assertEquals(logger.setLevel('debug'), 'debug'); + assertEquals(logger.level("info"), "info"); + assertEquals(logger.setLevel("debug"), "debug"); }); Deno.test("Logger Level Management - Log Level Filtering - should filter debug messages when level is info", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'info' }); - logger.debug('debug message'); + const logger = new Logger({ level: "info" }); + logger.debug("debug message"); assertEquals(getCapturedLogs().length, 0); }); Deno.test("Logger Level Management - Log Level Filtering - should show info messages when level is info", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'info' }); - logger.info('info message'); + const logger = new Logger({ level: "info" }); + logger.info("info message"); assertEquals(getCapturedLogs().length, 1); }); Deno.test("Logger Level Management - Log Level Filtering - should show error messages at any level", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'error' }); - logger.error('error message'); + const logger = new Logger({ level: "error" }); + logger.error("error message"); assertEquals(getCapturedLogs().length, 1); }); Deno.test("Logger Level Management - Log Level Filtering - should filter warn and info when level is error", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'error' }); - logger.warn('warn message'); - logger.info('info message'); + const logger = new Logger({ level: "error" }); + logger.warn("warn message"); + logger.info("info message"); assertEquals(getCapturedLogs().length, 0); }); Deno.test("Logger Level Management - Log Level Filtering - should show all messages when level is debug", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'debug' }); - logger.error('error message'); - logger.warn('warn message'); - logger.info('info message'); - logger.debug('debug message'); + const logger = new Logger({ level: "debug" }); + logger.error("error message"); + logger.warn("warn message"); + logger.info("info message"); + logger.debug("debug message"); assertEquals(getCapturedLogs().length, 4); }); Deno.test("Logger Level Management - Log Level Filtering - should show warn and above when level is warn", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'warn' }); - logger.error('error message'); - logger.warn('warn message'); - logger.info('info message'); - logger.debug('debug message'); + const logger = new Logger({ level: "warn" }); + logger.error("error message"); + logger.warn("warn message"); + logger.info("info message"); + logger.debug("debug message"); assertEquals(getCapturedLogs().length, 2); }); Deno.test("Logger Level Management - Silent Level - should suppress all output when level is silent", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'silent' }); + const logger = new Logger({ level: "silent" }); - logger.error('error message'); - logger.warn('warn message'); - logger.info('info message'); - logger.debug('debug message'); + logger.error("error message"); + logger.warn("warn message"); + logger.info("info message"); + logger.debug("debug message"); // No messages should be logged assertEquals(getCapturedLogs().length, 0); @@ -153,21 +161,21 @@ Deno.test("Logger Level Management - Silent Level - should suppress all output w Deno.test("Logger Level Management - Silent Level - should allow setting level to silent", () => { const logger = new Logger(); - const result = logger.level('silent'); - assertEquals(result, 'silent'); - assertEquals(logger.options.level, 'silent'); + const result = logger.level("silent"); + assertEquals(result, "silent"); + assertEquals(logger.options.level, "silent"); }); Deno.test("Logger Level Management - Silent Level - should work with setLevel for silent level", () => { const logger = new Logger(); - const result = logger.setLevel('silent'); - assertEquals(result, 'silent'); - assertEquals(logger.options.level, 'silent'); + const result = logger.setLevel("silent"); + assertEquals(result, "silent"); + assertEquals(logger.options.level, "silent"); }); Deno.test("Logger Level Management - Silent Level - should remain silent after multiple log attempts", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'silent' }); + const logger = new Logger({ level: "silent" }); // Try logging multiple times for (let i = 0; i < 5; i++) { @@ -183,23 +191,23 @@ Deno.test("Logger Level Management - Silent Level - should remain silent after m Deno.test("Logger Level Management - Dynamic Level Changes - should respect level changes during runtime", () => { clearCapturedLogs(); - const logger = new Logger({ level: 'error' }); + const logger = new Logger({ level: "error" }); // Should not log at info level - logger.info('info message 1'); + logger.info("info message 1"); assertEquals(getCapturedLogs().length, 0); // Change to info level - logger.level('info'); + logger.level("info"); // Should now log info messages - logger.info('info message 2'); + logger.info("info message 2"); assertEquals(getCapturedLogs().length, 1); // Change to silent - logger.level('silent'); + logger.level("silent"); // Should not log anything - logger.error('error message'); + logger.error("error message"); assertEquals(getCapturedLogs().length, 1); // Still just the previous info message }); diff --git a/test/logger.robustness.test.js b/test/logger.robustness.test.js index bf1de9f..64569b7 100644 --- a/test/logger.robustness.test.js +++ b/test/logger.robustness.test.js @@ -1,10 +1,10 @@ -import { assertEquals, assert } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import { assert, assertEquals } from "@std/assert"; +import Logger from "../lib/logger.ts"; import { - setupMocks, - getCapturedLogs, clearCapturedLogs, -} from './helpers/logger-test-helpers.js'; + getCapturedLogs, + setupMocks, +} from "./helpers/logger-test-helpers.js"; // Setup and teardown for all tests setupMocks(); @@ -14,12 +14,12 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should not crash o const logger = new Logger(); // This should not throw - logger.info('test message'); + logger.info("test message"); }); Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle undefined and null messages gracefully", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); // These should not crash logger.info(undefined); @@ -31,14 +31,14 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle unde const parsed1 = JSON.parse(logs[0]); const parsed2 = JSON.parse(logs[1]); - assertEquals(parsed1.msg, 'undefined'); - assertEquals(parsed2.msg, 'null'); + assertEquals(parsed1.msg, "undefined"); + assertEquals(parsed2.msg, "null"); }); Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle extremely large messages", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - const hugeMessage = 'x'.repeat(100000); + const logger = new Logger({ format: "json" }); + const hugeMessage = "x".repeat(100000); logger.info(hugeMessage); @@ -48,12 +48,12 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle extr Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle circular objects in message formatting", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); - const circular = { name: 'test' }; + const circular = { name: "test" }; circular.self = circular; - logger.info('Circular: %j', circular); + logger.info("Circular: %j", circular); // Should still log something assertEquals(getCapturedLogs().length, 1); @@ -61,7 +61,7 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle circ Deno.test("Logger Robustness - Performance and Memory - should handle rapid consecutive logging without issues", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); for (let i = 0; i < 1000; i++) { logger.info(`rapid message ${i}`); @@ -72,7 +72,7 @@ Deno.test("Logger Robustness - Performance and Memory - should handle rapid cons Deno.test("Logger Robustness - Performance and Memory - should handle repeated logging operations efficiently", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); + const logger = new Logger({ format: "simple" }); const startTime = Date.now(); @@ -91,8 +91,8 @@ Deno.test("Logger Robustness - Performance and Memory - should handle repeated l Deno.test("Logger Robustness - Performance and Memory - should handle mixed format types in rapid succession", () => { clearCapturedLogs(); - const jsonLogger = new Logger({ format: 'json' }); - const simpleLogger = new Logger({ format: 'simple' }); + const jsonLogger = new Logger({ format: "json" }); + const simpleLogger = new Logger({ format: "simple" }); for (let i = 0; i < 50; i++) { jsonLogger.info(`json message ${i}`); @@ -104,40 +104,40 @@ Deno.test("Logger Robustness - Performance and Memory - should handle mixed form Deno.test("Logger Robustness - Complex Data Structures - should handle deeply nested objects", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); const deepObject = { - level1: { level2: { level3: { level4: { value: 'deep' } } } }, + level1: { level2: { level3: { level4: { value: "deep" } } } }, }; - logger.info('Deep object: %j', deepObject); + logger.info("Deep object: %j", deepObject); assertEquals(getCapturedLogs().length, 1); }); Deno.test("Logger Robustness - Complex Data Structures - should handle arrays with mixed data types", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); const mixedArray = [ 1, - 'string', + "string", { obj: true }, [1, 2, 3], null, undefined, ]; - logger.info('Mixed array: %j', mixedArray); + logger.info("Mixed array: %j", mixedArray); assertEquals(getCapturedLogs().length, 1); }); Deno.test("Logger Robustness - Complex Data Structures - should handle special characters and unicode", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); - const specialMessage = 'Special chars: \n\t\r\\"\'๐Ÿš€ Unicode: ใ“ใ‚“ใซใกใฏ'; + const specialMessage = "Special chars: \n\t\r\\\"'๐Ÿš€ Unicode: ใ“ใ‚“ใซใกใฏ"; logger.info(specialMessage); diff --git a/test/logger.simple-formatter.test.js b/test/logger.simple-formatter.test.js index 914883c..158bea4 100644 --- a/test/logger.simple-formatter.test.js +++ b/test/logger.simple-formatter.test.js @@ -1,182 +1,182 @@ -import { assertEquals, assert } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import { assert, assertEquals } from "@std/assert"; +import Logger from "../lib/logger.ts"; import { - setupMocks, - getCapturedLogs, clearCapturedLogs, - setTTYMode, + getCapturedLogs, restoreTTY, -} from './helpers/logger-test-helpers.js'; + setTTYMode, + setupMocks, +} from "./helpers/logger-test-helpers.js"; // Setup and teardown for all tests setupMocks(); Deno.test("Logger Simple Formatter - Basic Simple Format - should produce simple text format", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); - logger.info('test message'); + const logger = new Logger({ format: "simple" }); + logger.info("test message"); assertEquals(getCapturedLogs().length, 1); const logOutput = getCapturedLogs()[0]; // Should contain timestamp, level, caller, and message - assert(logOutput.includes('[INFO ]')); - assert(logOutput.includes('test message')); + assert(logOutput.includes("[INFO ]")); + assert(logOutput.includes("test message")); // Should contain short timestamp by default assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/)); }); Deno.test("Logger Simple Formatter - Basic Simple Format - should pad log levels correctly", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple', level: 'debug' }); - logger.error('error msg'); - logger.debug('debug msg'); + const logger = new Logger({ format: "simple", level: "debug" }); + logger.error("error msg"); + logger.debug("debug msg"); const logs = getCapturedLogs(); - assert(logs[0].includes('[ERROR]')); - assert(logs[1].includes('[DEBUG]')); + assert(logs[0].includes("[ERROR]")); + assert(logs[1].includes("[DEBUG]")); }); Deno.test("Logger Simple Formatter - Basic Simple Format - should include caller information", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple', callerLevel: 'info' }); - logger.info('test message'); + const logger = new Logger({ format: "simple", callerLevel: "info" }); + logger.info("test message"); const logOutput = getCapturedLogs()[0]; // Should contain filename and line number - assert(logOutput.includes('.js:')); + assert(logOutput.includes(".js:")); }); Deno.test("Logger Simple Formatter - Basic Simple Format - should format with long timestamp when specified", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple', time: 'long' }); - logger.info('test message'); + const logger = new Logger({ format: "simple", time: "long" }); + logger.info("test message"); const logOutput = getCapturedLogs()[0]; // Should contain long time format in brackets assert( - logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/) + logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/), ); - assert(logOutput.includes('T')); - assert(logOutput.includes('Z')); + assert(logOutput.includes("T")); + assert(logOutput.includes("Z")); }); Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format error level correctly", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); - logger.error('error message'); + const logger = new Logger({ format: "simple" }); + logger.error("error message"); const logOutput = getCapturedLogs()[0]; - assert(logOutput.includes('[ERROR]')); - assert(logOutput.includes('error message')); + assert(logOutput.includes("[ERROR]")); + assert(logOutput.includes("error message")); }); Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format warn level correctly", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); - logger.warn('warn message'); + const logger = new Logger({ format: "simple" }); + logger.warn("warn message"); const logOutput = getCapturedLogs()[0]; - assert(logOutput.includes('[WARN ]')); - assert(logOutput.includes('warn message')); + assert(logOutput.includes("[WARN ]")); + assert(logOutput.includes("warn message")); }); Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format info level correctly", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); - logger.info('info message'); + const logger = new Logger({ format: "simple" }); + logger.info("info message"); const logOutput = getCapturedLogs()[0]; - assert(logOutput.includes('[INFO ]')); - assert(logOutput.includes('info message')); + assert(logOutput.includes("[INFO ]")); + assert(logOutput.includes("info message")); }); Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format debug level correctly", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple', level: 'debug' }); - logger.debug('debug message'); + const logger = new Logger({ format: "simple", level: "debug" }); + logger.debug("debug message"); const logOutput = getCapturedLogs()[0]; - assert(logOutput.includes('[DEBUG]')); - assert(logOutput.includes('debug message')); + assert(logOutput.includes("[DEBUG]")); + assert(logOutput.includes("debug message")); }); Deno.test("Logger Simple Formatter - Color Handling - should include color codes when output is TTY", () => { clearCapturedLogs(); setTTYMode(true); - const logger = new Logger({ format: 'simple' }); - logger.error('error message'); + const logger = new Logger({ format: "simple" }); + logger.error("error message"); const logOutput = getCapturedLogs()[0]; // Should contain ANSI color codes - assert(logOutput.includes('\x1b[91m')); // red for error - assert(logOutput.includes('\x1b[0m')); // reset + assert(logOutput.includes("\x1b[91m")); // red for error + assert(logOutput.includes("\x1b[0m")); // reset }); Deno.test("Logger Simple Formatter - Color Handling - should not include color codes when output is redirected", () => { clearCapturedLogs(); setTTYMode(false); - const logger = new Logger({ format: 'simple' }); - logger.error('error message'); + const logger = new Logger({ format: "simple" }); + logger.error("error message"); const logOutput = getCapturedLogs()[0]; // Should not contain ANSI color codes - assert(!logOutput.includes('\x1b[')); + assert(!logOutput.includes("\x1b[")); }); Deno.test("Logger Simple Formatter - Color Handling - should use appropriate colors for different levels", () => { clearCapturedLogs(); setTTYMode(true); - const logger = new Logger({ format: 'simple', level: 'debug' }); + const logger = new Logger({ format: "simple", level: "debug" }); - logger.error('error'); - logger.warn('warn'); - logger.info('info'); - logger.debug('debug'); + logger.error("error"); + logger.warn("warn"); + logger.info("info"); + logger.debug("debug"); const logs = getCapturedLogs(); // Error should be red - assert(logs[0].includes('\x1b[91m')); + assert(logs[0].includes("\x1b[91m")); // Warn should be yellow - assert(logs[1].includes('\x1b[33m')); + assert(logs[1].includes("\x1b[33m")); // Info and debug might have different or no colors, but should have reset codes - assert(logs[2].includes('\x1b[0m')); - assert(logs[3].includes('\x1b[0m')); + assert(logs[2].includes("\x1b[0m")); + assert(logs[3].includes("\x1b[0m")); }); Deno.test("Logger Simple Formatter - Color Handling - should respect custom color configuration", () => { clearCapturedLogs(); setTTYMode(true); const logger = new Logger({ - format: 'simple', + format: "simple", colours: { - error: '\x1b[31m', // different red - warn: '\x1b[35m', // magenta instead of yellow + error: "\x1b[31m", // different red + warn: "\x1b[35m", // magenta instead of yellow }, }); - logger.error('error message'); - logger.warn('warn message'); + logger.error("error message"); + logger.warn("warn message"); const logs = getCapturedLogs(); - assert(logs[0].includes('\x1b[31m')); - assert(logs[1].includes('\x1b[35m')); + assert(logs[0].includes("\x1b[31m")); + assert(logs[1].includes("\x1b[35m")); }); Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle multiple arguments", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); - logger.info('Hello %s, you are %d years old', 'John', 25); + const logger = new Logger({ format: "simple" }); + logger.info("Hello %s, you are %d years old", "John", 25); const logOutput = getCapturedLogs()[0]; - assert(logOutput.includes('Hello John, you are 25 years old')); + assert(logOutput.includes("Hello John, you are 25 years old")); }); Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle special characters", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); + const logger = new Logger({ format: "simple" }); logger.info('Special chars: "quotes", \\backslash, \nnewline'); const logOutput = getCapturedLogs()[0]; @@ -185,12 +185,12 @@ Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle empty messages", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); - logger.info(''); + const logger = new Logger({ format: "simple" }); + logger.info(""); const logOutput = getCapturedLogs()[0]; // Should still have the level and timestamp parts - assert(logOutput.includes('[INFO ]')); + assert(logOutput.includes("[INFO ]")); }); Deno.test("Logger Simple Formatter - TTY Detection Integration - should detect TTY mode changes correctly", () => { @@ -198,20 +198,20 @@ Deno.test("Logger Simple Formatter - TTY Detection Integration - should detect T // Test with TTY setTTYMode(true); - const ttyLogger = new Logger({ format: 'simple' }); - ttyLogger.error('tty error'); + const ttyLogger = new Logger({ format: "simple" }); + ttyLogger.error("tty error"); // Test without TTY setTTYMode(false); - const noTtyLogger = new Logger({ format: 'simple' }); - noTtyLogger.error('no tty error'); + const noTtyLogger = new Logger({ format: "simple" }); + noTtyLogger.error("no tty error"); const logs = getCapturedLogs(); // First should have colors, second should not - assert(logs[0].includes('\x1b[')); - assert(!logs[1].includes('\x1b[')); + assert(logs[0].includes("\x1b[")); + assert(!logs[1].includes("\x1b[")); }); // Cleanup -restoreTTY(); \ No newline at end of file +restoreTTY(); diff --git a/test/logger.time-formatting.test.js b/test/logger.time-formatting.test.js index 89e6a29..e9fef40 100644 --- a/test/logger.time-formatting.test.js +++ b/test/logger.time-formatting.test.js @@ -1,43 +1,43 @@ -import { assertEquals, assertThrows, assert } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import { assert, assertEquals, assertThrows } from "@std/assert"; +import Logger from "../lib/logger.ts"; import { - setupMocks, - getCapturedLogs, clearCapturedLogs, + getCapturedLogs, getFirstLogAsJSON, -} from './helpers/logger-test-helpers.js'; + setupMocks, +} from "./helpers/logger-test-helpers.js"; // Setup and teardown for all tests setupMocks(); Deno.test("Logger Time Formatting - Default Time Format - should default to short time format", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'json' }); - logger.info('test message'); + const logger = new Logger({ format: "json" }); + logger.info("test message"); const parsed = getFirstLogAsJSON(); // Short format should be YYYY-MM-DD HH:MM (without seconds) assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)); - assert(!parsed.time.includes('T')); - assert(!parsed.time.includes('Z')); - assert(!parsed.time.includes('.')); + assert(!parsed.time.includes("T")); + assert(!parsed.time.includes("Z")); + assert(!parsed.time.includes(".")); }); Deno.test("Logger Time Formatting - Default Time Format - should include time option in logger options", () => { - const shortLogger = new Logger({ time: 'short' }); - const longLogger = new Logger({ time: 'long' }); + const shortLogger = new Logger({ time: "short" }); + const longLogger = new Logger({ time: "long" }); const defaultLogger = new Logger(); - assertEquals(shortLogger.options.time, 'short'); - assertEquals(longLogger.options.time, 'long'); - assertEquals(defaultLogger.options.time, 'short'); // default + assertEquals(shortLogger.options.time, "short"); + assertEquals(longLogger.options.time, "long"); + assertEquals(defaultLogger.options.time, "short"); // default }); Deno.test("Logger Time Formatting - Short Time Format - should format time as short when time option is 'short'", () => { clearCapturedLogs(); - const logger = new Logger({ time: 'short', format: 'json' }); - logger.info('test message'); + const logger = new Logger({ time: "short", format: "json" }); + logger.info("test message"); const parsed = getFirstLogAsJSON(); @@ -48,30 +48,30 @@ Deno.test("Logger Time Formatting - Short Time Format - should format time as sh Deno.test("Logger Time Formatting - Short Time Format - should work with simple formatter and short time", () => { clearCapturedLogs(); - const logger = new Logger({ time: 'short', format: 'simple' }); - logger.info('test message'); + const logger = new Logger({ time: "short", format: "simple" }); + logger.info("test message"); const logOutput = getCapturedLogs()[0]; // Should contain short time format in brackets assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/)); - assert(!logOutput.includes('T')); - assert(!logOutput.includes('Z')); + assert(!logOutput.includes("T")); + assert(!logOutput.includes("Z")); }); Deno.test("Logger Time Formatting - Short Time Format - should truncate time correctly in short format", () => { clearCapturedLogs(); - const logger = new Logger({ time: 'short', format: 'json' }); + const logger = new Logger({ time: "short", format: "json" }); - logger.info('test message'); + logger.info("test message"); const parsed = getFirstLogAsJSON(); // Short format should not have seconds or milliseconds assert( - !parsed.time.includes(':') || parsed.time.split(':').length === 2 + !parsed.time.includes(":") || parsed.time.split(":").length === 2, ); - assert(!parsed.time.includes('.')); + assert(!parsed.time.includes(".")); // Should be exactly 16 characters: YYYY-MM-DD HH:MM assertEquals(parsed.time.length, 16); @@ -79,46 +79,46 @@ Deno.test("Logger Time Formatting - Short Time Format - should truncate time cor Deno.test("Logger Time Formatting - Long Time Format - should format time as long ISO string when time is 'long'", () => { clearCapturedLogs(); - const logger = new Logger({ time: 'long', format: 'json' }); - logger.info('test message'); + const logger = new Logger({ time: "long", format: "json" }); + logger.info("test message"); const parsed = getFirstLogAsJSON(); // Long format should be full ISO string - assert(parsed.time.includes('T')); - assert(parsed.time.includes('Z')); - assert(parsed.time.includes('.')); + assert(parsed.time.includes("T")); + assert(parsed.time.includes("Z")); + assert(parsed.time.includes(".")); // Should be valid ISO string new Date(parsed.time); // This should not throw // Should match ISO format pattern assert( - parsed.time.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + parsed.time.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/), ); }); Deno.test("Logger Time Formatting - Long Time Format - should work with simple formatter and long time", () => { clearCapturedLogs(); - const logger = new Logger({ time: 'long', format: 'simple' }); - logger.info('test message'); + const logger = new Logger({ time: "long", format: "simple" }); + logger.info("test message"); const logOutput = getCapturedLogs()[0]; // Should contain long time format in brackets assert( - logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/) + logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/), ); - assert(logOutput.includes('T')); - assert(logOutput.includes('Z')); + assert(logOutput.includes("T")); + assert(logOutput.includes("Z")); }); Deno.test("Logger Time Formatting - Long Time Format - should preserve time precision in long format", () => { clearCapturedLogs(); - const logger = new Logger({ time: 'long', format: 'json' }); + const logger = new Logger({ time: "long", format: "json" }); const startTime = Date.now(); - logger.info('test message'); + logger.info("test message"); const endTime = Date.now(); const parsed = getFirstLogAsJSON(); @@ -129,15 +129,15 @@ Deno.test("Logger Time Formatting - Long Time Format - should preserve time prec assert(logTime <= endTime); // Should have millisecond precision - assert(parsed.time.includes('.')); + assert(parsed.time.includes(".")); }); Deno.test("Logger Time Formatting - Time Format Consistency - should use consistent time format across multiple log calls", () => { clearCapturedLogs(); - const logger = new Logger({ time: 'short', format: 'json' }); + const logger = new Logger({ time: "short", format: "json" }); - logger.info('first message'); - logger.warn('second message'); + logger.info("first message"); + logger.warn("second message"); const logs = getCapturedLogs(); const parsed1 = JSON.parse(logs[0]); @@ -150,34 +150,42 @@ Deno.test("Logger Time Formatting - Time Format Consistency - should use consist Deno.test("Logger Time Formatting - Time Option Validation - should validate time option in constructor", () => { // Valid options should not throw - new Logger({ time: 'long' }); - new Logger({ time: 'short' }); + new Logger({ time: "long" }); + new Logger({ time: "short" }); // Invalid option should throw - assertThrows(() => { - new Logger({ time: 'medium' }); - }, Error, "Invalid time: medium. Valid times are: long, short"); + assertThrows( + () => { + new Logger({ time: "medium" }); + }, + Error, + "Invalid time: medium. Valid times are: long, short", + ); - assertThrows(() => { - new Logger({ time: 'invalid' }); - }, Error, "Invalid time: invalid. Valid times are: long, short"); + assertThrows( + () => { + new Logger({ time: "invalid" }); + }, + Error, + "Invalid time: invalid. Valid times are: long, short", + ); }); Deno.test("Logger Time Formatting - Backward Compatibility - should maintain existing behavior for existing code", () => { clearCapturedLogs(); // Code that doesn't specify time option should work as before - const logger = new Logger({ format: 'json', level: 'info' }); - logger.info('test message'); + const logger = new Logger({ format: "json", level: "info" }); + logger.info("test message"); const parsed = getFirstLogAsJSON(); // Should still have all expected fields - assertEquals(parsed.level, 'info'); - assertEquals(parsed.msg, 'test message'); - assertEquals(typeof parsed.time, 'string'); - assertEquals(typeof parsed.pid, 'number'); - assertEquals(typeof parsed.hostname, 'string'); + assertEquals(parsed.level, "info"); + assertEquals(parsed.msg, "test message"); + assertEquals(typeof parsed.time, "string"); + assertEquals(typeof parsed.pid, "number"); + assertEquals(typeof parsed.hostname, "string"); // Time should be in short format (new default) assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)); @@ -185,16 +193,16 @@ Deno.test("Logger Time Formatting - Backward Compatibility - should maintain exi Deno.test("Logger Time Formatting - Backward Compatibility - should not break existing simple formatter tests", () => { clearCapturedLogs(); - const logger = new Logger({ format: 'simple' }); - logger.warn('warning message'); + const logger = new Logger({ format: "simple" }); + logger.warn("warning message"); const logOutput = getCapturedLogs()[0]; // Should still contain expected elements - assert(logOutput.includes('[WARN ]')); - assert(logOutput.includes('warning message')); - assert(logOutput.includes('.js:')); + assert(logOutput.includes("[WARN ]")); + assert(logOutput.includes("warning message")); + assert(logOutput.includes(".js:")); // Should use short time format (new default) assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/)); -}); \ No newline at end of file +}); diff --git a/test/logger.util-format.test.js b/test/logger.util-format.test.js index 67cf9fc..ca52650 100644 --- a/test/logger.util-format.test.js +++ b/test/logger.util-format.test.js @@ -1,89 +1,89 @@ import { assertEquals, assertMatch } from "@std/assert"; -import Logger from '../lib/logger.ts'; +import Logger from "../lib/logger.ts"; Deno.test("Logger util.format functionality - Format specifiers - should handle %s string formatting", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('User %s logged in', 'john'); + const logger = new Logger({ format: "json" }); + logger.info("User %s logged in", "john"); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'User john logged in'); + assertEquals(output.msg, "User john logged in"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Format specifiers - should handle %d number formatting", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('User has %d points', 100); + const logger = new Logger({ format: "json" }); + logger.info("User has %d points", 100); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'User has 100 points'); + assertEquals(output.msg, "User has 100 points"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Format specifiers - should handle %i integer formatting", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('Value: %i', 42.7); + const logger = new Logger({ format: "json" }); + logger.info("Value: %i", 42.7); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Value: 42'); + assertEquals(output.msg, "Value: 42"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Format specifiers - should handle %f float formatting", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('Price: %f', 19.99); + const logger = new Logger({ format: "json" }); + logger.info("Price: %f", 19.99); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Price: 19.99'); + assertEquals(output.msg, "Price: 19.99"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Format specifiers - should handle %j JSON formatting", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - const obj = { name: 'test', value: 42 }; - logger.info('Config: %j', obj); + const logger = new Logger({ format: "json" }); + const obj = { name: "test", value: 42 }; + logger.info("Config: %j", obj); const output = JSON.parse(capturedOutput); assertEquals(output.msg, 'Config: {"name":"test","value":42}'); @@ -93,43 +93,43 @@ Deno.test("Logger util.format functionality - Format specifiers - should handle }); Deno.test("Logger util.format functionality - Format specifiers - should handle %% literal percentage", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('Progress: 50%% complete'); + const logger = new Logger({ format: "json" }); + logger.info("Progress: 50%% complete"); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Progress: 50%% complete'); + assertEquals(output.msg, "Progress: 50%% complete"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Multiple format specifiers - should handle multiple format specifiers", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); logger.info( - 'User %s has %d points and %f%% completion', - 'alice', + "User %s has %d points and %f%% completion", + "alice", 150, - 75.5 + 75.5, ); const output = JSON.parse(capturedOutput); assertEquals( output.msg, - 'User alice has 150 points and 75.5% completion' + "User alice has 150 points and 75.5% completion", ); } finally { console.log = originalLog; @@ -137,26 +137,26 @@ Deno.test("Logger util.format functionality - Multiple format specifiers - shoul }); Deno.test("Logger util.format functionality - Multiple format specifiers - should handle mixed format specifiers with JSON", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); const config = { debug: true, port: 3000 }; logger.info( - 'Server %s running on port %d with config %j', - 'api', + "Server %s running on port %d with config %j", + "api", 8080, - config + config, ); const output = JSON.parse(capturedOutput); assertEquals( output.msg, - 'Server api running on port 8080 with config {"debug":true,"port":3000}' + 'Server api running on port 8080 with config {"debug":true,"port":3000}', ); } finally { console.log = originalLog; @@ -164,34 +164,34 @@ Deno.test("Logger util.format functionality - Multiple format specifiers - shoul }); Deno.test("Logger util.format functionality - Multiple arguments without format specifiers - should handle multiple arguments without format specifiers", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('Message', 'arg1', 'arg2', 123); + const logger = new Logger({ format: "json" }); + logger.info("Message", "arg1", "arg2", 123); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Message arg1 arg2 123'); + assertEquals(output.msg, "Message arg1 arg2 123"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Multiple arguments without format specifiers - should handle mixed objects and primitives", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - const obj = { key: 'value' }; - logger.info('Data:', obj, 42, true); + const logger = new Logger({ format: "json" }); + const obj = { key: "value" }; + logger.info("Data:", obj, 42, true); const output = JSON.parse(capturedOutput); assertEquals(output.msg, "Data: { key: 'value' } 42 true"); @@ -201,89 +201,89 @@ Deno.test("Logger util.format functionality - Multiple arguments without format }); Deno.test("Logger util.format functionality - Edge cases - should handle more format specifiers than arguments", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('Hello %s, you are %d years old', 'John'); + const logger = new Logger({ format: "json" }); + logger.info("Hello %s, you are %d years old", "John"); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Hello John, you are %d years old'); + assertEquals(output.msg, "Hello John, you are %d years old"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Edge cases - should handle more arguments than format specifiers", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('Hello %s', 'John', 'extra', 'args', 123); + const logger = new Logger({ format: "json" }); + logger.info("Hello %s", "John", "extra", "args", 123); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Hello John extra args 123'); + assertEquals(output.msg, "Hello John extra args 123"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Edge cases - should handle null and undefined values", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - logger.info('Values: %s %s %d', null, undefined, null); + const logger = new Logger({ format: "json" }); + logger.info("Values: %s %s %d", null, undefined, null); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Values: null undefined 0'); + assertEquals(output.msg, "Values: null undefined 0"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Edge cases - should handle arrays and objects without %j", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); const arr = [1, 2, 3]; const obj = { a: 1 }; - logger.info('Data %s and %s', arr, obj); + logger.info("Data %s and %s", arr, obj); const output = JSON.parse(capturedOutput); - assertEquals(output.msg, 'Data [ 1, 2, 3 ] and { a: 1 }'); + assertEquals(output.msg, "Data [ 1, 2, 3 ] and { a: 1 }"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Simple format output - should format messages correctly in simple format", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'simple' }); - logger.info('User %s has %d points', 'bob', 200); + const logger = new Logger({ format: "simple" }); + logger.info("User %s has %d points", "bob", 200); assertMatch(capturedOutput, /User bob has 200 points/); } finally { @@ -292,16 +292,16 @@ Deno.test("Logger util.format functionality - Simple format output - should form }); Deno.test("Logger util.format functionality - Simple format output - should handle JSON formatting in simple format", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'simple' }); - const data = { status: 'active', count: 5 }; - logger.warn('Status: %j', data); + const logger = new Logger({ format: "simple" }); + const data = { status: "active", count: 5 }; + logger.warn("Status: %j", data); assertMatch(capturedOutput, /Status: {"status":"active","count":5}/); } finally { @@ -310,44 +310,44 @@ Deno.test("Logger util.format functionality - Simple format output - should hand }); Deno.test("Logger util.format functionality - Error handling in util.format - should handle objects that throw during toString", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); + const logger = new Logger({ format: "json" }); const problematicObj = { toString() { - throw new Error('toString failed'); + throw new Error("toString failed"); }, }; // The logger might handle the error gracefully, so let's test the actual output - logger.info('Object: %s', problematicObj); + logger.info("Object: %s", problematicObj); // Check that something was logged (the logger should handle the error) const output = JSON.parse(capturedOutput); - assertEquals(typeof output.msg, 'string'); + assertEquals(typeof output.msg, "string"); } finally { console.log = originalLog; } }); Deno.test("Logger util.format functionality - Error handling in util.format - should handle circular references with %j", () => { - let capturedOutput = ''; + let capturedOutput = ""; const originalLog = console.log; console.log = (message) => { capturedOutput = message; }; try { - const logger = new Logger({ format: 'json' }); - const circular = { name: 'test' }; + const logger = new Logger({ format: "json" }); + const circular = { name: "test" }; circular.self = circular; - logger.info('Circular: %j', circular); + logger.info("Circular: %j", circular); const output = JSON.parse(capturedOutput); assertMatch(output.msg, /Circular: \[Circular\]/); @@ -355,4 +355,3 @@ Deno.test("Logger util.format functionality - Error handling in util.format - sh console.log = originalLog; } }); -