|
1 | 1 | import cp = require('child_process');
|
2 | 2 | import fs = require('fs');
|
3 |
| -import path = require('path'); |
| 3 | +import ncp = require('child_process'); |
4 | 4 | import os = require('os');
|
| 5 | +import path = require('path'); |
5 | 6 | import cmdm = require('./taskcommand');
|
6 | 7 | import shelljs = require('shelljs');
|
| 8 | +import syncRequest = require('sync-request'); |
7 | 9 |
|
8 | 10 | const COMMAND_TAG = '[command]';
|
9 | 11 | const COMMAND_LENGTH = COMMAND_TAG.length;
|
| 12 | +const downloadDirectory = path.join(process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE, 'azure-pipelines-task-lib', '_download'); |
10 | 13 |
|
11 | 14 | export class MockTestRunner {
|
12 |
| - constructor(private _testPath: string) { |
| 15 | + constructor(testPath: string, taskJsonPath?: string) { |
| 16 | + this._taskJsonPath = taskJsonPath || ''; |
| 17 | + this._testPath = testPath; |
| 18 | + this.nodePath = this.getNodePath(); |
13 | 19 | }
|
14 | 20 |
|
| 21 | + private _testPath = ''; |
| 22 | + private _taskJsonPath = ''; |
| 23 | + public nodePath = ''; |
15 | 24 | public stdout = '';
|
16 |
| - public stderr = '' |
| 25 | + public stderr = ''; |
17 | 26 | public cmdlines = {};
|
18 | 27 | public invokedToolCount = 0;
|
19 | 28 | public succeeded = false;
|
@@ -44,22 +53,18 @@ export class MockTestRunner {
|
44 | 53 | return this.stderr.indexOf(message) > 0;
|
45 | 54 | }
|
46 | 55 |
|
47 |
| - public run(): void { |
| 56 | + public run(nodeVersion?: number): void { |
48 | 57 | this.cmdlines = {};
|
49 | 58 | this.invokedToolCount = 0;
|
50 | 59 | this.succeeded = true;
|
51 | 60 |
|
52 | 61 | this.errorIssues = [];
|
53 | 62 | this.warningIssues = [];
|
54 | 63 |
|
55 |
| - // we use node in the path. |
56 |
| - // if you want to test with a specific node, ensure it's in the path |
57 |
| - let nodePath = shelljs.which('node'); |
58 |
| - if (!nodePath) { |
59 |
| - console.error('Could not find node in path'); |
60 |
| - return; |
| 64 | + let nodePath = this.nodePath; |
| 65 | + if (nodeVersion) { |
| 66 | + nodePath = this.getNodePath(nodeVersion); |
61 | 67 | }
|
62 |
| - |
63 | 68 | let spawn = cp.spawnSync(nodePath, [this._testPath]);
|
64 | 69 | if (spawn.error) {
|
65 | 70 | console.error('Running test failed');
|
@@ -123,4 +128,184 @@ export class MockTestRunner {
|
123 | 128 | console.log('TRACE FILE: ' + traceFile);
|
124 | 129 | }
|
125 | 130 | }
|
| 131 | + |
| 132 | + // Returns a path to node.exe with the correct version for this task (based on if its node10 or node) |
| 133 | + private getNodePath(nodeVersion?: number): string { |
| 134 | + const version: number = nodeVersion || this.getNodeVersion(); |
| 135 | + |
| 136 | + let downloadVersion: string; |
| 137 | + switch (version) { |
| 138 | + case 5: |
| 139 | + downloadVersion = 'v5.10.1'; |
| 140 | + break; |
| 141 | + case 6: |
| 142 | + downloadVersion = 'v6.10.3'; |
| 143 | + break; |
| 144 | + case 10: |
| 145 | + downloadVersion = 'v10.15.1'; |
| 146 | + break; |
| 147 | + default: |
| 148 | + throw new Error('Invalid node version, must be 5, 6, or 10 (received ' + version + ')'); |
| 149 | + } |
| 150 | + |
| 151 | + // Install node in home directory if it isn't already there. |
| 152 | + const downloadDestination: string = path.join(downloadDirectory, 'node' + version); |
| 153 | + const pathToExe: string = this.getPathToNodeExe(downloadVersion, downloadDestination); |
| 154 | + if (pathToExe) { |
| 155 | + return pathToExe; |
| 156 | + } |
| 157 | + else { |
| 158 | + return this.downloadNode(downloadVersion, downloadDestination); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + // Determines the correct version of node to use based on the contents of the task's task.json. Defaults to Node 10. |
| 163 | + private getNodeVersion(): number { |
| 164 | + const taskJsonPath: string = this.getTaskJsonPath(); |
| 165 | + if (!taskJsonPath) { |
| 166 | + console.warn('Unable to find task.json, defaulting to use Node 10'); |
| 167 | + return 10; |
| 168 | + } |
| 169 | + const taskJsonContents = fs.readFileSync(taskJsonPath, { encoding: 'utf-8' }); |
| 170 | + const taskJson: object = JSON.parse(taskJsonContents); |
| 171 | + |
| 172 | + let nodeVersionFound = false; |
| 173 | + const execution: object = taskJson['execution']; |
| 174 | + const keys = Object.keys(execution); |
| 175 | + for (let i = 0; i < keys.length; i++) { |
| 176 | + if (keys[i].toLowerCase() == 'node10') { |
| 177 | + // Prefer node 10 and return immediately. |
| 178 | + return 10; |
| 179 | + } |
| 180 | + else if (keys[i].toLowerCase() == 'node') { |
| 181 | + nodeVersionFound = true; |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + if (!nodeVersionFound) { |
| 186 | + console.warn('Unable to determine execution type from task.json, defaulting to use Node 10'); |
| 187 | + return 10; |
| 188 | + } |
| 189 | + |
| 190 | + return 6; |
| 191 | + } |
| 192 | + |
| 193 | + // Returns the path to the task.json for the task being tested. Returns null if unable to find it. |
| 194 | + // Searches by moving up the directory structure from the initial starting point and checking at each level. |
| 195 | + private getTaskJsonPath(): string { |
| 196 | + if (this._taskJsonPath) { |
| 197 | + return this._taskJsonPath; |
| 198 | + } |
| 199 | + let curPath: string = this._testPath; |
| 200 | + let newPath: string = path.join(this._testPath, '..'); |
| 201 | + while (curPath != newPath) { |
| 202 | + curPath = newPath; |
| 203 | + let taskJsonPath: string = path.join(curPath, 'task.json'); |
| 204 | + if (fs.existsSync(taskJsonPath)) { |
| 205 | + return taskJsonPath; |
| 206 | + } |
| 207 | + newPath = path.join(curPath, '..'); |
| 208 | + } |
| 209 | + return ''; |
| 210 | + } |
| 211 | + |
| 212 | + // Downloads the specified node version to the download destination. Returns a path to node.exe |
| 213 | + private downloadNode(nodeVersion: string, downloadDestination: string): string { |
| 214 | + shelljs.rm('-rf', downloadDestination); |
| 215 | + const nodeUrl: string = 'https://nodejs.org/dist'; |
| 216 | + let downloadPath = ''; |
| 217 | + switch (this.getPlatform()) { |
| 218 | + case 'darwin': |
| 219 | + this.downloadTarGz(nodeUrl + '/' + nodeVersion + '/node-' + nodeVersion + '-darwin-x64.tar.gz', downloadDestination); |
| 220 | + downloadPath = path.join(downloadDestination, 'node-' + nodeVersion + '-darwin-x64', 'bin', 'node'); |
| 221 | + break; |
| 222 | + case 'linux': |
| 223 | + this.downloadTarGz(nodeUrl + '/' + nodeVersion + '/node-' + nodeVersion + '-linux-x64.tar.gz', downloadDestination); |
| 224 | + downloadPath = path.join(downloadDestination, 'node-' + nodeVersion + '-linux-x64', 'bin', 'node'); |
| 225 | + break; |
| 226 | + case 'win32': |
| 227 | + this.downloadFile(nodeUrl + '/' + nodeVersion + '/win-x64/node.exe', downloadDestination, 'node.exe'); |
| 228 | + this.downloadFile(nodeUrl + '/' + nodeVersion + '/win-x64/node.lib', downloadDestination, 'node.lib'); |
| 229 | + downloadPath = path.join(downloadDestination, 'node.exe') |
| 230 | + } |
| 231 | + |
| 232 | + // Write marker to indicate download completed. |
| 233 | + const marker = downloadDestination + '.completed'; |
| 234 | + fs.writeFileSync(marker, ''); |
| 235 | + |
| 236 | + return downloadPath; |
| 237 | + } |
| 238 | + |
| 239 | + // Downloads file to the downloadDestination, making any necessary folders along the way. |
| 240 | + private downloadFile(url: string, downloadDestination: string, fileName: string): void { |
| 241 | + const filePath: string = path.join(downloadDestination, fileName); |
| 242 | + if (!url) { |
| 243 | + throw new Error('Parameter "url" must be set.'); |
| 244 | + } |
| 245 | + if (!downloadDestination) { |
| 246 | + throw new Error('Parameter "downloadDestination" must be set.'); |
| 247 | + } |
| 248 | + console.log('Downloading file:', url); |
| 249 | + shelljs.mkdir('-p', downloadDestination); |
| 250 | + const result: any = syncRequest('GET', url); |
| 251 | + fs.writeFileSync(filePath, result.getBody()); |
| 252 | + } |
| 253 | + |
| 254 | + // Downloads tarGz to the download destination, making any necessary folders along the way. |
| 255 | + private downloadTarGz(url: string, downloadDestination: string): void { |
| 256 | + if (!url) { |
| 257 | + throw new Error('Parameter "url" must be set.'); |
| 258 | + } |
| 259 | + if (!downloadDestination) { |
| 260 | + throw new Error('Parameter "downloadDestination" must be set.'); |
| 261 | + } |
| 262 | + const tarGzName: string = 'node.tar.gz'; |
| 263 | + this.downloadFile(url, downloadDestination, tarGzName); |
| 264 | + |
| 265 | + // Extract file |
| 266 | + const originalCwd: string = process.cwd(); |
| 267 | + process.chdir(downloadDestination); |
| 268 | + try { |
| 269 | + ncp.execSync(`tar -xzf "${path.join(downloadDestination, tarGzName)}"`); |
| 270 | + } |
| 271 | + catch { |
| 272 | + throw new Error('Failed to unzip node tar.gz from ' + url); |
| 273 | + } |
| 274 | + finally { |
| 275 | + process.chdir(originalCwd); |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + // Checks if node is installed at downloadDestination. If it is, returns a path to node.exe, otherwise returns null. |
| 280 | + private getPathToNodeExe(nodeVersion: string, downloadDestination: string): string { |
| 281 | + let exePath = ''; |
| 282 | + switch (this.getPlatform()) { |
| 283 | + case 'darwin': |
| 284 | + exePath = path.join(downloadDestination, 'node-' + nodeVersion + '-darwin-x64', 'bin', 'node'); |
| 285 | + break; |
| 286 | + case 'linux': |
| 287 | + exePath = path.join(downloadDestination, 'node-' + nodeVersion + '-linux-x64', 'bin', 'node'); |
| 288 | + break; |
| 289 | + case 'win32': |
| 290 | + exePath = path.join(downloadDestination, 'node.exe'); |
| 291 | + } |
| 292 | + |
| 293 | + // Only use path if marker is found indicating download completed successfully (and not partially) |
| 294 | + const marker = downloadDestination + '.completed'; |
| 295 | + |
| 296 | + if (fs.existsSync(exePath) && fs.existsSync(marker)) { |
| 297 | + return exePath; |
| 298 | + } |
| 299 | + else { |
| 300 | + return ''; |
| 301 | + } |
| 302 | + } |
| 303 | + |
| 304 | + private getPlatform(): string { |
| 305 | + let platform: string = os.platform(); |
| 306 | + if (platform != 'darwin' && platform != 'linux' && platform != 'win32') { |
| 307 | + throw new Error('Unexpected platform: ' + platform); |
| 308 | + } |
| 309 | + return platform; |
| 310 | + } |
126 | 311 | }
|
0 commit comments