@libp2p/interface-stream-muxer-compliance-tests
Version:
Compliance tests for implementations of the libp2p Stream Muxer interface
269 lines • 12.9 kB
JavaScript
/* eslint max-nested-callbacks: ["error", 8] */
import { abortableSource } from 'abortable-iterator';
import { expect } from 'aegir/chai';
import delay from 'delay';
import all from 'it-all';
import drain from 'it-drain';
import { duplexPair } from 'it-pair/duplex';
import { pipe } from 'it-pipe';
import pDefer from 'p-defer';
import { Uint8ArrayList } from 'uint8arraylist';
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
function randomBuffer() {
return uint8ArrayFromString(Math.random().toString());
}
const infiniteRandom = {
[Symbol.asyncIterator]: async function* () {
while (true) {
yield new Uint8ArrayList(randomBuffer());
await delay(50);
}
}
};
export default (common) => {
describe('close', () => {
it('closing underlying socket closes streams', async () => {
let openedStreams = 0;
const expectedStreams = 5;
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({ direction: 'outbound' });
// Listener is echo server :)
const listenerFactory = await common.setup();
const listener = listenerFactory.createStreamMuxer({
direction: 'inbound',
onIncomingStream: (stream) => {
openedStreams++;
void pipe(stream, stream);
}
});
const p = duplexPair();
void pipe(p[0], dialer, p[0]);
void pipe(p[1], listener, p[1]);
const streams = await Promise.all(Array(expectedStreams).fill(0).map(async () => dialer.newStream()));
void Promise.all(streams.map(async (stream) => {
await pipe(infiniteRandom, stream, drain);
}));
expect(dialer.streams).to.have.lengthOf(expectedStreams);
// Pause, and then send some data and close the dialer
await delay(50);
await pipe([randomBuffer()], dialer, drain);
expect(openedStreams).to.have.equal(expectedStreams);
expect(dialer.streams).to.have.lengthOf(0);
});
it('calling close closes streams', async () => {
let openedStreams = 0;
const expectedStreams = 5;
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({ direction: 'outbound' });
// Listener is echo server :)
const listenerFactory = await common.setup();
const listener = listenerFactory.createStreamMuxer({
direction: 'inbound',
onIncomingStream: (stream) => {
openedStreams++;
void pipe(stream, stream).catch(() => { });
}
});
const p = duplexPair();
void pipe(p[0], dialer, p[0]);
void pipe(p[1], listener, p[1]);
const streams = await Promise.all(Array(expectedStreams).fill(0).map(async () => dialer.newStream()));
void Promise.all(streams.map(async (stream) => {
await pipe(infiniteRandom, stream, drain);
}));
expect(dialer.streams, 'dialer - number of opened streams should match number of calls to newStream').to.have.lengthOf(expectedStreams);
// Pause, and then close the dialer
await delay(50);
dialer.close();
expect(openedStreams, 'listener - number of opened streams should match number of calls to newStream').to.have.equal(expectedStreams);
expect(dialer.streams, 'all tracked streams should be deleted after the muxer has called close').to.have.lengthOf(0);
});
it('calling close with an error aborts streams', async () => {
let openedStreams = 0;
const expectedStreams = 5;
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({ direction: 'outbound' });
// Listener is echo server :)
const listenerFactory = await common.setup();
const listener = listenerFactory.createStreamMuxer({
direction: 'inbound',
onIncomingStream: (stream) => {
openedStreams++;
void pipe(stream, stream).catch(() => { });
}
});
const p = duplexPair();
void pipe(p[0], dialer, p[0]);
void pipe(p[1], listener, p[1]);
const streams = await Promise.all(Array(expectedStreams).fill(0).map(async () => dialer.newStream()));
const streamPipes = streams.map(async (stream) => {
await pipe(infiniteRandom, stream, drain);
});
expect(dialer.streams, 'dialer - number of opened streams should match number of calls to newStream').to.have.lengthOf(expectedStreams);
// Pause, and then close the dialer
await delay(50);
// close _with an error_
dialer.close(new Error());
const timeoutError = new Error('timeout');
for (const pipe of streamPipes) {
try {
await Promise.race([
pipe,
new Promise((_resolve, reject) => setTimeout(() => { reject(timeoutError); }, 20))
]);
expect.fail('stream pipe with infinite source should never return');
}
catch (e) {
if (e === timeoutError) {
expect.fail('expected stream pipe to throw an error after muxer closed with error');
}
}
}
expect(openedStreams, 'listener - number of opened streams should match number of calls to newStream').to.have.equal(expectedStreams);
expect(dialer.streams, 'all tracked streams should be deleted after the muxer has called close').to.have.lengthOf(0);
});
it('calling newStream after close throws an error', async () => {
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({ direction: 'outbound' });
dialer.close();
try {
await dialer.newStream();
expect.fail('newStream should throw if called after close');
}
catch (e) {
expect(dialer.streams, 'closed muxer should have no streams').to.have.lengthOf(0);
}
});
it('closing one of the muxed streams doesn\'t close others', async () => {
const p = duplexPair();
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({ direction: 'outbound' });
// Listener is echo server :)
const listenerFactory = await common.setup();
const listener = listenerFactory.createStreamMuxer({
direction: 'inbound',
onIncomingStream: (stream) => {
void pipe(stream, stream).catch(() => { });
}
});
void pipe(p[0], dialer, p[0]);
void pipe(p[1], listener, p[1]);
const stream = await dialer.newStream();
const streams = await Promise.all(Array.from(Array(5), async () => dialer.newStream()));
let closed = false;
const controllers = [];
const streamResults = streams.map(async (stream) => {
const controller = new AbortController();
controllers.push(controller);
try {
const abortableRand = abortableSource(infiniteRandom, controller.signal, { abortCode: 'ERR_TEST_ABORT' });
await pipe(abortableRand, stream, drain);
}
catch (err) {
if (err.code !== 'ERR_TEST_ABORT')
throw err;
}
if (!closed)
throw new Error('stream should not have ended yet!');
});
// Pause, and then send some data and close the first stream
await delay(50);
await pipe([new Uint8ArrayList(randomBuffer())], stream, drain);
closed = true;
// Abort all the other streams later
await delay(50);
controllers.forEach(c => { c.abort(); });
// These should now all resolve without error
await Promise.all(streamResults);
});
it('can close a stream for writing', async () => {
const deferred = pDefer();
const p = duplexPair();
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({ direction: 'outbound' });
const data = [randomBuffer(), randomBuffer()];
const listenerFactory = await common.setup();
const listener = listenerFactory.createStreamMuxer({
direction: 'inbound',
onIncomingStream: (stream) => {
void Promise.resolve().then(async () => {
// Immediate close for write
stream.closeWrite();
const results = await pipe(stream, async (source) => {
const data = [];
for await (const chunk of source) {
data.push(chunk.slice());
}
return data;
});
expect(results).to.eql(data);
try {
await stream.sink([new Uint8ArrayList(randomBuffer())]);
}
catch (err) {
deferred.resolve(err);
}
deferred.reject(new Error('should not support writing to closed writer'));
});
}
});
void pipe(p[0], dialer, p[0]);
void pipe(p[1], listener, p[1]);
const stream = await dialer.newStream();
await stream.sink(data);
const err = await deferred.promise;
expect(err).to.have.property('message').that.matches(/stream closed for writing/);
});
it('can close a stream for reading', async () => {
const deferred = pDefer();
const p = duplexPair();
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({ direction: 'outbound' });
const data = [randomBuffer(), randomBuffer()].map(d => new Uint8ArrayList(d));
const listenerFactory = await common.setup();
const listener = listenerFactory.createStreamMuxer({
direction: 'inbound',
onIncomingStream: (stream) => {
void all(stream.source).then(deferred.resolve, deferred.reject);
}
});
void pipe(p[0], dialer, p[0]);
void pipe(p[1], listener, p[1]);
const stream = await dialer.newStream();
stream.closeRead();
// Source should be done
void Promise.resolve().then(async () => {
expect(await stream.source.next()).to.have.property('done', true);
await stream.sink(data);
});
const results = await deferred.promise;
expect(results).to.eql(data);
});
it('calls onStreamEnd for closed streams not previously written', async () => {
const deferred = pDefer();
const onStreamEnd = () => { deferred.resolve(); };
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({
direction: 'outbound',
onStreamEnd
});
const stream = await dialer.newStream();
stream.close();
await deferred.promise;
});
it('calls onStreamEnd for read and write closed streams not previously written', async () => {
const deferred = pDefer();
const onStreamEnd = () => { deferred.resolve(); };
const dialerFactory = await common.setup();
const dialer = dialerFactory.createStreamMuxer({
direction: 'outbound',
onStreamEnd
});
const stream = await dialer.newStream();
stream.closeWrite();
stream.closeRead();
await deferred.promise;
});
});
};
//# sourceMappingURL=close-test.js.map