Skip to content

Commit dc16a48

Browse files
committed
Add simple unit test for 'rush build'
1 parent a417ca2 commit dc16a48

File tree

9 files changed

+206
-0
lines changed

9 files changed

+206
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,6 @@ temp
6767
# Rush files
6868
common/temp/**
6969
package-deps.json
70+
71+
# Keep temp folders in mocked 'repos' that are used for unit tests
72+
!**/test/**/temp
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
const EventEmitter = require('events');
5+
6+
const childProcess = jest.genMockFromModule('child_process');
7+
childProcess.spawn.mockImplementation(spawn);
8+
childProcess.__setSpawnMockConfig = setSpawnMockConfig;
9+
10+
let spawnMockConfig = normalizeSpawnMockConfig();
11+
12+
/**
13+
* Helper to initialize how the `spawn` mock should behave.
14+
*/
15+
function normalizeSpawnMockConfig(maybeConfig) {
16+
const config = maybeConfig || {};
17+
return {
18+
emitError: typeof config.emitError !== 'undefined' ? config.emitError : false,
19+
returnCode: typeof config.returnCode !== 'undefined' ? config.returnCode : 0
20+
};
21+
}
22+
23+
/**
24+
* Initialize the `spawn` mock behavior.
25+
*
26+
* Not a pure function.
27+
*/
28+
function setSpawnMockConfig(spawnConfig) {
29+
spawnMockConfig = normalizeSpawnMockConfig(spawnConfig);
30+
}
31+
32+
/**
33+
* Mock of `spawn`.
34+
*/
35+
function spawn(file, args, options) {
36+
const cpMock = new childProcess.ChildProcess();
37+
38+
// Add working event emitters ourselves since `genMockFromModule` does not add them because they
39+
// are dynamically added by `spawn`.
40+
const cpEmitter = new EventEmitter();
41+
const cp = Object.assign({}, cpMock, {
42+
stdin: new EventEmitter(),
43+
stdout: new EventEmitter(),
44+
stderr: new EventEmitter(),
45+
on: cpEmitter.on,
46+
emit: cpEmitter.emit
47+
});
48+
49+
setTimeout(() => {
50+
cp.stdout.emit('data', `${file} ${args}: Mock task is spawned`)
51+
52+
if (spawnMockConfig.emitError) {
53+
cp.stderr.emit('data', `${file} ${args}: A mock error occurred in the task`)
54+
}
55+
56+
cp.emit('close', spawnMockConfig.returnCode);
57+
}, 0);
58+
59+
return cp;
60+
}
61+
62+
module.exports = childProcess;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// Mock process so we can verify tasks are (or are not) invoked as we expect
5+
jest.mock('child_process');
6+
7+
import { resolve } from 'path';
8+
import { chdir } from 'process';
9+
import { ChildProcessModuleMock, ISpawnMockConfig } from 'child_process';
10+
import { FileSystem } from '@microsoft/node-core-library';
11+
import { RushCommandLineParser } from '../RushCommandLineParser';
12+
13+
/**
14+
* Configure the `child_process` `spawn` mock for these tests. This relies on the mock implementation
15+
* in `__mocks__/child_process.js`.
16+
*/
17+
function setSpawnMock(options?: ISpawnMockConfig): jest.Mock {
18+
const cpMocked: ChildProcessModuleMock = require('child_process');
19+
cpMocked.__setSpawnMockConfig(options);
20+
21+
const spawnMock: jest.Mock = cpMocked.spawn;
22+
spawnMock.mockName('spawn');
23+
return spawnMock;
24+
}
25+
26+
// Ordinals into the `mock.calls` array referencing each of the arguments to `spawn`
27+
const SPAWN_ARG_ARGS: number = 1;
28+
const SPAWN_ARG_OPTIONS: number = 2;
29+
30+
describe('RushCommandLineParser', () => {
31+
describe('execute', () => {
32+
describe(`'build' action`, () => {
33+
it(`executes the package's 'build' script`, () => {
34+
// Point to the test repo folder
35+
chdir(resolve(__dirname, 'buildActionsRepo'));
36+
37+
// The `build` task is hard-coded to be incremental. So delete the `package-deps.json` files in
38+
// the test repo to override so the test actually runs.
39+
FileSystem.deleteFile(resolve(__dirname, 'buildActionsRepo/a/package-deps.json'));
40+
FileSystem.deleteFile(resolve(__dirname, 'buildActionsRepo/b/package-deps.json'));
41+
42+
// Create a Rush CLI instance. This instance is heavy-weight and relies on setting process.exit
43+
// to exit and clear the Rush file lock. So running multiple `it` test blocks over the same test
44+
// repo will fail due to contention over the same lock.
45+
const parser: RushCommandLineParser = new RushCommandLineParser();
46+
47+
// Mock the command
48+
process.argv = ['pretend-this-is-node.exe', 'pretend-this-is-rush', 'build'];
49+
const spawnMock: jest.Mock = setSpawnMock();
50+
51+
expect.assertions(8);
52+
return expect(parser.execute()).resolves.toEqual(true)
53+
.then(() => {
54+
// There should be 1 build per package
55+
const packageCount: number = spawnMock.mock.calls.length;
56+
expect(packageCount).toEqual(2);
57+
58+
// Use regex for task name in case spaces were prepended or appended to spawned command
59+
const expectedBuildTaskRegexp: RegExp = /fake_build_task_but_works_with_mock/;
60+
61+
// tslint:disable-next-line: no-any
62+
const firstSpawn: any[] = spawnMock.mock.calls[0];
63+
expect(firstSpawn[SPAWN_ARG_ARGS]).toEqual(expect.arrayContaining([
64+
expect.stringMatching(expectedBuildTaskRegexp)
65+
]));
66+
expect(firstSpawn[SPAWN_ARG_OPTIONS]).toEqual(expect.any(Object));
67+
expect(firstSpawn[SPAWN_ARG_OPTIONS].cwd).toEqual(resolve(__dirname, 'buildActionsRepo/a'));
68+
69+
// tslint:disable-next-line: no-any
70+
const secondSpawn: any[] = spawnMock.mock.calls[1];
71+
expect(secondSpawn[SPAWN_ARG_ARGS]).toEqual(expect.arrayContaining([
72+
expect.stringMatching(expectedBuildTaskRegexp)
73+
]));
74+
expect(secondSpawn[SPAWN_ARG_OPTIONS]).toEqual(expect.any(Object));
75+
expect(secondSpawn[SPAWN_ARG_OPTIONS].cwd).toEqual(resolve(__dirname, 'buildActionsRepo/b'));
76+
});
77+
});
78+
});
79+
});
80+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "a",
3+
"version": "1.0.0",
4+
"description": "Test package a",
5+
"scripts": {
6+
"build": "fake_build_task_but_works_with_mock"
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "b",
3+
"version": "1.0.0",
4+
"description": "Test package b",
5+
"scripts": {
6+
"build": "fake_build_task_but_works_with_mock"
7+
}
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"localLinks": {
3+
"a": [],
4+
"b": []
5+
}
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"npmVersion": "6.4.1",
3+
"rushVersion": "5.5.2",
4+
"projectFolderMinDepth": 1,
5+
"projectFolderMaxDepth": 99,
6+
7+
"projects": [
8+
{
9+
"packageName": "a",
10+
"projectFolder": "a"
11+
},
12+
{
13+
"packageName": "b",
14+
"projectFolder": "b"
15+
}
16+
]
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as child from 'child_process';
2+
3+
declare module 'child_process' {
4+
/**
5+
* See `__mocks__/child_process.js`.
6+
*/
7+
export interface ISpawnMockConfig {
8+
emitError: boolean;
9+
returnCode: number;
10+
}
11+
12+
export interface ChildProcessModuleMock {
13+
/**
14+
* Initialize the `spawn` mock behavior.
15+
*/
16+
__setSpawnMockConfig(config?: ISpawnMockConfig): void;
17+
18+
spawn: jest.Mock;
19+
}
20+
}
21+

apps/rush-lib/typings/tsd.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
/// <reference path="builtins/builtins.d.ts" />
55
/// <reference path="glob-escape/glob-escape.d.ts" />
6+
/// <reference path="mocks/child_process.d.ts" />
67
/// <reference path="npm-package-arg/npm-package-arg.d.ts" />
78
/// <reference path="read-package-tree/read-package-tree.d.ts" />
89
/// <reference path="strict-uri-encode/strict-uri-encode.d.ts" />

0 commit comments

Comments
 (0)