This commit is contained in:
IanKulin
2025-09-25 21:26:31 +08:00
parent b95d3ea50a
commit 01cc58aa7a
14 changed files with 924 additions and 824 deletions

View File

@@ -6,7 +6,8 @@
- **Multiple log levels**: silent, error, warn, info, debug - **Multiple log levels**: silent, error, warn, info, debug
- **Flexible output formats**: JSON or simple text - **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 - **Color support**: Automatic TTY detection with colored output
## Install ## Install
@@ -27,8 +28,8 @@ $ deno run --allow-env --allow-sys your-script.js
import Logger from "@iankulin/logger"; import Logger from "@iankulin/logger";
const logger = new Logger(); const logger = new Logger();
logger.info('Hello from logger'); logger.info("Hello from logger");
logger.error('Something went wrong'); logger.error("Something went wrong");
``` ```
## Usage Examples ## Usage Examples
@@ -37,12 +38,12 @@ logger.error('Something went wrong');
```typescript ```typescript
import Logger from "@iankulin/logger"; import Logger from "@iankulin/logger";
const logger = new Logger({ level: 'info' }); const logger = new Logger({ level: "info" });
logger.error('Critical error occurred'); logger.error("Critical error occurred");
logger.warn('This is a warning'); logger.warn("This is a warning");
logger.info('Informational message'); logger.info("Informational message");
logger.debug('Debug info'); // Won't be shown (level is 'info') logger.debug("Debug info"); // Won't be shown (level is 'info')
``` ```
### Log Levels ### Log Levels
@@ -56,17 +57,17 @@ The logger supports five log levels (from least to most verbose):
- `debug` - All messages - `debug` - All messages
```typescript ```typescript
const logger = new Logger({ level: 'debug' }); const logger = new Logger({ level: "debug" });
// All of these will be logged // All of these will be logged
logger.error('Error message'); logger.error("Error message");
logger.warn('Warning message'); logger.warn("Warning message");
logger.info('Info message'); logger.info("Info message");
logger.debug('Debug message'); logger.debug("Debug message");
// Change level dynamically // Change level dynamically
logger.level('error'); logger.level("error");
logger.info('This will not be logged'); logger.info("This will not be logged");
// Get current level // Get current level
console.log(logger.level()); // 'error' console.log(logger.level()); // 'error'
@@ -77,8 +78,8 @@ console.log(logger.level()); // 'error'
#### JSON Format (Default) #### JSON Format (Default)
```typescript ```typescript
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Hello world'); logger.info("Hello world");
``` ```
```json ```json
@@ -97,8 +98,8 @@ logger.info('Hello world');
#### Simple Format #### Simple Format
```typescript ```typescript
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.error('Something failed'); logger.error("Something failed");
``` ```
``` ```
@@ -107,22 +108,23 @@ logger.error('Something failed');
### Message Formatting ### 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 ```typescript
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// String formatting // 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",...} // Output: {"level":"info","msg":"User john has 100 points",...}
// JSON formatting // 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}",...} // Output: {"level":"info","msg":"Config: {\"debug\":true,\"port\":3000}",...}
// Simple format example // Simple format example
const simpleLogger = new Logger({ format: 'simple' }); const simpleLogger = new Logger({ format: "simple" });
simpleLogger.warn('Processing file %s (%d bytes)', 'data.txt', 1024); 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) // 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 ```typescript
const logger = new Logger({ const logger = new Logger({
colours: { colours: {
error: '\x1b[31m', // Red error: "\x1b[31m", // Red
warn: '\x1b[93m', // Bright yellow warn: "\x1b[93m", // Bright yellow
info: '\x1b[36m', // Cyan info: "\x1b[36m", // Cyan
debug: '\x1b[90m', // Dark gray debug: "\x1b[90m", // Dark gray
}, },
}); });
``` ```
### Caller Level Control ### 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 ```typescript
// Default: only include caller info for warnings and errors // 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.error("Critical error"); // Includes caller info
logger.warn('Warning message'); // Includes caller info logger.warn("Warning message"); // Includes caller info
logger.info('Info message'); // No caller info logger.info("Info message"); // No caller info
logger.debug('Debug message'); // No caller info logger.debug("Debug message"); // No caller info
``` ```
**JSON Format Output:** **JSON Format Output:**
@@ -175,7 +179,9 @@ logger.debug('Debug message'); // No caller info
- `'info'` - Include caller info for info, warnings, and errors - `'info'` - Include caller info for info, warnings, and errors
- `'debug'` - Always include caller info - `'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 ## Constructor Options
@@ -194,19 +200,19 @@ import Logger from "@iankulin/logger";
// Production: JSON format with environment-based level // Production: JSON format with environment-based level
const prodLogger = new Logger({ const prodLogger = new Logger({
level: Deno.env.get('LOG_LEVEL') || 'info', level: Deno.env.get("LOG_LEVEL") || "info",
format: 'json', format: "json",
callerLevel: 'error', // Performance optimization callerLevel: "error", // Performance optimization
}); });
// Development: Simple format with debug level // Development: Simple format with debug level
const devLogger = new Logger({ const devLogger = new Logger({
level: 'debug', level: "debug",
format: 'simple', format: "simple",
}); });
// Testing: Silent mode // Testing: Silent mode
const testLogger = new Logger({ level: 'silent' }); const testLogger = new Logger({ level: "silent" });
``` ```
## Requirements ## Requirements
@@ -220,7 +226,8 @@ const testLogger = new Logger({ level: 'silent' });
## Versions ## Versions
- **1.0.0** - JSR release with full Deno support - **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 - Full TypeScript support
- Native Deno compatibility - Native Deno compatibility

124
demo.js
View File

@@ -1,94 +1,102 @@
import Logger from './lib/logger.ts'; import Logger from "./lib/logger.ts";
const logger = new Logger({ level: 'debug' }); const logger = new Logger({ level: "debug" });
logger.error('Unable to fetch student'); logger.error("Unable to fetch student");
logger.info('Hello from logger'); logger.info("Hello from logger");
logger.warn('This is a warning'); 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.debug("This is a debug message"); // This won't be logged if level is set to 'info'
logger.level('error'); logger.level("error");
logger.debug('This is a debug message'); // This won't be logged if level is set to 'info' or higher 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.error("Unable to fetch student");
simple_logger.info('Hello from logger'); simple_logger.info("Hello from logger");
simple_logger.warn('This is a warning'); 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.debug("This is a debug message"); // This won't be logged if level is set to 'info'
simple_logger.level('error'); 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.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 longLogger = new Logger({ time: "long", format: "simple" });
const shortLogger = new Logger({ time: 'short', format: 'simple' }); const shortLogger = new Logger({ time: "short", format: "simple" });
longLogger.info('This uses long time format'); longLogger.info("This uses long time format");
shortLogger.info('This uses short time format'); shortLogger.info("This uses short time format");
// Demonstrate callerLevel functionality // 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 // 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( 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.error("Error with caller info");
defaultCallerLogger.warn('Warning with caller info'); defaultCallerLogger.warn("Warning with caller info");
defaultCallerLogger.info('Info without caller info'); defaultCallerLogger.info("Info without caller info");
defaultCallerLogger.debug('Debug without caller info'); defaultCallerLogger.debug("Debug without caller info");
// Set callerLevel to 'error' - only errors include caller info // Set callerLevel to 'error' - only errors include caller info
const errorOnlyLogger = new Logger({ format: 'simple', callerLevel: 'error' }); const errorOnlyLogger = new Logger({ format: "simple", callerLevel: "error" });
console.log('\nCallerLevel set to error - only errors show caller info:'); console.log("\nCallerLevel set to error - only errors show caller info:");
errorOnlyLogger.error('Error with caller info'); errorOnlyLogger.error("Error with caller info");
errorOnlyLogger.warn('Warning without caller info'); errorOnlyLogger.warn("Warning without caller info");
errorOnlyLogger.info('Info without caller info'); errorOnlyLogger.info("Info without caller info");
// Set callerLevel to 'debug' - all levels include caller info // Set callerLevel to 'debug' - all levels include caller info
const allLevelsLogger = new Logger({ format: 'simple', callerLevel: 'debug' }); const allLevelsLogger = new Logger({ format: "simple", callerLevel: "debug" });
console.log('\nCallerLevel set to debug - all levels show caller info:'); console.log("\nCallerLevel set to debug - all levels show caller info:");
allLevelsLogger.error('Error with caller info'); allLevelsLogger.error("Error with caller info");
allLevelsLogger.warn('Warning with caller info'); allLevelsLogger.warn("Warning with caller info");
allLevelsLogger.info('Info with caller info'); allLevelsLogger.info("Info with caller info");
allLevelsLogger.debug('Debug with caller info'); allLevelsLogger.debug("Debug with caller info");
// Set callerLevel to 'silent' - no levels include caller info // Set callerLevel to 'silent' - no levels include caller info
const noneLogger = new Logger({ format: 'simple', callerLevel: 'silent' }); const noneLogger = new Logger({ format: "simple", callerLevel: "silent" });
console.log('\nCallerLevel set to silent - no levels show caller info:'); console.log("\nCallerLevel set to silent - no levels show caller info:");
noneLogger.error('Error without caller info'); noneLogger.error("Error without caller info");
noneLogger.warn('Warning without caller info'); noneLogger.warn("Warning without caller info");
noneLogger.info('Info without caller info'); noneLogger.info("Info without caller info");
// Demonstrate format string functionality (util.format style) // 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' }); const formatLogger = new Logger({ format: "simple", level: "debug" });
console.log('Format strings with various specifiers:'); console.log("Format strings with various specifiers:");
// String formatting (%s) // 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) // Number formatting (%d, %i, %f)
formatLogger.warn('Database has %d connections, CPU usage: %f%%', 25, 84.3); formatLogger.warn("Database has %d connections, CPU usage: %f%%", 25, 84.3);
formatLogger.debug('Processing item %i of %d', 42, 100); formatLogger.debug("Processing item %i of %d", 42, 100);
// JSON formatting (%j) // 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 }; 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 // 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 // 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 %% // Literal percentage with %%
formatLogger.info('Upload progress: 50%% complete'); formatLogger.info("Upload progress: 50%% complete");
// Edge cases // 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:'); console.log("\nJSON format with same messages:");
const jsonFormatLogger = new Logger({ format: 'json' }); const jsonFormatLogger = new Logger({ format: "json" });
jsonFormatLogger.info('User %s logged in with %d failed attempts', 'alice', 2); jsonFormatLogger.info("User %s logged in with %d failed attempts", "alice", 2);
jsonFormatLogger.warn('Config loaded: %j', { env: 'production', debug: false }); jsonFormatLogger.warn("Config loaded: %j", { env: "production", debug: false });

View File

@@ -1,29 +1,29 @@
// Native implementation of util.format functionality // Native implementation of util.format functionality
function format(f: unknown, ...args: unknown[]): string { 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 // If first argument is not a string, just join all args with spaces
return [f, ...args].map(arg => { return [f, ...args].map((arg) => {
if (arg === null) return 'null'; if (arg === null) return "null";
if (arg === undefined) return 'undefined'; if (arg === undefined) return "undefined";
if (typeof arg === 'object') { if (typeof arg === "object") {
try { try {
return JSON.stringify(arg, null, 0); return JSON.stringify(arg, null, 0);
} catch { } catch {
// Handle circular references and other JSON errors // Handle circular references and other JSON errors
return '[object Object]'; return "[object Object]";
} }
} }
return String(arg); return String(arg);
}).join(' '); }).join(" ");
} }
let i = 0; let i = 0;
// First, handle %% replacement - if there are no args, %% stays as %% // First, handle %% replacement - if there are no args, %% stays as %%
// If there are args, %% becomes % // If there are args, %% becomes %
const handlePercentPercent = args.length === 0 ? '%%' : '%'; const handlePercentPercent = args.length === 0 ? "%%" : "%";
const str = f.replace(/%[sdifj%]/g, (match: string) => { const str = f.replace(/%[sdifj%]/g, (match: string) => {
if (match === '%%') { if (match === "%%") {
return handlePercentPercent; return handlePercentPercent;
} }
@@ -32,48 +32,50 @@ function format(f: unknown, ...args: unknown[]): string {
const arg = args[i++]; const arg = args[i++];
switch (match) { switch (match) {
case '%s': case "%s":
if (arg === null) return 'null'; if (arg === null) return "null";
if (arg === undefined) return 'undefined'; if (arg === undefined) return "undefined";
if (typeof arg === 'object') { if (typeof arg === "object") {
try { try {
// For objects without %j, use a simplified string representation // For objects without %j, use a simplified string representation
if (Array.isArray(arg)) { if (Array.isArray(arg)) {
return `[ ${arg.join(', ')} ]`; return `[ ${arg.join(", ")} ]`;
} }
// For plain objects, show key-value pairs // For plain objects, show key-value pairs
const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`); const entries = Object.entries(arg).map(([k, v]) =>
return `{ ${entries.join(', ')} }`; `${k}: ${typeof v === "string" ? `'${v}'` : v}`
);
return `{ ${entries.join(", ")} }`;
} catch { } catch {
return '[object Object]'; return "[object Object]";
} }
} }
try { try {
return String(arg); return String(arg);
} catch { } catch {
return '[object Object]'; return "[object Object]";
} }
case '%d': case "%d":
if (arg === null) return '0'; if (arg === null) return "0";
if (arg === undefined) return 'NaN'; if (arg === undefined) return "NaN";
return String(Number(arg)); return String(Number(arg));
case '%i': case "%i":
if (arg === null) return '0'; if (arg === null) return "0";
if (arg === undefined) return 'NaN'; if (arg === undefined) return "NaN";
return String(parseInt(String(Number(arg)), 10)); return String(parseInt(String(Number(arg)), 10));
case '%f': case "%f":
if (arg === null) return '0'; if (arg === null) return "0";
if (arg === undefined) return 'NaN'; if (arg === undefined) return "NaN";
return String(parseFloat(String(Number(arg)))); return String(parseFloat(String(Number(arg))));
case '%j': case "%j":
try { try {
return JSON.stringify(arg); return JSON.stringify(arg);
} catch { } catch {
return '[Circular]'; return "[Circular]";
} }
default: default:
@@ -84,28 +86,30 @@ function format(f: unknown, ...args: unknown[]): string {
// Append any remaining arguments // Append any remaining arguments
const remainingArgs = args.slice(i); const remainingArgs = args.slice(i);
if (remainingArgs.length > 0) { if (remainingArgs.length > 0) {
return str + ' ' + remainingArgs.map(arg => { return str + " " + remainingArgs.map((arg) => {
if (arg === null) return 'null'; if (arg === null) return "null";
if (arg === undefined) return 'undefined'; if (arg === undefined) return "undefined";
if (typeof arg === 'object') { if (typeof arg === "object") {
try { try {
if (Array.isArray(arg)) { if (Array.isArray(arg)) {
return `[ ${arg.join(', ')} ]`; return `[ ${arg.join(", ")} ]`;
} }
const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`); const entries = Object.entries(arg).map(([k, v]) =>
return `{ ${entries.join(', ')} }`; `${k}: ${typeof v === "string" ? `'${v}'` : v}`
);
return `{ ${entries.join(", ")} }`;
} catch { } catch {
return '[object Object]'; return "[object Object]";
} }
} }
return String(arg); return String(arg);
}).join(' '); }).join(" ");
} }
return str; return str;
} }
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug'; export type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
export interface LogLevels { export interface LogLevels {
[level: string]: number; [level: string]: number;
@@ -142,8 +146,8 @@ export type Formatter = (logEntry: LogEntry) => string;
export interface LoggerOptions { export interface LoggerOptions {
level?: LogLevel; level?: LogLevel;
levels?: Partial<LogLevels>; levels?: Partial<LogLevels>;
format?: 'json' | 'simple'; format?: "json" | "simple";
time?: 'long' | 'short'; time?: "long" | "short";
callerLevel?: LogLevel; callerLevel?: LogLevel;
colours?: Partial<Colours>; colours?: Partial<Colours>;
} }
@@ -152,8 +156,8 @@ class Logger {
options: { options: {
level: LogLevel; level: LogLevel;
levels: LogLevels; levels: LogLevels;
format: 'json' | 'simple'; format: "json" | "simple";
time: 'long' | 'short'; time: "long" | "short";
callerLevel: LogLevel; callerLevel: LogLevel;
colours: Colours; colours: Colours;
}; };
@@ -174,19 +178,19 @@ class Logger {
}; };
const defaultColours: Colours = { const defaultColours: Colours = {
error: '\x1b[91m', error: "\x1b[91m",
warn: '\x1b[33m', warn: "\x1b[33m",
info: '\x1b[94m', info: "\x1b[94m",
debug: '\x1b[37m', debug: "\x1b[37m",
reset: '\x1b[0m', reset: "\x1b[0m",
}; };
this.options = { this.options = {
level: options.level || 'info', level: options.level || "info",
levels: Object.assign({}, defaultLevels, options.levels), levels: Object.assign({}, defaultLevels, options.levels),
format: options.format || 'json', format: options.format || "json",
time: options.time || 'short', time: options.time || "short",
callerLevel: options.callerLevel || 'warn', callerLevel: options.callerLevel || "warn",
colours: Object.assign({}, defaultColours, options.colours), colours: Object.assign({}, defaultColours, options.colours),
}; };
@@ -207,71 +211,83 @@ class Logger {
validateOptions(options: LoggerOptions): void { validateOptions(options: LoggerOptions): void {
// Validate level if provided // Validate level if provided
if (options.level !== undefined) { 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)) { if (!validLevels.includes(options.level)) {
throw new Error( throw new Error(
`Invalid log level: ${ `Invalid log level: ${options.level}. Valid levels are: ${
options.level validLevels.join(", ")
}. Valid levels are: ${validLevels.join(', ')}` }`,
); );
} }
} }
// Validate format if provided // Validate format if provided
if (options.format !== undefined) { if (options.format !== undefined) {
const validFormats = ['json', 'simple']; const validFormats = ["json", "simple"];
if (!validFormats.includes(options.format)) { if (!validFormats.includes(options.format)) {
throw new Error( throw new Error(
`Invalid format: ${ `Invalid format: ${options.format}. Valid formats are: ${
options.format validFormats.join(", ")
}. Valid formats are: ${validFormats.join(', ')}` }`,
); );
} }
} }
// Validate time if provided // Validate time if provided
if (options.time !== undefined) { if (options.time !== undefined) {
const validTimes = ['long', 'short']; const validTimes = ["long", "short"];
if (!validTimes.includes(options.time)) { if (!validTimes.includes(options.time)) {
throw new Error( throw new Error(
`Invalid time: ${ `Invalid time: ${options.time}. Valid times are: ${
options.time validTimes.join(", ")
}. Valid times are: ${validTimes.join(', ')}` }`,
); );
} }
} }
// Validate callerLevel if provided // Validate callerLevel if provided
if (options.callerLevel !== undefined) { 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)) { if (!validLevels.includes(options.callerLevel)) {
throw new Error( throw new Error(
`Invalid callerLevel: ${ `Invalid callerLevel: ${options.callerLevel}. Valid levels are: ${
options.callerLevel validLevels.join(", ")
}. Valid levels are: ${validLevels.join(', ')}` }`,
); );
} }
} }
// Validate colours if provided (should be an object) // Validate colours if provided (should be an object)
if (options.colours !== undefined && typeof options.colours !== 'object') { if (options.colours !== undefined && typeof options.colours !== "object") {
throw new Error('colours option must be an object'); throw new Error("colours option must be an object");
} }
// Validate levels if provided (should be an object with numeric values) // Validate levels if provided (should be an object with numeric values)
if (options.levels !== undefined) { if (options.levels !== undefined) {
if (typeof options.levels !== 'object') { if (typeof options.levels !== "object") {
throw new Error('levels option must be an object'); throw new Error("levels option must be an object");
} }
for (const [level, value] of Object.entries(options.levels)) { for (const [level, value] of Object.entries(options.levels)) {
if ( if (
typeof value !== 'number' || typeof value !== "number" ||
value < 0 || value < 0 ||
!Number.isInteger(value) !Number.isInteger(value)
) { ) {
throw new Error( 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 msg: String(logEntry.msg), // Convert to string safely
callerFile: logEntry.callerFile, callerFile: logEntry.callerFile,
callerLine: logEntry.callerLine, 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); return JSON.stringify(safeEntry);
} catch { } catch {
// Last resort - return a plain string // Last resort - return a plain string
return `{"level":"${logEntry.level}","msg":"${String( return `{"level":"${logEntry.level}","msg":"${
logEntry.msg String(
logEntry.msg,
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
).replace(/"/g, '\"')}","jsonError":"Multiple JSON errors occurred"}`; ).replace(/"/g, '"')
}","jsonError":"Multiple JSON errors occurred"}`;
} }
} }
} }
@@ -312,7 +332,7 @@ class Logger {
simpleFormatter(logEntry: LogEntry): string { simpleFormatter(logEntry: LogEntry): string {
const levelPadded = logEntry.level.toUpperCase().padEnd(5); const levelPadded = logEntry.level.toUpperCase().padEnd(5);
const caller = logEntry.callerFile const caller = logEntry.callerFile
? `${logEntry.callerFile.split('/').pop()}:${logEntry.callerLine}` ? `${logEntry.callerFile.split("/").pop()}:${logEntry.callerLine}`
: null; : null;
return caller return caller
@@ -321,17 +341,22 @@ class Logger {
} }
getCallerInfo(): { callerFile: string; callerLine: number } { getCallerInfo(): { callerFile: string; callerLine: number } {
const originalFunc = (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace; const originalFunc =
let callerFile = 'unknown'; (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace;
let callerFile = "unknown";
let callerLine = 0; let callerLine = 0;
try { try {
const err = new Error(); const err = new Error();
// deno-lint-ignore prefer-const // deno-lint-ignore prefer-const
let currentFile: string | undefined; let currentFile: string | undefined;
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = function (_err: unknown, stack: unknown) { (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace =
function (_err: unknown, stack: unknown) {
return stack; 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(); currentFile = stack.shift().getFileName();
while (stack.length) { while (stack.length) {
const stackFrame = stack.shift(); const stackFrame = stack.shift();
@@ -345,17 +370,18 @@ class Logger {
} catch (e) { } catch (e) {
this.callerErrorCount++; this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) { if (this.callerErrorCount <= this.maxCallerErrors) {
console.error('Error retrieving caller info:', e); console.error("Error retrieving caller info:", e);
if (this.callerErrorCount === this.maxCallerErrors) { if (this.callerErrorCount === this.maxCallerErrors) {
// loop detected // loop detected
console.error( 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 // callerFile and callerLine already set to defaults above
} finally { } finally {
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = originalFunc; (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace =
originalFunc;
} }
return { callerFile, callerLine }; return { callerFile, callerLine };
} }
@@ -366,18 +392,16 @@ class Logger {
} }
// Only get caller info if current level is at or above callerLevel threshold // Only get caller info if current level is at or above callerLevel threshold
const shouldIncludeCaller = const shouldIncludeCaller = this.options.levels[level] <=
this.options.levels[level] <=
this.options.levels[this.options.callerLevel]; this.options.levels[this.options.callerLevel];
const { callerFile, callerLine } = shouldIncludeCaller const { callerFile, callerLine } = shouldIncludeCaller
? this.getCallerInfo() ? this.getCallerInfo()
: { callerFile: undefined, callerLine: undefined }; : { callerFile: undefined, callerLine: undefined };
const now = new Date(); const now = new Date();
const time = const time = this.options.time === "long"
this.options.time === 'long'
? now.toISOString() ? now.toISOString()
: now.toISOString().slice(0, 16).replace('T', ' '); : now.toISOString().slice(0, 16).replace("T", " ");
const logEntry: LogEntry = { const logEntry: LogEntry = {
level, level,
@@ -389,7 +413,10 @@ class Logger {
}; };
// Only include caller info if it was requested // Only include caller info if it was requested
if (shouldIncludeCaller && callerFile !== undefined && callerLine !== undefined) { if (
shouldIncludeCaller && callerFile !== undefined &&
callerLine !== undefined
) {
logEntry.callerFile = callerFile; logEntry.callerFile = callerFile;
logEntry.callerLine = callerLine; logEntry.callerLine = callerLine;
} }
@@ -398,17 +425,17 @@ class Logger {
const resetColour = this.options.colours.reset; const resetColour = this.options.colours.reset;
// Select the appropriate formatter // Select the appropriate formatter
const formatter = const formatter = this.formatters[this.options.format] ||
this.formatters[this.options.format] || this.formatters.json; this.formatters.json;
let formattedLog: string; let formattedLog: string;
try { try {
formattedLog = formatter(logEntry); formattedLog = formatter(logEntry);
// Ensure formatter returned a string // Ensure formatter returned a string
if (typeof formattedLog !== 'string') { if (typeof formattedLog !== "string") {
throw new Error( throw new Error(
`Formatter returned ${typeof formattedLog} instead of string` `Formatter returned ${typeof formattedLog} instead of string`,
); );
} }
} catch (error) { } catch (error) {
@@ -416,18 +443,24 @@ class Logger {
try { try {
const safeEntry = { const safeEntry = {
...logEntry, ...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); formattedLog = this.formatters.json(safeEntry);
} catch { } catch {
// Even JSON formatter failed, create minimal safe output // Even JSON formatter failed, create minimal safe output
formattedLog = `{"level":"${logEntry.level}","msg":"${String( formattedLog = `{"level":"${logEntry.level}","msg":"${
logEntry.msg String(
logEntry.msg,
).replace( ).replace(
/"/g, /"/g,
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
'\"' '"',
)}","formatterError":"Formatter failed: ${error instanceof Error ? error.message : 'Unknown error'}"}`; )
}","formatterError":"Formatter failed: ${
error instanceof Error ? error.message : "Unknown error"
}"}`;
} }
} }
@@ -440,19 +473,19 @@ class Logger {
} }
error(message: unknown, ...args: unknown[]): void { error(message: unknown, ...args: unknown[]): void {
this.log('error', message, ...args); this.log("error", message, ...args);
} }
warn(message: unknown, ...args: unknown[]): void { warn(message: unknown, ...args: unknown[]): void {
this.log('warn', message, ...args); this.log("warn", message, ...args);
} }
info(message: unknown, ...args: unknown[]): void { info(message: unknown, ...args: unknown[]): void {
this.log('info', message, ...args); this.log("info", message, ...args);
} }
debug(message: unknown, ...args: unknown[]): void { debug(message: unknown, ...args: unknown[]): void {
this.log('debug', message, ...args); this.log("debug", message, ...args);
} }
level(): LogLevel; level(): LogLevel;

View File

@@ -9,7 +9,7 @@ const originalIsTTY = Deno.stdout.isTerminal();
export function mockConsole() { export function mockConsole() {
console.log = (...args) => { 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 // Helper to get parsed JSON from first captured log
export function getFirstLogAsJSON() { export function getFirstLogAsJSON() {
if (capturedLogs.length === 0) { if (capturedLogs.length === 0) {
throw new Error('No logs captured'); throw new Error("No logs captured");
} }
return JSON.parse(capturedLogs[0]); return JSON.parse(capturedLogs[0]);
} }

View File

@@ -1,28 +1,28 @@
import { assertEquals, assert } from "@std/assert"; import { assert, assertEquals } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for error when callerLevel is warn", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
logger.error('test message'); logger.error("test message");
// Parse JSON output and check for caller info // Parse JSON output and check for caller info
const logEntry = JSON.parse(capturedOutput); const logEntry = JSON.parse(capturedOutput);
assert( assert(
logEntry.callerFile, logEntry.callerFile,
'Should include callerFile for error level' "Should include callerFile for error level",
); );
assert( assert(
typeof logEntry.callerLine === 'number', typeof logEntry.callerLine === "number",
'Should include callerLine for error level' "Should include callerLine for error level",
); );
} finally { } finally {
console.log = originalLog; 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", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
logger.warn('test message'); logger.warn("test message");
// Parse JSON output and check for caller info // Parse JSON output and check for caller info
const logEntry = JSON.parse(capturedOutput); const logEntry = JSON.parse(capturedOutput);
assert( assert(
logEntry.callerFile, logEntry.callerFile,
'Should include callerFile for warn level' "Should include callerFile for warn level",
); );
assert( assert(
typeof logEntry.callerLine === 'number', typeof logEntry.callerLine === "number",
'Should include callerLine for warn level' "Should include callerLine for warn level",
); );
} finally { } finally {
console.log = originalLog; 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", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
logger.info('test message'); logger.info("test message");
// Parse JSON output and check for absence of caller info // Parse JSON output and check for absence of caller info
const logEntry = JSON.parse(capturedOutput); const logEntry = JSON.parse(capturedOutput);
assertEquals( assertEquals(
logEntry.callerFile, logEntry.callerFile,
undefined, undefined,
'Should NOT include callerFile for info level' "Should NOT include callerFile for info level",
); );
assertEquals( assertEquals(
logEntry.callerLine, logEntry.callerLine,
undefined, undefined,
'Should NOT include callerLine for info level' "Should NOT include callerLine for info level",
); );
} finally { } finally {
console.log = originalLog; 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", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
logger.debug('test message'); logger.debug("test message");
// Parse JSON output and check for absence of caller info // Parse JSON output and check for absence of caller info
const logEntry = JSON.parse(capturedOutput); const logEntry = JSON.parse(capturedOutput);
assertEquals( assertEquals(
logEntry.callerFile, logEntry.callerFile,
undefined, undefined,
'Should NOT include callerFile for debug level' "Should NOT include callerFile for debug level",
); );
assertEquals( assertEquals(
logEntry.callerLine, logEntry.callerLine,
undefined, undefined,
'Should NOT include callerLine for debug level' "Should NOT include callerLine for debug level",
); );
} finally { } finally {
console.log = originalLog; 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", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
// Test each level // Test each level
const levels = ['error', 'warn', 'info', 'debug']; const levels = ["error", "warn", "info", "debug"];
for (const level of levels) { for (const level of levels) {
logger[level]('test message'); logger[level]("test message");
const logEntry = JSON.parse(capturedOutput); const logEntry = JSON.parse(capturedOutput);
assert( assert(
logEntry.callerFile, logEntry.callerFile,
`Should include callerFile for ${level} level` `Should include callerFile for ${level} level`,
); );
assert( assert(
typeof logEntry.callerLine === 'number', typeof logEntry.callerLine === "number",
`Should include callerLine for ${level} level` `Should include callerLine for ${level} level`,
); );
} }
} finally { } 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", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
// Test each level // Test each level
const levels = ['error', 'warn', 'info', 'debug']; const levels = ["error", "warn", "info", "debug"];
for (const level of levels) { for (const level of levels) {
logger[level]('test message'); logger[level]("test message");
const logEntry = JSON.parse(capturedOutput); const logEntry = JSON.parse(capturedOutput);
assertEquals( assertEquals(
logEntry.callerFile, logEntry.callerFile,
undefined, undefined,
`Should NOT include callerFile for ${level} level` `Should NOT include callerFile for ${level} level`,
); );
assertEquals( assertEquals(
logEntry.callerLine, logEntry.callerLine,
undefined, undefined,
`Should NOT include callerLine for ${level} level` `Should NOT include callerLine for ${level} level`,
); );
} }
} finally { } 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", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
logger.info('test message'); logger.info("test message");
// Should not contain caller info pattern // Should not contain caller info pattern
assert( assert(
!capturedOutput.includes('unknown'), !capturedOutput.includes("unknown"),
'Should not include caller placeholder' "Should not include caller placeholder",
); );
assert( assert(
!capturedOutput.includes('.js:'), !capturedOutput.includes(".js:"),
'Should not include file:line pattern' "Should not include file:line pattern",
); );
// Should still contain other parts // Should still contain other parts
assert(capturedOutput.includes('INFO'), 'Should include log level'); assert(capturedOutput.includes("INFO"), "Should include log level");
assert( assert(
capturedOutput.includes('test message'), capturedOutput.includes("test message"),
'Should include message' "Should include message",
); );
} finally { } finally {
console.log = originalLog; 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", () => { 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 // Mock console.log to capture output
const originalLog = console.log; const originalLog = console.log;
let capturedOutput = ''; let capturedOutput = "";
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
logger.info('test message'); logger.info("test message");
// Should contain caller info pattern // Should contain caller info pattern
assert( assert(
capturedOutput.includes('.js:'), capturedOutput.includes(".js:"),
'Should include file:line pattern' "Should include file:line pattern",
); );
assert(capturedOutput.includes('INFO'), 'Should include log level'); assert(capturedOutput.includes("INFO"), "Should include log level");
assert( assert(
capturedOutput.includes('test message'), capturedOutput.includes("test message"),
'Should include message' "Should include message",
); );
} finally { } finally {
console.log = originalLog; 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", () => { 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 // Spy on getCallerInfo method
let getCallerInfoCalled = false; let getCallerInfoCalled = false;
@@ -259,11 +259,11 @@ Deno.test("Logger callerLevel - Performance Considerations - should not call get
console.log = () => {}; console.log = () => {};
try { try {
logger.info('test message'); logger.info("test message");
assertEquals( assertEquals(
getCallerInfoCalled, getCallerInfoCalled,
false, 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 { } finally {
console.log = originalLog; 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", () => { 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 // Spy on getCallerInfo method
let getCallerInfoCalled = false; let getCallerInfoCalled = false;
@@ -286,11 +286,11 @@ Deno.test("Logger callerLevel - Performance Considerations - should call getCall
console.log = () => {}; console.log = () => {};
try { try {
logger.error('test message'); logger.error("test message");
assertEquals( assertEquals(
getCallerInfoCalled, getCallerInfoCalled,
true, true,
'getCallerInfo should be called for error level when callerLevel is warn' "getCallerInfo should be called for error level when callerLevel is warn",
); );
} finally { } finally {
console.log = originalLog; console.log = originalLog;

View File

@@ -1,74 +1,110 @@
import { assertEquals, assertThrows } from "@std/assert"; 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", () => { Deno.test("Logger Constructor - should throw error for invalid log level", () => {
assertThrows(() => { assertThrows(
new Logger({ level: 'invalid' }); () => {
}, Error, "Invalid log level: invalid. Valid levels are: silent, error, warn, info, debug"); 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", () => { Deno.test("Logger Constructor - should throw error for invalid format", () => {
assertThrows(() => { assertThrows(
new Logger({ format: 'invalid' }); () => {
}, Error, "Invalid format: invalid. Valid formats are: json, simple"); new Logger({ format: "invalid" });
},
Error,
"Invalid format: invalid. Valid formats are: json, simple",
);
}); });
Deno.test("Logger Constructor - should throw error for invalid time option", () => { Deno.test("Logger Constructor - should throw error for invalid time option", () => {
assertThrows(() => { assertThrows(
new Logger({ time: 'invalid' }); () => {
}, Error, "Invalid time: invalid. Valid times are: long, short"); new Logger({ time: "invalid" });
},
Error,
"Invalid time: invalid. Valid times are: long, short",
);
}); });
Deno.test("Logger Constructor - should throw error for invalid callerLevel", () => { Deno.test("Logger Constructor - should throw error for invalid callerLevel", () => {
assertThrows(() => { assertThrows(
new Logger({ callerLevel: 'invalid' }); () => {
}, Error, "Invalid callerLevel: invalid. Valid levels are: silent, error, warn, info, debug"); 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", () => { Deno.test("Logger Constructor - should throw error for non-object colours", () => {
assertThrows(() => { assertThrows(
new Logger({ colours: 'not an object' }); () => {
}, Error, "colours option must be an object"); new Logger({ colours: "not an object" });
},
Error,
"colours option must be an object",
);
}); });
Deno.test("Logger Constructor - should throw error for non-object levels", () => { Deno.test("Logger Constructor - should throw error for non-object levels", () => {
assertThrows(() => { assertThrows(
new Logger({ levels: 'not an object' }); () => {
}, Error, "levels option must be an object"); new Logger({ levels: "not an object" });
},
Error,
"levels option must be an object",
);
}); });
Deno.test("Logger Constructor - should throw error for invalid level values", () => { Deno.test("Logger Constructor - should throw error for invalid level values", () => {
assertThrows(() => { assertThrows(
() => {
new Logger({ levels: { error: -1 } }); new Logger({ levels: { error: -1 } });
}, Error, "Level value for 'error' must be a non-negative integer"); },
Error,
"Level value for 'error' must be a non-negative integer",
);
assertThrows(() => { assertThrows(
new Logger({ levels: { error: 'not a number' } }); () => {
}, Error, "Level value for 'error' must be a non-negative integer"); new Logger({ levels: { error: "not a number" } });
},
Error,
"Level value for 'error' must be a non-negative integer",
);
assertThrows(() => { assertThrows(
() => {
new Logger({ levels: { error: 1.5 } }); new Logger({ levels: { error: 1.5 } });
}, Error, "Level value for 'error' must be a non-negative integer"); },
Error,
"Level value for 'error' must be a non-negative integer",
);
}); });
Deno.test("Logger Constructor - should accept valid options without throwing", () => { Deno.test("Logger Constructor - should accept valid options without throwing", () => {
// This should not throw // This should not throw
new Logger({ new Logger({
level: 'debug', level: "debug",
format: 'simple', format: "simple",
time: 'long', time: "long",
callerLevel: 'error', callerLevel: "error",
colours: { error: '\x1b[31m' }, colours: { error: "\x1b[31m" },
levels: { custom: 4 }, levels: { custom: 4 },
}); });
}); });
Deno.test("Logger Constructor - should instantiate with default options", () => { Deno.test("Logger Constructor - should instantiate with default options", () => {
const logger = new Logger(); const logger = new Logger();
assertEquals(logger.options.level, 'info'); assertEquals(logger.options.level, "info");
assertEquals(logger.options.format, 'json'); assertEquals(logger.options.format, "json");
assertEquals(logger.options.time, 'short'); assertEquals(logger.options.time, "short");
assertEquals(logger.options.callerLevel, 'warn'); assertEquals(logger.options.callerLevel, "warn");
assertEquals(logger.options.levels, { assertEquals(logger.options.levels, {
silent: -1, silent: -1,
error: 0, error: 0,
@@ -80,49 +116,49 @@ Deno.test("Logger Constructor - should instantiate with default options", () =>
Deno.test("Logger Constructor - should instantiate with custom options", () => { Deno.test("Logger Constructor - should instantiate with custom options", () => {
const logger = new Logger({ const logger = new Logger({
level: 'debug', level: "debug",
format: 'simple', format: "simple",
time: 'long', time: "long",
callerLevel: 'error', callerLevel: "error",
}); });
assertEquals(logger.options.level, 'debug'); assertEquals(logger.options.level, "debug");
assertEquals(logger.options.format, 'simple'); assertEquals(logger.options.format, "simple");
assertEquals(logger.options.time, 'long'); assertEquals(logger.options.time, "long");
assertEquals(logger.options.callerLevel, 'error'); assertEquals(logger.options.callerLevel, "error");
}); });
Deno.test("Logger Constructor - should merge options correctly", () => { Deno.test("Logger Constructor - should merge options correctly", () => {
const customOptions = { const customOptions = {
level: 'debug', level: "debug",
format: 'simple', format: "simple",
time: 'long', time: "long",
colours: { colours: {
error: '\x1b[31m', // different red error: "\x1b[31m", // different red
}, },
}; };
const logger = new Logger(customOptions); const logger = new Logger(customOptions);
assertEquals(logger.options.level, 'debug'); assertEquals(logger.options.level, "debug");
assertEquals(logger.options.format, 'simple'); assertEquals(logger.options.format, "simple");
assertEquals(logger.options.time, 'long'); assertEquals(logger.options.time, "long");
assertEquals(logger.options.colours.error, '\x1b[31m'); assertEquals(logger.options.colours.error, "\x1b[31m");
// Should still have other default colors // 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", () => { Deno.test("Logger Constructor - should have all log level methods", () => {
const logger = new Logger(); const logger = new Logger();
assertEquals(typeof logger.error, 'function'); assertEquals(typeof logger.error, "function");
assertEquals(typeof logger.warn, 'function'); assertEquals(typeof logger.warn, "function");
assertEquals(typeof logger.info, 'function'); assertEquals(typeof logger.info, "function");
assertEquals(typeof logger.debug, 'function'); assertEquals(typeof logger.debug, "function");
}); });
Deno.test("Logger Constructor - should have level management methods", () => { Deno.test("Logger Constructor - should have level management methods", () => {
const logger = new Logger(); const logger = new Logger();
assertEquals(typeof logger.level, 'function'); assertEquals(typeof logger.level, "function");
assertEquals(typeof logger.setLevel, 'function'); assertEquals(typeof logger.setLevel, "function");
}); });
Deno.test("Logger Constructor - should detect TTY correctly", () => { 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(); new Logger();
// Partial options - should not throw // Partial options - should not throw
new Logger({ level: 'debug' }); new Logger({ level: "debug" });
// Full options (without time) - should not throw // Full options (without time) - should not throw
new Logger({ new Logger({
level: 'warn', level: "warn",
format: 'simple', format: "simple",
colours: { error: '\x1b[31m' }, colours: { error: "\x1b[31m" },
levels: { custom: 5 }, levels: { custom: 5 },
}); });
}); });

View File

@@ -1,24 +1,24 @@
import { assertEquals, assert } from "@std/assert"; import { assert, assertEquals } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
import { import {
setupMocks,
getCapturedLogs,
clearCapturedLogs, clearCapturedLogs,
} from './helpers/logger-test-helpers.js'; getCapturedLogs,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests // Setup and teardown for all tests
setupMocks(); setupMocks();
Deno.test("Logger Internal Error Handling - Formatter Error Handling - should fall back to JSON formatter when custom formatter throws", () => { Deno.test("Logger Internal Error Handling - Formatter Error Handling - should fall back to JSON formatter when custom formatter throws", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
// Replace the simple formatter with one that throws // Replace the simple formatter with one that throws
logger.formatters.simple = function () { 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 // Should still produce output using JSON formatter fallback
assertEquals(getCapturedLogs().length, 1); 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) // Should be valid JSON (fallback to JSON formatter)
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
const parsed = JSON.parse(logOutput); const parsed = JSON.parse(logOutput);
assertEquals(parsed.msg, 'test message'); assertEquals(parsed.msg, "test message");
assert( assert(
parsed.formatterError.includes( 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", () => { Deno.test("Logger Internal Error Handling - Formatter Error Handling - should not crash when formatter returns non-string", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
// Replace formatter with one that returns an object instead of string // Replace formatter with one that returns an object instead of string
logger.formatters.simple = function () { logger.formatters.simple = function () {
return { notAString: true }; return { notAString: true };
}; };
logger.info('test message'); logger.info("test message");
// Should still produce output (fallback should handle this) // Should still produce output (fallback should handle this)
assertEquals(getCapturedLogs().length, 1); 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 // Should be valid JSON from fallback
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
const parsed = JSON.parse(logOutput); 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", () => { Deno.test("Logger Internal Error Handling - Formatter Error Handling - should preserve original formatters after error", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
// Temporarily break the formatter // Temporarily break the formatter
const originalSimple = logger.formatters.simple; const originalSimple = logger.formatters.simple;
logger.formatters.simple = function () { 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 // Restore the formatter
logger.formatters.simple = originalSimple; logger.formatters.simple = originalSimple;
logger.info('second message'); logger.info("second message");
// First message should have used fallback, second should work normally // First message should have used fallback, second should work normally
assertEquals(getCapturedLogs().length, 2); 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 JSON.parse(getCapturedLogs()[0]); // This should not throw
// Second log should be simple format // Second log should be simple format
assert(getCapturedLogs()[1].includes('[INFO ]')); assert(getCapturedLogs()[1].includes("[INFO ]"));
assert(getCapturedLogs()[1].includes('second message')); assert(getCapturedLogs()[1].includes("second message"));
}); });
Deno.test("Logger Internal Error Handling - Formatter Error Handling - should handle repeated formatter failures without memory leaks", () => { Deno.test("Logger Internal Error Handling - Formatter Error Handling - should handle repeated formatter failures without memory leaks", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
// Break the formatter // Break the formatter
logger.formatters.simple = function () { logger.formatters.simple = function () {
throw new Error('Always fails'); throw new Error("Always fails");
}; };
// Log many times // Log many times

View File

@@ -1,89 +1,89 @@
import { assertEquals, assertThrows, assert } from "@std/assert"; import { assert, assertEquals, assertThrows } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
import { import {
setupMocks, clearCapturedErrors,
getCapturedLogs,
clearCapturedLogs, clearCapturedLogs,
getCapturedErrors, getCapturedErrors,
clearCapturedErrors, getCapturedLogs,
} from './helpers/logger-test-helpers.js'; setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests // Setup and teardown for all tests
setupMocks(); setupMocks();
Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle circular references in log data", () => { Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle circular references in log data", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Create circular reference // Create circular reference
const obj = { name: 'test' }; const obj = { name: "test" };
obj.self = obj; obj.self = obj;
logger.info('Message with circular ref: %j', obj); logger.info("Message with circular ref: %j", obj);
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should be valid JSON despite circular reference // Should be valid JSON despite circular reference
const parsed = JSON.parse(logOutput); const parsed = JSON.parse(logOutput);
assertEquals(parsed.level, 'info'); assertEquals(parsed.level, "info");
// Check for either jsonError or that the message was logged successfully // Check for either jsonError or that the message was logged successfully
assert( assert(
parsed.jsonError?.includes('JSON stringify failed') || parsed.jsonError?.includes("JSON stringify failed") ||
parsed.msg.includes('Message with circular ref') parsed.msg.includes("Message with circular ref"),
); );
}); });
Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle objects with non-serializable properties", () => { Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle objects with non-serializable properties", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Create object with function (non-serializable) // Create object with function (non-serializable)
const objWithFunction = { const objWithFunction = {
name: 'test', name: "test",
func: function () { func: function () {
return 'hello'; return "hello";
}, },
symbol: Symbol('test'), symbol: Symbol("test"),
undefined: undefined, undefined: undefined,
}; };
logger.info('Object: %j', objWithFunction); logger.info("Object: %j", objWithFunction);
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should produce valid JSON // Should produce valid JSON
const parsed = JSON.parse(logOutput); const parsed = JSON.parse(logOutput);
assertEquals(parsed.level, 'info'); assertEquals(parsed.level, "info");
// Should have the message in some form // 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", () => { Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle when JSON formatter itself is broken", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Break the JSON formatter // Break the JSON formatter
logger.formatters.json = function () { 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) // Should still produce some output (last resort fallback)
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should contain the message even if not perfectly formatted // 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", () => { Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - should handle stack manipulation errors", () => {
clearCapturedLogs(); clearCapturedLogs();
clearCapturedErrors(); clearCapturedErrors();
const logger = new Logger({ format: 'json', callerLevel: 'info' }); const logger = new Logger({ format: "json", callerLevel: "info" });
// Override getCallerInfo to simulate an error // Override getCallerInfo to simulate an error
const originalGetCallerInfo = logger.getCallerInfo; const originalGetCallerInfo = logger.getCallerInfo;
@@ -91,26 +91,26 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
this.callerErrorCount++; this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) { if (this.callerErrorCount <= this.maxCallerErrors) {
console.error( console.error(
'Error retrieving caller info:', "Error retrieving caller info:",
new Error('Simulated caller error') new Error("Simulated caller error"),
); );
if (this.callerErrorCount === this.maxCallerErrors) { if (this.callerErrorCount === this.maxCallerErrors) {
console.error( 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 { try {
logger.info('test with simulated caller error'); logger.info("test with simulated caller error");
// Should still log the message // Should still log the message
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
const parsed = JSON.parse(getCapturedLogs()[0]); const parsed = JSON.parse(getCapturedLogs()[0]);
assertEquals(parsed.msg, 'test with simulated caller error'); assertEquals(parsed.msg, "test with simulated caller error");
assertEquals(parsed.callerFile, 'unknown'); assertEquals(parsed.callerFile, "unknown");
assertEquals(parsed.callerLine, 0); assertEquals(parsed.callerLine, 0);
// Should have logged an error about caller detection // Should have logged an error about caller detection
@@ -125,7 +125,7 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
clearCapturedLogs(); clearCapturedLogs();
clearCapturedErrors(); clearCapturedErrors();
const logger = new Logger({ format: 'json', callerLevel: 'info' }); const logger = new Logger({ format: "json", callerLevel: "info" });
// Override getCallerInfo to always simulate errors // Override getCallerInfo to always simulate errors
const originalGetCallerInfo = logger.getCallerInfo; const originalGetCallerInfo = logger.getCallerInfo;
@@ -133,16 +133,16 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
this.callerErrorCount++; this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) { if (this.callerErrorCount <= this.maxCallerErrors) {
console.error( console.error(
'Error retrieving caller info:', "Error retrieving caller info:",
new Error('Always fails') new Error("Always fails"),
); );
if (this.callerErrorCount === this.maxCallerErrors) { if (this.callerErrorCount === this.maxCallerErrors) {
console.error( 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 { try {
@@ -163,8 +163,8 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
const suppressionFound = errorLogs.some((errorArgs) => const suppressionFound = errorLogs.some((errorArgs) =>
errorArgs.some( errorArgs.some(
(arg) => (arg) =>
typeof arg === 'string' && typeof arg === "string" &&
arg.includes('Suppressing further caller error messages') arg.includes("Suppressing further caller error messages"),
) )
); );
assert(suppressionFound); assert(suppressionFound);
@@ -178,50 +178,50 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
clearCapturedLogs(); clearCapturedLogs();
clearCapturedErrors(); clearCapturedErrors();
const logger = new Logger({ format: 'json', callerLevel: 'info' }); const logger = new Logger({ format: "json", callerLevel: "info" });
// Override getCallerInfo to simulate different phases // Override getCallerInfo to simulate different phases
const originalGetCallerInfo = logger.getCallerInfo; const originalGetCallerInfo = logger.getCallerInfo;
let phase = 'error1'; let phase = "error1";
logger.getCallerInfo = function () { logger.getCallerInfo = function () {
if (phase === 'error1') { if (phase === "error1") {
this.callerErrorCount++; this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) { if (this.callerErrorCount <= this.maxCallerErrors) {
console.error( console.error(
'Error retrieving caller info:', "Error retrieving caller info:",
new Error('Phase 1 error') new Error("Phase 1 error"),
); );
} }
return { callerFile: 'unknown', callerLine: 0 }; return { callerFile: "unknown", callerLine: 0 };
} else if (phase === 'working') { } else if (phase === "working") {
// Reset error count on successful call // Reset error count on successful call
this.callerErrorCount = 0; this.callerErrorCount = 0;
return originalGetCallerInfo.call(this); return originalGetCallerInfo.call(this);
} else if (phase === 'error2') { } else if (phase === "error2") {
this.callerErrorCount++; this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) { if (this.callerErrorCount <= this.maxCallerErrors) {
console.error( console.error(
'Error retrieving caller info:', "Error retrieving caller info:",
new Error('Phase 2 error') new Error("Phase 2 error"),
); );
} }
return { callerFile: 'unknown', callerLine: 0 }; return { callerFile: "unknown", callerLine: 0 };
} }
}; };
try { try {
// Cause some errors // Cause some errors
logger.info('test 1'); logger.info("test 1");
logger.info('test 2'); logger.info("test 2");
// Switch to working mode // Switch to working mode
phase = 'working'; phase = "working";
logger.info('test 3'); logger.info("test 3");
// Break it again // Break it again
phase = 'error2'; phase = "error2";
logger.info('test 4'); logger.info("test 4");
const errorLogs = getCapturedErrors(); const errorLogs = getCapturedErrors();
// Should have errors from both phases // 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", () => { Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when console.log itself throws", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Break console.log // Break console.log
const originalLog = console.log; const originalLog = console.log;
console.log = function () { console.log = function () {
throw new Error('Console is broken'); throw new Error("Console is broken");
}; };
try { try {
// This should not crash the process, but the error will bubble up // This should not crash the process, but the error will bubble up
// since there's no try-catch around console.log in the logger // since there's no try-catch around console.log in the logger
assertThrows(() => { assertThrows(
logger.info('test message'); () => {
}, Error, "Console is broken"); logger.info("test message");
},
Error,
"Console is broken",
);
} finally { } finally {
console.log = originalLog; 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", () => { Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when util.format has issues with complex objects", () => {
setupMocks(); setupMocks();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Create an object that will cause issues with string conversion // Create an object that will cause issues with string conversion
const problematicObject = { const problematicObject = {
toString: function () { toString: function () {
throw new Error('toString failed'); throw new Error("toString failed");
}, },
valueOf: function () { valueOf: function () {
throw new Error('valueOf failed'); throw new Error("valueOf failed");
}, },
}; };
// The logger should handle this gracefully and not throw // The logger should handle this gracefully and not throw
logger.info('Message: %s', problematicObject); logger.info("Message: %s", problematicObject);
const logs = getCapturedLogs(); const logs = getCapturedLogs();
assertEquals(logs.length, 1); assertEquals(logs.length, 1);
@@ -276,15 +280,14 @@ Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should
const output = JSON.parse(logs[0]); const output = JSON.parse(logs[0]);
// The logger should handle the problematic object gracefully // The logger should handle the problematic object gracefully
// Either by showing [object Object] or the actual object structure // Either by showing [object Object] or the actual object structure
assertEquals(typeof output.msg, 'string'); assertEquals(typeof output.msg, "string");
assertEquals(output.msg.startsWith('Message: '), true); assertEquals(output.msg.startsWith("Message: "), true);
}); });
Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when hostname fails", () => { Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when hostname fails", () => {
setupMocks(); setupMocks();
clearCapturedLogs(); 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) // 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 // 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 Deno, we could mock the hostname call if it were extracted to a mockable function
// For now, this is more of a documentation test // For now, this is more of a documentation test
logger.info('test message'); logger.info("test message");
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
}); });
Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should handle formatter failures gracefully", () => { 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(); clearCapturedLogs();
clearCapturedErrors(); clearCapturedErrors();
const logger = new Logger({ format: 'simple', callerLevel: 'info' }); const logger = new Logger({ format: "simple", callerLevel: "info" });
// Break the simple formatter // Break the simple formatter
logger.formatters.simple = function () { logger.formatters.simple = function () {
throw new Error('Simple formatter broken'); throw new Error("Simple formatter broken");
}; };
// Also simulate caller detection failure // Also simulate caller detection failure
@@ -315,16 +317,16 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha
this.callerErrorCount++; this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) { if (this.callerErrorCount <= this.maxCallerErrors) {
console.error( console.error(
'Error retrieving caller info:', "Error retrieving caller info:",
new Error('Caller detection failed') new Error("Caller detection failed"),
); );
} }
return { callerFile: 'unknown', callerLine: 0 }; return { callerFile: "unknown", callerLine: 0 };
}; };
try { try {
// Should still produce some output despite multiple failures // 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) // Should produce some kind of output (fallback to JSON formatter)
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
@@ -332,9 +334,9 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha
// Should be valid JSON (fallback formatter) // Should be valid JSON (fallback formatter)
const parsed = JSON.parse(output); const parsed = JSON.parse(output);
assertEquals(parsed.msg, 'test message'); assertEquals(parsed.msg, "test message");
assert(parsed.formatterError.includes('Simple formatter broken')); assert(parsed.formatterError.includes("Simple formatter broken"));
assertEquals(parsed.callerFile, 'unknown'); assertEquals(parsed.callerFile, "unknown");
} finally { } finally {
logger.getCallerInfo = originalGetCallerInfo; logger.getCallerInfo = originalGetCallerInfo;
logger.callerErrorCount = 0; logger.callerErrorCount = 0;
@@ -346,11 +348,11 @@ Deno.test("Logger Additional Fallback Tests - Resource Cleanup - should not leak
clearCapturedLogs(); clearCapturedLogs();
clearCapturedErrors(); clearCapturedErrors();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
// Break the formatter // Break the formatter
logger.formatters.simple = function () { logger.formatters.simple = function () {
throw new Error('Always fails'); throw new Error("Always fails");
}; };
// Log many times to check for memory leaks // 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 // Check that we're not accumulating error state
// (This is more of a smoke test - real memory leak detection would need different tools) // (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 assert(true); // If we get here without crashing, that's good
}); });

View File

@@ -1,19 +1,19 @@
import { assertEquals, assert } from "@std/assert"; import { assert, assertEquals } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
import { import {
setupMocks,
getCapturedLogs,
clearCapturedLogs, clearCapturedLogs,
getCapturedLogs,
getFirstLogAsJSON, getFirstLogAsJSON,
} from './helpers/logger-test-helpers.js'; setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests // Setup and teardown for all tests
setupMocks(); setupMocks();
Deno.test("Logger JSON Formatter - Basic JSON Output - should produce valid JSON output", () => { Deno.test("Logger JSON Formatter - Basic JSON Output - should produce valid JSON output", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('test message'); logger.info("test message");
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0]; 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", () => { Deno.test("Logger JSON Formatter - Basic JSON Output - should include all required fields in JSON output", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json', callerLevel: 'info' }); const logger = new Logger({ format: "json", callerLevel: "info" });
logger.info('test message'); logger.info("test message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'info'); assertEquals(parsed.level, "info");
assertEquals(parsed.levelNumber, 2); assertEquals(parsed.levelNumber, 2);
assertEquals(parsed.msg, 'test message'); assertEquals(parsed.msg, "test message");
assertEquals(typeof parsed.time, 'string'); assertEquals(typeof parsed.time, "string");
assertEquals(typeof parsed.pid, 'number'); assertEquals(typeof parsed.pid, "number");
assertEquals(typeof parsed.hostname, 'string'); assertEquals(typeof parsed.hostname, "string");
assert(parsed.callerFile); 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", () => { Deno.test("Logger JSON Formatter - Basic JSON Output - should format timestamp correctly based on time option", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json', time: 'short' }); const logger = new Logger({ format: "json", time: "short" });
logger.info('test message'); logger.info("test message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
// Should be short format, not ISO // Should be short format, not ISO
assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)); assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
assert(!parsed.time.includes('T')); assert(!parsed.time.includes("T"));
assert(!parsed.time.includes('Z')); assert(!parsed.time.includes("Z"));
}); });
Deno.test("Logger JSON Formatter - JSON Error Handling - should handle circular references in log entry", () => { Deno.test("Logger JSON Formatter - JSON Error Handling - should handle circular references in log entry", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Create a circular reference by modifying the logger's formatters // Create a circular reference by modifying the logger's formatters
const originalJsonFormatter = logger.formatters.json; 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); return originalJsonFormatter.call(this, logEntry);
}; };
logger.info('test with circular reference'); logger.info("test with circular reference");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should be valid JSON despite circular reference // Should be valid JSON despite circular reference
const parsed = JSON.parse(logOutput); const parsed = JSON.parse(logOutput);
// Should contain error information // Should contain error information
assert(parsed.jsonError.includes('JSON stringify failed')); assert(parsed.jsonError.includes("JSON stringify failed"));
assertEquals(parsed.msg, 'test with circular reference'); assertEquals(parsed.msg, "test with circular reference");
}); });
Deno.test("Logger JSON Formatter - JSON Error Handling - should handle JSON stringify errors with fallback", () => { Deno.test("Logger JSON Formatter - JSON Error Handling - should handle JSON stringify errors with fallback", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Create a problematic object that will cause JSON.stringify to fail // Create a problematic object that will cause JSON.stringify to fail
const problematic = {}; const problematic = {};
Object.defineProperty(problematic, 'badProp', { Object.defineProperty(problematic, "badProp", {
get() { get() {
throw new Error('Property access error'); throw new Error("Property access error");
}, },
enumerable: true, enumerable: true,
}); });
// Test the formatter directly with a problematic object // Test the formatter directly with a problematic object
const problematicLogEntry = { const problematicLogEntry = {
level: 'info', level: "info",
msg: 'test message', msg: "test message",
problematic: problematic, 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 // Should produce valid JSON with error info
const parsed = JSON.parse(result); 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", () => { Deno.test("Logger JSON Formatter - JSON Error Handling - should handle extreme JSON stringify failures", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// Create an object that will fail even the safe fallback // Create an object that will fail even the safe fallback
// by mocking JSON.stringify to always throw // 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) { JSON.stringify = function (...args) {
callCount++; callCount++;
if (callCount <= 2) { if (callCount <= 2) {
throw new Error('Mock JSON error'); throw new Error("Mock JSON error");
} }
return originalStringify.apply(this, args); return originalStringify.apply(this, args);
}; };
try { try {
const result = logger.formatters.json({ const result = logger.formatters.json({
level: 'error', level: "error",
msg: 'test message', msg: "test message",
}); });
// Should still produce valid JSON string even after multiple failures // Should still produce valid JSON string even after multiple failures
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
assertEquals(parsed.level, 'error'); assertEquals(parsed.level, "error");
assertEquals(parsed.msg, 'test message'); assertEquals(parsed.msg, "test message");
assert(parsed.jsonError.includes('Multiple JSON errors occurred')); assert(parsed.jsonError.includes("Multiple JSON errors occurred"));
} finally { } finally {
JSON.stringify = originalStringify; 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", () => { Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle special characters", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Special chars: "quotes", \\backslash, \nnewline'); logger.info('Special chars: "quotes", \\backslash, \nnewline');
// Should produce valid JSON despite special characters // 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", () => { Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle empty messages", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info(''); logger.info("");
const parsed = getFirstLogAsJSON(); 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", () => { Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle null and undefined arguments", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Value: %s', null); logger.info("Value: %s", null);
const parsed = getFirstLogAsJSON(); 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", () => { Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle very long messages", () => {
clearCapturedLogs(); clearCapturedLogs();
const longMessage = 'x'.repeat(10000); const longMessage = "x".repeat(10000);
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info(longMessage); logger.info(longMessage);
const parsed = getFirstLogAsJSON(); 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", () => { Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle objects in messages", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const obj = { key: 'value', nested: { prop: 123 } }; const obj = { key: "value", nested: { prop: 123 } };
logger.info('Object: %j', obj); logger.info("Object: %j", obj);
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
assert(parsed.msg.includes('{"key":"value","nested":{"prop":123}}')); 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", () => { Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log error messages with correct level", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.error('error message'); logger.error("error message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'error'); assertEquals(parsed.level, "error");
assertEquals(parsed.levelNumber, 0); assertEquals(parsed.levelNumber, 0);
}); });
Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log warn messages with correct level", () => { Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log warn messages with correct level", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.warn('warn message'); logger.warn("warn message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'warn'); assertEquals(parsed.level, "warn");
assertEquals(parsed.levelNumber, 1); assertEquals(parsed.levelNumber, 1);
}); });
Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log info messages with correct level", () => { Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log info messages with correct level", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('info message'); logger.info("info message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'info'); assertEquals(parsed.level, "info");
assertEquals(parsed.levelNumber, 2); assertEquals(parsed.levelNumber, 2);
}); });
Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log debug messages with correct level", () => { Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log debug messages with correct level", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'debug', format: 'json' }); const logger = new Logger({ level: "debug", format: "json" });
logger.debug('debug message'); logger.debug("debug message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'debug'); assertEquals(parsed.level, "debug");
assertEquals(parsed.levelNumber, 3); assertEquals(parsed.levelNumber, 3);
}); });

View File

@@ -1,151 +1,159 @@
import { assertEquals, assertThrows } from "@std/assert"; import { assertEquals, assertThrows } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
import { import {
setupMocks,
getCapturedLogs,
clearCapturedLogs, clearCapturedLogs,
} from './helpers/logger-test-helpers.js'; getCapturedLogs,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests // Setup and teardown for all tests
setupMocks(); setupMocks();
Deno.test("Logger Level Management - Level Setting and Getting - should change log level with level() method", () => { Deno.test("Logger Level Management - Level Setting and Getting - should change log level with level() method", () => {
const logger = new Logger(); const logger = new Logger();
logger.level('debug'); logger.level("debug");
assertEquals(logger.options.level, 'debug'); assertEquals(logger.options.level, "debug");
}); });
Deno.test("Logger Level Management - Level Setting and Getting - should return current level when called without arguments", () => { Deno.test("Logger Level Management - Level Setting and Getting - should return current level when called without arguments", () => {
const logger = new Logger({ level: 'debug' }); const logger = new Logger({ level: "debug" });
assertEquals(logger.level(), 'debug'); assertEquals(logger.level(), "debug");
}); });
Deno.test("Logger Level Management - Level Setting and Getting - should return new level when setting level", () => { Deno.test("Logger Level Management - Level Setting and Getting - should return new level when setting level", () => {
const logger = new Logger(); const logger = new Logger();
const result = logger.level('error'); const result = logger.level("error");
assertEquals(result, 'error'); assertEquals(result, "error");
assertEquals(logger.options.level, 'error'); assertEquals(logger.options.level, "error");
}); });
Deno.test("Logger Level Management - Level Setting and Getting - should throw error for invalid log level", () => { Deno.test("Logger Level Management - Level Setting and Getting - should throw error for invalid log level", () => {
const logger = new Logger(); const logger = new Logger();
assertThrows(() => { assertThrows(
logger.level('invalid'); () => {
}, Error, "Invalid log level: invalid"); logger.level("invalid");
},
Error,
"Invalid log level: invalid",
);
}); });
Deno.test("Logger Level Management - Level Setting and Getting - should allow method chaining after setting level", () => { Deno.test("Logger Level Management - Level Setting and Getting - should allow method chaining after setting level", () => {
const logger = new Logger(); const logger = new Logger();
// This should not throw and should return a level // This should not throw and should return a level
const result = logger.level('warn'); const result = logger.level("warn");
assertEquals(result, 'warn'); assertEquals(result, "warn");
assertEquals(typeof result, 'string'); assertEquals(typeof result, "string");
}); });
Deno.test("Logger Level Management - setLevel Method - should have setLevel method as alias", () => { Deno.test("Logger Level Management - setLevel Method - should have setLevel method as alias", () => {
const logger = new Logger(); 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", () => { Deno.test("Logger Level Management - setLevel Method - should set level correctly with setLevel method", () => {
const logger = new Logger(); const logger = new Logger();
const result = logger.setLevel('debug'); const result = logger.setLevel("debug");
assertEquals(result, 'debug'); assertEquals(result, "debug");
assertEquals(logger.options.level, 'debug'); assertEquals(logger.options.level, "debug");
}); });
Deno.test("Logger Level Management - setLevel Method - should return current level with setLevel when no args", () => { 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(); 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", () => { Deno.test("Logger Level Management - setLevel Method - should throw error for invalid level in setLevel", () => {
const logger = new Logger(); const logger = new Logger();
assertThrows(() => { assertThrows(
logger.setLevel('invalid'); () => {
}, Error, "Invalid log level: invalid"); logger.setLevel("invalid");
},
Error,
"Invalid log level: invalid",
);
}); });
Deno.test("Logger Level Management - setLevel Method - should maintain consistency between level() and setLevel()", () => { Deno.test("Logger Level Management - setLevel Method - should maintain consistency between level() and setLevel()", () => {
const logger = new Logger(); const logger = new Logger();
logger.level('error'); logger.level("error");
assertEquals(logger.setLevel(), 'error'); assertEquals(logger.setLevel(), "error");
logger.setLevel('debug'); logger.setLevel("debug");
assertEquals(logger.level(), 'debug'); assertEquals(logger.level(), "debug");
}); });
Deno.test("Logger Level Management - setLevel Method - should support fluent interface pattern", () => { Deno.test("Logger Level Management - setLevel Method - should support fluent interface pattern", () => {
const logger = new Logger(); const logger = new Logger();
// This demonstrates the fluent interface working // This demonstrates the fluent interface working
const currentLevel = logger.level('warn'); const currentLevel = logger.level("warn");
assertEquals(currentLevel, 'warn'); assertEquals(currentLevel, "warn");
// Both methods should return the current level for chaining // Both methods should return the current level for chaining
assertEquals(logger.level('info'), 'info'); assertEquals(logger.level("info"), "info");
assertEquals(logger.setLevel('debug'), 'debug'); assertEquals(logger.setLevel("debug"), "debug");
}); });
Deno.test("Logger Level Management - Log Level Filtering - should filter debug messages when level is info", () => { Deno.test("Logger Level Management - Log Level Filtering - should filter debug messages when level is info", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'info' }); const logger = new Logger({ level: "info" });
logger.debug('debug message'); logger.debug("debug message");
assertEquals(getCapturedLogs().length, 0); assertEquals(getCapturedLogs().length, 0);
}); });
Deno.test("Logger Level Management - Log Level Filtering - should show info messages when level is info", () => { Deno.test("Logger Level Management - Log Level Filtering - should show info messages when level is info", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'info' }); const logger = new Logger({ level: "info" });
logger.info('info message'); logger.info("info message");
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
}); });
Deno.test("Logger Level Management - Log Level Filtering - should show error messages at any level", () => { Deno.test("Logger Level Management - Log Level Filtering - should show error messages at any level", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'error' }); const logger = new Logger({ level: "error" });
logger.error('error message'); logger.error("error message");
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
}); });
Deno.test("Logger Level Management - Log Level Filtering - should filter warn and info when level is error", () => { Deno.test("Logger Level Management - Log Level Filtering - should filter warn and info when level is error", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'error' }); const logger = new Logger({ level: "error" });
logger.warn('warn message'); logger.warn("warn message");
logger.info('info message'); logger.info("info message");
assertEquals(getCapturedLogs().length, 0); assertEquals(getCapturedLogs().length, 0);
}); });
Deno.test("Logger Level Management - Log Level Filtering - should show all messages when level is debug", () => { Deno.test("Logger Level Management - Log Level Filtering - should show all messages when level is debug", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'debug' }); const logger = new Logger({ level: "debug" });
logger.error('error message'); logger.error("error message");
logger.warn('warn message'); logger.warn("warn message");
logger.info('info message'); logger.info("info message");
logger.debug('debug message'); logger.debug("debug message");
assertEquals(getCapturedLogs().length, 4); assertEquals(getCapturedLogs().length, 4);
}); });
Deno.test("Logger Level Management - Log Level Filtering - should show warn and above when level is warn", () => { Deno.test("Logger Level Management - Log Level Filtering - should show warn and above when level is warn", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'warn' }); const logger = new Logger({ level: "warn" });
logger.error('error message'); logger.error("error message");
logger.warn('warn message'); logger.warn("warn message");
logger.info('info message'); logger.info("info message");
logger.debug('debug message'); logger.debug("debug message");
assertEquals(getCapturedLogs().length, 2); assertEquals(getCapturedLogs().length, 2);
}); });
Deno.test("Logger Level Management - Silent Level - should suppress all output when level is silent", () => { Deno.test("Logger Level Management - Silent Level - should suppress all output when level is silent", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'silent' }); const logger = new Logger({ level: "silent" });
logger.error('error message'); logger.error("error message");
logger.warn('warn message'); logger.warn("warn message");
logger.info('info message'); logger.info("info message");
logger.debug('debug message'); logger.debug("debug message");
// No messages should be logged // No messages should be logged
assertEquals(getCapturedLogs().length, 0); 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", () => { Deno.test("Logger Level Management - Silent Level - should allow setting level to silent", () => {
const logger = new Logger(); const logger = new Logger();
const result = logger.level('silent'); const result = logger.level("silent");
assertEquals(result, 'silent'); assertEquals(result, "silent");
assertEquals(logger.options.level, 'silent'); assertEquals(logger.options.level, "silent");
}); });
Deno.test("Logger Level Management - Silent Level - should work with setLevel for silent level", () => { Deno.test("Logger Level Management - Silent Level - should work with setLevel for silent level", () => {
const logger = new Logger(); const logger = new Logger();
const result = logger.setLevel('silent'); const result = logger.setLevel("silent");
assertEquals(result, 'silent'); assertEquals(result, "silent");
assertEquals(logger.options.level, 'silent'); assertEquals(logger.options.level, "silent");
}); });
Deno.test("Logger Level Management - Silent Level - should remain silent after multiple log attempts", () => { Deno.test("Logger Level Management - Silent Level - should remain silent after multiple log attempts", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'silent' }); const logger = new Logger({ level: "silent" });
// Try logging multiple times // Try logging multiple times
for (let i = 0; i < 5; i++) { 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", () => { Deno.test("Logger Level Management - Dynamic Level Changes - should respect level changes during runtime", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ level: 'error' }); const logger = new Logger({ level: "error" });
// Should not log at info level // Should not log at info level
logger.info('info message 1'); logger.info("info message 1");
assertEquals(getCapturedLogs().length, 0); assertEquals(getCapturedLogs().length, 0);
// Change to info level // Change to info level
logger.level('info'); logger.level("info");
// Should now log info messages // Should now log info messages
logger.info('info message 2'); logger.info("info message 2");
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
// Change to silent // Change to silent
logger.level('silent'); logger.level("silent");
// Should not log anything // Should not log anything
logger.error('error message'); logger.error("error message");
assertEquals(getCapturedLogs().length, 1); // Still just the previous info message assertEquals(getCapturedLogs().length, 1); // Still just the previous info message
}); });

View File

@@ -1,10 +1,10 @@
import { assertEquals, assert } from "@std/assert"; import { assert, assertEquals } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
import { import {
setupMocks,
getCapturedLogs,
clearCapturedLogs, clearCapturedLogs,
} from './helpers/logger-test-helpers.js'; getCapturedLogs,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests // Setup and teardown for all tests
setupMocks(); setupMocks();
@@ -14,12 +14,12 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should not crash o
const logger = new Logger(); const logger = new Logger();
// This should not throw // 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", () => { Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle undefined and null messages gracefully", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
// These should not crash // These should not crash
logger.info(undefined); 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 parsed1 = JSON.parse(logs[0]);
const parsed2 = JSON.parse(logs[1]); const parsed2 = JSON.parse(logs[1]);
assertEquals(parsed1.msg, 'undefined'); assertEquals(parsed1.msg, "undefined");
assertEquals(parsed2.msg, 'null'); assertEquals(parsed2.msg, "null");
}); });
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle extremely large messages", () => { Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle extremely large messages", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const hugeMessage = 'x'.repeat(100000); const hugeMessage = "x".repeat(100000);
logger.info(hugeMessage); 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", () => { Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle circular objects in message formatting", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const circular = { name: 'test' }; const circular = { name: "test" };
circular.self = circular; circular.self = circular;
logger.info('Circular: %j', circular); logger.info("Circular: %j", circular);
// Should still log something // Should still log something
assertEquals(getCapturedLogs().length, 1); 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", () => { Deno.test("Logger Robustness - Performance and Memory - should handle rapid consecutive logging without issues", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
for (let i = 0; i < 1000; i++) { for (let i = 0; i < 1000; i++) {
logger.info(`rapid message ${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", () => { Deno.test("Logger Robustness - Performance and Memory - should handle repeated logging operations efficiently", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
const startTime = Date.now(); 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", () => { Deno.test("Logger Robustness - Performance and Memory - should handle mixed format types in rapid succession", () => {
clearCapturedLogs(); clearCapturedLogs();
const jsonLogger = new Logger({ format: 'json' }); const jsonLogger = new Logger({ format: "json" });
const simpleLogger = new Logger({ format: 'simple' }); const simpleLogger = new Logger({ format: "simple" });
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
jsonLogger.info(`json message ${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", () => { Deno.test("Logger Robustness - Complex Data Structures - should handle deeply nested objects", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const deepObject = { 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); assertEquals(getCapturedLogs().length, 1);
}); });
Deno.test("Logger Robustness - Complex Data Structures - should handle arrays with mixed data types", () => { Deno.test("Logger Robustness - Complex Data Structures - should handle arrays with mixed data types", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const mixedArray = [ const mixedArray = [
1, 1,
'string', "string",
{ obj: true }, { obj: true },
[1, 2, 3], [1, 2, 3],
null, null,
undefined, undefined,
]; ];
logger.info('Mixed array: %j', mixedArray); logger.info("Mixed array: %j", mixedArray);
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
}); });
Deno.test("Logger Robustness - Complex Data Structures - should handle special characters and unicode", () => { Deno.test("Logger Robustness - Complex Data Structures - should handle special characters and unicode", () => {
clearCapturedLogs(); 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); logger.info(specialMessage);

View File

@@ -1,182 +1,182 @@
import { assertEquals, assert } from "@std/assert"; import { assert, assertEquals } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
import { import {
setupMocks,
getCapturedLogs,
clearCapturedLogs, clearCapturedLogs,
setTTYMode, getCapturedLogs,
restoreTTY, restoreTTY,
} from './helpers/logger-test-helpers.js'; setTTYMode,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests // Setup and teardown for all tests
setupMocks(); setupMocks();
Deno.test("Logger Simple Formatter - Basic Simple Format - should produce simple text format", () => { Deno.test("Logger Simple Formatter - Basic Simple Format - should produce simple text format", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.info('test message'); logger.info("test message");
assertEquals(getCapturedLogs().length, 1); assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should contain timestamp, level, caller, and message // Should contain timestamp, level, caller, and message
assert(logOutput.includes('[INFO ]')); assert(logOutput.includes("[INFO ]"));
assert(logOutput.includes('test message')); assert(logOutput.includes("test message"));
// Should contain short timestamp by default // Should contain short timestamp by default
assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/)); 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", () => { Deno.test("Logger Simple Formatter - Basic Simple Format - should pad log levels correctly", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple', level: 'debug' }); const logger = new Logger({ format: "simple", level: "debug" });
logger.error('error msg'); logger.error("error msg");
logger.debug('debug msg'); logger.debug("debug msg");
const logs = getCapturedLogs(); const logs = getCapturedLogs();
assert(logs[0].includes('[ERROR]')); assert(logs[0].includes("[ERROR]"));
assert(logs[1].includes('[DEBUG]')); assert(logs[1].includes("[DEBUG]"));
}); });
Deno.test("Logger Simple Formatter - Basic Simple Format - should include caller information", () => { Deno.test("Logger Simple Formatter - Basic Simple Format - should include caller information", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple', callerLevel: 'info' }); const logger = new Logger({ format: "simple", callerLevel: "info" });
logger.info('test message'); logger.info("test message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should contain filename and line number // 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", () => { Deno.test("Logger Simple Formatter - Basic Simple Format - should format with long timestamp when specified", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple', time: 'long' }); const logger = new Logger({ format: "simple", time: "long" });
logger.info('test message'); logger.info("test message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should contain long time format in brackets // Should contain long time format in brackets
assert( 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("T"));
assert(logOutput.includes('Z')); assert(logOutput.includes("Z"));
}); });
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format error level correctly", () => { Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format error level correctly", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.error('error message'); logger.error("error message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[ERROR]')); assert(logOutput.includes("[ERROR]"));
assert(logOutput.includes('error message')); assert(logOutput.includes("error message"));
}); });
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format warn level correctly", () => { Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format warn level correctly", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.warn('warn message'); logger.warn("warn message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[WARN ]')); assert(logOutput.includes("[WARN ]"));
assert(logOutput.includes('warn message')); assert(logOutput.includes("warn message"));
}); });
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format info level correctly", () => { Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format info level correctly", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.info('info message'); logger.info("info message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[INFO ]')); assert(logOutput.includes("[INFO ]"));
assert(logOutput.includes('info message')); assert(logOutput.includes("info message"));
}); });
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format debug level correctly", () => { Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format debug level correctly", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple', level: 'debug' }); const logger = new Logger({ format: "simple", level: "debug" });
logger.debug('debug message'); logger.debug("debug message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[DEBUG]')); assert(logOutput.includes("[DEBUG]"));
assert(logOutput.includes('debug message')); assert(logOutput.includes("debug message"));
}); });
Deno.test("Logger Simple Formatter - Color Handling - should include color codes when output is TTY", () => { Deno.test("Logger Simple Formatter - Color Handling - should include color codes when output is TTY", () => {
clearCapturedLogs(); clearCapturedLogs();
setTTYMode(true); setTTYMode(true);
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.error('error message'); logger.error("error message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should contain ANSI color codes // Should contain ANSI color codes
assert(logOutput.includes('\x1b[91m')); // red for error assert(logOutput.includes("\x1b[91m")); // red for error
assert(logOutput.includes('\x1b[0m')); // reset assert(logOutput.includes("\x1b[0m")); // reset
}); });
Deno.test("Logger Simple Formatter - Color Handling - should not include color codes when output is redirected", () => { Deno.test("Logger Simple Formatter - Color Handling - should not include color codes when output is redirected", () => {
clearCapturedLogs(); clearCapturedLogs();
setTTYMode(false); setTTYMode(false);
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.error('error message'); logger.error("error message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should not contain ANSI color codes // 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", () => { Deno.test("Logger Simple Formatter - Color Handling - should use appropriate colors for different levels", () => {
clearCapturedLogs(); clearCapturedLogs();
setTTYMode(true); setTTYMode(true);
const logger = new Logger({ format: 'simple', level: 'debug' }); const logger = new Logger({ format: "simple", level: "debug" });
logger.error('error'); logger.error("error");
logger.warn('warn'); logger.warn("warn");
logger.info('info'); logger.info("info");
logger.debug('debug'); logger.debug("debug");
const logs = getCapturedLogs(); const logs = getCapturedLogs();
// Error should be red // Error should be red
assert(logs[0].includes('\x1b[91m')); assert(logs[0].includes("\x1b[91m"));
// Warn should be yellow // 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 // Info and debug might have different or no colors, but should have reset codes
assert(logs[2].includes('\x1b[0m')); assert(logs[2].includes("\x1b[0m"));
assert(logs[3].includes('\x1b[0m')); assert(logs[3].includes("\x1b[0m"));
}); });
Deno.test("Logger Simple Formatter - Color Handling - should respect custom color configuration", () => { Deno.test("Logger Simple Formatter - Color Handling - should respect custom color configuration", () => {
clearCapturedLogs(); clearCapturedLogs();
setTTYMode(true); setTTYMode(true);
const logger = new Logger({ const logger = new Logger({
format: 'simple', format: "simple",
colours: { colours: {
error: '\x1b[31m', // different red error: "\x1b[31m", // different red
warn: '\x1b[35m', // magenta instead of yellow warn: "\x1b[35m", // magenta instead of yellow
}, },
}); });
logger.error('error message'); logger.error("error message");
logger.warn('warn message'); logger.warn("warn message");
const logs = getCapturedLogs(); const logs = getCapturedLogs();
assert(logs[0].includes('\x1b[31m')); assert(logs[0].includes("\x1b[31m"));
assert(logs[1].includes('\x1b[35m')); assert(logs[1].includes("\x1b[35m"));
}); });
Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle multiple arguments", () => { Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle multiple arguments", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.info('Hello %s, you are %d years old', 'John', 25); logger.info("Hello %s, you are %d years old", "John", 25);
const logOutput = getCapturedLogs()[0]; 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", () => { Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle special characters", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.info('Special chars: "quotes", \\backslash, \nnewline'); logger.info('Special chars: "quotes", \\backslash, \nnewline');
const logOutput = getCapturedLogs()[0]; 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", () => { Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle empty messages", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.info(''); logger.info("");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should still have the level and timestamp parts // 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", () => { Deno.test("Logger Simple Formatter - TTY Detection Integration - should detect TTY mode changes correctly", () => {
@@ -198,19 +198,19 @@ Deno.test("Logger Simple Formatter - TTY Detection Integration - should detect T
// Test with TTY // Test with TTY
setTTYMode(true); setTTYMode(true);
const ttyLogger = new Logger({ format: 'simple' }); const ttyLogger = new Logger({ format: "simple" });
ttyLogger.error('tty error'); ttyLogger.error("tty error");
// Test without TTY // Test without TTY
setTTYMode(false); setTTYMode(false);
const noTtyLogger = new Logger({ format: 'simple' }); const noTtyLogger = new Logger({ format: "simple" });
noTtyLogger.error('no tty error'); noTtyLogger.error("no tty error");
const logs = getCapturedLogs(); const logs = getCapturedLogs();
// First should have colors, second should not // First should have colors, second should not
assert(logs[0].includes('\x1b[')); assert(logs[0].includes("\x1b["));
assert(!logs[1].includes('\x1b[')); assert(!logs[1].includes("\x1b["));
}); });
// Cleanup // Cleanup

View File

@@ -1,43 +1,43 @@
import { assertEquals, assertThrows, assert } from "@std/assert"; import { assert, assertEquals, assertThrows } from "@std/assert";
import Logger from '../lib/logger.ts'; import Logger from "../lib/logger.ts";
import { import {
setupMocks,
getCapturedLogs,
clearCapturedLogs, clearCapturedLogs,
getCapturedLogs,
getFirstLogAsJSON, getFirstLogAsJSON,
} from './helpers/logger-test-helpers.js'; setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests // Setup and teardown for all tests
setupMocks(); setupMocks();
Deno.test("Logger Time Formatting - Default Time Format - should default to short time format", () => { Deno.test("Logger Time Formatting - Default Time Format - should default to short time format", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('test message'); logger.info("test message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
// Short format should be YYYY-MM-DD HH:MM (without seconds) // 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.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
assert(!parsed.time.includes('T')); assert(!parsed.time.includes("T"));
assert(!parsed.time.includes('Z')); assert(!parsed.time.includes("Z"));
assert(!parsed.time.includes('.')); assert(!parsed.time.includes("."));
}); });
Deno.test("Logger Time Formatting - Default Time Format - should include time option in logger options", () => { Deno.test("Logger Time Formatting - Default Time Format - should include time option in logger options", () => {
const shortLogger = new Logger({ time: 'short' }); const shortLogger = new Logger({ time: "short" });
const longLogger = new Logger({ time: 'long' }); const longLogger = new Logger({ time: "long" });
const defaultLogger = new Logger(); const defaultLogger = new Logger();
assertEquals(shortLogger.options.time, 'short'); assertEquals(shortLogger.options.time, "short");
assertEquals(longLogger.options.time, 'long'); assertEquals(longLogger.options.time, "long");
assertEquals(defaultLogger.options.time, 'short'); // default assertEquals(defaultLogger.options.time, "short"); // default
}); });
Deno.test("Logger Time Formatting - Short Time Format - should format time as short when time option is 'short'", () => { Deno.test("Logger Time Formatting - Short Time Format - should format time as short when time option is 'short'", () => {
clearCapturedLogs(); 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(); 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", () => { Deno.test("Logger Time Formatting - Short Time Format - should work with simple formatter and short time", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ time: 'short', format: 'simple' }); const logger = new Logger({ time: "short", format: "simple" });
logger.info('test message'); logger.info("test message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should contain short time format in brackets // Should contain short time format in brackets
assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/)); assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/));
assert(!logOutput.includes('T')); assert(!logOutput.includes("T"));
assert(!logOutput.includes('Z')); assert(!logOutput.includes("Z"));
}); });
Deno.test("Logger Time Formatting - Short Time Format - should truncate time correctly in short format", () => { Deno.test("Logger Time Formatting - Short Time Format - should truncate time correctly in short format", () => {
clearCapturedLogs(); 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(); const parsed = getFirstLogAsJSON();
// Short format should not have seconds or milliseconds // Short format should not have seconds or milliseconds
assert( 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 // Should be exactly 16 characters: YYYY-MM-DD HH:MM
assertEquals(parsed.time.length, 16); 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'", () => { Deno.test("Logger Time Formatting - Long Time Format - should format time as long ISO string when time is 'long'", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ time: 'long', format: 'json' }); const logger = new Logger({ time: "long", format: "json" });
logger.info('test message'); logger.info("test message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
// Long format should be full ISO string // Long format should be full ISO string
assert(parsed.time.includes('T')); assert(parsed.time.includes("T"));
assert(parsed.time.includes('Z')); assert(parsed.time.includes("Z"));
assert(parsed.time.includes('.')); assert(parsed.time.includes("."));
// Should be valid ISO string // Should be valid ISO string
new Date(parsed.time); // This should not throw new Date(parsed.time); // This should not throw
// Should match ISO format pattern // Should match ISO format pattern
assert( 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", () => { Deno.test("Logger Time Formatting - Long Time Format - should work with simple formatter and long time", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ time: 'long', format: 'simple' }); const logger = new Logger({ time: "long", format: "simple" });
logger.info('test message'); logger.info("test message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should contain long time format in brackets // Should contain long time format in brackets
assert( 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("T"));
assert(logOutput.includes('Z')); assert(logOutput.includes("Z"));
}); });
Deno.test("Logger Time Formatting - Long Time Format - should preserve time precision in long format", () => { Deno.test("Logger Time Formatting - Long Time Format - should preserve time precision in long format", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ time: 'long', format: 'json' }); const logger = new Logger({ time: "long", format: "json" });
const startTime = Date.now(); const startTime = Date.now();
logger.info('test message'); logger.info("test message");
const endTime = Date.now(); const endTime = Date.now();
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
@@ -129,15 +129,15 @@ Deno.test("Logger Time Formatting - Long Time Format - should preserve time prec
assert(logTime <= endTime); assert(logTime <= endTime);
// Should have millisecond precision // 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", () => { Deno.test("Logger Time Formatting - Time Format Consistency - should use consistent time format across multiple log calls", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ time: 'short', format: 'json' }); const logger = new Logger({ time: "short", format: "json" });
logger.info('first message'); logger.info("first message");
logger.warn('second message'); logger.warn("second message");
const logs = getCapturedLogs(); const logs = getCapturedLogs();
const parsed1 = JSON.parse(logs[0]); 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", () => { Deno.test("Logger Time Formatting - Time Option Validation - should validate time option in constructor", () => {
// Valid options should not throw // Valid options should not throw
new Logger({ time: 'long' }); new Logger({ time: "long" });
new Logger({ time: 'short' }); new Logger({ time: "short" });
// Invalid option should throw // Invalid option should throw
assertThrows(() => { assertThrows(
new Logger({ time: 'medium' }); () => {
}, Error, "Invalid time: medium. Valid times are: long, short"); new Logger({ time: "medium" });
},
Error,
"Invalid time: medium. Valid times are: long, short",
);
assertThrows(() => { assertThrows(
new Logger({ time: 'invalid' }); () => {
}, Error, "Invalid time: invalid. Valid times are: long, short"); 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", () => { Deno.test("Logger Time Formatting - Backward Compatibility - should maintain existing behavior for existing code", () => {
clearCapturedLogs(); clearCapturedLogs();
// Code that doesn't specify time option should work as before // Code that doesn't specify time option should work as before
const logger = new Logger({ format: 'json', level: 'info' }); const logger = new Logger({ format: "json", level: "info" });
logger.info('test message'); logger.info("test message");
const parsed = getFirstLogAsJSON(); const parsed = getFirstLogAsJSON();
// Should still have all expected fields // Should still have all expected fields
assertEquals(parsed.level, 'info'); assertEquals(parsed.level, "info");
assertEquals(parsed.msg, 'test message'); assertEquals(parsed.msg, "test message");
assertEquals(typeof parsed.time, 'string'); assertEquals(typeof parsed.time, "string");
assertEquals(typeof parsed.pid, 'number'); assertEquals(typeof parsed.pid, "number");
assertEquals(typeof parsed.hostname, 'string'); assertEquals(typeof parsed.hostname, "string");
// Time should be in short format (new default) // Time should be in short format (new default)
assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)); assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
@@ -185,15 +193,15 @@ Deno.test("Logger Time Formatting - Backward Compatibility - should maintain exi
Deno.test("Logger Time Formatting - Backward Compatibility - should not break existing simple formatter tests", () => { Deno.test("Logger Time Formatting - Backward Compatibility - should not break existing simple formatter tests", () => {
clearCapturedLogs(); clearCapturedLogs();
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.warn('warning message'); logger.warn("warning message");
const logOutput = getCapturedLogs()[0]; const logOutput = getCapturedLogs()[0];
// Should still contain expected elements // Should still contain expected elements
assert(logOutput.includes('[WARN ]')); assert(logOutput.includes("[WARN ]"));
assert(logOutput.includes('warning message')); assert(logOutput.includes("warning message"));
assert(logOutput.includes('.js:')); assert(logOutput.includes(".js:"));
// Should use short time format (new default) // Should use short time format (new default)
assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/)); assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/));

View File

@@ -1,89 +1,89 @@
import { assertEquals, assertMatch } from "@std/assert"; 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", () => { Deno.test("Logger util.format functionality - Format specifiers - should handle %s string formatting", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('User %s logged in', 'john'); logger.info("User %s logged in", "john");
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'User john logged in'); assertEquals(output.msg, "User john logged in");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Format specifiers - should handle %d number formatting", () => { Deno.test("Logger util.format functionality - Format specifiers - should handle %d number formatting", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('User has %d points', 100); logger.info("User has %d points", 100);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'User has 100 points'); assertEquals(output.msg, "User has 100 points");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Format specifiers - should handle %i integer formatting", () => { Deno.test("Logger util.format functionality - Format specifiers - should handle %i integer formatting", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Value: %i', 42.7); logger.info("Value: %i", 42.7);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Value: 42'); assertEquals(output.msg, "Value: 42");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Format specifiers - should handle %f float formatting", () => { Deno.test("Logger util.format functionality - Format specifiers - should handle %f float formatting", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Price: %f', 19.99); logger.info("Price: %f", 19.99);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Price: 19.99'); assertEquals(output.msg, "Price: 19.99");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Format specifiers - should handle %j JSON formatting", () => { Deno.test("Logger util.format functionality - Format specifiers - should handle %j JSON formatting", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const obj = { name: 'test', value: 42 }; const obj = { name: "test", value: 42 };
logger.info('Config: %j', obj); logger.info("Config: %j", obj);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Config: {"name":"test","value":42}'); 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", () => { Deno.test("Logger util.format functionality - Format specifiers - should handle %% literal percentage", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Progress: 50%% complete'); logger.info("Progress: 50%% complete");
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Progress: 50%% complete'); assertEquals(output.msg, "Progress: 50%% complete");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Multiple format specifiers - should handle multiple format specifiers", () => { Deno.test("Logger util.format functionality - Multiple format specifiers - should handle multiple format specifiers", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info( logger.info(
'User %s has %d points and %f%% completion', "User %s has %d points and %f%% completion",
'alice', "alice",
150, 150,
75.5 75.5,
); );
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals( assertEquals(
output.msg, output.msg,
'User alice has 150 points and 75.5% completion' "User alice has 150 points and 75.5% completion",
); );
} finally { } finally {
console.log = originalLog; 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", () => { Deno.test("Logger util.format functionality - Multiple format specifiers - should handle mixed format specifiers with JSON", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const config = { debug: true, port: 3000 }; const config = { debug: true, port: 3000 };
logger.info( logger.info(
'Server %s running on port %d with config %j', "Server %s running on port %d with config %j",
'api', "api",
8080, 8080,
config config,
); );
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals( assertEquals(
output.msg, 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 { } finally {
console.log = originalLog; 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", () => { 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; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Message', 'arg1', 'arg2', 123); logger.info("Message", "arg1", "arg2", 123);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Message arg1 arg2 123'); assertEquals(output.msg, "Message arg1 arg2 123");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Multiple arguments without format specifiers - should handle mixed objects and primitives", () => { 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; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const obj = { key: 'value' }; const obj = { key: "value" };
logger.info('Data:', obj, 42, true); logger.info("Data:", obj, 42, true);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, "Data: { key: 'value' } 42 true"); 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", () => { Deno.test("Logger util.format functionality - Edge cases - should handle more format specifiers than arguments", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Hello %s, you are %d years old', 'John'); logger.info("Hello %s, you are %d years old", "John");
const output = JSON.parse(capturedOutput); 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 { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Edge cases - should handle more arguments than format specifiers", () => { Deno.test("Logger util.format functionality - Edge cases - should handle more arguments than format specifiers", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Hello %s', 'John', 'extra', 'args', 123); logger.info("Hello %s", "John", "extra", "args", 123);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Hello John extra args 123'); assertEquals(output.msg, "Hello John extra args 123");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Edge cases - should handle null and undefined values", () => { Deno.test("Logger util.format functionality - Edge cases - should handle null and undefined values", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
logger.info('Values: %s %s %d', null, undefined, null); logger.info("Values: %s %s %d", null, undefined, null);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Values: null undefined 0'); assertEquals(output.msg, "Values: null undefined 0");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Edge cases - should handle arrays and objects without %j", () => { Deno.test("Logger util.format functionality - Edge cases - should handle arrays and objects without %j", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const arr = [1, 2, 3]; const arr = [1, 2, 3];
const obj = { a: 1 }; 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); 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 { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Simple format output - should format messages correctly in simple format", () => { Deno.test("Logger util.format functionality - Simple format output - should format messages correctly in simple format", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
logger.info('User %s has %d points', 'bob', 200); logger.info("User %s has %d points", "bob", 200);
assertMatch(capturedOutput, /User bob has 200 points/); assertMatch(capturedOutput, /User bob has 200 points/);
} finally { } 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", () => { Deno.test("Logger util.format functionality - Simple format output - should handle JSON formatting in simple format", () => {
let capturedOutput = ''; let capturedOutput = "";
const originalLog = console.log; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'simple' }); const logger = new Logger({ format: "simple" });
const data = { status: 'active', count: 5 }; const data = { status: "active", count: 5 };
logger.warn('Status: %j', data); logger.warn("Status: %j", data);
assertMatch(capturedOutput, /Status: {"status":"active","count":5}/); assertMatch(capturedOutput, /Status: {"status":"active","count":5}/);
} finally { } 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", () => { 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; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const problematicObj = { const problematicObj = {
toString() { 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 // 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) // Check that something was logged (the logger should handle the error)
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertEquals(typeof output.msg, 'string'); assertEquals(typeof output.msg, "string");
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
}); });
Deno.test("Logger util.format functionality - Error handling in util.format - should handle circular references with %j", () => { 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; const originalLog = console.log;
console.log = (message) => { console.log = (message) => {
capturedOutput = message; capturedOutput = message;
}; };
try { try {
const logger = new Logger({ format: 'json' }); const logger = new Logger({ format: "json" });
const circular = { name: 'test' }; const circular = { name: "test" };
circular.self = circular; circular.self = circular;
logger.info('Circular: %j', circular); logger.info("Circular: %j", circular);
const output = JSON.parse(capturedOutput); const output = JSON.parse(capturedOutput);
assertMatch(output.msg, /Circular: \[Circular\]/); assertMatch(output.msg, /Circular: \[Circular\]/);
@@ -355,4 +355,3 @@ Deno.test("Logger util.format functionality - Error handling in util.format - sh
console.log = originalLog; console.log = originalLog;
} }
}); });