import { createInterface, Interface } from 'readline'; import chai, { expect } from 'chai'; import 'mocha'; import chaiFs from 'chai-fs'; import fc from 'fast-check'; import fs from 'fs-extra'; import { container } from '../../core/container'; import { setDev } from '../../core/env.spec'; import { ILogger } from './i-logger'; import { LogLevel } from './log-level'; chai.use(chaiFs); describe('Logger Service', () => { function getLastLine(rl: Interface): Promise { return new Promise((resolve) => { let lastLine = ''; rl.on('line', (line) => { lastLine = line; }); rl.on('close', () => { resolve(lastLine); }); }); } function getDefaultLogReadLineInterface(logger: ILogger) { return createInterface(fs.createReadStream(logger.getLogFile())); } // no debug because it gets tested separately const logLevels = ['fatal', 'error', 'warning', 'notice', 'info']; const logLevelsNumber = [0, 1, 2, 3, 4]; // hard-coded because it is hardcoded in the logger as well and therefore can be tested to be this way, in bytes const maxLogSize = 50000; type LogLevelArbitrary = fc.Arbitrary<'fatal' | 'error' | 'warning' | 'notice' | 'info'>; const logLevelArbitrary = fc.constantFrom(...logLevels); const logLevelNumberArbitrary = fc.constantFrom(...logLevelsNumber); it('creates log files', () => { const logger: ILogger = container.get('logger'); expect(logger.getLogFile()).path('log file is not created'); expect(logger.getExceptionsLogFile()).path('exception log file is not created'); }); it('logs exceptions', async () => { const logger: ILogger = container.get('logger'); await logger.exception(new Error('this is an error')); }); it("default log file doesn't get bigger than 50KB @slow", async () => { const logger: ILogger = container.get('logger'); let prevLogFileSize = (await fs.stat(logger.getLogFile())).size; let minNumberOfLines = maxLogSize; const sizeIncreases = []; for (let i = 0; i <= minNumberOfLines; i++) { await logger.log(4, 'your waifu is trash'); const currLogFileSize = (await fs.stat(logger.getLogFile())).size; const sizeIncrease = currLogFileSize - prevLogFileSize; if (sizeIncrease) { sizeIncreases.push(sizeIncrease); minNumberOfLines = maxLogSize / (sizeIncreases.reduce((sum, e) => sum + e, 0) / sizeIncreases.length); } prevLogFileSize = currLogFileSize; } const logFileStats = await fs.stat(logger.getLogFile()); expect(logFileStats.size).lessThan(maxLogSize, 'log is bigger than its max size'); }).timeout(15000); it("exception log file doesn't get bigger than 50KB @slow", async () => { const logger: ILogger = container.get('logger'); let prevLogFileSize = (await fs.stat(logger.getExceptionsLogFile())).size; let minNumberOfLines = maxLogSize; const sizeIncreases = []; for (let i = 0; i <= minNumberOfLines; i++) { await logger.exception(new Error('your waifu is trash')); const currLogFileSize = (await fs.stat(logger.getExceptionsLogFile())).size; const sizeIncrease = currLogFileSize - prevLogFileSize; if (sizeIncrease) { sizeIncreases.push(sizeIncrease); minNumberOfLines = maxLogSize / (sizeIncreases.reduce((sum, e) => sum + e, 0) / sizeIncreases.length); } prevLogFileSize = currLogFileSize; } const logFileStats = await fs.stat(logger.getExceptionsLogFile()); expect(logFileStats.size).lessThan(maxLogSize, 'exception log is bigger than its max size'); }).timeout(15000); it('logs different levels directly', () => { const logger: ILogger = container.get('logger'); return fc.assert( fc.asyncProperty(logLevelArbitrary as LogLevelArbitrary, fc.string(), async (logLevel, message) => { await logger[logLevel](message); const lastLine = await getLastLine(getDefaultLogReadLineInterface(logger)); expect(lastLine).contains(message, 'the log line does not contain the message'); expect(lastLine).contains(logLevel, `the log line does not contain the '${logLevel}' keyword`); }), { numRuns: 50, } ); }); it('logs different levels indirectly via the generic log function', () => { const logger: ILogger = container.get('logger'); return fc.assert( fc.asyncProperty(logLevelNumberArbitrary, fc.string(), async (logLevelNumber, message) => { await logger.log(logLevelNumber, message); const lastLine = await getLastLine(getDefaultLogReadLineInterface(logger)); expect(lastLine).contains(message, 'the log line does not contain the message'); expect(lastLine).contains( logLevels[logLevelNumber], `the log line does not contain the '${logLevels[logLevelNumber]}' keyword` ); }), { numRuns: 50, } ); }); it('logs debug only in dev mode', async () => { const logger: ILogger = container.get('logger'); setDev(); await logger.debug('this is a development message'); const lastLine = await getLastLine(getDefaultLogReadLineInterface(logger)); expect(lastLine).contains('this is a development message', 'the dev log line does not contain the message'); expect(lastLine).contains('debug', `the dev log line does not contain the 'debug' keyword`); setDev(false); await logger.log(LogLevel.warning, 'this is a warning'); await logger.debug('this is a second development message, should not be here'); expect(lastLine).not.contain( 'this is a second development message, should not be here', 'debug is logged even in non-dev mode' ); }); });