diff --git a/asynciterator.ts b/asynciterator.ts index 15aaed1..a8b9d46 100644 --- a/asynciterator.ts +++ b/asynciterator.ts @@ -25,6 +25,11 @@ export function setTaskScheduler(scheduler: TaskScheduler): void { taskScheduler = scheduler; } +/** Binds a function to an object */ +function bind(fn: Function, self: any) { + return self ? fn.bind(self) : fn; +} + /** ID of the INIT state. An iterator is initializing if it is preparing main item generation. @@ -161,7 +166,7 @@ export class AsyncIterator extends EventEmitter { @param {object?} self The `this` pointer for the callback */ forEach(callback: (item: T) => void, self?: object) { - this.on('data', self ? callback.bind(self) : callback); + this.on('data', bind(callback, self)); } /** @@ -451,12 +456,13 @@ export class AsyncIterator extends EventEmitter { /** Maps items from this iterator using the given function. After this operation, only read the returned iterator instead of the current one. - @param {Function} map A mapping function to call on this iterator's (remaining) items + @param {Function} map A mapping function to call on this iterator's (remaining) items. + A `null` value indicates that nothing should be returned for a particular item.. @param {object?} self The `this` pointer for the mapping function @returns {module:asynciterator.AsyncIterator} A new iterator that maps the items from this iterator */ - map(map: (item: T) => D, self?: any): AsyncIterator { - return this.transform({ map: self ? map.bind(self) : map }); + map(map: (item: T) => D | null, self?: any): AsyncIterator { + return new MappingIterator(this, { fn: bind(map, self) }); } /** @@ -469,7 +475,8 @@ export class AsyncIterator extends EventEmitter { filter(filter: (item: T) => item is K, self?: any): AsyncIterator; filter(filter: (item: T) => boolean, self?: any): AsyncIterator; filter(filter: (item: T) => boolean, self?: any): AsyncIterator { - return this.transform({ filter: self ? filter.bind(self) : filter }); + filter = bind(filter, self); + return this.map(item => filter(item) ? item : null); } /** @@ -510,7 +517,7 @@ export class AsyncIterator extends EventEmitter { @returns {module:asynciterator.AsyncIterator} A new iterator that skips the given number of items */ skip(offset: number): AsyncIterator { - return this.transform({ offset }); + return this.map(item => offset-- > 0 ? null : item); } /** @@ -520,7 +527,7 @@ export class AsyncIterator extends EventEmitter { @returns {module:asynciterator.AsyncIterator} A new iterator with at most the given number of items */ take(limit: number): AsyncIterator { - return this.transform({ limit }); + return new HeadIterator(this, limit); } /** @@ -531,7 +538,7 @@ export class AsyncIterator extends EventEmitter { @returns {module:asynciterator.AsyncIterator} A new iterator with items in the given range */ range(start: number, end: number): AsyncIterator { - return this.transform({ offset: start, limit: Math.max(end - start + 1, 0) }); + return this.skip(start).take(Math.max(end - start + 1, 0)); } /** @@ -1251,6 +1258,119 @@ function destinationFillBuffer(this: InternalSource) { (this._destination as any)._fillBuffer(); } +interface ComposedFunction { + fn: Function, + next?: ComposedFunction +} + +export class MappingIterator extends AsyncIterator { + private _fn?: Function; + private _destroySource: boolean; + private onSourceError = (err: Error) => this.emit('error', err); + private onSourceReadable = () => this.emit('readable'); + private onSourceEnd = () => this.close(); + + get readable() { + return this.source.readable; + } + + set readable(readable) { + this.source.readable = readable; + } + + constructor( + protected source: AsyncIterator, + private transforms?: ComposedFunction, + private upstream: AsyncIterator = source, + options: { destroySource?: boolean } = {} + ) { + // Subscribe the iterator directly upstream rather than the original source to avoid over-subscribing + // listeners to the original source + super(); + this._destroySource = options.destroySource !== false; + if (upstream.done) { + this.close(); + } + else { + upstream.on('end', this.onSourceEnd); + upstream.on('error', this.onSourceError); + upstream.on('readable', this.onSourceReadable); + } + } + + get fn() { + if (!this._fn) { + const funcs: Function[] = []; + // eslint-disable-next-line prefer-destructuring + let transforms: ComposedFunction | undefined = this.transforms!; + do + funcs.push(transforms.fn); + // eslint-disable-next-line no-cond-assign + while (transforms = transforms.next); + + const endIndex = funcs.length - 1; + this._fn = (item: any) => { + // Do not use a for-of loop here, it slows down transformations + // by approximately a factor of 2. + for (let index = endIndex; index >= 1; index -= 1) { + if ((item = funcs[index](item)) === null) + return null; + } + return funcs[0](item); + }; + } + return this._fn; + } + + read(): D | null { + const { source, fn } = this; + let item; + while ((item = source.read()) !== null) { + if ((item = fn(item)) !== null) + return item; + } + return null; + } + + map(map: (item: D) => K | null, self?: any): AsyncIterator { + return new MappingIterator(this.source, { fn: bind(map, self), next: this.transforms }, this); + } + + destroy(cause?: Error): void { + this.upstream.destroy(cause); + super.destroy(cause); + } + + public close() { + this.upstream.removeListener('end', this.onSourceEnd); + this.upstream.removeListener('error', this.onSourceError); + this.upstream.removeListener('readable', this.onSourceReadable); + if (this._destroySource) + this.upstream.destroy(); + scheduleTask(() => { + delete this.source; + }); + super.close(); + } +} + +export class HeadIterator extends MappingIterator { + protected count: number = 0; + + constructor(source: AsyncIterator, protected readonly limit: number) { + super(source); + } + + read(): T | null { + const item = this.source.read(); + if (item !== null && this.count < this.limit) { + this.count += 1; + return item; + } + this.close(); + return null; + } +} /** An iterator that generates items based on a source iterator diff --git a/test/AsyncIterator-test.js b/test/AsyncIterator-test.js index 37a74d6..15fb685 100644 --- a/test/AsyncIterator-test.js +++ b/test/AsyncIterator-test.js @@ -5,6 +5,10 @@ import { ENDED, DESTROYED, scheduleTask, + range, + fromArray, + wrap, + ArrayIterator, } from '../dist/asynciterator.js'; import { EventEmitter } from 'events'; @@ -1307,4 +1311,266 @@ describe('AsyncIterator', () => { }); }); }); + + describe('A chain of maps and filters', () => { + for (const iteratorGen of [() => range(0, 2), () => fromArray([0, 1, 2]), () => wrap(range(0, 2))]) { + // eslint-disable-next-line no-loop-func + describe(`with ${iteratorGen()}`, () => { + let iterator; + + beforeEach(() => { + iterator = iteratorGen(); + }); + + it('should handle no transforms arrayified', async () => { + (await iterator.toArray()).should.deep.equal([0, 1, 2]); + }); + + it('should apply maps that doubles correctly', async () => { + (await iterator.map(x => x * 2).toArray()).should.deep.equal([0, 2, 4]); + }); + + it('should apply maps that doubles correctly and then maybemaps', async () => { + (await iterator.map(x => x * 2).map(x => x === 2 ? null : x * 3).toArray()).should.deep.equal([0, 12]); + }); + + it('should apply maps that maybemaps correctly', async () => { + (await iterator.map(x => x === 2 ? null : x * 3).toArray()).should.deep.equal([0, 3]); + }); + + it('should apply maps that maybemaps twice', async () => { + (await iterator.map(x => x === 2 ? null : x * 3).map(x => x === 0 ? null : x * 3).toArray()).should.deep.equal([9]); + }); + + it('should apply maps that converts to string', async () => { + (await iterator.map(x => `x${x}`).toArray()).should.deep.equal(['x0', 'x1', 'x2']); + }); + + it('should apply filter correctly', async () => { + (await iterator.filter(x => x % 2 === 0).toArray()).should.deep.equal([0, 2]); + }); + + it('should apply filter then map correctly', async () => { + (await iterator.filter(x => x % 2 === 0).map(x => `x${x}`).toArray()).should.deep.equal(['x0', 'x2']); + }); + + it('should apply map then filter correctly (1)', async () => { + (await iterator.map(x => x).filter(x => x % 2 === 0).toArray()).should.deep.equal([0, 2]); + }); + + it('should apply map then filter to false correctly', async () => { + (await iterator.map(x => `x${x}`).filter(x => true).toArray()).should.deep.equal(['x0', 'x1', 'x2']); + }); + + it('should apply map then filter to true correctly', async () => { + (await iterator.map(x => `x${x}`).filter(x => false).toArray()).should.deep.equal([]); + }); + + it('should apply filter to false then map correctly', async () => { + (await iterator.filter(x => true).map(x => `x${x}`).toArray()).should.deep.equal(['x0', 'x1', 'x2']); + }); + + it('should apply filter to true then map correctly', async () => { + (await iterator.filter(x => false).map(x => `x${x}`).filter(x => false).toArray()).should.deep.equal([]); + }); + + it('should apply filter one then double', async () => { + (await iterator.filter(x => x !== 1).map(x => x * 2).toArray()).should.deep.equal([0, 4]); + }); + + it('should apply double then filter one', async () => { + (await iterator.map(x => x * 2).filter(x => x !== 1).toArray()).should.deep.equal([0, 2, 4]); + }); + + it('should apply map then filter correctly', async () => { + (await iterator.map(x => `x${x}`).filter(x => (x[1] === '0')).toArray()).should.deep.equal(['x0']); + }); + + it('should correctly apply 3 filters', async () => { + (await range(0, 5).filter(x => x !== 1).filter(x => x !== 2).filter(x => x !== 2).toArray()).should.deep.equal([0, 3, 4, 5]); + }); + + it('should correctly apply 3 maps', async () => { + (await range(0, 1).map(x => x * 2).map(x => `z${x}`).map(x => `y${x}`).toArray()).should.deep.equal(['yz0', 'yz2']); + }); + + it('should correctly apply a map, followed by a filter, followed by another map', async () => { + (await range(0, 1).map(x => x * 2).filter(x => x !== 2).map(x => `y${x}`).toArray()).should.deep.equal(['y0']); + }); + + it('should correctly apply a filter-map-filter', async () => { + (await range(0, 2).filter(x => x !== 1).map(x => x * 3).filter(x => x !== 6).toArray()).should.deep.equal([0]); + }); + + it('should destroy when closed before being read after map', () => { + iterator.map(x => x).close(); + iterator.destroyed.should.be.true; + }); + + it('should destroy when closed before being read after map then filter', () => { + it = iterator.map(x => x); + it.filter(x => true).close(); + iterator.destroyed.should.be.true; + it.destroyed.should.be.true; + }); + + describe('when called on an iterator with a `this` argument', () => { + const self = {}; + let map, result; + + before(() => { + let i = 0; + iterator = new ArrayIterator(['a', 'b', 'c']); + map = sinon.spy(item => item + (++i)); + result = iterator.map(map, self); + }); + + describe('the return value', () => { + const items = []; + + before(done => { + result.on('data', item => { items.push(item); }); + result.on('end', done); + }); + + it('should call the map function once for each item', () => { + map.should.have.been.calledThrice; + }); + + it('should call the map function with the passed argument as `this`', () => { + map.alwaysCalledOn(self).should.be.true; + }); + }); + }); + + describe('when called on an iterator with a `this` argument with nested map', () => { + const self = {}; + let map, result; + + before(() => { + let i = 0; + iterator = new ArrayIterator(['a', 'b', 'c']); + map = sinon.spy(item => item + (++i)); + result = iterator.map(x => x).map(map, self); + }); + + describe('the return value', () => { + const items = []; + + before(done => { + result.on('data', item => { items.push(item); }); + result.on('end', done); + }); + + it('should call the map function once for each item', () => { + map.should.have.been.calledThrice; + }); + + it('should call the map function with the passed argument as `this`', () => { + map.alwaysCalledOn(self).should.be.true; + }); + }); + }); + }); + } + }); + describe('Skipping', () => { + describe('The SkippingIterator function', () => { + describe('the result when called with `new`', () => { + let instance; + + before(() => { + instance = new ArrayIterator([]).skip(10); + }); + + it('should be an AsyncIterator object', () => { + instance.should.be.an.instanceof(AsyncIterator); + }); + + it('should be an EventEmitter object', () => { + instance.should.be.an.instanceof(EventEmitter); + }); + }); + }); + + describe('A SkippingIterator', () => { + let iterator, source; + + before(() => { + source = new ArrayIterator([0, 1, 2, 3, 4, 5, 6]); + iterator = source.skip(4); + }); + + describe('when reading items', () => { + const items = []; + + before(done => { + iterator.on('data', item => { items.push(item); }); + iterator.on('end', done); + }); + + it('should return items skipping the specified amount', () => { + items.should.deep.equal([4, 5, 6]); + }); + }); + }); + + describe('A SkippingIterator', () => { + let iterator, source; + + before(() => { + source = range(0, 6); + iterator = source.skip(4); + }); + + describe('when reading items', () => { + const items = []; + + before(done => { + iterator.on('data', item => { items.push(item); }); + iterator.on('end', done); + }); + + it('should return items skipping the specified amount', () => { + items.should.deep.equal([4, 5, 6]); + }); + }); + }); + + describe('A SkippingIterator with a source that emits 0 items', () => { + it('should not return any items', done => { + const items = []; + const iterator = new ArrayIterator([]).skip(10); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([]); + done(); + }); + }); + }); + + describe('A SkippingIterator with a limit of 0 items', () => { + it('should emit all items', done => { + const items = []; + const iterator = new ArrayIterator([0, 1, 2, 3, 4, 5, 6]).skip(0); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([0, 1, 2, 3, 4, 5, 6]); + done(); + }); + }); + }); + + describe('A SkippingIterator with a limit of Infinity items', () => { + it('should skip all items', done => { + const items = []; + const iterator = new ArrayIterator([0, 1, 2, 3, 4, 5, 6]).skip(Infinity); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([]); + done(); + }); + }); + }); + }); }); diff --git a/test/LimitingIterator-test.js b/test/LimitingIterator-test.js new file mode 100644 index 0000000..0933a30 --- /dev/null +++ b/test/LimitingIterator-test.js @@ -0,0 +1,86 @@ +import { + AsyncIterator, + ArrayIterator, + HeadIterator, +} from '../dist/asynciterator.js'; + +import { EventEmitter } from 'events'; + +describe('LimitingIterator', () => { + describe('The LimitingIterator function', () => { + describe('the result when called with `new`', () => { + let instance; + before(() => { + instance = new HeadIterator(new ArrayIterator([]), 10); + }); + + it('should be a LimitingIterator object', () => { + instance.should.be.an.instanceof(HeadIterator); + }); + + it('should be an AsyncIterator object', () => { + instance.should.be.an.instanceof(AsyncIterator); + }); + + it('should be an EventEmitter object', () => { + instance.should.be.an.instanceof(EventEmitter); + }); + }); + }); + + describe('A LimitingIterator', () => { + let iterator, source; + before(() => { + source = new ArrayIterator([0, 1, 2, 3, 4, 5, 6]); + iterator = new HeadIterator(source, 4); + }); + + describe('when reading items', () => { + const items = []; + before(done => { + iterator.on('data', item => { items.push(item); }); + iterator.on('end', done); + }); + + it('should return items limited to the specified limit', () => { + items.should.deep.equal([0, 1, 2, 3]); + }); + }); + }); + + describe('A LimitingIterator with a source that emits 0 items', () => { + it('should not return any items', done => { + const items = []; + const iterator = new HeadIterator(new ArrayIterator([]), 10); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([]); + done(); + }); + }); + }); + + describe('A LimitingIterator with a limit of 0 items', () => { + it('should not emit any items', done => { + const items = []; + const iterator = new HeadIterator(new ArrayIterator([0, 1, 2]), 0); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([]); + done(); + }); + }); + }); + + describe('A LimitingIterator with a limit of Infinity items', () => { + it('should emit all items', done => { + const items = []; + const iterator = new HeadIterator(new ArrayIterator([0, 1, 2, 3, 4, 5, 6]), Infinity); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([0, 1, 2, 3, 4, 5, 6]); + done(); + }); + }); + }); +}); diff --git a/test/MappingIterator.js b/test/MappingIterator.js new file mode 100644 index 0000000..58d7f49 --- /dev/null +++ b/test/MappingIterator.js @@ -0,0 +1,209 @@ +import { + AsyncIterator, + ArrayIterator, + MappingIterator, +} from '../dist/asynciterator.js'; + +import { EventEmitter } from 'events'; + +class CustomTransformIterator extends MappingIterator { + read() { + return this.source.read(); + } +} + +describe('CustomTransformIterator', () => { + describe('The CustomTransformIterator function', () => { + describe('the result when called with `new`', () => { + let instance; + + before(() => { + instance = new CustomTransformIterator(new ArrayIterator([])); + }); + + it('should be a CustomTransformIterator object', () => { + instance.should.be.an.instanceof(MappingIterator); + }); + + it('should be an AsyncIterator object', () => { + instance.should.be.an.instanceof(AsyncIterator); + }); + + it('should be an EventEmitter object', () => { + instance.should.be.an.instanceof(EventEmitter); + }); + }); + }); + + describe('A CustomTransformIterator', () => { + let iterator, source; + + before(() => { + source = new ArrayIterator([0, 1, 2, 3, 4, 5, 6]); + iterator = new CustomTransformIterator(source); + }); + + describe('when reading items', () => { + const items = []; + + before(done => { + iterator.on('data', item => { items.push(item); }); + iterator.on('end', done); + }); + + it('should return all items', () => { + items.should.deep.equal([0, 1, 2, 3, 4, 5, 6]); + }); + }); + }); + + describe('A CustomTransformIterator', () => { + let iterator, source; + + before(() => { + source = new ArrayIterator([1]); + source._readable = false; + iterator = new CustomTransformIterator(source); + }); + + it('Should emit readable when readable is set to true', done => { + iterator.on('readable', done); + iterator.readable = true; + }); + }); + + describe('A CustomTransformIterator with a source that emits 0 items', () => { + it('should not return any items', done => { + const items = []; + const iterator = new CustomTransformIterator(new ArrayIterator([])); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([]); + done(); + }); + }); + }); + + describe('A CustomTransformIterator with a source that is already ended', () => { + it('should not return any items', done => { + const items = []; + const source = new ArrayIterator([]); + source.on('end', () => { + const iterator = new CustomTransformIterator(source); + iterator.on('data', item => { items.push(item); }); + iterator.on('end', () => { + items.should.deep.equal([]); + done(); + }); + }); + }); + }); + + + describe('A TransformIterator with destroySource set to its default', () => { + let iterator, source; + + before(() => { + source = new ArrayIterator([1, 2, 3]); + iterator = new CustomTransformIterator(source); + }); + + describe('after being closed', () => { + before(done => { + iterator.read(); + iterator.close(); + iterator.on('end', done); + }); + + it('should have destroyed the source', () => { + expect(source).to.have.property('destroyed', true); + }); + }); + }); + + describe('A TransformIterator with destroySource set to false', () => { + let iterator, source; + + before(() => { + source = new ArrayIterator([1, 2, 3]); + iterator = new CustomTransformIterator(source, undefined, undefined, { destroySource: false }); + }); + + describe('after being closed', () => { + before(done => { + iterator.read(); + iterator.close(); + iterator.on('end', done); + }); + + it('should not have destroyed the source', () => { + expect(source).to.have.property('destroyed', false); + }); + }); + }); + + describe('A TransformIterator with a source that errors', () => { + let iterator, source, errorHandler; + + before(() => { + source = new AsyncIterator(); + iterator = new CustomTransformIterator(source); + iterator.on('error', errorHandler = sinon.stub()); + }); + + describe('before an error occurs', () => { + it('should not have emitted any error', () => { + errorHandler.should.not.have.been.called; + }); + }); + + describe('after a first error occurs', () => { + let error1; + before(() => { + errorHandler.reset(); + source.emit('error', error1 = new Error('error1')); + }); + + it('should re-emit the error', () => { + errorHandler.should.have.been.calledOnce; + errorHandler.should.have.been.calledWith(error1); + }); + }); + + describe('after a second error occurs', () => { + let error2; + + before(() => { + errorHandler.reset(); + source.emit('error', error2 = new Error('error2')); + }); + + it('should re-emit the error', () => { + errorHandler.should.have.been.calledOnce; + errorHandler.should.have.been.calledWith(error2); + }); + }); + + describe('after the source has ended and errors again', () => { + before(done => { + errorHandler.reset(); + source.close(); + iterator.on('end', () => { + function noop() { /* */ } + source.on('error', noop); // avoid triggering the default error handler + source.emit('error', new Error('error3')); + source.removeListener('error', noop); + done(); + }); + }); + + it('should not re-emit the error', () => { + errorHandler.should.not.have.been.called; + }); + + it('should not leave any error handlers attached', () => { + source.listenerCount('error').should.equal(0); + }); + }); + }); +}); diff --git a/test/SimpleTransformIterator-test.js b/test/SimpleTransformIterator-test.js index e033b8e..8fb2761 100644 --- a/test/SimpleTransformIterator-test.js +++ b/test/SimpleTransformIterator-test.js @@ -1110,10 +1110,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should execute the map function on all items in order', () => { items.should.deep.equal(['a1', 'b2', 'c3']); }); @@ -1121,10 +1117,6 @@ describe('SimpleTransformIterator', () => { it('should call the map function once for each item', () => { map.should.have.been.calledThrice; }); - - it('should call the map function with the returned iterator as `this`', () => { - map.alwaysCalledOn(result).should.be.true; - }); }); }); @@ -1145,14 +1137,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - - it('should execute the map function on all items in order', () => { - items.should.deep.equal(['a1', 'b2', 'c3']); - }); - it('should call the map function once for each item', () => { map.should.have.been.calledThrice; }); @@ -1184,10 +1168,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should execute the filter function on all items in order', () => { items.should.deep.equal(['a', 'c']); }); @@ -1195,10 +1175,6 @@ describe('SimpleTransformIterator', () => { it('should call the filter function once for each item', () => { filter.should.have.been.calledThrice; }); - - it('should call the filter function with the returned iterator as `this`', () => { - filter.alwaysCalledOn(result).should.be.true; - }); }); }); @@ -1218,10 +1194,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should execute the filter function on all items in order', () => { items.should.deep.equal(['a', 'c']); }); @@ -1346,10 +1318,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should skip the given number of items', () => { items.should.deep.equal(['c', 'd', 'e']); }); @@ -1376,10 +1344,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should take the given number of items', () => { items.should.deep.equal(['a', 'b', 'c']); }); @@ -1406,10 +1370,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should contain the indicated range', () => { items.should.have.length(10); items[0].should.equal(20); @@ -1433,17 +1393,9 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should be empty', () => { items.should.be.empty; }); - - it('should not have called `read` on the iterator', () => { - iterator.read.should.not.have.been.called; - }); }); }); });