Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion src/manage/Uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ export class EmulatorConnector extends Uploader {
private connectSocket(program: string, listener?: (chunk: any) => void): Promise<SubProcess> {
const that = this;

return new Promise(function (resolve, _reject) {
return new Promise(function (resolve, reject) {
const client = new net.Socket();
client.once('error', reject);
client.connect(that.port, () => {
that.emit(UploaderEvents.connected);
if (listener !== undefined) {
Expand Down Expand Up @@ -148,6 +149,7 @@ export class EmulatorUploader extends Uploader {

if (data.includes('Listening')) {
const client = new net.Socket();
client.once('error', reject);
client.connect(that.port, () => {
that.emit(UploaderEvents.connected);
if (listener !== undefined) {
Expand Down
4 changes: 2 additions & 2 deletions src/reporter/ink/ActiveFailures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export function ActiveFailures({snapshot}: Props) {
<Text> {run.suiteTitle}</Text>
<Text color="gray"> ({scenarios.length}/{run.plannedScenarios ?? run.scenarios.length})</Text>
</Text>
{failureRows(scenarios).map(({scenario, step}) => (
<Box key={`${run.id}-failure-${scenario.name}-${step?.name ?? 'scenario'}`} marginLeft={5}>
{failureRows(scenarios).map(({scenario, step}, index) => (
<Box key={`${run.id}-failure-${index}`} marginLeft={5}>
<Text>
<Text color="gray">TEST</Text>
<Text> {scenario.name}</Text>
Expand Down
5 changes: 4 additions & 1 deletion src/testbeds/Emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,18 @@ export class DummyProxy extends Emulator {

this.dummy.on('connection', (connection) => {
this.supervisor = connection;
connection.on('error', (error: Error) => this.failPending(error));
connection.on('data', (data) => {
this.connection.channel.write(data.toString());
});
this.emit(TestbedEvents.Ready);
});
this.dummy.on('error', (error: Error) => this.failPending(error));
this.dummy.listen(specification.dummy.port);
}

protected listen(): void {
this.listenForErrors();
this.connection.channel.on('data', (data: Buffer) => {
if (this.waitingForMessages()) {
this.messages.push(data.toString());
Expand All @@ -74,4 +77,4 @@ export class DummyProxy extends Emulator {
private waitingForMessages(): boolean {
return this.requests.length > 0;
}
}
}
25 changes: 22 additions & 3 deletions src/testbeds/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {Connection} from '../bridge/Connection';
import {SourceMap} from '../sourcemap/SourceMap';

type PromiseResolver<R> = (value: R | PromiseLike<R>) => void;
type PromiseRejector = (reason?: unknown) => void;

export abstract class Platform extends EventEmitter implements Testbed {
abstract connection: Connection;

protected requests: [Request<any>, PromiseResolver<any>][];
protected requests: [Request<unknown>, PromiseResolver<unknown>, PromiseRejector][];

protected messages: MessageQueue;

Expand All @@ -32,6 +33,11 @@ export abstract class Platform extends EventEmitter implements Testbed {
this.messages.push(data.toString());
this.process();
});
this.listenForErrors();
}

protected listenForErrors(): void {
this.connection.channel.on('error', (error: Error) => this.failPending(error));
}

// listen on duplex channel
Expand All @@ -56,6 +62,13 @@ export abstract class Platform extends EventEmitter implements Testbed {
}
}

protected failPending(error: Error): void {
const pending = this.requests.splice(0);
for (const [, , reject] of pending) {
reject(error);
}
}

// search for oldest request matching message
private search(message: string): number {
let index: number = 0;
Expand Down Expand Up @@ -84,10 +97,16 @@ export abstract class Platform extends EventEmitter implements Testbed {
const message = `${request.type}${request.payload?.(map) ?? ''}\n`;
this.emit(TestbedEvents.Send, message);
return new Promise((resolve, reject) => {
this.requests.push([request, resolve]);
const resolver: PromiseResolver<unknown> = value => resolve(value as R);
const pending: [Request<unknown>, PromiseResolver<unknown>, PromiseRejector] = [request as Request<unknown>, resolver, reject];
this.requests.push(pending);
this.connection.channel.write(message, (err: Error | null | undefined) => {
if (err !== null && err !== undefined) {
reject(err);
const index = this.requests.indexOf(pending);
if (index !== -1) {
this.requests.splice(index, 1);
reject(err);
}
}
});
});
Expand Down
28 changes: 27 additions & 1 deletion tests/unit/interface.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
import test from 'ava';
import {PassThrough} from 'node:stream';
import {SubProcess} from '../../src/bridge/SubProcess';
import {Message} from '../../src/messaging/Message';
import {SourceMap} from '../../src/sourcemap/SourceMap';
import {Platform} from '../../src/testbeds/Platform';

class TestPlatform extends Platform {
readonly name = 'test';
connection: SubProcess;

constructor(channel: PassThrough) {
super();
this.connection = new SubProcess(channel);
this.listen();
}
}

// file is currently excluded from tests

test('[warduino] start emulator', t => {
t.pass();
});

test('[platform] rejects outstanding requests when the connection closes with an error', async t => {
const channel = new PassThrough();
const platform = new TestPlatform(channel);
const expected = new Error('read ECONNRESET');
const request = platform.sendRequest(new SourceMap.Mapping(), Message.run);

channel.emit('error', expected);

t.is(await t.throwsAsync(request), expected);
});

test('[warduino] start oop testbed', t => {
t.pass();
});
Expand Down Expand Up @@ -45,4 +72,3 @@ test('[dummy] log file create', t => {
test('[dummy] log file correct', t => {
t.pass();
});

34 changes: 34 additions & 0 deletions tests/unit/reporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,40 @@ test('Ink App shows compact active failures before progress at normal verbosity'
t.true(frame.indexOf('TEST failed-scenario · failed-step') < frame.indexOf('Progress'));
});

test('Ink App assigns unique keys to identical active failures', t => {
const suite = suiteResult('active-suite');
const firstScenario = new ScenarioResult(scenario('duplicate-scenario'));
const secondScenario = new ScenarioResult(scenario('duplicate-scenario'));
const firstStep = new StepOutcome(step('duplicate-step')).update(Outcome.failed);
const secondStep = new StepOutcome(step('duplicate-step')).update(Outcome.failed);
firstScenario.add(firstStep);
secondScenario.add(secondStep);

const state = new ReporterState();
state.start();
state.suiteStarted({...run('active', suite), plannedScenarios: 2});
state.scenarioStarted('active', firstScenario);
state.stepFinished('active', firstScenario, firstStep);
state.scenarioFinished('active', firstScenario);
state.scenarioStarted('active', secondScenario);
state.stepFinished('active', secondScenario, secondStep);

const originalError = console.error;
const errors: unknown[][] = [];
console.error = (...args: unknown[]) => errors.push(args);
try {
render(React.createElement(App, {
snapshot: state.snapshot(),
archive: 'suite.log',
verbosity: Verbosity.normal
}));
} finally {
console.error = originalError;
}

t.false(errors.some(([message]) => `${message}`.includes('same key')));
});

test('Suite views show RUN while active and terminal outcomes after completion', t => {
const cases: Array<[Outcome, string]> = [
[Outcome.succeeded, 'PASS'],
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"noEmit": false,
"declaration": true,
"declarationDir": "dist/types",
"incremental": true,
"incremental": false,
"resolveJsonModule": true
}
}
Loading