-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Detect memory leaks #4895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Detect memory leaks #4895
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,23 @@ export default class TestScheduler { | |
| }); | ||
| return Promise.resolve(); | ||
| } | ||
| if (testResult.leaks) { | ||
| const message = [ | ||
| 'Your test suite is leaking memory! Please ensure all references are cleaned.', | ||
| '', | ||
| 'There is a number of things that can leak memory:', | ||
| ' - Async operations that have not finished (e.g. fs.readFile).', | ||
| ' - Timers not properly mocked (e.g. setInterval, setTimeout).', | ||
| ' - Missed global listeners (e.g. process.on).', | ||
|
||
| ' - Keeping references to the global scope.', | ||
| ].join('\n'); | ||
|
||
|
|
||
| await onFailure(test, { | ||
| message, | ||
| stack: new Error(message).stack, | ||
| }); | ||
| return Promise.resolve(); | ||
|
||
| } | ||
| addResult(aggregatedResults, testResult); | ||
| await this._dispatcher.onTestResult(test, testResult, aggregatedResults); | ||
| return this._bailIfNeeded(contexts, aggregatedResults, watcher); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,29 +23,32 @@ import { | |
| setGlobal, | ||
| } from 'jest-util'; | ||
| import jasmine2 from 'jest-jasmine2'; | ||
| import LeakDetector from 'jest-leak-detector'; | ||
| import {getTestEnvironment} from 'jest-config'; | ||
| import * as docblock from 'jest-docblock'; | ||
|
|
||
| // The default jest-runner is required because it is the default test runner | ||
| // and required implicitly through the `testRunner` ProjectConfig option. | ||
| jasmine2; | ||
|
|
||
| export default (async function runTest( | ||
| // Keeping the core of "runTest" as a separate function (as "runTestHelper") is | ||
| // key to be able to detect memory leaks. Since all variables are local to the | ||
| // function, when "runTestHelper" finishes its execution, they can all be freed, | ||
| // UNLESS someone else is leaking them (and that's why we can detect the leak!). | ||
|
||
| // | ||
| // If we had all the code in a single function, we should manually nullify all | ||
| // references to verify if there is a leak, which is not maintainable and error | ||
| // prone. That's why "runTestHelper" CANNOT be inlined inside "runTest". | ||
| async function runTestHelper( | ||
|
||
| path: Path, | ||
| globalConfig: GlobalConfig, | ||
| config: ProjectConfig, | ||
| resolver: Resolver, | ||
| ) { | ||
| let testSource; | ||
|
|
||
| try { | ||
| testSource = fs.readFileSync(path, 'utf8'); | ||
| } catch (e) { | ||
| return Promise.reject(e); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Such clowny code.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function wasn't async at the point it was written 🙂 |
||
| } | ||
|
|
||
| ): Promise<TestResult> { | ||
| const testSource = fs.readFileSync(path, 'utf8'); | ||
| const parsedDocblock = docblock.parse(docblock.extract(testSource)); | ||
| const customEnvironment = parsedDocblock['jest-environment']; | ||
|
|
||
| let testEnvironment = config.testEnvironment; | ||
|
|
||
| if (customEnvironment) { | ||
|
|
@@ -66,6 +69,10 @@ export default (async function runTest( | |
| >); | ||
|
|
||
| const environment = new TestEnvironment(config); | ||
| const environmentLeakDetector = config.detectLeaks | ||
|
||
| ? new LeakDetector(environment) | ||
| : false; | ||
|
||
|
|
||
| const consoleOut = globalConfig.useStderr ? process.stderr : process.stdout; | ||
| const consoleFormatter = (type, message) => | ||
| getConsoleOutput( | ||
|
|
@@ -76,24 +83,25 @@ export default (async function runTest( | |
| ); | ||
|
|
||
| let testConsole; | ||
|
|
||
| if (globalConfig.silent) { | ||
| testConsole = new NullConsole(consoleOut, process.stderr, consoleFormatter); | ||
| } else if (globalConfig.verbose) { | ||
| testConsole = new Console(consoleOut, process.stderr, consoleFormatter); | ||
| } else { | ||
| if (globalConfig.verbose) { | ||
| testConsole = new Console(consoleOut, process.stderr, consoleFormatter); | ||
| } else { | ||
| testConsole = new BufferedConsole(); | ||
| } | ||
| testConsole = new BufferedConsole(); | ||
| } | ||
|
|
||
| const cacheFS = {[path]: testSource}; | ||
| setGlobal(environment.global, 'console', testConsole); | ||
|
|
||
| const runtime = new Runtime(config, environment, resolver, cacheFS, { | ||
| collectCoverage: globalConfig.collectCoverage, | ||
| collectCoverageFrom: globalConfig.collectCoverageFrom, | ||
| collectCoverageOnlyFrom: globalConfig.collectCoverageOnlyFrom, | ||
| mapCoverage: globalConfig.mapCoverage, | ||
| }); | ||
|
|
||
| const start = Date.now(); | ||
| await environment.setup(); | ||
| try { | ||
|
|
@@ -106,22 +114,49 @@ export default (async function runTest( | |
| ); | ||
| const testCount = | ||
| result.numPassingTests + result.numFailingTests + result.numPendingTests; | ||
|
|
||
| result.leaks = environmentLeakDetector; | ||
| result.perfStats = {end: Date.now(), start}; | ||
| result.testFilePath = path; | ||
| result.coverage = runtime.getAllCoverageInfo(); | ||
| result.sourceMaps = runtime.getSourceMapInfo(); | ||
| result.console = testConsole.getBuffer(); | ||
| result.skipped = testCount === result.numPendingTests; | ||
| result.displayName = config.displayName; | ||
|
|
||
| if (globalConfig.logHeapUsage) { | ||
| if (global.gc) { | ||
| global.gc(); | ||
| } | ||
| result.memoryUsage = process.memoryUsage().heapUsed; | ||
| } | ||
|
|
||
| // Delay the resolution to allow log messages to be output. | ||
| return new Promise(resolve => setImmediate(() => resolve(result))); | ||
| await new Promise(resolve => setImmediate(resolve)); | ||
|
|
||
| return result; | ||
|
||
| } finally { | ||
| await environment.teardown(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| export default async function runTest( | ||
| path: Path, | ||
| globalConfig: GlobalConfig, | ||
| config: ProjectConfig, | ||
| resolver: Resolver, | ||
| ): Promise<TestResult> { | ||
| const result: TestResult = await runTestHelper( | ||
| path, | ||
| globalConfig, | ||
| config, | ||
| resolver, | ||
| ); | ||
|
|
||
| // Resolve leak detector, so that the object is JSON serializable. | ||
| if (result.leaks instanceof LeakDetector) { | ||
| result.leaks = result.leaks.isLeaking(); | ||
|
||
| } | ||
|
|
||
| return result; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| * @flow | ||
| */ | ||
|
|
||
| import LeakDetector from 'jest-leak-detector'; | ||
| import type {ConsoleBuffer} from './Console'; | ||
|
|
||
| export type RawFileCoverage = {| | ||
|
|
@@ -138,6 +139,7 @@ export type TestResult = {| | |
| console: ?ConsoleBuffer, | ||
| coverage?: RawCoverage, | ||
| displayName: ?string, | ||
| leaks: LeakDetector | boolean, | ||
|
||
| memoryUsage?: Bytes, | ||
| failureMessage: ?string, | ||
| numFailingTests: number, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put a bold statement on top and say "Experimental" etc. Nobody reads
jest --help.