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:
parent
28c26ff258
commit
9672c9b5ed
|
@ -12,6 +12,7 @@ include:
|
|||
- 'src/**'
|
||||
exclude:
|
||||
- 'src/**/*.spec.*'
|
||||
- 'src/**/*.mock.*'
|
||||
- 'src/main/entities/**'
|
||||
watermarks:
|
||||
statements: [80, 95]
|
||||
|
|
|
@ -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:
|
||||
|
||||
- `@slow` when the test is particularly slow
|
||||
| tag | usage when |
|
||||
| ----------- | ------------------------- |
|
||||
| `@slow` | test is particularly slow |
|
||||
| `@spectron` | test uses spectron |
|
||||
|
||||
#### 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.\
|
||||
The code coverage does not work with Spectron since that runs in its own node process.
|
||||
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.
|
||||
|
||||
### Updating Dependencies
|
||||
|
||||
|
|
|
@ -847,6 +847,16 @@
|
|||
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
|
@ -1766,6 +1776,16 @@
|
|||
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
|
||||
|
@ -1921,6 +1941,15 @@
|
|||
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==",
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
|
||||
|
@ -2388,6 +2426,12 @@
|
|||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
@ -2436,6 +2480,16 @@
|
|||
"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": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
|
@ -4193,6 +4247,12 @@
|
|||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
||||
|
@ -5460,6 +5530,12 @@
|
|||
"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": {
|
||||
"version": "2.1.12",
|
||||
"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": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
|
||||
|
@ -9854,6 +9941,12 @@
|
|||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
|
@ -12075,6 +12168,12 @@
|
|||
"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": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"postinstall": "npm run rebuild",
|
||||
"start": "electron . --enable-logging --dev",
|
||||
"start": "electron . --enable-logging --env=dev",
|
||||
"rebuild": "electron-rebuild -f -b -t prod,dev,optional",
|
||||
"electron-version": "electron electron-version.js",
|
||||
"typeorm:migrate": "npm run typeorm:migrate:library && npm run typeorm:migrate:store",
|
||||
|
@ -29,7 +29,7 @@
|
|||
"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\"",
|
||||
"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",
|
||||
"coverage:fast": "nyc npm run test:fast",
|
||||
"coverage": "nyc npm run test",
|
||||
|
@ -60,6 +60,7 @@
|
|||
"@electron-forge/cli": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.52",
|
||||
"@types/chai": "^4.2.12",
|
||||
"@types/chai-fs": "^2.0.2",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/jsdom": "^16.2.3",
|
||||
"@types/minimist": "^1.2.0",
|
||||
|
@ -72,6 +73,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^3.7.0",
|
||||
"@typescript-eslint/parser": "^3.7.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-fs": "^2.0.0",
|
||||
"chokidar": "^3.4.1",
|
||||
"concurrently": "^5.2.0",
|
||||
"electron": "^9.1.1",
|
||||
|
|
|
@ -5,40 +5,36 @@ import rewiremock from 'rewiremock';
|
|||
|
||||
import 'mocha';
|
||||
import { Application } from 'spectron';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
rewiremock.disable();
|
||||
|
||||
describe('Application @slow', function () {
|
||||
describe('Application @spectron', function () {
|
||||
this.timeout(20000);
|
||||
|
||||
interface IApplicationContext extends Context {
|
||||
app: Application;
|
||||
app?: Application;
|
||||
}
|
||||
|
||||
before(function (this, done): void {
|
||||
const context = this as IApplicationContext;
|
||||
context.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)
|
||||
// spectron writes its electron files into a temporary directory, the local app installation should not be compromised
|
||||
before(function (this: IApplicationContext) {
|
||||
this.app = new Application({
|
||||
path: ((electron as unknown) as { default: string }).default,
|
||||
args: [packageJson.main],
|
||||
args: ['.', '--enable-logging'],
|
||||
});
|
||||
context.app
|
||||
.start()
|
||||
.then(() => done())
|
||||
.catch((reason) => done(reason));
|
||||
return this.app.start();
|
||||
});
|
||||
|
||||
after(function (this) {
|
||||
const context = this as IApplicationContext;
|
||||
if (context.app && context.app.isRunning()) {
|
||||
return context.app.stop();
|
||||
after(function (this: IApplicationContext) {
|
||||
if (this.app && this.app.isRunning()) {
|
||||
return this.app.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows an initial window', function (this: Context) {
|
||||
const context = this as IApplicationContext;
|
||||
return context.app.client.getWindowCount().then((count: number) => {
|
||||
it('shows an initial window', function (this: IApplicationContext) {
|
||||
if (!this.app) {
|
||||
throw Error('this.app is falsy');
|
||||
}
|
||||
return this.app.client.getWindowCount().then((count: number) => {
|
||||
expect(count).to.be.gte(1);
|
||||
});
|
||||
});
|
||||
|
|
19
src/main.ts
19
src/main.ts
|
@ -3,10 +3,27 @@ import { container } from './main/core/container';
|
|||
import './main/core/install';
|
||||
|
||||
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 { ILogger } from './main/modules/logger/i-logger';
|
||||
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> {
|
||||
const session: ISession = container.get(Symbol.for('session'));
|
||||
session.setHeaders();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
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' : ''}`);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'reflect-metadata';
|
||||
import { Container } from 'inversify';
|
||||
import { MainAppWindow } from '../modules/app-window/main-app-window';
|
||||
import { Logger } from '../modules/logger/logger';
|
||||
import { NhentaiApi } from '../modules/nhentai/nhentai-api';
|
||||
import '../modules/nhentai/nhentai-ipc-controller';
|
||||
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('session')).to(Session);
|
||||
|
||||
container.bind(Symbol.for('logger')).to(Logger);
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import minimist from 'minimist';
|
||||
|
||||
export function isDev(): boolean {
|
||||
return !!minimist(process.argv).dev;
|
||||
}
|
|
@ -3,9 +3,17 @@ import '../../../mocks/electron';
|
|||
|
||||
import { expect } from 'chai';
|
||||
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(() => {
|
||||
rewiremock.enable();
|
||||
});
|
||||
|
@ -15,9 +23,9 @@ describe('Development Mode Service', () => {
|
|||
});
|
||||
|
||||
it('correctly identifies the development process argument', () => {
|
||||
process.argv.push('--dev');
|
||||
setDev();
|
||||
expect(isDev()).to.be.true;
|
||||
process.argv = process.argv.filter((value) => value !== '--dev');
|
||||
setDev(false);
|
||||
expect(isDev()).to.be.false;
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { session } from 'electron';
|
||||
import { injectable } from 'inversify';
|
||||
import { isDev } from '../../core/dev';
|
||||
import { isDev } from '../../core/env';
|
||||
import { ISession } from './i-session';
|
||||
|
||||
@injectable()
|
||||
|
|
Loading…
Reference in New Issue