diff --git a/tests/unit/formatter.test.ts b/tests/unit/formatter.test.ts new file mode 100644 index 0000000..c105629 --- /dev/null +++ b/tests/unit/formatter.test.ts @@ -0,0 +1,143 @@ +import {expect, jest, test} from '@jest/globals'; +import { Formatter, DEFAULT_FORMATTER } from '../../src/formatter'; +import { LogRecord } from '../../src/log-record'; +import { DEBUG, WARNING, INFO } from '../../src/log-level'; +import { MyError } from '../../src/helper/error'; + +function makeRecord(level: number, msg: string): LogRecord { + return new LogRecord('test.module', { level, msg }); +} + +describe('Formatter', () => { + describe('constructor', () => { + test('uses default format when no options given', () => { + const fmt = new Formatter(); + const record = makeRecord(WARNING, 'hello'); + const result = fmt.format(record); + expect(result).toBe('hello'); + }); + + test('accepts custom format string', () => { + const fmt = new Formatter({ fmt: '%(levelname)s - %(message)s' }); + const record = makeRecord(DEBUG, 'debug msg'); + expect(fmt.format(record)).toBe('DEBUG - debug msg'); + }); + + test('throws on invalid style', () => { + expect(() => new Formatter({ style: '{' })).toThrow('style must be one of'); + }); + }); + + describe('format', () => { + test('substitutes %(name)s with logger scope', () => { + const fmt = new Formatter({ fmt: '[%(name)s] %(message)s' }); + const record = makeRecord(INFO, 'test'); + expect(fmt.format(record)).toBe('[test.module] test'); + }); + + test('substitutes %(levelno)d with numeric level', () => { + const fmt = new Formatter({ fmt: '%(levelno)d: %(message)s' }); + const record = makeRecord(WARNING, 'warn'); + expect(fmt.format(record)).toBe('30: warn'); + }); + + test('substitutes %(levelname)s with level name', () => { + const fmt = new Formatter({ fmt: '%(levelname)s %(message)s' }); + const record = makeRecord(DEBUG, 'msg'); + expect(fmt.format(record)).toBe('DEBUG msg'); + }); + + test('handles multiple placeholders', () => { + const fmt = new Formatter({ + fmt: '%(levelname)s:%(name)s:%(levelno)d:%(message)s' + }); + const record = makeRecord(WARNING, 'multi'); + expect(fmt.format(record)).toBe('WARNING:test.module:30:multi'); + }); + + test('throws on unknown field', () => { + const fmt = new Formatter({ fmt: '%(nonexistent)s' }); + const record = makeRecord(DEBUG, 'test'); + expect(() => fmt.format(record)).toThrow('formatting field not found'); + }); + + test('populates asctime when format uses it', () => { + const fmt = new Formatter({ + fmt: '%(asctime)s %(message)s' + }); + const record = makeRecord(INFO, 'timed'); + const result = fmt.format(record); + expect(result).toContain('timed'); + expect(record.asctime.length).toBeGreaterThan(0); + }); + + test('does not populate asctime when format does not use it', () => { + const fmt = new Formatter({ fmt: '%(message)s' }); + const record = makeRecord(INFO, 'no time'); + fmt.format(record); + expect(record.asctime).toBe(''); + }); + }); + + describe('formatTime', () => { + test('returns ISO8601-like string by default', () => { + const fmt = new Formatter(); + const record = makeRecord(INFO, 'test'); + const result = fmt.formatTime(record); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/); + }); + + test('uses custom datefmt with strftime tokens', () => { + const fmt = new Formatter(); + const record = makeRecord(INFO, 'test'); + const result = fmt.formatTime(record, '%Y/%m/%d'); + expect(result).toMatch(/^\d{4}\/\d{2}\/\d{2}$/); + }); + }); + + describe('formatError', () => { + test('returns stack trace when available', () => { + const fmt = new Formatter(); + const err = new MyError('test error'); + const result = fmt.formatError(err); + expect(result).toContain('test error'); + expect(result).toContain('\n'); + }); + + test('returns string representation when no stack', () => { + const fmt = new Formatter(); + const err = new MyError('no stack'); + err.stack = undefined; + const result = fmt.formatError(err); + expect(result).toContain('no stack'); + }); + }); + + describe('usesTime', () => { + test('returns true when format contains asctime', () => { + const fmt = new Formatter({ fmt: '%(asctime)s %(message)s' }); + expect(fmt.usesTime()).toBe(true); + }); + + test('returns false when format does not contain asctime', () => { + const fmt = new Formatter({ fmt: '%(message)s' }); + expect(fmt.usesTime()).toBe(false); + }); + }); +}); + +describe('LogRecord.getMessage', () => { + test('returns the message string', () => { + const record = makeRecord(INFO, 'plain message'); + expect(record.getMessage()).toBe('plain message'); + }); + + test('substitutes %s args into message', () => { + const record = new LogRecord('test', { + level: INFO, + msg: 'hello %s, you have %s items', + args: ['world', '5'], + }); + expect(record.getMessage()).toBe('hello world, you have 5 items'); + }); +});