You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
11 KiB
359 lines
11 KiB
2 years ago
|
import sinon from 'sinon';
|
||
|
|
||
|
import {
|
||
|
dropQueue,
|
||
|
callBackgroundMethod,
|
||
|
submitRequestToBackground,
|
||
|
_setBackgroundConnection,
|
||
|
} from '.';
|
||
|
|
||
|
// This file tests only MV3 queue scenario
|
||
|
// MV2 tests are already covered by '../actions.test.js'
|
||
|
|
||
|
jest.mock('../../../shared/modules/mv3.utils', () => {
|
||
|
return {
|
||
|
isManifestV3: () => true,
|
||
|
};
|
||
|
});
|
||
|
|
||
|
describe('ActionQueue', () => {
|
||
|
afterEach(() => {
|
||
|
sinon.restore();
|
||
|
dropQueue(true);
|
||
|
});
|
||
|
|
||
|
describe('dropQueue', () => {
|
||
|
it('rejects all pending actions by default', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
backgroundFunction: sinon.stub().yields(),
|
||
|
};
|
||
|
|
||
|
_setBackgroundConnection(background);
|
||
|
const result = submitRequestToBackground('backgroundFunction');
|
||
|
dropQueue();
|
||
|
|
||
|
await expect(result).rejects.toThrow(
|
||
|
'Background operation cancelled while waiting for connection.',
|
||
|
);
|
||
|
expect(background.backgroundFunction.called).toStrictEqual(false);
|
||
|
});
|
||
|
});
|
||
|
describe('submitRequestToBackground', () => {
|
||
|
it('calls promisified background method if the stream is connected', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: true,
|
||
|
},
|
||
|
backgroundFunction1: sinon.stub().yields(),
|
||
|
};
|
||
|
|
||
|
_setBackgroundConnection(background);
|
||
|
submitRequestToBackground('backgroundFunction1');
|
||
|
expect(background.backgroundFunction1.called).toStrictEqual(true);
|
||
|
});
|
||
|
|
||
|
it('does not calls promisified background method if the stream is not connected', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
backgroundFunction2: sinon.stub().yields(),
|
||
|
};
|
||
|
|
||
|
_setBackgroundConnection(background);
|
||
|
submitRequestToBackground('backgroundFunction2');
|
||
|
expect(background.backgroundFunction2.called).toStrictEqual(false);
|
||
|
});
|
||
|
|
||
|
it('calls promisified background method on stream reconnection', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
backgroundFunction3: sinon.stub().yields(),
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const requestPromise = submitRequestToBackground('backgroundFunction3');
|
||
|
|
||
|
background.connectionStream = {
|
||
|
readable: true,
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
await requestPromise;
|
||
|
expect(background.backgroundFunction3.calledOnce).toStrictEqual(true);
|
||
|
});
|
||
|
|
||
|
it('resolves if backgroundFunction resolves', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: true,
|
||
|
},
|
||
|
backgroundFunction4: (cb) => {
|
||
|
return cb(null, 'test');
|
||
|
},
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
await expect(
|
||
|
submitRequestToBackground('backgroundFunction4'),
|
||
|
).resolves.toStrictEqual('test');
|
||
|
});
|
||
|
|
||
|
it('rejects if backgroundFunction throws exception', async () => {
|
||
|
expect.assertions(1);
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: true,
|
||
|
},
|
||
|
backgroundFunction: () => {
|
||
|
throw Error('test');
|
||
|
},
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
await expect(
|
||
|
submitRequestToBackground('backgroundFunction'),
|
||
|
).rejects.toThrow('test');
|
||
|
});
|
||
|
|
||
|
it('calls methods in parallel when connection available', async () => {
|
||
|
const trace = {};
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: true,
|
||
|
},
|
||
|
first: (cb) => {
|
||
|
setTimeout(() => {
|
||
|
trace.firstDone = Date.now();
|
||
|
cb(null, 'first');
|
||
|
}, 5);
|
||
|
},
|
||
|
second: (cb) => {
|
||
|
trace.secondStarted = Date.now();
|
||
|
setTimeout(() => cb(null, 'second'), 10);
|
||
|
},
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const scheduled = Promise.all([
|
||
|
submitRequestToBackground('first'),
|
||
|
submitRequestToBackground('second'),
|
||
|
]);
|
||
|
await scheduled;
|
||
|
expect(trace.firstDone).toBeGreaterThan(trace.secondStarted);
|
||
|
});
|
||
|
|
||
|
it('processes the queue sequentially when connection is restored', async () => {
|
||
|
const trace = {};
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
first: (cb) => {
|
||
|
setTimeout(() => {
|
||
|
trace.firstDone = Date.now();
|
||
|
cb(null, 'first');
|
||
|
}, 5);
|
||
|
},
|
||
|
second: (cb) => {
|
||
|
trace.secondStarted = Date.now();
|
||
|
setTimeout(() => cb(null, 'second'), 10);
|
||
|
},
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const scheduled = Promise.all([
|
||
|
submitRequestToBackground('first'),
|
||
|
submitRequestToBackground('second'),
|
||
|
]);
|
||
|
background.connectionStream.readable = true;
|
||
|
_setBackgroundConnection(background);
|
||
|
await scheduled;
|
||
|
expect(trace.firstDone).toBeLessThanOrEqual(trace.secondStarted);
|
||
|
});
|
||
|
|
||
|
it('ensures actions in queue will not repeat once finished', async () => {
|
||
|
const trace = { calls: 0 };
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
first: (cb) => {
|
||
|
trace.calls += 1;
|
||
|
setTimeout(() => {
|
||
|
trace.firstDone = Date.now();
|
||
|
cb(null, 'first');
|
||
|
}, 5);
|
||
|
},
|
||
|
second: (cb) => {
|
||
|
trace.calls += 1;
|
||
|
trace.secondStarted = Date.now();
|
||
|
setTimeout(() => cb(null, 'second'), 10);
|
||
|
},
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const scheduled = Promise.all([
|
||
|
submitRequestToBackground('first'),
|
||
|
submitRequestToBackground('second'),
|
||
|
]);
|
||
|
background.connectionStream.readable = true;
|
||
|
_setBackgroundConnection(background);
|
||
|
await scheduled;
|
||
|
_setBackgroundConnection(background); // once all actions finished, this triggers draining the queue again
|
||
|
expect(trace.firstDone).toBeLessThanOrEqual(trace.secondStarted);
|
||
|
expect(trace.calls).toStrictEqual(2);
|
||
|
});
|
||
|
|
||
|
it('stops processng the queue if connection is lost', async () => {
|
||
|
const trace = {};
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
first: (cb) => {
|
||
|
setTimeout(() => {
|
||
|
trace.firstDone = true;
|
||
|
background.connectionStream.readable = false;
|
||
|
cb(Error('lost connection'));
|
||
|
}, 5);
|
||
|
},
|
||
|
second: sinon.stub().yields(),
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const scheduled = Promise.race([
|
||
|
submitRequestToBackground('first').catch(() => ({})),
|
||
|
submitRequestToBackground('second'),
|
||
|
]);
|
||
|
background.connectionStream.readable = true;
|
||
|
_setBackgroundConnection(background);
|
||
|
await scheduled;
|
||
|
await Promise.resolve('one more tick'); // One asynchronous tick to avoid depending on implementation details
|
||
|
expect(trace.firstDone).toStrictEqual(true);
|
||
|
expect(background.second.called).toStrictEqual(false);
|
||
|
});
|
||
|
|
||
|
// Failing test for a race condition related to how items are removed from queue
|
||
|
it('avoids race conditions', async () => {
|
||
|
const trace = { first: 0, second: 0 };
|
||
|
const flowControl = {};
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
first: (cb) => {
|
||
|
trace.first += 1;
|
||
|
setTimeout(() => {
|
||
|
flowControl.triggerRaceCondition();
|
||
|
cb(null, 'first');
|
||
|
}, 5);
|
||
|
},
|
||
|
second: (cb) => {
|
||
|
trace.second += 1;
|
||
|
setTimeout(() => cb(null, 'second'), 10);
|
||
|
},
|
||
|
third: sinon.stub().yields(),
|
||
|
};
|
||
|
flowControl.triggerRaceCondition = () => {
|
||
|
flowControl.waitFor = submitRequestToBackground('third');
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const scheduled = Promise.all([
|
||
|
submitRequestToBackground('first'),
|
||
|
submitRequestToBackground('second'),
|
||
|
]);
|
||
|
background.connectionStream.readable = true;
|
||
|
_setBackgroundConnection(background);
|
||
|
await scheduled;
|
||
|
await flowControl.waitFor;
|
||
|
expect(trace.first).toStrictEqual(1);
|
||
|
expect(trace.second).toStrictEqual(1);
|
||
|
expect(background.third.calledOnce).toStrictEqual(true);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('callBackgroundMethod', () => {
|
||
|
afterEach(() => {
|
||
|
sinon.restore();
|
||
|
});
|
||
|
|
||
|
it('calls background method if the stream is connected', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: true,
|
||
|
},
|
||
|
backgroundFunction: sinon.stub().yields(),
|
||
|
};
|
||
|
|
||
|
_setBackgroundConnection(background);
|
||
|
callBackgroundMethod('backgroundFunction', [], () => ({}));
|
||
|
expect(background.backgroundFunction.called).toStrictEqual(true);
|
||
|
});
|
||
|
|
||
|
it('does not call background method if the stream is not connected', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
backgroundFunction: sinon.stub(),
|
||
|
};
|
||
|
|
||
|
_setBackgroundConnection(background);
|
||
|
callBackgroundMethod('backgroundFunction', [], () => ({}));
|
||
|
expect(background.backgroundFunction.called).toStrictEqual(false);
|
||
|
});
|
||
|
|
||
|
it('calls background method on stream reconnection', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: false,
|
||
|
},
|
||
|
backgroundFunction: sinon.stub().yields(),
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
callBackgroundMethod('backgroundFunction', [], () => ({}));
|
||
|
expect(background.backgroundFunction.called).toStrictEqual(false);
|
||
|
|
||
|
background.connectionStream = {
|
||
|
readable: true,
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
expect(background.backgroundFunction.calledOnce).toStrictEqual(true);
|
||
|
});
|
||
|
|
||
|
it('resolves if backgroundFunction called resolves', async () => {
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: true,
|
||
|
},
|
||
|
backgroundFunction: (cb) => {
|
||
|
return cb(null, 'successViaCallback');
|
||
|
},
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const value = await new Promise((resolve) => {
|
||
|
callBackgroundMethod('backgroundFunction', [], (_err, result) => {
|
||
|
resolve(result);
|
||
|
});
|
||
|
});
|
||
|
expect(value).toStrictEqual('successViaCallback');
|
||
|
});
|
||
|
it('rejects if backgroundFunction called rejects', async () => {
|
||
|
const errorViaCallback = Error('errorViaCallback');
|
||
|
const background = {
|
||
|
connectionStream: {
|
||
|
readable: true,
|
||
|
},
|
||
|
backgroundFunction: (cb) => {
|
||
|
return cb(errorViaCallback);
|
||
|
},
|
||
|
};
|
||
|
_setBackgroundConnection(background);
|
||
|
const value = await new Promise((resolve) => {
|
||
|
callBackgroundMethod('backgroundFunction', [], (err) => {
|
||
|
resolve(err);
|
||
|
});
|
||
|
});
|
||
|
expect(value).toStrictEqual(errorViaCallback);
|
||
|
});
|
||
|
});
|
||
|
});
|