jest-doctor 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,7 @@ declare const createEnvMixin: <EnvironmentConstructor extends JestDoctorConstruc
55
55
  totalDelay: number;
56
56
  };
57
57
  tearDownError?: Error;
58
+ asyncIdToPromise: Map<number, Promise<unknown>>;
58
59
  setup(): Promise<void>;
59
60
  handleEvent(event: unknown, state: unknown): Promise<void>;
60
61
  handleTestEvent: JestEnvironment["handleTestEvent"];
@@ -22,6 +22,7 @@ const getAllAfterEach_1 = __importDefault(require("./utils/getAllAfterEach"));
22
22
  const normalizeOptions_1 = __importDefault(require("./utils/normalizeOptions"));
23
23
  const getReporterTmpDir_1 = __importDefault(require("./utils/getReporterTmpDir"));
24
24
  const promise_1 = __importDefault(require("./patch/promise"));
25
+ const promiseConcurrency_1 = __importDefault(require("./patch/promiseConcurrency"));
25
26
  const createEnvMixin = (Environment) => {
26
27
  // @ts-expect-error strange ts rule where constructor arguments should be any[] where again TypeScript complains
27
28
  return class Base extends Environment {
@@ -38,6 +39,7 @@ const createEnvMixin = (Environment) => {
38
39
  testPath;
39
40
  aggregatedReport;
40
41
  tearDownError;
42
+ asyncIdToPromise = new Map();
41
43
  constructor(config, context) {
42
44
  super(config, context);
43
45
  this.testPath = context.testPath.replace(/\W/g, '_');
@@ -72,8 +74,11 @@ const createEnvMixin = (Environment) => {
72
74
  if (report.console) {
73
75
  (0, console_1.default)(this, report.console);
74
76
  }
75
- if (report.promises && report.promises.patch === 'promise') {
76
- (0, promise_1.default)(this);
77
+ if (report.promises) {
78
+ (0, promiseConcurrency_1.default)(this);
79
+ if (report.promises.patch === 'promise') {
80
+ (0, promise_1.default)(this);
81
+ }
77
82
  }
78
83
  }
79
84
  // unfortunately @jest/types doesn't export the necessary types,
@@ -33,6 +33,7 @@ declare const _default: {
33
33
  totalDelay: number;
34
34
  };
35
35
  tearDownError?: Error;
36
+ asyncIdToPromise: Map<number, Promise<unknown>>;
36
37
  setup(): Promise<void>;
37
38
  handleEvent(event: unknown, state: unknown): Promise<void>;
38
39
  handleTestEvent: import("@jest/environment").JestEnvironment["handleTestEvent"];
@@ -33,6 +33,7 @@ declare const _default: {
33
33
  totalDelay: number;
34
34
  };
35
35
  tearDownError?: Error;
36
+ asyncIdToPromise: Map<number, Promise<unknown>>;
36
37
  setup(): Promise<void>;
37
38
  handleEvent(event: unknown, state: unknown): Promise<void>;
38
39
  handleTestEvent: import("@jest/environment").JestEnvironment["handleTestEvent"];
@@ -24,7 +24,7 @@ const patchConsole = (that, consoleOptions) => {
24
24
  if (!(0, exports.isIgnored)(message, consoleOptions.ignore)) {
25
25
  that.leakRecords.get(that.currentTestName)?.console.push({
26
26
  method: consoleMethod,
27
- stack: (0, getStack_1.default)(env.console[consoleMethod]),
27
+ stack: (0, getStack_1.default)(env.console[consoleMethod], 'Console.' + consoleMethod),
28
28
  testName: that.currentTestName,
29
29
  message,
30
30
  });
@@ -30,7 +30,7 @@ const patchFakeTimers = (that) => {
30
30
  fakeTimeout?.set(timerId, {
31
31
  type: 'fakeTimeout',
32
32
  delay: delay || 0,
33
- stack: (0, getStack_1.default)(that.global.setTimeout),
33
+ stack: (0, getStack_1.default)(that.global.setTimeout, 'setTimeout'),
34
34
  testName: that.currentTestName,
35
35
  });
36
36
  return timerId;
@@ -40,7 +40,7 @@ const patchFakeTimers = (that) => {
40
40
  that.leakRecords.get(that.currentTestName)?.fakeTimers.set(intervalId, {
41
41
  type: 'fakeInterval',
42
42
  delay: delay || 0,
43
- stack: (0, getStack_1.default)(that.global.setInterval),
43
+ stack: (0, getStack_1.default)(that.global.setInterval, 'setInterval'),
44
44
  testName: that.currentTestName,
45
45
  });
46
46
  return intervalId;
@@ -17,7 +17,7 @@ const patchPromise = (that) => {
17
17
  const promiseLeaks = that.leakRecords.get(that.currentTestName)?.promises;
18
18
  promiseLeaks?.set(this, {
19
19
  testName: that.currentTestName,
20
- stack: (0, getStack_1.default)(that.global.Promise),
20
+ stack: (0, getStack_1.default)(that.global.Promise, 'Promise'),
21
21
  });
22
22
  executor((value) => {
23
23
  promiseLeaks?.delete(this);
@@ -0,0 +1,3 @@
1
+ import { JestDoctorEnvironment } from '../types';
2
+ declare const patchPromiseConcurrency: (that: JestDoctorEnvironment) => void;
3
+ export default patchPromiseConcurrency;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const patchPromiseConcurrency = (that) => {
4
+ const env = that.global;
5
+ const concurrencyFactor = (fn) => (concurrentPromises) => {
6
+ const promises = that.leakRecords.get(that.currentTestName)?.promises;
7
+ // remove all related promises is a concurrent promise ends
8
+ const removePromises = () => {
9
+ if (promises) {
10
+ concurrentPromises.forEach((racePromise) => {
11
+ promises.delete(racePromise);
12
+ });
13
+ }
14
+ };
15
+ return fn(concurrentPromises).then((returnValues) => {
16
+ removePromises();
17
+ return returnValues;
18
+ }, (error) => {
19
+ removePromises();
20
+ throw error;
21
+ });
22
+ };
23
+ env.Promise.race = concurrencyFactor(env.Promise.race.bind(env.Promise));
24
+ env.Promise.any = concurrencyFactor(env.Promise.any.bind(env.Promise));
25
+ };
26
+ exports.default = patchPromiseConcurrency;
@@ -0,0 +1 @@
1
+ declare const patchRace: () => void;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ const patchRace = () => { };
@@ -22,7 +22,7 @@ const timers = (that) => {
22
22
  leakRecord?.timers.set(timerId, {
23
23
  type: 'timeout',
24
24
  delay: delay || 0,
25
- stack: (0, getStack_1.default)(env.setTimeout),
25
+ stack: (0, getStack_1.default)(env.setTimeout, 'fake setTimeout'),
26
26
  testName: owner,
27
27
  });
28
28
  return timerId;
@@ -41,7 +41,7 @@ const timers = (that) => {
41
41
  that.leakRecords.get(owner)?.timers.set(intervalId, {
42
42
  type: 'interval',
43
43
  delay: delay || 0,
44
- stack: (0, getStack_1.default)(env.setInterval),
44
+ stack: (0, getStack_1.default)(env.setInterval, 'fake setInterval'),
45
45
  testName: that.currentTestName,
46
46
  });
47
47
  return intervalId;
package/dist/reporter.js CHANGED
@@ -15,7 +15,6 @@ const isAlive = (pid) => {
15
15
  return true;
16
16
  }
17
17
  catch (error) {
18
- console.log(error.code);
19
18
  return error.code !== 'ESRCH';
20
19
  }
21
20
  };
@@ -34,6 +34,7 @@ declare const requireEnvironment: (envName: string) => {
34
34
  totalDelay: number;
35
35
  };
36
36
  tearDownError?: Error;
37
+ asyncIdToPromise: Map<number, Promise<unknown>>;
37
38
  setup(): Promise<void>;
38
39
  handleEvent(event: unknown, state: unknown): Promise<void>;
39
40
  handleTestEvent: import("@jest/environment").JestEnvironment["handleTestEvent"];
package/dist/types.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface ConsoleRecord {
18
18
  message: string;
19
19
  }
20
20
  export interface LeakRecord {
21
- promises: Map<number | Promise<unknown>, PromiseRecord>;
21
+ promises: Map<Promise<unknown>, PromiseRecord>;
22
22
  timers: Map<NodeJS.Timeout, TimerRecord>;
23
23
  console: ConsoleRecord[];
24
24
  totalDelay: number;
@@ -58,6 +58,7 @@ export interface NormalizedOptions {
58
58
  patch: Patch;
59
59
  };
60
60
  };
61
+ verbose: boolean;
61
62
  delayThreshold: number;
62
63
  timerIsolation: TimerIsolation;
63
64
  clearTimers: boolean;
@@ -78,6 +79,7 @@ export interface RawOptions {
78
79
  fakeTimers?: OnError;
79
80
  promises?: RawPromise;
80
81
  };
82
+ verbose?: boolean;
81
83
  delayThreshold?: number;
82
84
  timerIsolation?: TimerIsolation;
83
85
  clearTimers?: boolean;
@@ -100,5 +102,6 @@ export interface JestDoctorEnvironment {
100
102
  currentAfterEachCount: number;
101
103
  options: NormalizedOptions;
102
104
  aggregatedReport: AggregatedReport;
105
+ asyncIdToPromise: Map<number, Promise<unknown>>;
103
106
  }
104
107
  export {};
@@ -7,8 +7,12 @@ const createAsyncHookCleaner = (that) => {
7
7
  const owner = that.promiseOwner.get(asyncId);
8
8
  if (!owner)
9
9
  return;
10
- that.leakRecords.get(owner)?.promises.delete(asyncId);
11
- that.promiseOwner.delete(asyncId);
10
+ const promise = that.asyncIdToPromise.get(asyncId);
11
+ if (promise) {
12
+ that.leakRecords.get(owner)?.promises.delete(promise);
13
+ that.promiseOwner.delete(asyncId);
14
+ that.asyncIdToPromise.delete(asyncId);
15
+ }
12
16
  },
13
17
  });
14
18
  };
@@ -6,13 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const node_async_hooks_1 = require("node:async_hooks");
7
7
  const getStack_1 = __importDefault(require("./getStack"));
8
8
  const createAsyncHookDetector = (that) => {
9
- const init = (asyncId, type) => {
9
+ const init = (asyncId, type, _, resource) => {
10
10
  if (type !== 'PROMISE')
11
11
  return;
12
+ const promise = resource;
12
13
  const owner = that.currentTestName;
13
14
  that.promiseOwner.set(asyncId, owner);
14
- that.leakRecords.get(owner)?.promises.set(asyncId, {
15
- stack: (0, getStack_1.default)(init),
15
+ that.asyncIdToPromise.set(asyncId, promise);
16
+ that.leakRecords.get(owner)?.promises.set(promise, {
17
+ stack: (0, getStack_1.default)(init, 'Promise'),
16
18
  testName: owner,
17
19
  });
18
20
  };
@@ -1,2 +1,2 @@
1
- declare const getStack: (stackFrom: Function) => string;
1
+ declare const getStack: (stackFrom: Function, prefix: string) => string;
2
2
  export default getStack;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const getStack = (stackFrom) => {
3
+ const getStack = (stackFrom, prefix) => {
4
4
  const error = {
5
5
  stack: '',
6
6
  };
7
7
  Error.captureStackTrace(error, stackFrom);
8
- return error.stack;
8
+ return prefix + ' ' + error.stack;
9
9
  };
10
10
  exports.default = getStack;
@@ -15,6 +15,7 @@ const DEFAULTS = {
15
15
  patch: 'async_hooks',
16
16
  },
17
17
  },
18
+ verbose: false,
18
19
  delayThreshold: 0,
19
20
  timerIsolation: 'afterEach',
20
21
  clearTimers: true,
@@ -57,6 +58,7 @@ function normalizeOptions(raw) {
57
58
  fakeTimers: report.fakeTimers ?? DEFAULTS.report.fakeTimers,
58
59
  promises: normalizePromise(report.promises),
59
60
  },
61
+ verbose: raw.verbose ?? DEFAULTS.verbose,
60
62
  delayThreshold: raw.delayThreshold ?? DEFAULTS.delayThreshold,
61
63
  timerIsolation: raw.timerIsolation ?? DEFAULTS.timerIsolation,
62
64
  clearTimers: raw.clearTimers ?? DEFAULTS.clearTimers,
@@ -33,6 +33,19 @@ const reportLeaks = (that, leakRecord) => {
33
33
  that.aggregatedReport.fakeTimers += leakRecord.fakeTimers.size;
34
34
  that.aggregatedReport.totalDelay += leakRecord.totalDelay;
35
35
  }
36
+ if (that.options.verbose) {
37
+ const messages = [];
38
+ const addLeak = (leak) => {
39
+ messages.push(leak.stack);
40
+ };
41
+ leakRecord.promises.forEach(addLeak);
42
+ leakRecord.timers.forEach(addLeak);
43
+ leakRecord.fakeTimers.forEach(addLeak);
44
+ leakRecord.console.forEach(addLeak);
45
+ if (messages.length) {
46
+ node_console_1.default.log(messages.join('\n'));
47
+ }
48
+ }
36
49
  try {
37
50
  if (leakRecord.console.length) {
38
51
  const message = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jest-doctor",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "jest environment for leak detection",
5
5
  "license": "MIT",
6
6
  "author": "Stephan Dum",