Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/seven-geese-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@bigtest/cli": patch
"@bigtest/client": patch
---

Provide a nice error message when running tests without a server
18 changes: 17 additions & 1 deletion packages/cli/src/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,25 @@ import * as query from './query';
import { StreamingFormatter } from './format-helpers';

export function* runTest(config: ProjectOptions, formatter: StreamingFormatter): Operation<void> {
let client: Client = yield Client.create(`ws://localhost:${config.port}`);

let uri = `ws://localhost:${config.port}`;

let client: Client = yield function*() {
try {
return yield Client.create(uri);
} catch (e) {
if (e.name === 'ConnectionAttemptFailed') {
throw new MainError({
exitCode: 1,
message: `Could not connect to BigTest server on ${uri}. Run "bigtest server" to start the server.`
});
}
throw e;
}
};

let subscription = yield client.subscription(query.run());

let stepCounts = { ok: 0, failed: 0, disregarded: 0 };
let assertionCounts = { ok: 0, failed: 0, disregarded: 0 };
let testRunStatus: ResultStatus | undefined;
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ describe('@bigtest/cli', function() {
});

describe('test', () => {

describe('running without server', () => {
let runChild: Process;

beforeEach(async () => {
runChild = await World.spawn(run('test'));
await World.spawn(runChild.join());
});

afterEach(async () => {
await World.spawn(runChild.close());
});

it("provides a nice error with advice to start `bigtest server`", () => {
expect(runChild.stderr?.output).toContain('bigtest server');
});
});

describe('running the suite successfully', () => {
let startChild: Process;
let runChild: Process;
Expand Down
33 changes: 27 additions & 6 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { w3cwebsocket } from 'websocket';
import { resource, Operation } from 'effection';
import { resource, Operation, spawn } from 'effection';

import { ensure, Mailbox } from '@bigtest/effection';
import { Mailbox } from '@bigtest/effection';
import { on, once } from '@effection/events';

import { Message, isErrorResponse, isDataResponse, isDoneResponse } from './protocol';
import { ConnectionAttemptFailed } from './errors';

let responseIds = 0;

Expand All @@ -16,13 +17,25 @@ export class Client {
static *create(url: string): Operation<Client> {
let socket = new w3cwebsocket(url) as WebSocket;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but in order to make this work in the browser, we're going to have to loosely couple to our underlying websocket library. I wonder what is the best way to do that.


yield spawn(function* detectStartupError(): Operation<void> {
let [error] = yield once(socket, 'error');

if (isYaetiError(error)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call this isErrorEvent since Yaeti is just the nodejs error event emulator for the ErrorEvent. When the client is running on the browser, it will actually be an ErrorEvent

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that on the web, this will always be an error event.

throw new ConnectionAttemptFailed(`Could not connect to server at ${url}`);
} else {
throw error;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to imagine a scenario where we execute this code branch, and can't think of one other than a bug in the websocket library. Still, it's good to have in case there is and it will throw a big nasty stack trace.

}
});

let client = new Client(socket);
let res = yield resource(client, function*() {
yield ensure(() => socket.close());

let [{ reason, code }] = yield once(socket, 'close');
if(code !== 1000) {
try {
let [{ reason, code }] = yield once(socket, 'close');
if(code !== 1000) {
throw new Error(`websocket server closed connection unexpectedly: [${code}] ${reason}`);
}
} finally {
socket.close();
}
});

Expand Down Expand Up @@ -91,3 +104,11 @@ interface Query {
query: string;
live?: boolean;
}

interface YaetiError {
type: 'error';
}

function isYaetiError(error: { type?: 'error' }): error is YaetiError {
return error.type === 'error';
}
3 changes: 3 additions & 0 deletions packages/client/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class ConnectionAttemptFailed extends Error {
get name() { return 'ConnectionAttemptFailed' }
}
211 changes: 117 additions & 94 deletions packages/client/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,117 +5,140 @@ import { TestConnection, TestServer, run } from './helpers';
import { Client, Message, Response, isQuery } from '../src';

describe('@bigtest/client', () => {
let server: TestServer;
let client: Client;
let connection: TestConnection;
beforeEach(async () => {
server = await TestServer.start(3300);
let nextConnection = server.connection();
client = await run(Client.create('http://localhost:3300'));
connection = await nextConnection;
});

describe('sending a query', () => {
let message: Message | undefined;
let queryResponse: Promise<Response>;
describe('connecting without a server present', () => {
let error: Error;

beforeEach(async () => {
queryResponse = run(client.query('echo(message: "Hello World")'));
message = await connection.receive();
try {
await run(Client.create('http://localhost:3300'));
} catch (e) {
error = e;
}
});

it('is received on the server', () => {
expect(message).toBeDefined();
expect(message?.query).toEqual('echo(message: "Hello World")');
it('throws a ConnectionAttemptFailed error', () => {
expect(error).toHaveProperty('name', 'ConnectionAttemptFailed');
});

describe('when the server responds', () => {
let response: {};
});

describe('interacting with the server', () => {
let server: TestServer;
let client: Client;
let connection: TestConnection;

beforeEach(async () => {
server = await TestServer.start(3300);
let nextConnection = server.connection();
client = await run(Client.create('http://localhost:3300'));
connection = await nextConnection;
});

describe('sending a query', () => {
let message: Message | undefined;
let queryResponse: Promise<Response>;
beforeEach(async () => {
await connection.send({
done: true,
data: { echo: { message: "Hello World" }},
responseId: message?.responseId
});

response = await queryResponse;
queryResponse = run(client.query('echo(message: "Hello World")'));
message = await connection.receive();
});

it('returns the data to the original query', () => {
expect(response).toBeDefined();
expect(response).toEqual({echo: { message: "Hello World" }});
it('is received on the server', () => {
expect(message).toBeDefined();
expect(message?.query).toEqual('echo(message: "Hello World")');
});
});

describe('when the server responds with an error response', () => {
beforeEach(async() => {
await connection.send({
responseId: message?.responseId,
errors: [
{ message: 'failed' }
]
})

describe('when the server responds', () => {
let response: {};
beforeEach(async () => {
await connection.send({
done: true,
data: { echo: { message: "Hello World" }},
responseId: message?.responseId
});

response = await queryResponse;
});

it('returns the data to the original query', () => {
expect(response).toBeDefined();
expect(response).toEqual({echo: { message: "Hello World" }});
});
});

it('rejects the original response', async () => {
await expect(queryResponse).rejects.toEqual(new Error('failed'));

describe('when the server responds with an error response', () => {
beforeEach(async() => {
await connection.send({
responseId: message?.responseId,
errors: [
{ message: 'failed' }
]
})
});

it('rejects the original response', async () => {
await expect(queryResponse).rejects.toEqual(new Error('failed'));
});
});
});
});

describe('creating a live query', () => {
let mailbox: Mailbox<Response>;
let message: Message;
beforeEach(async () => {
mailbox = await run(client.liveQuery('echo(message: "Hello World")'));
message = await connection.receive() as Message;
});

it('sends the live query message to the server', () => {
expect(message).toBeDefined();
expect(message?.query).toEqual('echo(message: "Hello World")');
expect(isQuery(message)).toEqual(true);
});

describe('sending a result', () => {
let response: Response;


describe('creating a live query', () => {
let mailbox: Mailbox<Response>;
let message: Message;
beforeEach(async () => {
await connection.send({
responseId: message?.responseId,
data: {echo: { message: "Hello World" }}
})
response = await run(mailbox.receive());
mailbox = await run(client.liveQuery('echo(message: "Hello World")'));
message = await connection.receive() as Message;
});

it('is delivered to the query mailbox', () => {
expect(response).toBeDefined();
expect(response).toEqual({echo: { message: "Hello World" }});

it('sends the live query message to the server', () => {
expect(message).toBeDefined();
expect(message?.query).toEqual('echo(message: "Hello World")');
expect(isQuery(message)).toEqual(true);
});

describe('sending a result', () => {
let response: Response;

beforeEach(async () => {
await connection.send({
responseId: message?.responseId,
data: {echo: { message: "Hello World" }}
})
response = await run(mailbox.receive());
});

it('is delivered to the query mailbox', () => {
expect(response).toBeDefined();
expect(response).toEqual({echo: { message: "Hello World" }});
});
});
});
});

describe('sending a mutation', () => {
let message: Message;
let mutationResponse: Promise<Response>;

beforeEach(async () => {
mutationResponse = run(client.mutation('{ run }'));
message = await connection.receive() as Message;

expect(message?.mutation).toEqual('{ run }');
});

describe('when the sever responds', () => {

describe('sending a mutation', () => {
let message: Message;
let mutationResponse: Promise<Response>;

beforeEach(async () => {
await connection.send({
responseId: message?.responseId,
data: { run: 'TestRun:1' }
})
mutationResponse = run(client.mutation('{ run }'));
message = await connection.receive() as Message;

expect(message?.mutation).toEqual('{ run }');
});

it('returns the mutation to the client', async () => {
await expect(mutationResponse).resolves.toEqual({ run: 'TestRun:1' });

describe('when the sever responds', () => {
beforeEach(async () => {
await connection.send({
responseId: message?.responseId,
data: { run: 'TestRun:1' }
})
});

it('returns the mutation to the client', async () => {
await expect(mutationResponse).resolves.toEqual({ run: 'TestRun:1' });
});
});
});
});
});

})

});