Skip to content
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

Closed
wants to merge 2 commits into from
Closed

Conversation

remorses
Copy link

@remorses remorses commented Jan 4, 2024

Make sandbox concurrency safe

Fix #2472, sandbox not working when running tests concurrently because of global callId variable

Solution

Store callId in a context passed with an optional argument, which is the sandbox object when using sandbox

How to verify - mandatory

Added a test with the #2472 reproduction

  1. Check out this branch
  2. npm install
  3. npm test

Checklist for author

  • npm run lint passes
  • References to standard library functions are cached.

@remorses remorses marked this pull request as ready for review January 4, 2024 17:49
Copy link

@Ethan-Arrowood Ethan-Arrowood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work. Test case looks good to me. Appreciate you making this happen 🚀

Copy link
Contributor

@fatso83 fatso83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for having a go at this, @remorses! I tried taking a proper look, and I think I got there in the end, even though it was late.

There are two main issues I need covered:

  1. this needs to work for fakes, stubs and mocks as well
  2. Optional context seems like a bad idea

Since stubs are spies that should be a wrap. I guess mocks are covered by being proxies ... ? But AFAIK fakes do not share much code with these, so that needs to be addressed (I noted that in #2472 (comment)).

I am pretty sure you could template the test case so that you can make the same test for both spies and fakes, btw, just parameterising it by passing in some factory method.

Wrt the context, the fact that it has been made optional concerns me. Why is it optional? Is it just to avoid having to change testcases? A good IDE should be able to fill in default values for you when adding a new parameter. If it's just for convenience, I would rather just fix it. All spies are created from a sandbox, even the ones on the default Sinon instance, so there should be no need for the global module, AFAIK.

Comment on lines +2380 to +2400
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();
}
}
Copy link
Contributor

@fatso83 fatso83 Jan 4, 2024

Choose a reason for hiding this comment

The 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.

object,
property,
types,
sandbox,
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

@@ -253,7 +254,7 @@ function createProxy(func, originalFunc) {
return proxy;
}

function wrapFunction(func, originalFunc) {
function wrapFunction(func, originalFunc, ctx = globalContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Comment on lines +15 to +17
if (ctx.callId == null) {
ctx.callId = 0;
}
Copy link
Contributor

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.

Comment on lines +1 to +5
"use strict";

module.exports = {
callId: 0,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this even needed?

Copy link

codecov bot commented Jan 4, 2024

Codecov Report

Attention: 11 lines in your changes are missing coverage. Please review.

Comparison is base (adcf936) 96.02% compared to head (fabf95c) 95.98%.

Files Patch % Lines
lib/sinon/proxy.js 37.50% 10 Missing ⚠️
lib/sinon/proxy-invoke.js 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2575      +/-   ##
==========================================
- Coverage   96.02%   95.98%   -0.04%     
==========================================
  Files          40       41       +1     
  Lines        1912     1918       +6     
==========================================
+ Hits         1836     1841       +5     
- Misses         76       77       +1     
Flag Coverage Δ
unit 95.98% <63.33%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@fatso83
Copy link
Contributor

fatso83 commented Jan 4, 2024

This currently fails on your branch:

cat > test/concurrency-test.js << EOF
const assert = require("node:assert");
const sinon = require("../lib/sinon");

const sbA = sinon.createSandbox();
const sbB = sinon.createSandbox();
const F = sbA.fake();

const f1 = sbB.fake();
const f2 = sbB.fake();

f1();
F();
f2();
assert(f1.calledImmediatelyBefore(f2));
EOF

node test/concurrency-test.js

@fatso83
Copy link
Contributor

fatso83 commented Jan 9, 2024

@remorses Do you need some guidance in how to progress? AFAIK the old-skool API is covered (haven't checked the mocks API, but I think that should be covered by the proxy stuff).

@fatso83
Copy link
Contributor

fatso83 commented Jan 25, 2024

Ping?

@fatso83
Copy link
Contributor

fatso83 commented Feb 15, 2024

This does not seem like it is going places. Closing due to unresponsiveness.

@fatso83 fatso83 closed this Feb 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sinon fails in comparing call order when running tests in parallel
3 participants