import { expect, spy } from 'chai'; import fs from 'fs'; import sinon from 'sinon'; import { it, describe, before, beforeEach, after } from 'mocha'; import { Logger } from '../src/shared/logging'; import { backupLogFile, rotateOrDeleteFile } from '../src/main/logging'; import { LogLevel } from '../src/shared/logging-types'; const aPath = 'log-directory/a.log'; const oldAPath = 'log-directory/a.old.log'; const bPath = 'log-directory/b.log'; const oldBPath = 'log-directory/b.old.log'; const initialFileState = { [aPath]: 'a', [bPath]: 'b', [oldBPath]: 'old b', }; describe('Logging', () => { let files: Record; let sandbox: sinon.SinonSandbox; before(() => { sandbox = sinon.createSandbox(); sandbox.stub(fs, 'accessSync').callsFake((path) => { if (files[path as string] === undefined) { throw Error('File not found'); } }); sandbox.stub(fs, 'renameSync').callsFake((oldPath, newPath) => { files[newPath as string] = files[oldPath as string]; fs.unlinkSync(oldPath); }); sandbox.stub(fs, 'unlinkSync').callsFake((path) => { delete files[path as string]; }); sandbox.stub(fs, 'readFileSync').callsFake((path) => { return files[path as string] ?? ''; }); }); after(() => { sandbox.restore(); }); beforeEach(() => { files = { ...initialFileState }; }); it('should backup log file', () => { backupLogFile(aPath); const oldA = fs.readFileSync(oldAPath).toString(); expect(fs.accessSync.bind(null, aPath)).to.throw(); expect(oldA).to.equal(initialFileState[aPath]); }); it('should replace backup file', () => { backupLogFile(bPath); const oldB = fs.readFileSync(oldBPath).toString(); expect(fs.accessSync.bind(null, bPath)).to.throw(); expect(oldB).to.equal(initialFileState[bPath]); }); it('should clean up old log files', () => { rotateOrDeleteFile(bPath); const oldB = fs.readFileSync(oldBPath).toString(); expect(fs.accessSync.bind(null, bPath)).to.throw(); expect(oldB).to.equal(initialFileState[bPath]); rotateOrDeleteFile(bPath); expect(fs.accessSync.bind(null, bPath)).to.throw(); expect(fs.accessSync.bind(null, oldBPath)).to.throw(); }); it('should only log for the correct log level', () => { const logger = new Logger(); const errorSpy = spy(); const warningSpy = spy(); const infoSpy = spy(); const verboseSpy = spy(); const debugSpy = spy(); logger.addOutput({ level: LogLevel.error, write: errorSpy }); logger.addOutput({ level: LogLevel.warning, write: warningSpy }); logger.addOutput({ level: LogLevel.info, write: infoSpy }); logger.addOutput({ level: LogLevel.verbose, write: verboseSpy }); logger.addOutput({ level: LogLevel.debug, write: debugSpy }); logger.error(); logger.warn(); logger.info(); logger.verbose(); logger.debug(); expect(errorSpy).to.have.been.called.exactly(1); expect(warningSpy).to.have.been.called.exactly(2); expect(infoSpy).to.have.been.called.exactly(3); expect(verboseSpy).to.have.been.called.exactly(4); expect(debugSpy).to.have.been.called.exactly(5); }); it('should format JavaScript types correctly', async () => { const logger = new Logger(); const promise = new Promise((resolve, _reject) => { logger.addOutput({ level: LogLevel.info, write: (_, message) => resolve(message.replace(/^.*\[info\] /, '')), }); }); logger.info('zero', 'one two', 3, [4, 5, 'six', { seven: 'eight' }], { nine: 10, eleven: [true], }); await expect(promise).to.eventually.be.fulfilled.then((result) => { expect(result).to.equal( 'zero one two 3 [4,5,"six",{"seven":"eight"}] {"nine":10,"eleven":[true]}', ); }); }); });