test: add unit tests for handler, logger, manager, and config
Cover Handler property accessors, format delegation, emit contract, and close lifecycle. Cover Logger level methods, isEnabledFor caching, manager property assignment, handler management, debug invocation, and makeRecord extra key collision. Cover Manager getLogger hierarchy setup, placeholder fixup, disable level, and loggerClass validation. Cover basicConfig option handling, force flag, mutual exclusion of stream/filename/handlers, and formatter assignment.
This commit is contained in:
parent
0b87f5516a
commit
3b6b116b00
4 changed files with 494 additions and 0 deletions
93
tests/unit/config.test.ts
Normal file
93
tests/unit/config.test.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import {expect, jest, test} from '@jest/globals';
|
||||||
|
import { basicConfig } from '../../src/config';
|
||||||
|
import { MANAGER } from '../../src/manager';
|
||||||
|
import { Handler } from '../../src/handler';
|
||||||
|
import { LogRecord } from '../../src/log-record';
|
||||||
|
import { Formatter } from '../../src/formatter';
|
||||||
|
import { DEBUG, WARNING, ERROR } from '../../src/log-level';
|
||||||
|
describe('basicConfig', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MANAGER.root.handlers.splice(0, MANAGER.root.handlers.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds a handler to the root logger', () => {
|
||||||
|
basicConfig({});
|
||||||
|
expect(MANAGER.root.handlers.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sets root logger level', () => {
|
||||||
|
basicConfig({ level: DEBUG });
|
||||||
|
expect(MANAGER.root.level).toBe(DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does nothing when handlers already exist and force is false', () => {
|
||||||
|
basicConfig({ level: DEBUG });
|
||||||
|
const handlerCount = MANAGER.root.handlers.length;
|
||||||
|
basicConfig({ level: ERROR });
|
||||||
|
expect(MANAGER.root.handlers.length).toBe(handlerCount);
|
||||||
|
expect(MANAGER.root.level).toBe(DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('replaces handlers when force is true', () => {
|
||||||
|
basicConfig({ level: DEBUG });
|
||||||
|
basicConfig({ level: ERROR, force: true });
|
||||||
|
expect(MANAGER.root.level).toBe(ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws on invalid style', () => {
|
||||||
|
expect(() => {
|
||||||
|
basicConfig({ style: 'X' });
|
||||||
|
}).toThrow('style must be one of');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when stream and filename both specified', () => {
|
||||||
|
expect(() => {
|
||||||
|
basicConfig({
|
||||||
|
stream: process.stderr,
|
||||||
|
filename: 'test.log',
|
||||||
|
});
|
||||||
|
}).toThrow('should not be specified together');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when handlers and stream both specified', () => {
|
||||||
|
class TestHandler extends Handler {
|
||||||
|
emit(record: LogRecord) {}
|
||||||
|
}
|
||||||
|
expect(() => {
|
||||||
|
basicConfig({
|
||||||
|
handlers: [new TestHandler()],
|
||||||
|
stream: process.stderr,
|
||||||
|
});
|
||||||
|
}).toThrow('should not be specified together');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses provided handlers', () => {
|
||||||
|
class TestHandler extends Handler {
|
||||||
|
emit(record: LogRecord) {}
|
||||||
|
}
|
||||||
|
const handler = new TestHandler();
|
||||||
|
basicConfig({ handlers: [handler] });
|
||||||
|
expect(MANAGER.root.handlers).toContain(handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assigns formatter to handlers without one', () => {
|
||||||
|
class TestHandler extends Handler {
|
||||||
|
emit(record: LogRecord) {}
|
||||||
|
}
|
||||||
|
const handler = new TestHandler();
|
||||||
|
expect(handler.formatter).toBeNull();
|
||||||
|
basicConfig({ handlers: [handler] });
|
||||||
|
expect(handler.formatter).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves existing formatter on handlers', () => {
|
||||||
|
class TestHandler extends Handler {
|
||||||
|
emit(record: LogRecord) {}
|
||||||
|
}
|
||||||
|
const handler = new TestHandler();
|
||||||
|
const fmt = new Formatter({ fmt: '%(message)s' });
|
||||||
|
handler.formatter = fmt;
|
||||||
|
basicConfig({ handlers: [handler] });
|
||||||
|
expect(handler.formatter).toBe(fmt);
|
||||||
|
});
|
||||||
|
});
|
||||||
92
tests/unit/handler.test.ts
Normal file
92
tests/unit/handler.test.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import {expect, jest, test} from '@jest/globals';
|
||||||
|
import { Handler, StreamHandler, StderrHandler } from '../../src/handler';
|
||||||
|
import { Formatter, DEFAULT_FORMATTER } from '../../src/formatter';
|
||||||
|
import { LogRecord } from '../../src/log-record';
|
||||||
|
import { DEBUG, WARNING, ERROR, NOTSET } from '../../src/log-level';
|
||||||
|
function makeRecord(level: number, msg: string): LogRecord {
|
||||||
|
return new LogRecord('test', { level, msg });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Handler', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('defaults to NOTSET level', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
expect(h.level).toBe(NOTSET);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts a level argument', () => {
|
||||||
|
const h = new Handler(WARNING);
|
||||||
|
expect(h.level).toBe(WARNING);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('level setter', () => {
|
||||||
|
test('sets the level without infinite recursion', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
h.level = ERROR;
|
||||||
|
expect(h.level).toBe(ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatter', () => {
|
||||||
|
test('getter returns null when no formatter set', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
expect(h.formatter).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getter returns assigned formatter', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
const fmt = new Formatter();
|
||||||
|
h.formatter = fmt;
|
||||||
|
expect(h.formatter).toBe(fmt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format', () => {
|
||||||
|
test('uses DEFAULT_FORMATTER when no formatter assigned', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
const record = makeRecord(DEBUG, 'hello');
|
||||||
|
const result = h.format(record);
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses assigned formatter', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
const fmt = new Formatter({ fmt: '%(message)s' });
|
||||||
|
h.formatter = fmt;
|
||||||
|
const record = makeRecord(DEBUG, 'hello');
|
||||||
|
const result = h.format(record);
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns a string', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
const record = makeRecord(WARNING, 'test message');
|
||||||
|
expect(typeof h.format(record)).toBe('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emit', () => {
|
||||||
|
test('throws NotImplementedError on base class', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
const record = makeRecord(DEBUG, 'hello');
|
||||||
|
expect(() => h.emit(record)).toThrow('emit must be implemented');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('close', () => {
|
||||||
|
test('sets closed to true', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
expect(h.closed).toBe(false);
|
||||||
|
h.close();
|
||||||
|
expect(h.closed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('StderrHandler', () => {
|
||||||
|
test('accepts a level', () => {
|
||||||
|
const h = new StderrHandler(ERROR);
|
||||||
|
expect(h.level).toBe(ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
181
tests/unit/logger.test.ts
Normal file
181
tests/unit/logger.test.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import {expect, jest, test} from '@jest/globals';
|
||||||
|
import { Logger, RootLogger, ROOT } from '../../src/logger';
|
||||||
|
import { Handler } from '../../src/handler';
|
||||||
|
import { LogRecord } from '../../src/log-record';
|
||||||
|
import { Formatter } from '../../src/formatter';
|
||||||
|
import { Manager } from '../../src/manager';
|
||||||
|
import {
|
||||||
|
DEBUG,
|
||||||
|
INFO,
|
||||||
|
WARNING,
|
||||||
|
ERROR,
|
||||||
|
CRITICAL,
|
||||||
|
NOTSET,
|
||||||
|
} from '../../src/log-level';
|
||||||
|
import { MyError } from '../../src/helper/error';
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('sets scope and default level', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
expect(logger.scope).toBe('test');
|
||||||
|
expect(logger.level).toBe(NOTSET);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts explicit level', () => {
|
||||||
|
const logger = new Logger('test', WARNING);
|
||||||
|
expect(logger.level).toBe(WARNING);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setLevel', () => {
|
||||||
|
test('changes the level', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
logger.setLevel(ERROR);
|
||||||
|
expect(logger.level).toBe(ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEffectiveLevel', () => {
|
||||||
|
test('returns own level when set', () => {
|
||||||
|
const logger = new Logger('test', WARNING);
|
||||||
|
expect(logger.getEffectiveLevel()).toBe(WARNING);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns NOTSET when no level set and no parent', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
expect(logger.getEffectiveLevel()).toBe(NOTSET);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isEnabledFor', () => {
|
||||||
|
test('returns true when level meets effective level', () => {
|
||||||
|
const logger = new Logger('test', DEBUG);
|
||||||
|
expect(logger.isEnabledFor(WARNING)).toBe(true);
|
||||||
|
expect(logger.isEnabledFor(DEBUG)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when level is below effective level', () => {
|
||||||
|
const logger = new Logger('test', WARNING);
|
||||||
|
expect(logger.isEnabledFor(DEBUG)).toBe(false);
|
||||||
|
expect(logger.isEnabledFor(INFO)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('caches results for repeated calls', () => {
|
||||||
|
const logger = new Logger('test', WARNING);
|
||||||
|
const first = logger.isEnabledFor(DEBUG);
|
||||||
|
const second = logger.isEnabledFor(DEBUG);
|
||||||
|
expect(first).toBe(second);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('respects manager disable level', () => {
|
||||||
|
const root = new RootLogger(WARNING);
|
||||||
|
const manager = new Manager(root);
|
||||||
|
const logger = manager.getLogger('test');
|
||||||
|
logger.setLevel(DEBUG);
|
||||||
|
logger.manager = manager;
|
||||||
|
manager.disable = ERROR;
|
||||||
|
logger.clear();
|
||||||
|
expect(logger.isEnabledFor(DEBUG)).toBe(false);
|
||||||
|
expect(logger.isEnabledFor(WARNING)).toBe(false);
|
||||||
|
expect(logger.isEnabledFor(CRITICAL)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('manager property', () => {
|
||||||
|
test('starts as null', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
expect(logger.manager).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be assigned once', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
const root = new RootLogger(WARNING);
|
||||||
|
const manager = new Manager(root);
|
||||||
|
logger.manager = manager;
|
||||||
|
expect(logger.manager).toBe(manager);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws on second assignment', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
const root = new RootLogger(WARNING);
|
||||||
|
const manager = new Manager(root);
|
||||||
|
logger.manager = manager;
|
||||||
|
expect(() => { logger.manager = manager }).toThrow('logger can only be assigned to manager once');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addHandler / removeHandler', () => {
|
||||||
|
test('adds and removes handlers', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
const handler = new Handler(DEBUG);
|
||||||
|
expect(logger.handlers.length).toBe(0);
|
||||||
|
logger.addHandler(handler);
|
||||||
|
expect(logger.handlers.length).toBe(1);
|
||||||
|
logger.removeHandler(handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not add duplicate handlers', () => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
const handler = new Handler(DEBUG);
|
||||||
|
logger.addHandler(handler);
|
||||||
|
logger.addHandler(handler);
|
||||||
|
expect(logger.handlers.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('debug', () => {
|
||||||
|
test('invokes handler when level is DEBUG', () => {
|
||||||
|
const logger = new Logger('test', DEBUG);
|
||||||
|
const emitted: LogRecord[] = [];
|
||||||
|
|
||||||
|
class TestHandler extends Handler {
|
||||||
|
emit(record: LogRecord) { emitted.push(record) }
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.addHandler(new TestHandler(DEBUG));
|
||||||
|
logger.debug('test message');
|
||||||
|
expect(emitted.length).toBe(1);
|
||||||
|
expect(emitted[0].scope).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not invoke handler when level is above DEBUG', () => {
|
||||||
|
const logger = new Logger('test', WARNING);
|
||||||
|
const emitted: LogRecord[] = [];
|
||||||
|
|
||||||
|
class TestHandler extends Handler {
|
||||||
|
emit(record: LogRecord) { emitted.push(record) }
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.addHandler(new TestHandler(DEBUG));
|
||||||
|
logger.debug('test message');
|
||||||
|
expect(emitted.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeRecord', () => {
|
||||||
|
test('throws KeyError when extra overwrites existing LogRecord key', () => {
|
||||||
|
const logger = new Logger('test', DEBUG);
|
||||||
|
expect(() => {
|
||||||
|
logger.debug('test', {
|
||||||
|
excInfo: null,
|
||||||
|
extra: { scope: 'override' },
|
||||||
|
stackInfo: false,
|
||||||
|
stackLevel: 1,
|
||||||
|
});
|
||||||
|
}).toThrow('attempt to overwrite scope in LogRecord');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RootLogger', () => {
|
||||||
|
test('has scope "root"', () => {
|
||||||
|
const root = new RootLogger(WARNING);
|
||||||
|
expect(root.scope).toBe('root');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts a level', () => {
|
||||||
|
const root = new RootLogger(ERROR);
|
||||||
|
expect(root.level).toBe(ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
128
tests/unit/manager.test.ts
Normal file
128
tests/unit/manager.test.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import {expect, jest, test} from '@jest/globals';
|
||||||
|
import { Manager } from '../../src/manager';
|
||||||
|
import { Logger, RootLogger } from '../../src/logger';
|
||||||
|
import { WARNING, DEBUG, NOTSET } from '../../src/log-level';
|
||||||
|
|
||||||
|
function makeManager(): Manager {
|
||||||
|
return new Manager(new RootLogger(WARNING));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Manager', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
test('stores the root logger', () => {
|
||||||
|
const root = new RootLogger(WARNING);
|
||||||
|
const manager = new Manager(root);
|
||||||
|
expect(manager.root).toBe(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogger', () => {
|
||||||
|
test('returns a Logger instance', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const logger = manager.getLogger('app');
|
||||||
|
expect(logger).toBeInstanceOf(Logger);
|
||||||
|
expect(logger.scope).toBe('app');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns the same logger for the same scope', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const a = manager.getLogger('app');
|
||||||
|
const b = manager.getLogger('app');
|
||||||
|
expect(a).toBe(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns different loggers for different scopes', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const a = manager.getLogger('app');
|
||||||
|
const b = manager.getLogger('lib');
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sets parent to root for top-level loggers', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const logger = manager.getLogger('app');
|
||||||
|
expect(logger.parent).toBe(manager.root);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sets parent to existing parent logger', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const parent = manager.getLogger('app');
|
||||||
|
const child = manager.getLogger('app.module');
|
||||||
|
expect(child.parent).toBe(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('establishes hierarchy for deeply nested loggers', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const top = manager.getLogger('a');
|
||||||
|
const mid = manager.getLogger('a.b');
|
||||||
|
const leaf = manager.getLogger('a.b.c');
|
||||||
|
expect(leaf.parent).toBe(mid);
|
||||||
|
expect(mid.parent).toBe(top);
|
||||||
|
expect(top.parent).toBe(manager.root);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates placeholders for intermediate loggers', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const leaf = manager.getLogger('a.b.c');
|
||||||
|
expect(leaf.parent).toBe(manager.root);
|
||||||
|
|
||||||
|
const mid = manager.getLogger('a.b');
|
||||||
|
expect(leaf.parent).toBe(mid);
|
||||||
|
expect(mid.parent).toBe(manager.root);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fixes up children when intermediate logger is created', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const leaf = manager.getLogger('a.b.c');
|
||||||
|
const top = manager.getLogger('a');
|
||||||
|
|
||||||
|
expect(top.parent).toBe(manager.root);
|
||||||
|
|
||||||
|
const mid = manager.getLogger('a.b');
|
||||||
|
expect(leaf.parent).toBe(mid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('disable', () => {
|
||||||
|
test('defaults to 0', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
expect(manager.disable).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be set to a level', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
manager.disable = WARNING;
|
||||||
|
expect(manager.disable).toBe(WARNING);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loggerClass', () => {
|
||||||
|
test('accepts Logger subclass', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
class CustomLogger extends Logger {}
|
||||||
|
expect(() => { manager.loggerClass = CustomLogger as any }).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-Logger class', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
class NotALogger {}
|
||||||
|
expect(() => {
|
||||||
|
manager.loggerClass = NotALogger as any;
|
||||||
|
}).toThrow(TypeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts Logger itself', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
expect(() => { manager.loggerClass = Logger as any }).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
test('clears logger caches', () => {
|
||||||
|
const manager = makeManager();
|
||||||
|
const logger = manager.getLogger('test');
|
||||||
|
logger.isEnabledFor(DEBUG);
|
||||||
|
manager.clear();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue