-
Notifications
You must be signed in to change notification settings - Fork 2
/
e2e-runner.ts
308 lines (261 loc) · 11 KB
/
e2e-runner.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
#!/usr/bin/env node
import {commandUtils} from '@gkalpak/cli-utils';
import {Stats} from 'fs';
import {join, resolve as resolvePath} from 'path';
import {cat, ls, mv, rm, set, test} from 'shelljs';
import {cyan} from './string-utils';
set('-e');
// #region Documentation
// tslint:disable: max-line-length
/**
* ## Usage:
* ```
* node out/test/helpers/e2e-runner "<run-tests-cmd>" "<code-version>"
* ```
*
* Where:
* - `<run-tests-cmd>`: The command to use for actually running the tests. E.g.: `npm run test-e2e`
* - `<code-version>`: The version of VSCode to run the tests with. E.g.: `1.33.7`
* (default: any) Special values:
* - `earliest`: Infer the earliest supported version, based on the version pattern in
* `package.json > engines > vscode`. It supports many common formats (e.g.
* `1.2.3` `^1.2.3`/`~1.2.3`, `1.2.x`, `1.2`), but not everything (e.g. `>`, `<`,
* version ranges).
* - `latest`: Always fetch the latest VSCode version. The script first determines what the
* latest version is (for the current platform) and avoids downloading it if it is
* already available in `.vscode-test/`. If not, VSCode will handle fetching the
* latest version, before running the tests.
* - `any`/`*`: Use the current version (i.e. `.vscode-test/stable/`) if one exists, else treat
* it as `latest`.
*
*
* ## What it does
* - Determine the actual targeted version.
* Normally, this is the `<code-version>` argument, but there are some special values (see above).
* - Does the specified version match the current one (i.e. the one in `.vscode-test/stable/` - if any)?
* - YES:
* - Run the tests command.
* - NO:
* - If `.vscode-test/stable/` exists, rename it to `stable-stashed`.
* - If `.vscode-test/stable-<version>/` exists, rename it to `stable`.
* - Run the tests command (setting the `CODE_VERSION` environment variable to `<version>`).
* - Is there a stashed version (i.e. does `.vscode-test/stable-stashed/` exist)?
* - YES:
* - Back the current version up for future use (i.e. rename `stable` to `stable-<version>`).
* - Restore the stashed version (i.e. rename `stable-stashed` to `stable`).
* - Ensure there are no more than `MAX_BACKUPS` backed-up versions (i.e. `stable-<some-version>` directories) in
* `.vscode-test/` to avoid bloat (e.g. in case `.vscode-test/` is cached CI).
* (Uses `ctime` to determine the least recently used directories.)
*
*
* ## Why
* VScode does not respect the `CODE_DOWNLOAD_URL`/`CODE_VERSION` [environment variables][1], if there is already a
* version available in `.vscode-test/`. Yet, sometimes it is desirable to run tests on specific (or multiple) versions,
* e.g. on both the latest and the minimum supported one.
*
* See [this issue][2] for more details.
*
*
* ## Limitations
* - Currently, only stable versions are supported. Not `insiders`.
*
* [1]: https://github.com/Microsoft/vscode-docs/blob/dc1792fded1fba89f6914a79ffd974d35c3e2869/docs/extensions/testing-extensions.md#running-tests-automatically-on-travis-ci-build-machines
* [2]: https://github.com/Microsoft/vscode-extension-vscode/issues/96
*/
// tslint:enable: max-line-length
// #endregion
// Constants
const CURRENT_APP_DIR_NAME = 'stable';
const STASHED_APP_DIR_NAME = `${CURRENT_APP_DIR_NAME}-stashed`;
const MAX_BACKUPS = 3;
const ROOT_DIR = resolvePath(__dirname, '../../..');
const VSCODE_DIR = join(ROOT_DIR, '.vscode-test');
const CURRENT_APP_DIR = join(VSCODE_DIR, CURRENT_APP_DIR_NAME);
const STASHED_APP_DIR = join(VSCODE_DIR, STASHED_APP_DIR_NAME);
// Classes / Interfaces
interface ILsLReturnType extends Stats {
name: string;
}
interface ICleanUpable {
cleanUp(): void;
}
class CleanUpBackups implements ICleanUpable {
public cleanUp(): void {
// Get all existing backups, sort them in descending `ctime` order, and ignore the `MAX_BACKUPS` first.
const excessBackups = (ls('-l', VSCODE_DIR) as any as ILsLReturnType[]).
filter(dir => dir.isDirectory() && !this.isIgnoredDir(dir.name)).
sort((a, b) => b.ctimeMs - a.ctimeMs).
slice(MAX_BACKUPS);
// Delete old, unused backups.
excessBackups.forEach(dir => rmRf(join(VSCODE_DIR, dir.name)));
}
private isIgnoredDir(dirName: string): boolean {
return (dirName === CURRENT_APP_DIR_NAME) || (dirName === STASHED_APP_DIR_NAME);
}
}
class CleanUpRename implements ICleanUpable {
constructor(private from: string, private to: string = '') {
}
public cleanUp(): void {
if (!this.to) {
const version = getCurrentVersion() || '';
this.to = version && getBackupDir(version);
}
if (this.to && test('-e', this.from)) {
move(this.from, this.to);
}
}
}
// Run
if (require.main === module) {
_main(process.argv.slice(2));
}
// Exports
export function getCurrentVersion(): string | null {
return getVersion(CURRENT_APP_DIR);
}
export async function runTests(runTestsCmd: string, codeVersion = 'any'): Promise<void> {
const cleanUpItems: ICleanUpable[] = [];
try {
// Register a clean-up task to remove old, unused backups from `.vscode-test/`.
cleanUpItems.push(new CleanUpBackups());
// If a current version does not exist, check if a previous run crashed and left a version stashed.
if (!test('-d', CURRENT_APP_DIR) && test('-d', STASHED_APP_DIR)) {
move(STASHED_APP_DIR, CURRENT_APP_DIR);
}
// Get the current version (if any).
const originalCurrentVersion = getCurrentVersion();
// Resolve special `codeVersion` values.
codeVersion = await resolveCodeVersion(codeVersion, originalCurrentVersion);
// If the current version is the specified one, just run the tests.
if (originalCurrentVersion === codeVersion) {
await runTestsWithVersion(runTestsCmd, codeVersion);
return;
}
// If a current version exists, stack it away (and restore it later).
if (originalCurrentVersion) {
move(CURRENT_APP_DIR, STASHED_APP_DIR);
cleanUpItems.push(new CleanUpRename(STASHED_APP_DIR, CURRENT_APP_DIR));
}
// Check if the specified version is already available.
const codeVersionBackupDir = getBackupDir(codeVersion);
const isVersionAvailable = !!codeVersion &&
test('-d', codeVersionBackupDir) &&
(getVersion(codeVersionBackupDir) === codeVersion);
// Either way, if there was a current version originally, ensure the specified version
// (pre-existing or downloaded) is eventually backed up for future use.
if (originalCurrentVersion) {
// If no version specified, read it later from the downloaded version.
cleanUpItems.push(new CleanUpRename(CURRENT_APP_DIR, codeVersion && codeVersionBackupDir));
}
// If the specified version is available, make it the current version,
// run the tests, and finally restore directories.
if (isVersionAvailable) {
move(codeVersionBackupDir, CURRENT_APP_DIR);
}
// Run the tests with the specified version (either pre-existing or downloaded).
await runTestsWithVersion(runTestsCmd, codeVersion);
} finally {
// Restore any moved directories in reverse order.
// (Wait for resources to be released by previous commands first.)
await new Promise(res => setTimeout(res, 1000));
cleanUpItems.reverse().forEach(item => item.cleanUp());
}
}
// Helpers
function _main([runTestsCmd, codeVersion]: string[]): void {
runTests(runTestsCmd, codeVersion).catch(err => {
console.error(`Error: ${err}`);
process.exit(1);
});
}
function debugMessage(msg: string): void {
console.debug(`${cyan('[e2e-runner]')} ${msg}`);
}
function getBackupDir(version: string): string {
return `${CURRENT_APP_DIR}-${version}`;
}
function getEarliestVersion(): string {
debugMessage('Getting earliest supported version...');
let {engines: {vscode: version}} = require(`${ROOT_DIR}/package.json`);
// Replace leading symbols (e.g. `^`, `~`, `>=`).
version = version.replace(/^\D+/, '');
// Replace `.x` with `.0`.
version = version.replace(/\.x/gi, '.0');
// Ensure enough `.D` parts are present (e.g. `1.2` --> `1.2.0`).
version = version.replace(/^\d+$/, '$&.0.0').replace(/^\d+\.\d+$/, '$&.0');
return version;
}
async function getLatestVersion(): Promise<string> {
// URL taken from
// https://github.com/Microsoft/vscode-extension-vscode/blob/47f02ad0618869686599ddec6f4239dbce487802/bin/test#L133.
const platformId = (process.platform === 'darwin') ?
'darwin' : (process.platform === 'win32') ?
'win32-archive' :
'linux-x64';
const releasesUrl = `https://vscode-update.azurewebsites.net/api/releases/stable/${platformId}`;
const response = await httpsGet(releasesUrl);
const releases = JSON.parse(response);
return releases[0] || '';
}
function getVersion(appDir: string): string | null {
try {
const resourcesDir = (process.platform === 'darwin') ?
'Visual Studio Code.app/Contents/Resources/' : (process.platform === 'win32') ?
'resources/' :
'VSCode-linux-x64/resources/';
const pkgPath = join(appDir, resourcesDir, 'app/package.json');
const pkgObj = JSON.parse(cat(pkgPath));
return pkgObj.version || null;
} catch (_ignored) {
return null;
}
}
async function httpsGet(url: string): Promise<string> {
const https = await import('https');
debugMessage(`Fetching '${url}'...`);
return new Promise<string>((resolve, reject) => https.
get(url, response => {
const statusCode = response.statusCode || -1;
const isSuccess = (200 <= statusCode) && (statusCode < 300);
let responseText = '';
response.
on('error', reject).
on('data', d => responseText += d).
on('end', () => isSuccess ?
resolve(responseText) :
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`));
}).
on('error', reject));
}
function move(from: string, to: string): void {
debugMessage(`Moving '${from}' to '${to}'...`);
mv(from, to);
}
async function resolveCodeVersion(codeVersion: string, currentVersion: string | null): Promise<string> {
switch (codeVersion) {
case '*':
case 'any':
return currentVersion || await getLatestVersion();
case 'earliest':
return getEarliestVersion();
case 'latest':
return await getLatestVersion();
default:
return codeVersion;
}
}
function rmRf(dir: string): void {
debugMessage(`Deleting directory '${dir}'...`);
rm('-rf', dir);
}
async function runTestsWithVersion(runTestsCmd: string, codeVersion: string): Promise<void> {
const originalCodeVersion = process.env.CODE_VERSION;
process.env.CODE_VERSION = codeVersion;
try {
debugMessage(`Running tests (specified version: ${codeVersion || '-'})...`);
await commandUtils.spawnAsPromised(runTestsCmd);
} finally {
process.env.CODE_VERSION = originalCodeVersion;
}
}