This commit is contained in:
IanKulin
2025-09-25 20:50:13 +08:00
commit 9546dd3ab3
17 changed files with 3248 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
// Shared test utilities for logger tests
// Mock console.log to capture output
let capturedLogs = [];
let capturedErrors = [];
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalIsTTY = Deno.stdout.isTerminal();
export function mockConsole() {
console.log = (...args) => {
capturedLogs.push(args.join(' '));
};
}
export function mockConsoleError() {
console.error = (...args) => {
capturedErrors.push(args);
};
}
export function restoreConsole() {
console.log = originalConsoleLog;
capturedLogs = [];
}
export function restoreConsoleError() {
console.error = originalConsoleError;
capturedErrors = [];
}
export function getCapturedLogs() {
return capturedLogs;
}
export function getCapturedErrors() {
return capturedErrors;
}
export function clearCapturedLogs() {
capturedLogs = [];
}
export function clearCapturedErrors() {
capturedErrors = [];
}
let mockIsTTY = originalIsTTY;
export function setTTYMode(isTTY) {
mockIsTTY = isTTY;
// Mock Deno.stdout.isTerminal for testing
Deno.stdout.isTerminal = () => mockIsTTY;
}
export function restoreTTY() {
mockIsTTY = originalIsTTY;
// Restore original Deno.stdout.isTerminal
Deno.stdout.isTerminal = () => originalIsTTY;
}
// Helper to setup both console mocks
export function setupMocks() {
mockConsole();
mockConsoleError();
}
// Helper to restore both console mocks
export function restoreMocks() {
restoreConsole();
restoreConsoleError();
}
// Helper to get parsed JSON from first captured log
export function getFirstLogAsJSON() {
if (capturedLogs.length === 0) {
throw new Error('No logs captured');
}
return JSON.parse(capturedLogs[0]);
}

View File

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

View File

@@ -0,0 +1,158 @@
import { assertEquals, assertThrows } from "@std/assert";
import Logger from '../lib/logger.ts';
Deno.test("Logger Constructor - should throw error for invalid log level", () => {
assertThrows(() => {
new Logger({ level: 'invalid' });
}, Error, "Invalid log level: invalid. Valid levels are: silent, error, warn, info, debug");
});
Deno.test("Logger Constructor - should throw error for invalid format", () => {
assertThrows(() => {
new Logger({ format: 'invalid' });
}, Error, "Invalid format: invalid. Valid formats are: json, simple");
});
Deno.test("Logger Constructor - should throw error for invalid time option", () => {
assertThrows(() => {
new Logger({ time: 'invalid' });
}, Error, "Invalid time: invalid. Valid times are: long, short");
});
Deno.test("Logger Constructor - should throw error for invalid callerLevel", () => {
assertThrows(() => {
new Logger({ callerLevel: 'invalid' });
}, Error, "Invalid callerLevel: invalid. Valid levels are: silent, error, warn, info, debug");
});
Deno.test("Logger Constructor - should throw error for non-object colours", () => {
assertThrows(() => {
new Logger({ colours: 'not an object' });
}, Error, "colours option must be an object");
});
Deno.test("Logger Constructor - should throw error for non-object levels", () => {
assertThrows(() => {
new Logger({ levels: 'not an object' });
}, Error, "levels option must be an object");
});
Deno.test("Logger Constructor - should throw error for invalid level values", () => {
assertThrows(() => {
new Logger({ levels: { error: -1 } });
}, Error, "Level value for 'error' must be a non-negative integer");
assertThrows(() => {
new Logger({ levels: { error: 'not a number' } });
}, Error, "Level value for 'error' must be a non-negative integer");
assertThrows(() => {
new Logger({ levels: { error: 1.5 } });
}, Error, "Level value for 'error' must be a non-negative integer");
});
Deno.test("Logger Constructor - should accept valid options without throwing", () => {
// This should not throw
new Logger({
level: 'debug',
format: 'simple',
time: 'long',
callerLevel: 'error',
colours: { error: '\x1b[31m' },
levels: { custom: 4 },
});
});
Deno.test("Logger Constructor - should instantiate with default options", () => {
const logger = new Logger();
assertEquals(logger.options.level, 'info');
assertEquals(logger.options.format, 'json');
assertEquals(logger.options.time, 'short');
assertEquals(logger.options.callerLevel, 'warn');
assertEquals(logger.options.levels, {
silent: -1,
error: 0,
warn: 1,
info: 2,
debug: 3,
});
});
Deno.test("Logger Constructor - should instantiate with custom options", () => {
const logger = new Logger({
level: 'debug',
format: 'simple',
time: 'long',
callerLevel: 'error',
});
assertEquals(logger.options.level, 'debug');
assertEquals(logger.options.format, 'simple');
assertEquals(logger.options.time, 'long');
assertEquals(logger.options.callerLevel, 'error');
});
Deno.test("Logger Constructor - should merge options correctly", () => {
const customOptions = {
level: 'debug',
format: 'simple',
time: 'long',
colours: {
error: '\x1b[31m', // different red
},
};
const logger = new Logger(customOptions);
assertEquals(logger.options.level, 'debug');
assertEquals(logger.options.format, 'simple');
assertEquals(logger.options.time, 'long');
assertEquals(logger.options.colours.error, '\x1b[31m');
// Should still have other default colors
assertEquals(logger.options.colours.warn, '\x1b[33m');
});
Deno.test("Logger Constructor - should have all log level methods", () => {
const logger = new Logger();
assertEquals(typeof logger.error, 'function');
assertEquals(typeof logger.warn, 'function');
assertEquals(typeof logger.info, 'function');
assertEquals(typeof logger.debug, 'function');
});
Deno.test("Logger Constructor - should have level management methods", () => {
const logger = new Logger();
assertEquals(typeof logger.level, 'function');
assertEquals(typeof logger.setLevel, 'function');
});
Deno.test("Logger Constructor - should detect TTY correctly", () => {
const originalIsTTY = Deno.stdout.isTerminal();
// Mock TTY mode
Deno.stdout.isTerminal = () => true;
const logger1 = new Logger();
assertEquals(logger1.isRedirected, false);
Deno.stdout.isTerminal = () => false;
const logger2 = new Logger();
assertEquals(logger2.isRedirected, true);
// Restore original
Deno.stdout.isTerminal = () => originalIsTTY;
});
Deno.test("Logger Constructor - should work with all existing constructor patterns", () => {
// No options - should not throw
new Logger();
// Partial options - should not throw
new Logger({ level: 'debug' });
// Full options (without time) - should not throw
new Logger({
level: 'warn',
format: 'simple',
colours: { error: '\x1b[31m' },
levels: { custom: 5 },
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,146 @@
import { assertEquals, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import {
setupMocks,
getCapturedLogs,
clearCapturedLogs,
} from './helpers/logger-test-helpers.js';
// Setup and teardown for all tests
setupMocks();
Deno.test("Logger Robustness - Edge Cases and Data Handling - should not crash on logging errors", () => {
clearCapturedLogs();
const logger = new Logger();
// This should not throw
logger.info('test message');
});
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle undefined and null messages gracefully", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
// These should not crash
logger.info(undefined);
logger.info(null);
const logs = getCapturedLogs();
assertEquals(logs.length, 2);
const parsed1 = JSON.parse(logs[0]);
const parsed2 = JSON.parse(logs[1]);
assertEquals(parsed1.msg, 'undefined');
assertEquals(parsed2.msg, 'null');
});
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle extremely large messages", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const hugeMessage = 'x'.repeat(100000);
logger.info(hugeMessage);
const parsed = JSON.parse(getCapturedLogs()[0]);
assertEquals(parsed.msg, hugeMessage);
});
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle circular objects in message formatting", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const circular = { name: 'test' };
circular.self = circular;
logger.info('Circular: %j', circular);
// Should still log something
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Robustness - Performance and Memory - should handle rapid consecutive logging without issues", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
for (let i = 0; i < 1000; i++) {
logger.info(`rapid message ${i}`);
}
assertEquals(getCapturedLogs().length, 1000);
});
Deno.test("Logger Robustness - Performance and Memory - should handle repeated logging operations efficiently", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
const startTime = Date.now();
// Log a reasonable number of messages
for (let i = 0; i < 500; i++) {
logger.info(`performance test message ${i}`);
}
const endTime = Date.now();
const duration = endTime - startTime;
// Should complete within reasonable time (adjust threshold as needed)
assert(duration < 5000, `Logging took too long: ${duration}ms`);
assertEquals(getCapturedLogs().length, 500);
});
Deno.test("Logger Robustness - Performance and Memory - should handle mixed format types in rapid succession", () => {
clearCapturedLogs();
const jsonLogger = new Logger({ format: 'json' });
const simpleLogger = new Logger({ format: 'simple' });
for (let i = 0; i < 50; i++) {
jsonLogger.info(`json message ${i}`);
simpleLogger.info(`simple message ${i}`);
}
assertEquals(getCapturedLogs().length, 100);
});
Deno.test("Logger Robustness - Complex Data Structures - should handle deeply nested objects", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const deepObject = {
level1: { level2: { level3: { level4: { value: 'deep' } } } },
};
logger.info('Deep object: %j', deepObject);
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Robustness - Complex Data Structures - should handle arrays with mixed data types", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const mixedArray = [
1,
'string',
{ obj: true },
[1, 2, 3],
null,
undefined,
];
logger.info('Mixed array: %j', mixedArray);
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Robustness - Complex Data Structures - should handle special characters and unicode", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const specialMessage = 'Special chars: \n\t\r\\"\'🚀 Unicode: こんにちは';
logger.info(specialMessage);
const parsed = JSON.parse(getCapturedLogs()[0]);
assertEquals(parsed.msg, specialMessage);
});

View File

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

View File

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

View File

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