feat: implement a logger service and make it log uncaught exceptions, make unhandled rejections throw an exception, fix spectron test

This commit is contained in:
Xymorot 2020-08-05 22:48:00 +02:00
parent 28c26ff258
commit 9672c9b5ed
17 changed files with 572 additions and 36 deletions

View File

@ -12,6 +12,7 @@ include:
- 'src/**' - 'src/**'
exclude: exclude:
- 'src/**/*.spec.*' - 'src/**/*.spec.*'
- 'src/**/*.mock.*'
- 'src/main/entities/**' - 'src/main/entities/**'
watermarks: watermarks:
statements: [80, 95] statements: [80, 95]

View File

@ -160,12 +160,14 @@ There are 2 ways in which mocks are defined/used:
Mocha does [not have a separate tagging feature](https://github.com/mochajs/mocha/wiki/Tagging), but it can filter via title. Use the following tags in your test titles: Mocha does [not have a separate tagging feature](https://github.com/mochajs/mocha/wiki/Tagging), but it can filter via title. Use the following tags in your test titles:
- `@slow` when the test is particularly slow | tag | usage when |
| ----------- | ------------------------- |
| `@slow` | test is particularly slow |
| `@spectron` | test uses spectron |
#### Coverage #### Coverage
Code coverage is provided by [nyc](https://github.com/istanbuljs/nyc). The detailed code coverage can be found under `.nyc_output/coverage/index.html` after running the coverage script `npm run coverage` (open in browser). The coverage script is separate because it does not allow simple debugging.\ Code coverage is provided by [nyc](https://github.com/istanbuljs/nyc). The detailed code coverage can be found under `.nyc_output/coverage/index.html` (open in browser) after running the coverage script `npm run coverage`. The coverage script is separate because it does not allow simple debugging.
The code coverage does not work with Spectron since that runs in its own node process.
### Updating Dependencies ### Updating Dependencies

99
package-lock.json generated
View File

@ -847,6 +847,16 @@
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==", "integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==",
"dev": true "dev": true
}, },
"@types/chai-fs": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/chai-fs/-/chai-fs-2.0.2.tgz",
"integrity": "sha512-nS385nRPNvi9UjSUCDE7f84IXZnIzooPh7Ky88kdRfSFk72juvQENqrqyrKkJeXJsN9bMG9/5zVGr06GcBRB5g==",
"dev": true,
"requires": {
"@types/chai": "*",
"@types/node": "*"
}
},
"@types/color-name": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@ -1766,6 +1776,16 @@
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
"dev": true "dev": true
}, },
"array-events": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/array-events/-/array-events-0.2.0.tgz",
"integrity": "sha1-/0KsU+ZvSF1viDI0wyJSvCKGEw4=",
"dev": true,
"requires": {
"async-arrays": "*",
"extended-emitter": "*"
}
},
"array-find-index": { "array-find-index": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
@ -1921,6 +1941,15 @@
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==",
"dev": true "dev": true
}, },
"async-arrays": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-arrays/-/async-arrays-1.0.1.tgz",
"integrity": "sha1-NHrytw8qeldnotVnnMQrvxwiD9k=",
"dev": true,
"requires": {
"sift": "*"
}
},
"async-each": { "async-each": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
@ -2055,6 +2084,15 @@
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
"dev": true "dev": true
}, },
"bit-mask": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bit-mask/-/bit-mask-1.0.2.tgz",
"integrity": "sha512-UGtq08LSiazxL4zVmBzrhdCWnT4RWx3JhhD/3crhfv8xxjnVHxf/WoVjEstjSUaZeZRP7kZrWNqup1VvUClCaQ==",
"dev": true,
"requires": {
"array-events": "^0.2.0"
}
},
"bl": { "bl": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
@ -2388,6 +2426,12 @@
"write-file-atomic": "^3.0.0" "write-file-atomic": "^3.0.0"
} }
}, },
"call-me-maybe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
"integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
"dev": true
},
"callsites": { "callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2436,6 +2480,16 @@
"type-detect": "^4.0.5" "type-detect": "^4.0.5"
} }
}, },
"chai-fs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chai-fs/-/chai-fs-2.0.0.tgz",
"integrity": "sha1-Na4Dn7uwcQ9RIqrhf6uh6PQRB8Y=",
"dev": true,
"requires": {
"bit-mask": "^1.0.1",
"readdir-enhanced": "^1.4.0"
}
},
"chalk": { "chalk": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -4193,6 +4247,12 @@
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true "dev": true
}, },
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"dev": true
},
"escape-string-regexp": { "escape-string-regexp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -4795,6 +4855,16 @@
} }
} }
}, },
"extended-emitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/extended-emitter/-/extended-emitter-1.0.4.tgz",
"integrity": "sha512-QBGuIo+pCXnYNeLUObaH/IKrCrzWzm4KhQNvA/mwNTs7/wzFylmA765zxh0WwWqpX1skQGXvzcRMHScc87Om/g==",
"dev": true,
"requires": {
"sift": "*",
"wolfy87-eventemitter": "*"
}
},
"external-editor": { "external-editor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@ -5460,6 +5530,12 @@
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
} }
}, },
"glob-to-regexp": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz",
"integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
"dev": true
},
"global-agent": { "global-agent": {
"version": "2.1.12", "version": "2.1.12",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.12.tgz", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.12.tgz",
@ -9336,6 +9412,17 @@
} }
} }
}, },
"readdir-enhanced": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/readdir-enhanced/-/readdir-enhanced-1.5.2.tgz",
"integrity": "sha1-YUYwSGkKxqRVt1ti+nioj43IXlM=",
"dev": true,
"requires": {
"call-me-maybe": "^1.0.1",
"es6-promise": "^4.1.0",
"glob-to-regexp": "^0.3.0"
}
},
"readdirp": { "readdirp": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
@ -9854,6 +9941,12 @@
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true "dev": true
}, },
"sift": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/sift/-/sift-13.2.0.tgz",
"integrity": "sha512-tngIupMS8j5oxmd+dwAUMwX1NkHTkYTJFin9qf8hIZV1Euz5p4iclRDmoEIrEI6OsNdVswORbKOe9P6XZgco0A==",
"dev": true
},
"signal-exit": { "signal-exit": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
@ -12075,6 +12168,12 @@
"wipe-node-cache": "^2.1.0" "wipe-node-cache": "^2.1.0"
} }
}, },
"wolfy87-eventemitter": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz",
"integrity": "sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==",
"dev": true
},
"word-wrap": { "word-wrap": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

View File

@ -15,7 +15,7 @@
}, },
"scripts": { "scripts": {
"postinstall": "npm run rebuild", "postinstall": "npm run rebuild",
"start": "electron . --enable-logging --dev", "start": "electron . --enable-logging --env=dev",
"rebuild": "electron-rebuild -f -b -t prod,dev,optional", "rebuild": "electron-rebuild -f -b -t prod,dev,optional",
"electron-version": "electron electron-version.js", "electron-version": "electron electron-version.js",
"typeorm:migrate": "npm run typeorm:migrate:library && npm run typeorm:migrate:store", "typeorm:migrate": "npm run typeorm:migrate:library && npm run typeorm:migrate:store",
@ -29,7 +29,7 @@
"watch:ts": "tsc -w --pretty --preserveWatchOutput", "watch:ts": "tsc -w --pretty --preserveWatchOutput",
"build": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run build:webpack\" \"npm run build:index\" \"npm run build:ts\"", "build": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run build:webpack\" \"npm run build:index\" \"npm run build:ts\"",
"watch": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run watch:webpack\" \"npm run watch:index\" \"npm run watch:ts\"", "watch": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run watch:webpack\" \"npm run watch:index\" \"npm run watch:ts\"",
"test:fast": "mocha --grep @slow --invert", "test:fast": "mocha --grep \"@(slow|spectron)\" --invert",
"test": "mocha", "test": "mocha",
"coverage:fast": "nyc npm run test:fast", "coverage:fast": "nyc npm run test:fast",
"coverage": "nyc npm run test", "coverage": "nyc npm run test",
@ -60,6 +60,7 @@
"@electron-forge/cli": "^6.0.0-beta.52", "@electron-forge/cli": "^6.0.0-beta.52",
"@electron-forge/maker-squirrel": "^6.0.0-beta.52", "@electron-forge/maker-squirrel": "^6.0.0-beta.52",
"@types/chai": "^4.2.12", "@types/chai": "^4.2.12",
"@types/chai-fs": "^2.0.2",
"@types/fs-extra": "^9.0.1", "@types/fs-extra": "^9.0.1",
"@types/jsdom": "^16.2.3", "@types/jsdom": "^16.2.3",
"@types/minimist": "^1.2.0", "@types/minimist": "^1.2.0",
@ -72,6 +73,7 @@
"@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0", "@typescript-eslint/parser": "^3.7.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-fs": "^2.0.0",
"chokidar": "^3.4.1", "chokidar": "^3.4.1",
"concurrently": "^5.2.0", "concurrently": "^5.2.0",
"electron": "^9.1.1", "electron": "^9.1.1",

View File

@ -5,40 +5,36 @@ import rewiremock from 'rewiremock';
import 'mocha'; import 'mocha';
import { Application } from 'spectron'; import { Application } from 'spectron';
import packageJson from '../package.json';
rewiremock.disable(); rewiremock.disable();
describe('Application @slow', function () { describe('Application @spectron', function () {
this.timeout(20000); this.timeout(20000);
interface IApplicationContext extends Context { interface IApplicationContext extends Context {
app: Application; app?: Application;
} }
before(function (this, done): void { // spectron writes its electron files into a temporary directory, the local app installation should not be compromised
const context = this as IApplicationContext; before(function (this: IApplicationContext) {
context.app = new Application({ this.app = new Application({
// @ts-ignore this does give the path to electron executable when this script is running outside of electron (which it does in the test files)
path: ((electron as unknown) as { default: string }).default, path: ((electron as unknown) as { default: string }).default,
args: [packageJson.main], args: ['.', '--enable-logging'],
}); });
context.app return this.app.start();
.start()
.then(() => done())
.catch((reason) => done(reason));
}); });
after(function (this) { after(function (this: IApplicationContext) {
const context = this as IApplicationContext; if (this.app && this.app.isRunning()) {
if (context.app && context.app.isRunning()) { return this.app.stop();
return context.app.stop();
} }
}); });
it('shows an initial window', function (this: Context) { it('shows an initial window', function (this: IApplicationContext) {
const context = this as IApplicationContext; if (!this.app) {
return context.app.client.getWindowCount().then((count: number) => { throw Error('this.app is falsy');
}
return this.app.client.getWindowCount().then((count: number) => {
expect(count).to.be.gte(1); expect(count).to.be.gte(1);
}); });
}); });

View File

@ -3,10 +3,27 @@ import { container } from './main/core/container';
import './main/core/install'; import './main/core/install';
import { app } from 'electron'; import { app } from 'electron';
import { isDev } from './main/core/dev'; import { isDev } from './main/core/env';
import { IAppWindow } from './main/modules/app-window/i-app-window'; import { IAppWindow } from './main/modules/app-window/i-app-window';
import { ILogger } from './main/modules/logger/i-logger';
import { ISession } from './main/modules/session/i-session'; import { ISession } from './main/modules/session/i-session';
/**
* have a read: https://github.com/nodejs/node/issues/20392, over 100 comments as of 2020-07-26
* https://nodejs.org/api/process.html#process_event_unhandledrejection
*/
process.on('unhandledRejection', (reason) => {
const logger: ILogger = container.get(Symbol.for('logger'));
void logger.fatal(`Unhandled Rejection, see ${logger.getExceptionsLogFile()}`);
throw reason;
});
process.on('uncaughtException', (error) => {
const logger: ILogger = container.get(Symbol.for('logger'));
void logger.exception(error);
throw error;
});
async function createWindow(): Promise<void> { async function createWindow(): Promise<void> {
const session: ISession = container.get(Symbol.for('session')); const session: ISession = container.get(Symbol.for('session'));
session.setHeaders(); session.setHeaders();

View File

@ -1,6 +1,6 @@
import { app } from 'electron'; import { app } from 'electron';
import path from 'path'; import path from 'path';
import packageJson from '../../../package.json'; import packageJson from '../../../package.json';
import { isDev } from './dev'; import { isDev } from './env';
export const appPath = path.resolve(app.getPath('userData'), `${packageJson.version}${isDev() ? '-dev' : ''}`); export const appPath = path.resolve(app.getPath('userData'), `${packageJson.version}${isDev() ? '-dev' : ''}`);

View File

@ -1,6 +1,7 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { Container } from 'inversify'; import { Container } from 'inversify';
import { MainAppWindow } from '../modules/app-window/main-app-window'; import { MainAppWindow } from '../modules/app-window/main-app-window';
import { Logger } from '../modules/logger/logger';
import { NhentaiApi } from '../modules/nhentai/nhentai-api'; import { NhentaiApi } from '../modules/nhentai/nhentai-api';
import '../modules/nhentai/nhentai-ipc-controller'; import '../modules/nhentai/nhentai-ipc-controller';
import { Session } from '../modules/session/session'; import { Session } from '../modules/session/session';
@ -18,3 +19,5 @@ container.bind(Symbol.for('nhentai-api')).to(NhentaiApi);
container.bind(Symbol.for('app-window-main')).to(MainAppWindow); container.bind(Symbol.for('app-window-main')).to(MainAppWindow);
container.bind(Symbol.for('session')).to(Session); container.bind(Symbol.for('session')).to(Session);
container.bind(Symbol.for('logger')).to(Logger);

View File

@ -1,5 +0,0 @@
import minimist from 'minimist';
export function isDev(): boolean {
return !!minimist(process.argv).dev;
}

View File

@ -3,9 +3,17 @@ import '../../../mocks/electron';
import { expect } from 'chai'; import { expect } from 'chai';
import 'mocha'; import 'mocha';
import { isDev } from './dev'; import { isDev } from './env';
describe('Development Mode Service', () => { export function setDev(dev = true): void {
if (dev) {
process.argv.push('--env=dev');
} else {
process.argv = process.argv.filter((value) => value !== '--env=dev');
}
}
describe('Environment Service', () => {
before(() => { before(() => {
rewiremock.enable(); rewiremock.enable();
}); });
@ -15,9 +23,9 @@ describe('Development Mode Service', () => {
}); });
it('correctly identifies the development process argument', () => { it('correctly identifies the development process argument', () => {
process.argv.push('--dev'); setDev();
expect(isDev()).to.be.true; expect(isDev()).to.be.true;
process.argv = process.argv.filter((value) => value !== '--dev'); setDev(false);
expect(isDev()).to.be.false; expect(isDev()).to.be.false;
}); });
}); });

20
src/main/core/env.ts Normal file
View File

@ -0,0 +1,20 @@
import minimist from 'minimist';
const enum Environment {
DEV = 'dev',
PROD = 'prod',
}
function getEnv(): Environment {
switch (minimist(process.argv).env) {
case 'd':
case 'dev':
return Environment.DEV;
default:
return Environment.PROD;
}
}
export function isDev(): boolean {
return getEnv() === Environment.DEV;
}

View File

@ -0,0 +1,63 @@
/**
* A Logger provides methods to save a developer message somewhere it can be retrieved
* @see ILogger.getLogFile
*/
export interface ILogger {
/**
* default logging method, the logging level needs to be specified
* @see LogLevel
*/
log(level: number, message: string): Promise<void>;
/**
* log with level 'fatal'
* @see LogLevel.fatal
*/
fatal(message: string): Promise<void>;
/**
* log with level 'error'
* @see LogLevel.error
*/
error(message: string): Promise<void>;
/**
* log with level 'warning'
* @see LogLevel.warning
*/
warning(message: string): Promise<void>;
/**
* log with level 'notice'
* @see LogLevel.notice
*/
notice(message: string): Promise<void>;
/**
* log with level 'info'
* @see LogLevel.info
*/
info(message: string): Promise<void>;
/**
* log with level 'debug'
* @see LogLevel.debug
*/
debug(message: string): Promise<void>;
/**
* logs the error, preferably with stack trace
* @see ILogger.getExceptionsLogFile
*/
exception(error: Error): Promise<void>;
/**
* @return path to the default log file
*/
getLogFile(): string;
/**
* @return path to the exceptions log file
*/
getExceptionsLogFile(): string;
}

View File

@ -0,0 +1,34 @@
/**
* This enum contains the different logging levels of this application
*/
export enum LogLevel {
/**
* something so horrible happened that the application is about to bite the dust
*/
fatal,
/**
* something happened which should not have happened, but is recoverable
*/
error,
/**
* something bad happened, as it be sometimes
*/
warning,
/**
* the developer wants to say something important, not necessarily bad
*/
notice,
/**
* information only for the interested
*/
info,
/**
* meant for development purposes
*/
debug,
}

View File

@ -0,0 +1,48 @@
import { injectable } from 'inversify';
import { ILogger } from './i-logger';
/**
* Mock of a logger, does not log anywhere
*/
@injectable()
export class LoggerMock implements ILogger {
public getLogFile(): string {
return '';
}
public getExceptionsLogFile(): string {
return '';
}
public log(): Promise<void> {
return Promise.resolve();
}
public fatal(): Promise<void> {
return this.log();
}
public error(): Promise<void> {
return this.log();
}
public warning(): Promise<void> {
return this.log();
}
public notice(): Promise<void> {
return this.log();
}
public info(): Promise<void> {
return this.log();
}
public debug(): Promise<void> {
return this.log();
}
public exception(): Promise<void> {
return Promise.resolve();
}
}

View File

@ -0,0 +1,141 @@
import rewiremock from 'rewiremock';
import '../../../../mocks/electron';
import chai, { expect } from 'chai';
import 'mocha';
import { container } from '../../core/container';
import { setDev } from '../../core/env.spec';
import { ILogger } from './i-logger';
import fs from 'fs-extra';
import { createInterface, Interface } from 'readline';
import fc from 'fast-check';
import { LogLevel } from './log-level';
import chaiFs from 'chai-fs';
chai.use(chaiFs);
describe('Logger Service', () => {
function getLastLine(rl: Interface): Promise<string> {
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 byte
const maxLogSize = 50000;
type LogLevelArbitrary = fc.Arbitrary<'fatal' | 'error' | 'warning' | 'notice' | 'info'>;
const logLevelArbitrary = fc.constantFrom(...logLevels);
const logLevelNumberArbitrary = fc.constantFrom(...logLevelsNumber);
before(() => {
rewiremock.enable();
});
after(() => {
rewiremock.disable();
});
it('creates log files', () => {
const logger: ILogger = container.get(Symbol.for('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(Symbol.for('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(Symbol.for('logger'));
for (let i = 0; i < maxLogSize; i++) {
await logger.log(4, 'your waifu is trash');
}
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(Symbol.for('logger'));
for (let i = 0; i < maxLogSize; ++i) {
await logger.exception(new Error('your waifu is trash'));
}
const logFileStats = await fs.stat(logger.getLogFile());
expect(logFileStats.size).lessThan(maxLogSize, 'log is bigger than its max size');
}).timeout(15000);
it('logs different levels directly', () => {
const logger: ILogger = container.get(Symbol.for('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(Symbol.for('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(Symbol.for('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'
);
});
});

View File

@ -0,0 +1,107 @@
import path from 'path';
import { Readable } from 'stream';
import * as fs from 'fs-extra';
import { injectable } from 'inversify';
import { appPath } from '../../core/app-path';
import { isDev } from '../../core/env';
import { ILogger } from './i-logger';
import { LogLevel } from './log-level';
const loggingDir = path.resolve(appPath, 'logs');
const logFile = path.resolve(loggingDir, 'default.log');
const exceptionLogFile = path.resolve(loggingDir, 'exception.log');
const maxLogSize = 50000;
/**
* A logger using winston to log to files in the appPath.
*/
@injectable()
export class Logger implements ILogger {
public constructor() {
fs.createFileSync(logFile);
fs.createFileSync(exceptionLogFile);
}
private static writeStream(stream: NodeJS.ReadableStream, filePath: string): Promise<void> {
return new Promise((resolve) => {
let buffer = fs.readFileSync(filePath, {
flag: 'r',
});
stream.on('data', (chunk) => {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
});
stream.on('end', () => {
const diff = buffer.byteLength - maxLogSize;
if (diff > 0) {
buffer = buffer.slice(diff);
const firstLineBreakIndex = buffer.findIndex((value) => String.fromCharCode(value) === '\n');
buffer = buffer.slice(firstLineBreakIndex + 1);
}
fs.writeFileSync(filePath, buffer);
resolve();
});
});
}
private static writeLine(line: string): Promise<void> {
return Logger.writeStream(Readable.from(`${line}\n`), logFile);
}
private static formatLine(level: LogLevel, message: string): string {
return `[${new Date().toISOString()}] ${LogLevel[level]}: ${message}`;
}
public getLogFile(): string {
return logFile;
}
public getExceptionsLogFile(): string {
return exceptionLogFile;
}
public exception(error: Error): Promise<void> {
return Logger.writeStream(
Readable.from(
`[${new Date().toISOString()}] ${(error as NodeJS.ErrnoException).code ?? 'Error'}: ${error.message}
${error.stack ?? 'no stack trace'}\n`
),
exceptionLogFile
);
}
public log(level: LogLevel, message: string): Promise<void> {
if (!isDev() && level === LogLevel.debug) {
return Promise.resolve();
}
return Logger.writeLine(Logger.formatLine(level, message)).catch((error) => {
// eslint-disable-next-line no-console -- I don't want logging to be a source of a fatal error, but i want to log it somewhere
console.error(error);
});
}
public fatal(message: string): Promise<void> {
return this.log(LogLevel.fatal, message);
}
public error(message: string): Promise<void> {
return this.log(LogLevel.error, message);
}
public warning(message: string): Promise<void> {
return this.log(LogLevel.warning, message);
}
public notice(message: string): Promise<void> {
return this.log(LogLevel.notice, message);
}
public info(message: string): Promise<void> {
return this.log(LogLevel.info, message);
}
public debug(message: string): Promise<void> {
return this.log(LogLevel.debug, message);
}
}

View File

@ -1,6 +1,6 @@
import { session } from 'electron'; import { session } from 'electron';
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { isDev } from '../../core/dev'; import { isDev } from '../../core/env';
import { ISession } from './i-session'; import { ISession } from './i-session';
@injectable() @injectable()