Skip to content

Commit 2d92d6d

Browse files
committed
fix: Makes concurrent jests non deterministic
* Related #4 fix: parameterize replacer toError and fromError, change fromError to return JSONValue, stringify fromError usages * Related #10
1 parent eaee21a commit 2d92d6d

File tree

6 files changed

+75
-28
lines changed

6 files changed

+75
-28
lines changed

src/RPCClient.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class RPCClient<M extends ClientManifest> {
9696
logger.info(`Created ${this.name}`);
9797
return rpcClient;
9898
}
99+
protected onTimeoutCallback?: () => void;
99100
protected idGen: IdGen;
100101
protected logger: Logger;
101102
protected streamFactory: StreamFactory;
@@ -110,7 +111,9 @@ class RPCClient<M extends ClientManifest> {
110111
errorData,
111112
metadata?: JSONValue,
112113
) => ErrorRPCRemote<unknown>;
113-
114+
public registerOnTimeoutCallback(callback: () => void) {
115+
this.onTimeoutCallback = callback;
116+
}
114117
// Method proxies
115118
public readonly streamKeepAliveTimeoutTime: number;
116119
public readonly methodsProxy = new Proxy(
@@ -353,6 +356,9 @@ class RPCClient<M extends ClientManifest> {
353356
void timer.then(
354357
() => {
355358
abortController.abort(timeoutError);
359+
if (this.onTimeoutCallback) {
360+
this.onTimeoutCallback();
361+
}
356362
},
357363
() => {}, // Ignore cancellation error
358364
);
@@ -547,7 +553,7 @@ class RPCClient<M extends ClientManifest> {
547553
...(rpcStream.meta ?? {}),
548554
command: method,
549555
};
550-
throw rpcUtils.toError(messageValue.error.data, metadata);
556+
throw this.toError(messageValue.error.data, metadata);
551557
}
552558
leadingMessage = messageValue;
553559
} catch (e) {

src/RPCServer.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class RPCServer extends EventTarget {
116116
logger.info(`Created ${this.name}`);
117117
return rpcServer;
118118
}
119+
protected onTimeoutCallback?: () => void;
119120
protected idGen: IdGen;
120121
protected logger: Logger;
121122
protected handlerMap: Map<string, RawHandlerImplementation> = new Map();
@@ -131,7 +132,10 @@ class RPCServer extends EventTarget {
131132
Uint8Array,
132133
JSONRPCResponseResult
133134
>;
134-
135+
// Function to register a callback for timeout
136+
public registerOnTimeoutCallback(callback: () => void) {
137+
this.onTimeoutCallback = callback;
138+
}
135139
public constructor({
136140
manifest,
137141
middlewareFactory,
@@ -347,7 +351,7 @@ class RPCServer extends EventTarget {
347351
const rpcError: JSONRPCError = {
348352
code: e.exitCode ?? JSONRPCErrorCode.InternalError,
349353
message: e.description ?? '',
350-
data: rpcUtils.fromError(e, this.sensitive),
354+
data: JSON.stringify(this.fromError(e), this.replacer),
351355
};
352356
const rpcErrorMessage: JSONRPCResponseError = {
353357
jsonrpc: '2.0',
@@ -468,6 +472,9 @@ class RPCServer extends EventTarget {
468472
delay: this.handlerTimeoutTime,
469473
handler: () => {
470474
abortController.abort(new rpcErrors.ErrorRPCTimedOut());
475+
if (this.onTimeoutCallback) {
476+
this.onTimeoutCallback();
477+
}
471478
},
472479
});
473480

@@ -608,7 +615,7 @@ class RPCServer extends EventTarget {
608615
const rpcError: JSONRPCError = {
609616
code: e.exitCode ?? JSONRPCErrorCode.InternalError,
610617
message: e.description ?? '',
611-
data: rpcUtils.fromError(e, this.sensitive),
618+
data: JSON.stringify(this.fromError(e), this.replacer),
612619
};
613620
const rpcErrorMessage: JSONRPCResponseError = {
614621
jsonrpc: '2.0',

src/types.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -322,12 +322,13 @@ declare const brand: unique symbol;
322322
type Opaque<K, T> = T & { readonly [brand]: K };
323323

324324
type JSONValue =
325-
| { [key: string]: JSONValue }
325+
| { [key: string]: JSONValue | undefined }
326326
| Array<JSONValue>
327327
| string
328328
| number
329329
| boolean
330-
| null;
330+
| null
331+
| undefined;
331332

332333
type POJO = { [key: string]: any };
333334
type PromiseDeconstructed<T> = {

src/utils/utils.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,16 @@ const replacer = (key: string, value: any) => {
263263
* prevent sensitive information from being sent over the network
264264
*/
265265
function fromError(error: ErrorRPC<any>): JSONValue {
266+
const data: { [key: string]: JSONValue } = {
267+
message: error.message,
268+
description: error.description,
269+
};
270+
if (error.code !== undefined) {
271+
data.code = error.code;
272+
}
266273
return {
267274
type: error.name,
268-
data: {
269-
message: error.message,
270-
code: error.code,
271-
description: error.description,
272-
},
275+
data,
273276
};
274277
}
275278

@@ -506,9 +509,9 @@ export {
506509
parseJSONRPCResponseError,
507510
parseJSONRPCResponse,
508511
parseJSONRPCMessage,
512+
replacer,
509513
fromError,
510514
toError,
511-
replacer,
512515
clientInputTransformStream,
513516
clientOutputTransformStream,
514517
getHandlerTypes,

tests/RPC.test.ts

+40-10
Original file line numberDiff line numberDiff line change
@@ -464,10 +464,10 @@ describe('RPC', () => {
464464

465465
// The promise should be rejected
466466
const rejection = await callProm;
467-
expect(rejection).toBeInstanceOf(rpcErrors.ErrorRPCRemote);
468467

469468
// The error should have specific properties
470-
expect(rejection).toMatchObject({ code: error.code });
469+
expect(rejection).toBeInstanceOf(rpcErrors.ErrorRPCRemote);
470+
expect(rejection).toMatchObject({ code: -32006 });
471471

472472
// Cleanup
473473
await rpcServer.destroy();
@@ -602,6 +602,8 @@ describe('RPC', () => {
602602
await rpcClient.destroy();
603603
});
604604
test('RPC client and server timeout concurrently', async () => {
605+
let serverTimedOut = false;
606+
let clientTimedOut = false;
605607
// Generate test data (assuming fc.array generates some mock array)
606608
const values = fc.array(rpcTestUtils.safeJsonValueArb, { minLength: 1 });
607609

@@ -638,6 +640,10 @@ describe('RPC', () => {
638640
idGen,
639641
handlerTimeoutTime: timeout,
640642
});
643+
// Register callback
644+
rpcServer.registerOnTimeoutCallback(() => {
645+
serverTimedOut = true;
646+
});
641647
rpcServer.handleStream({
642648
...serverPair,
643649
cancel: () => {},
@@ -659,9 +665,20 @@ describe('RPC', () => {
659665
const callerInterface = await rpcClient.methods.testMethod({
660666
timer: timeout,
661667
});
668+
// Register callback
669+
rpcClient.registerOnTimeoutCallback(() => {
670+
clientTimedOut = true;
671+
});
662672
const writer = callerInterface.writable.getWriter();
663673
const reader = callerInterface.readable.getReader();
664-
await utils.sleep(5);
674+
// Wait for server and client to timeout by checking the flag
675+
await new Promise<void>((resolve) => {
676+
const checkFlag = () => {
677+
if (serverTimedOut && clientTimedOut) resolve();
678+
else setTimeout(() => checkFlag(), 10);
679+
};
680+
checkFlag();
681+
});
665682
// Expect both the client and the server to time out
666683
await expect(writer.write(values[0])).rejects.toThrow(
667684
'Timed out waiting for header',
@@ -674,6 +691,8 @@ describe('RPC', () => {
674691
});
675692
// Test description
676693
test('RPC server times out before client', async () => {
694+
let serverTimedOut = false;
695+
677696
// Generate test data (assuming fc.array generates some mock array)
678697
const values = fc.array(rpcTestUtils.safeJsonValueArb, { minLength: 1 });
679698

@@ -707,6 +726,10 @@ describe('RPC', () => {
707726
idGen,
708727
handlerTimeoutTime: 1,
709728
});
729+
// Register callback
730+
rpcServer.registerOnTimeoutCallback(() => {
731+
serverTimedOut = true;
732+
});
710733
rpcServer.handleStream({ ...serverPair, cancel: () => {} });
711734

712735
// Create an instance of the RPC client with a longer timeout
@@ -723,8 +746,16 @@ describe('RPC', () => {
723746
});
724747
const writer = callerInterface.writable.getWriter();
725748
const reader = callerInterface.readable.getReader();
726-
await utils.sleep(2);
727-
// Actual tests: We expect server to timeout before the client
749+
// Wait for server to timeout by checking the flag
750+
await new Promise<void>((resolve) => {
751+
const checkFlag = () => {
752+
if (serverTimedOut) resolve();
753+
else setTimeout(() => checkFlag(), 10);
754+
};
755+
checkFlag();
756+
});
757+
758+
// We expect server to timeout before the client
728759
await expect(writer.write(values[0])).rejects.toThrow(
729760
'Timed out waiting for header',
730761
);
@@ -851,11 +882,10 @@ describe('RPC', () => {
851882
// Trigger a read that will hang indefinitely
852883

853884
const readPromise = reader.read();
854-
855-
// Adding a sleep here to check that neither timeout
856-
857-
await utils.sleep(10000);
858-
885+
// Adding a randomized sleep here to check that neither timeout
886+
const randomSleepTime = Math.floor(Math.random() * 1000) + 1;
887+
// Random time between 1 and 1,000 ms
888+
await utils.sleep(randomSleepTime);
859889
// At this point, writePromise and readPromise should neither be resolved nor rejected
860890
// because the server method is hanging.
861891

tests/utils.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { fc } from '@fast-check/jest';
1515
import * as utils from '@/utils';
1616
import { fromError } from '@/utils';
1717
import * as rpcErrors from '@/errors';
18+
import { ErrorRPC } from '@/errors';
1819

1920
/**
2021
* This is used to convert regular chunks into randomly sized chunks based on
@@ -142,15 +143,14 @@ const jsonRpcResponseResultArb = (
142143
})
143144
.noShrink() as fc.Arbitrary<JSONRPCResponseResult>;
144145
const jsonRpcErrorArb = (
145-
error: fc.Arbitrary<Error> = fc.constant(new Error('test error')),
146-
sensitive: boolean = false,
146+
error: fc.Arbitrary<ErrorRPC<any>> = fc.constant(new ErrorRPC('test error')),
147147
) =>
148148
fc
149149
.record(
150150
{
151151
code: fc.integer(),
152152
message: fc.string(),
153-
data: error.map((e) => fromError(e, sensitive)),
153+
data: error.map((e) => JSON.stringify(fromError(e))),
154154
},
155155
{
156156
requiredKeys: ['code', 'message'],
@@ -159,13 +159,13 @@ const jsonRpcErrorArb = (
159159
.noShrink() as fc.Arbitrary<JSONRPCError>;
160160

161161
const jsonRpcResponseErrorArb = (
162-
error?: fc.Arbitrary<Error>,
162+
error?: fc.Arbitrary<ErrorRPC<any>>,
163163
sensitive: boolean = false,
164164
) =>
165165
fc
166166
.record({
167167
jsonrpc: fc.constant('2.0'),
168-
error: jsonRpcErrorArb(error, sensitive),
168+
error: jsonRpcErrorArb(error),
169169
id: idArb,
170170
})
171171
.noShrink() as fc.Arbitrary<JSONRPCResponseError>;

0 commit comments

Comments
 (0)