Skip to content

Commit da9cb7d

Browse files
committed
test: avoid confusing global state in regression test logger
The original code incorrectly assumed that a log4js logger had its own level. Instead, changing a logger's level changes the level of the logger's category system-wide. That leads to very confusing behavior that depends on the order in which you create projects or database adapters. We're still changing global state for the trace level, but in a direct and more understandable way.
1 parent 1d816c1 commit da9cb7d

File tree

6 files changed

+70
-23
lines changed

6 files changed

+70
-23
lines changed

spec/dev/server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import bodyParser from 'body-parser';
33
import cors from 'cors';
44
import express from 'express';
55
import { resolve } from 'path';
6-
import { ArangoDBAdapter, Project } from '../..';
6+
import { ArangoDBAdapter, ExecutionOptions, Project } from '../..';
77
import { globalContext } from '../../src/config/global';
88
import { InMemoryAdapter } from '../../src/database/inmemory';
99
import { getMetaSchema } from '../../src/meta-schema/meta-schema';
@@ -18,7 +18,7 @@ const databaseURL = 'http://root:@localhost:8529';
1818
// const databaseURL = 'http://root:@localhost:7050';
1919

2020
export async function start() {
21-
const loggerProvider = new Log4jsLoggerProvider('error');
21+
const loggerProvider = new Log4jsLoggerProvider();
2222

2323
let db;
2424
if (process.argv.includes('--db=in-memory')) {
@@ -44,7 +44,7 @@ export async function start() {
4444
);
4545
},
4646
getOperationIdentifier: ({ context }) => context as object, // each operation is executed with an unique context object
47-
getExecutionOptions: ({ context }: { context: any }) => {
47+
getExecutionOptions: ({ context }: { context: any }): ExecutionOptions => {
4848
return {
4949
authContext: { authRoles: ['allusers'] },
5050
recordTimings: true,

spec/helpers/log4js-logger-provider.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ import { getLogger } from 'log4js';
22
import { Logger, LoggerProvider } from '../../src/config/logging';
33

44
export class Log4jsLoggerProvider implements LoggerProvider {
5-
constructor(
6-
public readonly level: string,
7-
public readonly levelByCategory: { [category: string]: string } = {},
8-
) {}
9-
105
getLogger(category: string): Logger {
11-
const logger = getLogger(category);
12-
logger.level =
13-
category in this.levelByCategory ? this.levelByCategory[category] : this.level;
14-
return logger;
6+
// note: we used to change the level here, but that does not work because in log4js,
7+
// everything is global state (you're actually changing a category's level globally if you
8+
// set .level on a logger)
9+
return getLogger(category);
1510
}
1611
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Logger, LoggerProvider } from '../../src/config/logging';
2+
3+
export class WarnAndErrorLoggerProvider implements LoggerProvider {
4+
getLogger(category: string): Logger {
5+
// note: can't use log4js's getLogger because we can't set a log level on a logger
6+
// (setting .level on a logger actually changes the level globally for that category)
7+
return new WarnAndErrorLogger(category);
8+
}
9+
}
10+
11+
export class WarnAndErrorLogger implements Logger {
12+
constructor(private category?: string) {}
13+
14+
trace = () => {};
15+
debug = () => {};
16+
info = () => {};
17+
warn = (message: string, ...args: unknown[]) =>
18+
console.warn(`[WARN] ${this.category}: ${message}`, ...args);
19+
error = (message: string, ...args: unknown[]) =>
20+
console.error(`[ERROR] ${this.category}: ${message}`, ...args);
21+
fatal = (message: string, ...args: unknown[]) =>
22+
console.error(`[FATAL] ${this.category}: ${message}`, ...args);
23+
24+
level = 'warn';
25+
26+
isLevelEnabled = (level: string) => ['warn', 'error', 'fatal'].includes(level.toLowerCase());
27+
isTraceEnabled = () => false;
28+
isDebugEnabled = () => false;
29+
isInfoEnabled = () => false;
30+
isWarnEnabled = () => true;
31+
isErrorEnabled = () => true;
32+
isFatalEnabled = () => true;
33+
}

spec/performance/support/helpers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { ArangoDBAdapter } from '../../../src/database/arangodb';
66
import { Project } from '../../../src/project/project';
77
import { loadProjectFromDir } from '../../../src/project/project-from-fs';
88
import { range } from '../../../src/utils/utils';
9-
import { Log4jsLoggerProvider } from '../../helpers/log4js-logger-provider';
109
import { createTempDatabase } from '../../regression/initialization';
10+
import { WarnAndErrorLoggerProvider } from '../../helpers/warn-and-error-logger-provider';
1111

12-
// arangojs typings for this are completely broken
12+
// arangojs typings for this are completely bro ken
1313
export const aql: (template: TemplateStringsArray, ...args: ReadonlyArray<any>) => any =
1414
require('arangojs').aql;
1515

@@ -22,7 +22,7 @@ export interface TestEnvironment {
2222
}
2323

2424
const schemaContext: ProjectOptions = {
25-
loggerProvider: new Log4jsLoggerProvider('warn'),
25+
loggerProvider: new WarnAndErrorLoggerProvider(),
2626
getExecutionOptions: ({}) => ({ authContext: { authRoles: ['admin'] } }),
2727
};
2828

spec/regression/regression-suite.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { createTempDatabase, initTestData, TestDataEnvironment } from './initial
1313
import { ErrorWithCause } from '../../src/utils/error-with-cause';
1414
import { InitTestDataContext } from './init-test-data-context';
1515
import deepEqual = require('deep-equal');
16+
import { WarnAndErrorLoggerProvider } from '../helpers/warn-and-error-logger-provider';
17+
import { getLogger } from 'log4js';
1618

1719
interface TestResult {
1820
readonly actualResult: any;
@@ -23,7 +25,6 @@ type DatabaseSpecifier = 'arangodb' | 'in-memory';
2325

2426
export interface RegressionSuiteOptions {
2527
readonly saveActualAsExpected?: boolean;
26-
readonly trace?: boolean;
2728
readonly database?: DatabaseSpecifier;
2829
}
2930

@@ -105,20 +106,19 @@ export class RegressionSuite {
105106
};
106107
const warnLevelOptions = {
107108
...generalOptions,
108-
loggerProvider: new Log4jsLoggerProvider('warn'),
109+
loggerProvider: new WarnAndErrorLoggerProvider(),
109110
};
110111
const debugLevelOptions = {
111112
...generalOptions,
112-
loggerProvider: new Log4jsLoggerProvider(this.options.trace ? 'trace' : 'warn', {
113-
'schema-builder': 'warn',
114-
}),
113+
loggerProvider: new Log4jsLoggerProvider(),
115114
};
116115

117116
// use a schema that logs less for initTestData and for schema migrations
118117
// the init db adapter also has a higher query memory limit
119118
const initProject = await loadProjectFromDir(resolve(this.path, 'model'), warnLevelOptions);
120119
const initAdapter = await this.createAdapter(warnLevelOptions, { isInitSchema: true });
121120
const initSchema = initProject.createSchema(initAdapter);
121+
const initTestDataContext = new InitTestDataContext(initSchema);
122122

123123
const project = await loadProjectFromDir(resolve(this.path, 'model'), debugLevelOptions);
124124
const adapter = await this.createAdapter(debugLevelOptions);
@@ -128,7 +128,6 @@ export class RegressionSuite {
128128

129129
const testDataJsonPath = resolve(this.path, 'test-data.json');
130130
const testDataTsPath = resolve(this.path, 'test-data.ts');
131-
const initTestDataContext = new InitTestDataContext(initSchema);
132131
if (existsSync(testDataJsonPath)) {
133132
this.testDataEnvironment = await initTestData(
134133
resolve(this.path, 'test-data.json'),
@@ -160,7 +159,7 @@ export class RegressionSuite {
160159
}
161160

162161
if (this.databaseSpecifier === 'arangodb') {
163-
const version = await (adapter as ArangoDBAdapter).getArangoDBVersion();
162+
const version = await (initAdapter as ArangoDBAdapter).getArangoDBVersion();
164163
if (version) {
165164
this.databaseVersion = `${version.major}.${version.minor}`;
166165
}

spec/regression/regressions.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readdirSync, statSync } from 'fs';
33
import { resolve } from 'path';
44
import { likePatternToRegExp } from '../../src/database/like-helpers';
55
import { RegressionSuite, RegressionSuiteOptions } from './regression-suite';
6+
import { getLogger } from 'log4js';
67

78
const regressionRootDir = __dirname;
89

@@ -28,6 +29,26 @@ describe('regression tests', async () => {
2829
testNameFilter = (name) => !!name.match(regex);
2930
}
3031

32+
// log levels can only bet set globally in log4js, so we're doing it here
33+
const trace = process.argv.includes('--log-trace');
34+
const traceLogNames = ['ArangoDBAdapter', 'InMemoryAdapter', 'query-resolvers'];
35+
const traceLoggers = traceLogNames.map((name) => getLogger(name));
36+
const previousLevels = traceLoggers.map((logger) => logger.level);
37+
beforeEach(async () => {
38+
if (trace) {
39+
for (const logger of traceLoggers) {
40+
logger.level = 'trace';
41+
}
42+
}
43+
});
44+
afterEach(async () => {
45+
if (trace) {
46+
for (let i = 0; i < traceLoggers.length; i++) {
47+
traceLoggers[i].level = previousLevels[i];
48+
}
49+
}
50+
});
51+
3152
for (const database of databases) {
3253
describe(`for ${database}`, async () => {
3354
for (const suiteName of dirs) {
@@ -36,7 +57,6 @@ describe('regression tests', async () => {
3657
// (first npm test run still marked as failure, subsequent runs will pass)
3758
const options: RegressionSuiteOptions = {
3859
saveActualAsExpected: process.argv.includes('--save-actual-as-expected'),
39-
trace: process.argv.includes('--log-trace'),
4060
database,
4161
};
4262
const suite = new RegressionSuite(suitePath, options);

0 commit comments

Comments
 (0)