Skip to content

Commit 6fc4177

Browse files
committed
Reliably end test tasks in Azure Pipelines (#5410)
For #5129
1 parent 21d28f9 commit 6fc4177

File tree

6 files changed

+158
-33
lines changed

6 files changed

+158
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ npm-debug.log
1616
coverage/
1717
.vscode-test/**
1818
**/.venv*/
19+
port.txt
1920
precommit.hook
2021
pythonFiles/experimental/ptvsd/**
2122
pythonFiles/lib/**

news/2 Fixes/5129.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reliably end test tasks in Azure Pipelines.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,10 +2115,10 @@
21152115
"test:unittests:cover": "npm run test:unittests",
21162116
"test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts",
21172117
"test:functional:cover": "npm run test:functional",
2118-
"testDebugger": "node ./out/test/debuggerTest.js",
2119-
"testSingleWorkspace": "node ./out/test/standardTest.js",
2120-
"testMultiWorkspace": "node ./out/test/multiRootTest.js",
2121-
"testPerformance": "node ./out/test/performanceTest.js",
2118+
"testDebugger": "node ./out/test/testBootstrap.js ./out/test/debuggerTest.js",
2119+
"testSingleWorkspace": "node ./out/test/testBootstrap.js ./out/test/standardTest.js",
2120+
"testMultiWorkspace": "node ./out/test/testBootstrap.js ./out/test/multiRootTest.js",
2121+
"testPerformance": "node ./out/test/testBootstrap.js ./out/test/performanceTest.js",
21222122
"testSmoke": "node ./out/test/smokeTest.js",
21232123
"lint-staged": "node gulpfile.js",
21242124
"lint": "tslint src/**/*.ts -t verbose",

src/test/common/exitCIAfterTestReporter.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,65 @@
88
// The hack is to force it to die when tests are done, if this doesn't work we've got a bigger problem on our hands.
99

1010
// tslint:disable:no-var-requires no-require-imports no-any no-console no-unnecessary-class no-default-export
11-
const log = require('why-is-node-running');
11+
import * as fs from 'fs-extra';
12+
import * as net from 'net';
13+
import * as path from 'path';
14+
import { EXTENSION_ROOT_DIR } from '../../client/constants';
15+
import { noop } from '../core';
16+
17+
let client: net.Socket | undefined;
1218
const mochaTests: any = require('mocha');
1319
const { EVENT_RUN_BEGIN, EVENT_RUN_END } = mochaTests.Runner.constants;
20+
21+
async function connectToServer() {
22+
const portFile = path.join(EXTENSION_ROOT_DIR, 'port.txt');
23+
if (!(await fs.pathExists(portFile))) {
24+
return;
25+
}
26+
const port = parseInt(await fs.readFile(portFile, 'utf-8'), 10);
27+
console.log(`Need to connect to port ${port}`);
28+
return new Promise(resolve => {
29+
try {
30+
client = new net.Socket();
31+
client.connect({ port }, () => {
32+
console.log(`Connected to port ${port}`);
33+
resolve();
34+
});
35+
} catch {
36+
console.error('Failed to connect to socket server to notify completion of tests');
37+
resolve();
38+
}
39+
});
40+
}
41+
function notifyCompleted(hasFailures: boolean) {
42+
if (!client || client.destroyed || !client.writable) {
43+
console.error('No client to write from');
44+
return;
45+
}
46+
try {
47+
const exitCode = hasFailures ? 1 : 0;
48+
console.log(`Notify server of test completion with code ${exitCode}`);
49+
// If there are failures, send a code of 1 else 0.
50+
client.write(exitCode.toString());
51+
client.end();
52+
console.log('Notified server of test completion');
53+
} catch (ex) {
54+
console.error('Socket client error', ex);
55+
}
56+
}
57+
1458
class ExitReporter {
1559
constructor(runner: any) {
1660
console.log('Initialize Exit Reporter for Mocha (PVSC).');
61+
connectToServer().catch(noop);
1762
const stats = runner.stats;
1863
runner
1964
.once(EVENT_RUN_BEGIN, () => {
2065
console.info('Start Exit Reporter for Mocha.');
2166
})
22-
.once(EVENT_RUN_END, () => {
23-
process.stdout.cork();
67+
.once(EVENT_RUN_END, async () => {
68+
notifyCompleted(stats.failures > 0);
2469
console.info('End Exit Reporter for Mocha.');
25-
process.stdout.write('If process does not die in 30s, then log and kill.');
26-
process.stdout.uncork();
27-
// NodeJs generally waits for pending timeouts, however the process running Mocha
28-
// No idea why it times, out. Once again, this is a hack.
29-
// Solution (i.e. hack), lets add a timeout with a delay of 30 seconds,
30-
// & if this process doesn't die, lets kill it.
31-
function die() {
32-
setTimeout(() => {
33-
console.info('Exiting from custom PVSC Mocha Reporter.');
34-
try {
35-
log();
36-
} catch (ex) {
37-
// Do nothing.
38-
}
39-
process.exit(stats.failures === 0 ? 0 : 1);
40-
try {
41-
// Lets just close VSC, hopefully that'll be sufficient (more graceful).
42-
const vscode = require('vscode');
43-
vscode.commands.executeCommand('workbench.action.closeWindow');
44-
} catch (ex) {
45-
// Do nothing.
46-
}
47-
}, 30000);
48-
}
49-
die();
5070
});
5171
}
5272
}

src/test/debugger/attach.ptvsd.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { IS_WINDOWS } from '../../client/common/platform/constants';
1616
import { FileSystem } from '../../client/common/platform/fileSystem';
1717
import { IPlatformService } from '../../client/common/platform/types';
1818
import { IConfigurationService } from '../../client/common/types';
19+
import { MultiStepInputFactory } from '../../client/common/utils/multiStepInput';
1920
import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants';
2021
import { PythonDebugConfigurationService } from '../../client/debugger/extension/configuration/debugConfigurationService';
2122
import { AttachConfigurationResolver } from '../../client/debugger/extension/configuration/resolvers/attach';
@@ -25,7 +26,6 @@ import { IServiceContainer } from '../../client/ioc/types';
2526
import { PYTHON_PATH, sleep } from '../common';
2627
import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
2728
import { continueDebugging, createDebugAdapter } from './utils';
28-
import { MultiStepInputFactory } from '../../client/common/utils/multiStepInput';
2929

3030
// tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement no-unused-variable no-console
3131
const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py');

src/test/testBootstrap.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { ChildProcess, spawn, SpawnOptions } from 'child_process';
7+
import * as fs from 'fs-extra';
8+
import { createServer, Server } from 'net';
9+
import * as path from 'path';
10+
import { EXTENSION_ROOT_DIR } from '../client/constants';
11+
import { noop, sleep } from './core';
12+
13+
// tslint:disable:no-console
14+
15+
/*
16+
This is a simple work around for tests tasks not completing on Azure Pipelines.
17+
What's been happening is, the tests run however for some readon the Node propcess (VS Code) does not exit.
18+
Here's what we've tried thus far:
19+
* Dispose all timers
20+
* Close all open streams/sockets.
21+
* Use `process.exit` and use the VSC commands to close itself.
22+
23+
Final solution:
24+
* Start a node.js procecss
25+
* This process will start a socket server
26+
* This procecss will start the tests in a separate procecss (spawn)
27+
* When the tests have completed,
28+
* Send a message to the socket server with a flag (true/false whether tests passed/failed)
29+
* Socket server (main procecss) will receive the test status flag.
30+
* This will kill the spawned process
31+
* This main process will kill itself with exit code 0 if tests pass succesfully, else 1.
32+
*/
33+
34+
const testFile = process.argv[2];
35+
const portFile = path.join(EXTENSION_ROOT_DIR, 'port.txt');
36+
37+
let proc: ChildProcess | undefined;
38+
let server: Server | undefined;
39+
40+
async function deletePortFile() {
41+
try {
42+
if (await fs.pathExists(portFile)) {
43+
await fs.unlink(portFile);
44+
}
45+
} catch {
46+
noop();
47+
}
48+
}
49+
async function end(exitCode: number) {
50+
if (exitCode === 0) {
51+
console.log('Exiting without errors');
52+
} else {
53+
console.error('Exiting with test failures');
54+
}
55+
if (proc) {
56+
try {
57+
const procToKill = proc;
58+
proc = undefined;
59+
console.log('Killing VSC');
60+
await deletePortFile();
61+
// Wait for the std buffers to get flushed before killing.
62+
await sleep(5_000);
63+
procToKill.kill();
64+
} catch {
65+
noop();
66+
}
67+
}
68+
if (server) {
69+
server.close();
70+
}
71+
// Exit with required code.
72+
process.exit(exitCode);
73+
}
74+
75+
async function startSocketServer() {
76+
return new Promise(resolve => {
77+
server = createServer(socket => {
78+
socket.on('data', buffer => {
79+
const data = buffer.toString('utf8');
80+
console.log(`Exit code from Tests is ${data}`);
81+
const code = parseInt(data.substring(0, 1), 10);
82+
end(code).catch(noop);
83+
});
84+
});
85+
86+
server.listen({ host: '127.0.0.1', port: 0 }, async () => {
87+
const port = server!.address().port;
88+
console.log(`Test server listening on port ${port}`);
89+
await deletePortFile();
90+
await fs.writeFile(portFile, port.toString());
91+
resolve();
92+
});
93+
});
94+
}
95+
96+
async function start() {
97+
await startSocketServer();
98+
const options: SpawnOptions = { cwd: process.cwd(), env: process.env, detached: true, stdio: 'inherit' };
99+
proc = spawn(process.execPath, [testFile], options);
100+
proc.once('close', end);
101+
}
102+
103+
start().catch(ex => console.error(ex));

0 commit comments

Comments
 (0)