Skip to content

Commit 42bd4f2

Browse files
authored
Faster datascience tests with fake timers (microsoft#9272)
Faster tests with fake timers. Speed up tests that have Sleep(n) Speed up of tests from 10s to 54ms with two easy lines (install & wait)
1 parent 5c2ca78 commit 42bd4f2

5 files changed

Lines changed: 170 additions & 45 deletions

File tree

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v10.5.0
1+
v10.11.0

package-lock.json

Lines changed: 77 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2841,7 +2841,7 @@
28412841
"@types/request": "^2.47.0",
28422842
"@types/semver": "^5.5.0",
28432843
"@types/shortid": "^0.0.29",
2844-
"@types/sinon": "^7.0.13",
2844+
"@types/sinon": "^7.5.1",
28452845
"@types/stack-trace": "0.0.29",
28462846
"@types/temp": "^0.8.32",
28472847
"@types/tmp": "0.0.33",
@@ -2906,6 +2906,7 @@
29062906
"less": "^3.9.0",
29072907
"less-loader": "^5.0.0",
29082908
"loader-utils": "^1.1.0",
2909+
"lolex": "^5.1.2",
29092910
"mocha": "^6.1.4",
29102911
"mocha-junit-reporter": "^1.17.0",
29112912
"mocha-multi-reporters": "^1.1.7",
@@ -2930,7 +2931,7 @@
29302931
"sass-loader": "^7.1.0",
29312932
"serialize-javascript": "^2.1.2",
29322933
"shortid": "^2.2.8",
2933-
"sinon": "^7.3.2",
2934+
"sinon": "^8.0.1",
29342935
"source-map-support": "^0.5.12",
29352936
"style-loader": "^0.23.1",
29362937
"styled-jsx": "^3.1.0",

src/test/common.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,80 @@ export async function openFile(file: string): Promise<TextDocument> {
445445
assert(vscode.window.activeTextEditor, 'No active editor');
446446
return textDocument;
447447
}
448+
449+
/**
450+
* Fakes for timers in nodejs when testing, using `lolex`.
451+
* An alternative to `sinon.useFakeTimers` (which in turn uses `lolex`, but doesn't expose the `async` methods).
452+
* Use this class when you have tests with `setTimeout` and which to avoid them for faster tests.
453+
*
454+
* For further information please refer:
455+
* - https://www.npmjs.com/package/lolex
456+
* - https://sinonjs.org/releases/v1.17.6/fake-timers/
457+
*
458+
* @class FakeClock
459+
*/
460+
export class FakeClock {
461+
// tslint:disable-next-line:no-any
462+
private clock?: any;
463+
/**
464+
* Creates an instance of FakeClock.
465+
* @param {number} [advacenTimeMs=10_000] Default `timeout` value. Defaults to 10s. Assuming we do not have anything bigger.
466+
* @memberof FakeClock
467+
*/
468+
constructor(private readonly advacenTimeMs: number = 10_000) {}
469+
public install() {
470+
// tslint:disable-next-line:no-require-imports
471+
const lolex = require('lolex');
472+
this.clock = lolex.install();
473+
}
474+
public uninstall() {
475+
this.clock?.uninstall();
476+
}
477+
/**
478+
* Wait for timers to kick in, and then wait for all of them to complete.
479+
*
480+
* @returns {Promise<void>}
481+
* @memberof FakeClock
482+
*/
483+
public async wait(): Promise<void> {
484+
await this.waitForTimersToStart();
485+
await this.waitForTimersToFinish();
486+
}
487+
488+
/**
489+
* Wait for timers to start.
490+
*
491+
* @returns {Promise<void>}
492+
* @memberof FakeClock
493+
*/
494+
private async waitForTimersToStart(): Promise<void> {
495+
if (!this.clock){
496+
throw new Error('Fake clock not installed');
497+
}
498+
while (this.clock.countTimers() === 0) {
499+
// Relinquish control to event loop, so other timer code will run.
500+
// We want to wait for `setTimeout` to kick in.
501+
await new Promise(resolve => process.nextTick(resolve));
502+
}
503+
}
504+
/**
505+
* Wait for timers to finish.
506+
*
507+
* @returns {Promise<void>}
508+
* @memberof FakeClock
509+
*/
510+
private async waitForTimersToFinish(): Promise<void> {
511+
if (!this.clock){
512+
throw new Error('Fake clock not installed');
513+
}
514+
while (this.clock.countTimers()) {
515+
// Advance clock by 10s (can be anything to ensure the next scheduled block of code executes).
516+
// Assuming we do not have timers > 10s
517+
// This will ensure any such such as `setTimeout(..., 10)` will get executed.
518+
this.clock.tick(this.advacenTimeMs);
519+
520+
// Wait for the timer code to run to completion (incase they are promises).
521+
await this.clock.runAllAsync();
522+
}
523+
}
524+
}

src/test/datascience/jupyter/kernels/kernelService.unit.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { EnvironmentActivationService } from '../../../../client/interpreter/act
3030
import { IEnvironmentActivationService } from '../../../../client/interpreter/activation/types';
3131
import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../../client/interpreter/contracts';
3232
import { InterpreterService } from '../../../../client/interpreter/interpreterService';
33+
import { FakeClock } from '../../../common';
3334

3435
// tslint:disable-next-line: max-func-body-length
3536
suite('Data Science - KernelService', () => {
@@ -222,6 +223,7 @@ suite('Data Science - KernelService', () => {
222223
// tslint:disable-next-line: max-func-body-length
223224
suite('Registering Interpreters as Kernels', () => {
224225
let findMatchingKernelSpecStub: sinon.SinonStub<[PythonInterpreter, IJupyterSessionManager?, (CancellationToken | undefined)?], Promise<IJupyterKernelSpec | undefined>>;
226+
let fakeTimer: FakeClock;
225227
const interpreter: PythonInterpreter = {
226228
architecture: Architecture.Unknown,
227229
path: path.join('interpreter', 'python'),
@@ -246,9 +248,12 @@ suite('Data Science - KernelService', () => {
246248

247249
setup(() => {
248250
findMatchingKernelSpecStub = sinon.stub(KernelService.prototype, 'findMatchingKernelSpec');
251+
fakeTimer = new FakeClock();
249252
initialize();
250253
});
251254

255+
teardown(() => fakeTimer.uninstall());
256+
252257
test('Fail if interpreter does not have a display name', async () => {
253258
const invalidInterpreter: PythonInterpreter = {
254259
architecture: Architecture.Unknown,
@@ -266,32 +271,36 @@ suite('Data Science - KernelService', () => {
266271
when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' });
267272
when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(true);
268273
findMatchingKernelSpecStub.resolves(undefined);
274+
fakeTimer.install();
269275

270276
const promise = kernelService.registerKernel(interpreter);
271277

278+
await fakeTimer.wait();
272279
await assert.isRejected(promise);
273280
verify(execService.execModule('ipykernel', anything(), anything())).once();
274281
const installArgs = capture(execService.execModule).first()[1] as string[];
275282
const kernelName = installArgs[3];
276283
assert.deepEqual(installArgs, ['install', '--user', '--name', kernelName, '--display-name', interpreter.displayName]);
277284
await assert.isRejected(promise, `Kernel not created with the name ${kernelName}, display_name ${interpreter.displayName}. Output is `);
278-
}).timeout(10_000);
285+
});
279286
test('If ipykernel is not installed, then prompt to install ipykernel', async () => {
280287
when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' });
281288
when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(false);
282289
when(installer.promptToInstall(anything(), anything(), anything())).thenResolve(InstallerResponse.Installed);
283290
findMatchingKernelSpecStub.resolves(undefined);
291+
fakeTimer.install();
284292

285293
const promise = kernelService.registerKernel(interpreter);
286294

295+
await fakeTimer.wait();
287296
await assert.isRejected(promise);
288297
verify(execService.execModule('ipykernel', anything(), anything())).once();
289298
const installArgs = capture(execService.execModule).first()[1] as string[];
290299
const kernelName = installArgs[3];
291300
assert.deepEqual(installArgs, ['install', '--user', '--name', kernelName, '--display-name', interpreter.displayName]);
292301
await assert.isRejected(promise, `Kernel not created with the name ${kernelName}, display_name ${interpreter.displayName}. Output is `);
293302
verify(installer.promptToInstall(anything(), anything(), anything())).once();
294-
}).timeout(10_000);
303+
});
295304
test('If ipykernel is not installed, and ipykerne installation is canclled, then do not reigster kernel', async () => {
296305
when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' });
297306
when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(false);
@@ -303,7 +312,7 @@ suite('Data Science - KernelService', () => {
303312
assert.isUndefined(kernel);
304313
verify(execService.execModule('ipykernel', anything(), anything())).never();
305314
verify(installer.promptToInstall(anything(), anything(), anything())).once();
306-
}).timeout(10_000);
315+
});
307316
test('Fail if installed kernel is not an instance of JupyterKernelSpec', async () => {
308317
when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' });
309318
when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(true);

0 commit comments

Comments
 (0)