-
-
Notifications
You must be signed in to change notification settings - Fork 770
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
Make sandbox concurrency safe #2575
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
const arrayProto = require("@sinonjs/commons").prototypes.array; | ||
const extend = require("./util/core/extend"); | ||
const globalContext = require("./util/core/global-context"); | ||
const functionToString = require("./util/core/function-to-string"); | ||
const proxyCall = require("./proxy-call"); | ||
const proxyCallUtil = require("./proxy-call-util"); | ||
|
@@ -240,8 +241,8 @@ | |
delegateToCalls(proxyApi, "calledWithNew", true); | ||
delegateToCalls(proxyApi, "alwaysCalledWithNew", false, "calledWithNew"); | ||
|
||
function createProxy(func, originalFunc) { | ||
const proxy = wrapFunction(func, originalFunc); | ||
function createProxy(func, originalFunc, ctx = globalContext) { | ||
const proxy = wrapFunction(func, originalFunc, ctx); | ||
|
||
// Inherit function properties: | ||
extend(proxy, func); | ||
|
@@ -253,7 +254,7 @@ | |
return proxy; | ||
} | ||
|
||
function wrapFunction(func, originalFunc) { | ||
function wrapFunction(func, originalFunc, ctx = globalContext) { | ||
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. Why are all of these context objects optional, with a default global? Even the default Sinon instance is a sandbox in itself. |
||
const arity = originalFunc.length; | ||
let p; | ||
// Do not change this to use an eval. Projects that depend on sinon block the use of eval. | ||
|
@@ -262,72 +263,72 @@ | |
/*eslint-disable no-unused-vars, max-len*/ | ||
case 0: | ||
p = function proxy() { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 1: | ||
p = function proxy(a) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 2: | ||
p = function proxy(a, b) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 3: | ||
p = function proxy(a, b, c) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 4: | ||
p = function proxy(a, b, c, d) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 5: | ||
p = function proxy(a, b, c, d, e) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 6: | ||
p = function proxy(a, b, c, d, e, f) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 7: | ||
p = function proxy(a, b, c, d, e, f, g) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 8: | ||
p = function proxy(a, b, c, d, e, f, g, h) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 9: | ||
p = function proxy(a, b, c, d, e, f, g, h, i) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 10: | ||
p = function proxy(a, b, c, d, e, f, g, h, i, j) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 11: | ||
p = function proxy(a, b, c, d, e, f, g, h, i, j, k) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
case 12: | ||
p = function proxy(a, b, c, d, e, f, g, h, i, j, k, l) { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
default: | ||
p = function proxy() { | ||
return p.invoke(func, this, slice(arguments)); | ||
return p.invoke(func, this, slice(arguments), ctx); | ||
}; | ||
break; | ||
/*eslint-enable*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,6 +78,7 @@ function checkForValidArguments(descriptor, property, replacement) { | |
*/ | ||
function Sandbox(opts = {}) { | ||
const sandbox = this; | ||
sandbox.callId = 0; | ||
const assertOptions = opts.assertOptions || {}; | ||
let fakeRestorers = []; | ||
let promiseLib; | ||
|
@@ -445,8 +446,13 @@ function Sandbox(opts = {}) { | |
return spy; | ||
} | ||
|
||
sandbox.spy = function spy() { | ||
const createdSpy = sinonSpy.apply(sinonSpy, arguments); | ||
sandbox.spy = function spy(object, property, types) { | ||
const createdSpy = sinonSpy.apply(sinonSpy, [ | ||
object, | ||
property, | ||
types, | ||
sandbox, | ||
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. If you are passing in the sandbox as the context, then what is the need for the global context object? |
||
]); | ||
return commonPostInitSetup(arguments, createdSpy); | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
"use strict"; | ||
|
||
module.exports = { | ||
callId: 0, | ||
}; | ||
Comment on lines
+1
to
+5
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. Is this even needed? |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2377,4 +2377,52 @@ describe("Sandbox", function () { | |
sandboxB.restore(); | ||
}); | ||
}); | ||
describe("concurrency safe", function () { | ||
it("spy", async function () { | ||
class F { | ||
constructor() { | ||
this.first = []; | ||
this.second = []; | ||
this.third = []; | ||
} | ||
async run() { | ||
await this.execute("first"); | ||
await Promise.resolve(); | ||
await this.execute("second"); | ||
await Promise.resolve(); | ||
await this.execute("third"); | ||
} | ||
|
||
async execute(s) { | ||
for (const f of this[s]) { | ||
await f(); | ||
} | ||
} | ||
Comment on lines
+2380
to
+2400
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. This test was a bit hard to wrap my head around, at least at midnight ... 😄 Could you clarify what you are really doing? I mean, I assume you are somehow testing quazi-concurrent runs, interlacing runs from different sandboxes, but I still find it a bit hard to reason around the order of the calls. Is there some way you could re-order or refactor this in some way that made the intent as expressed through the code clearer? To me, it is not at all clear how the sandbox runs are being interlaced and in what order functions are being called. Would it not be much clearer if doing something a la the following pseudo-code? const sandboxASpy1 = sandboxA.spy();
const sandboxASpy2 = sandboxA.spy();
const sandboxBSpy1 = sandboxB.spy();
const sandboxBSpy2 = sandboxB.spy();
sandboxASpy1();
sandboxBSpy1();
sandboxBSpy2();
sandboxASpy2();
assert(sandboxASpy1.calledImmediatelyBefore(sandboxASpy2));
assert(sandboxBSpy1.calledImmediatelyBefore(sandboxBSpy2)); There is no need for awaits AFAIK. JS is single threaded no matter what, and the asynchronicity just muddles up what is going on IMHO. |
||
} | ||
|
||
const fn = async () => { | ||
const sinonSandbox = createSandbox(); | ||
|
||
const f = new F(); | ||
|
||
const a = sinonSandbox.spy(); | ||
const b = sinonSandbox.spy(); | ||
const c = sinonSandbox.spy(); | ||
|
||
f.first.push(a); | ||
f.second.push(b); | ||
f.third.push(c); | ||
|
||
await f.run(); | ||
|
||
assert(a.calledBefore(b)); | ||
assert(b.calledBefore(c)); | ||
|
||
assert(a.calledImmediatelyBefore(b)); | ||
assert(b.calledImmediatelyBefore(c)); | ||
}; | ||
|
||
await Promise.all([fn(), fn(), fn(), fn()]); | ||
}); | ||
}); | ||
}); |
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.
When does this ever happen? I cannot see any tests exercising this.
I see the fallback global module, where it is already initialised, and I see the sandbox, which is also initialised.