Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure new runs by deleting current runner instance when server calls stop function #200

16 changes: 12 additions & 4 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ export async function startServer({ log, server, command, exitOnError }: ServerO
print(displayClient(ws), color("DISCONNECTION"), `${msg} (code = ${code})`);
if (code !== 1000 && exitOnError) {
print(displayClient(ws), chalk.red("EXITTING"), "after an abnormal disconnect");
process.exitCode = 1;
if (typeof process.exitCode !== "number") {
process.exitCode = 1;
}
cleanup();
}
});
Expand All @@ -132,7 +134,9 @@ export async function startServer({ log, server, command, exitOnError }: ServerO
}
// Exit right away
if (exitOnError) {
process.exitCode = 1;
if (typeof process.exitCode !== "number") {
process.exitCode = 1;
}
cleanup();
}
});
Expand Down Expand Up @@ -343,15 +347,19 @@ export function run(args = hideBin(process.argv)): void {
if (!argv.watch) {
// Run once and exit with the failures as exit code
server.run(failures => {
process.exitCode = failures;
if (typeof process.exitCode !== "number") {
process.exitCode = failures;
}
cleanup();
});
}
},
err => {
/* eslint-disable-next-line no-console */
console.error(chalk.red("ERROR"), err.message);
process.exitCode = 1;
if (typeof process.exitCode !== "number") {
process.exitCode = 1;
}
}
);
})
Expand Down
12 changes: 12 additions & 0 deletions packages/integration-tests/fixtures/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Client } from "mocha-remote-client"

const wait = parseInt(process.argv.pop() || "0", 10);

new Client({
autoConnect: true,
tests: () => {
it("should reject", (done) => {
setTimeout(done, wait);
});
}
});
112 changes: 112 additions & 0 deletions packages/integration-tests/src/disconnecting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import cp from "child_process";
import { expect } from "chai";
import { resolve } from "path";

import { Server } from "mocha-remote-server";

const TEST_CLIENT_PATH = resolve(__dirname, "../fixtures/client.ts");

/**
* @returns a promise of a child process for a client resolving when the client connects to the server.
*/
function startClient(server: Server, timeout: number) {
const clientProcess = cp.spawn(
process.execPath,
["--import", "tsx", TEST_CLIENT_PATH, timeout.toString()],
{ stdio: "inherit", env: { ...process.env, FORCE_COLOR: "false" }, timeout: 5_000 },
);
return new Promise<cp.ChildProcess>((resolve) => {
server.once("connection", () => resolve(clientProcess));
});
}

/**
* Stop a client previously started with {@link startClient}.
* @returns a promise of the client process exiting.
*/
function stopClient(clientProcess: cp.ChildProcess) {
const result = new Promise((resolve) => clientProcess.once("exit", resolve));
clientProcess.kill();
return result;
}

describe("disconnecting a client", () => {
it("should be able to start again, when the server is stopped during a run", async function() {
this.timeout(10000);
// Create and start the server
const server = new Server({
port: 8090,
reporter: "base",
autoRun: false,
});

await server.start();
const running = new Promise<unknown>(resolve => server.once("running", resolve));

// Start a client waiting longer then the test timeout
const childClientProcess = await startClient(server, this.timeout());

// Starting a run, which will be stopped before it completes naturally
let completed = false;
server.run(() => {
completed = true;
});

// Wait for the tests to start running
await running;

// Stop and restart the server
await server.stop();
await server.start();

// Finally stop the server and client
await server.stop();
await stopClient(childClientProcess);

// Stopping the server will call the callback passed to run
expect(completed).to.equal(false);
});

it("should be able to start again, on client disconnection while running", async function() {
this.timeout(10000);
// Create and start the server
const server = new Server({
port: 8090,
reporter: "base",
autoRun: false,
});

await server.start();
{
// Start a client waiting longer then the test timeout
const childClientProcess = await startClient(server, this.timeout() * 2);

// Abort the run by disconnecting the client
let completed = false;
server.run(() => {
completed = true
});
// Wait for the server to start running
await new Promise(resolve => server.once("running", resolve));
// Disconnect the client while running
await stopClient(childClientProcess);
// Expect no completion
expect(completed).to.equal(false);
}

{
// Connect with a new client
// Start a client completing fast
const childClientProcess = await startClient(server, 0);

// Run the tests again to completion
const result = await new Promise<number>((resolve) => server.run(resolve));
expect(result).to.equal(0);

// Stop the server and the client
await stopClient(childClientProcess);
}

await server.stop();
});
});
43 changes: 32 additions & 11 deletions packages/server/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export class Server extends ServerEventEmitter {
}
// Close the server
this.wss.close(err => {
this.handleReset();
// Forget about the server
delete this.wss;
// Reject or resolve the promise
Expand Down Expand Up @@ -198,11 +199,6 @@ export class Server extends ServerEventEmitter {
// Attach event listeners to update stats
createStatsCollector(this.runner as Mocha.Runner);

// Bind listeners to the runner, re-emitting events on the server itself
this.runner.on("end", () => {
this.emit("end");
});

// Set the client options, to be passed to the next running client
this.clientOptions = {
grep: this.config.grep,
Expand Down Expand Up @@ -238,15 +234,17 @@ export class Server extends ServerEventEmitter {
}
};

// Emit event when the tests starts running
this.runner.once(FakeRunner.constants.EVENT_RUN_BEGIN, () => {
this.emit("running", this.runner as Mocha.Runner);
});

// Attach a listener to the run ending
this.runner.once(FakeRunner.constants.EVENT_RUN_END, () => {
const failures = this.runner ? this.runner.failures : 0;
// Delete the runner to allow another run
delete this.runner;
// Get rid of the client options
delete this.clientOptions;
// Call any callbacks to signal completion
done(failures);
this.handleReset();
});

// If we already have a client, tell it to run
Expand Down Expand Up @@ -336,16 +334,17 @@ export class Server extends ServerEventEmitter {
}
if (this.client) {
this.debug("A client was already connected");
this.client.removeAllListeners();
this.client.close(
1013 /* try again later */,
"Got a connection from another client"
);
delete this.client;
// Reset the server to prepare for the incoming client
this.handleReset();
}
// Hang onto the client
this.client = ws;
this.client.on("message", this.handleMessage.bind(this, this.client));
this.client.once("close", this.handleReset);
// If we already have a runner, it can run now that we have a client
if (this.runner) {
if (this.clientOptions) {
Expand Down Expand Up @@ -398,6 +397,28 @@ export class Server extends ServerEventEmitter {
}
};

/**
* Resets the server for another test run.
*/
private handleReset = () => {
// Forget everything about the runner and the client
const { runner, client } = this;
delete this.runner;
delete this.client;
delete this.clientOptions;
if (runner) {
runner.removeAllListeners();
// Relay this onto the server itself
this.emit("end");
}
if (client) {
if (client.readyState !== WebSocket.CLOSED) {
client.terminate();
}
client.removeAllListeners();
}
};

/**
* @param reporter A constructor or a string containing the name of a builtin reporter or the module name or relative path of one.
* @returns A constructor for the reporter.
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/ServerEventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import type http from "http";
import type { Debugger } from "debug";

import type { Server } from "./Server";
import type { Runner } from "mocha";

export enum ServerEvents {
STARTED = "started",
RUNNING = "running",
CONNECTION = "connection",
DISCONNECTION = "disconnection",
ERROR = "error",
END = "end",
}

export type StartedListener = (server: Server) => void;
export type RunningListener = (runner: Runner) => void;
export type ConnectionListener = (ws: WebSocket, req: http.IncomingMessage) => void;
export type DisconnectionListener = (ws: WebSocket, code: number, reason: string) => void;
export type ErrorListener = (error: Error) => void;
Expand All @@ -26,8 +29,8 @@ export type MessageEvents = {
disconnection: DisconnectionListener,
error: ErrorListener,
end: EndListener,
/*
running: RunningListener,
/*
test: TestListener,
*/
}
Expand Down