diff --git a/docs/release-source/release/sandbox.md b/docs/release-source/release/sandbox.md index 74803acf7..8f346ff09 100644 --- a/docs/release-source/release/sandbox.md +++ b/docs/release-source/release/sandbox.md @@ -334,3 +334,7 @@ Verifies all mocks created through the sandbox. #### `sandbox.verifyAndRestore();` Verifies all mocks and restores all fakes created through the sandbox. + +#### `sandbox.leakThreshold` + +Gets/sets the threshold at which memory leak detection warnings are logged. diff --git a/lib/sinon/sandbox.js b/lib/sinon/sandbox.js index 7b1c4c814..66fbe1c6f 100644 --- a/lib/sinon/sandbox.js +++ b/lib/sinon/sandbox.js @@ -1,6 +1,7 @@ "use strict"; var arrayProto = require("@sinonjs/commons").prototypes.array; +var logger = require("@sinonjs/commons").deprecated; var collectOwnMethods = require("./collect-own-methods"); var getPropertyDescriptor = require("./util/core/get-property-descriptor"); var isPropertyConfigurable = require("./util/core/is-property-configurable"); @@ -16,6 +17,8 @@ var fakeServer = require("nise").fakeServer; var fakeXhr = require("nise").fakeXhr; var usePromiseLibrary = require("./util/core/use-promise-library"); +var DEFAULT_LEAK_THRESHOLD = 10000; + var filter = arrayProto.filter; var forEach = arrayProto.forEach; var push = arrayProto.push; @@ -33,10 +36,26 @@ function applyOnEach(fakes, method) { function Sandbox() { var sandbox = this; - var collection = []; var fakeRestorers = []; var promiseLib; + var collection = []; + var loggedLeakWarning = false; + sandbox.leakThreshold = DEFAULT_LEAK_THRESHOLD; + + function addToCollection(object) { + if ( + push(collection, object) > sandbox.leakThreshold && + !loggedLeakWarning + ) { + // eslint-disable-next-line no-console + logger.printWarning( + "Potential memory leak detected; be sure to call restore() to clean up your sandbox. To suppress this warning, modify the leakThreshold property of your sandbox." + ); + loggedLeakWarning = true; + } + } + sandbox.assert = sinonAssert.createAssertObject(); sandbox.serverPrototype = fakeServer; @@ -57,7 +76,7 @@ function Sandbox() { var ownMethods = collectOwnMethods(stubbed); forEach(ownMethods, function (method) { - push(collection, method); + addToCollection(method); }); usePromiseLibrary(promiseLib, ownMethods); @@ -115,7 +134,7 @@ function Sandbox() { sandbox.mock = function mock() { var m = sinonMock.apply(null, arguments); - push(collection, m); + addToCollection(m); usePromiseLibrary(promiseLib, m); return m; @@ -348,12 +367,12 @@ function Sandbox() { var ownMethods = collectOwnMethods(spy); forEach(ownMethods, function (method) { - push(collection, method); + addToCollection(method); }); usePromiseLibrary(promiseLib, ownMethods); } else { - push(collection, spy); + addToCollection(spy); usePromiseLibrary(promiseLib, spy); } @@ -374,7 +393,7 @@ function Sandbox() { sandbox.fake = function fake(f) { var s = sinonFake.apply(sinonFake, arguments); - push(collection, s); + addToCollection(s); return s; }; @@ -385,7 +404,7 @@ function Sandbox() { sandbox.fake[key] = function () { var s = fakeBehavior.apply(fakeBehavior, arguments); - push(collection, s); + addToCollection(s); return s; }; @@ -396,7 +415,7 @@ function Sandbox() { var clock = sinonClock.useFakeTimers.call(null, args); sandbox.clock = clock; - push(collection, clock); + addToCollection(clock); return clock; }; @@ -429,14 +448,14 @@ function Sandbox() { } sandbox.server = proto.create(); - push(collection, sandbox.server); + addToCollection(sandbox.server); return sandbox.server; }; sandbox.useFakeXMLHttpRequest = function useFakeXMLHttpRequest() { var xhr = fakeXhr.useFakeXMLHttpRequest(); - push(collection, xhr); + addToCollection(xhr); return xhr; }; diff --git a/test/sandbox-test.js b/test/sandbox-test.js index f51caefc7..419ecbfc5 100644 --- a/test/sandbox-test.js +++ b/test/sandbox-test.js @@ -134,6 +134,51 @@ describe("Sandbox", function () { assert.equals(fakes.length, 2); }); + + describe("warns of potential leak when", function () { + var warn; + + beforeEach(function () { + warn = this.sandbox.stub(deprecated, "printWarning"); + }); + + afterEach(function () { + warn.restore(); + }); + + it("many fakes are created", function () { + assert.equals(typeof this.sandbox.leakThreshold, "number"); + + createTooManyFakes(this.sandbox); + + assert(warn.called); + }); + + it("a configurable number of fakes are created", function () { + this.sandbox.leakThreshold = 20; + + createTooManyFakes(this.sandbox); + + assert(warn.called); + }); + + it("a leak warning has not already been output", function () { + this.sandbox.leakThreshold = 20; + + createTooManyFakes(this.sandbox); + this.sandbox.restore(); + warn.resetHistory(); + + createTooManyFakes(this.sandbox); + assert(!warn.called); + }); + + function createTooManyFakes(sandbox) { + for (var i = 0; i < sandbox.leakThreshold; i++) { + sandbox.spy(); + } + } + }); }); describe(".spy", function () {