bfj 9.0.2 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HISTORY.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # History
2
2
 
3
+ ## 9.1.0
4
+
5
+ ### New features
6
+
7
+ * match: implement `recursive` option (acdd744c2eeedad3b5b3df6bc9cf4c48272b6677)
8
+ * walk: implement `stringChunkSize` option and `stringChunk` event (ea8bafcdcbc9a45d177c3407d6e7e88e1e22072a)
9
+
3
10
  ## 9.0.2
4
11
 
5
12
  ### Bug fixes
package/README.md CHANGED
@@ -24,6 +24,8 @@ Big-Friendly JSON. Asynchronous streaming functions for large JSON data sets.
24
24
  * [Options for parsing functions](#options-for-parsing-functions)
25
25
  * [Options for serialisation functions](#options-for-serialisation-functions)
26
26
  * [Is it possible to pause parsing or serialisation from calling code?](#is-it-possible-to-pause-parsing-or-serialisation-from-calling-code)
27
+ * [Can it break long strings into chunks?](#can-it-break-long-strings-into-chunks)
28
+ * [Can it recursively parse JSON nested inside a JSON string?](#can-it-recursively-parse-json-nested-inside-a-json-string)
27
29
  * [Can it handle newline-delimited JSON (NDJSON)?](#can-it-handle-newline-delimited-json-ndjson)
28
30
  * [Is there a change log?](#is-there-a-change-log)
29
31
  * [How do I set up the dev environment?](#how-do-i-set-up-the-dev-environment)
@@ -475,6 +477,16 @@ of an object,
475
477
  the value
476
478
  as its argument.
477
479
 
480
+ * `bfj.events.stringChunk`
481
+ indicates that
482
+ a string chunk
483
+ has been encountered
484
+ if the `stringChunkSize` [option](#options-for-parsing-functions) was set.
485
+ The listener
486
+ will be passed
487
+ the chunk
488
+ as its argument.
489
+
478
490
  * `bfj.events.number`
479
491
  indicates that
480
492
  a number
@@ -687,6 +699,19 @@ of an object,
687
699
  discrete chunks of JSON.
688
700
  See [NDJSON](#can-it-handle-newline-delimited-json-ndjson) for more information.
689
701
 
702
+ * `options.stringChunkSize`:
703
+ For `bfj.walk` only,
704
+ set this to the character count
705
+ at which you wish to chunk strings.
706
+ Each chunk will be emitted as a `bfj.events.stringChunk` event,
707
+ followed by the regular `bfj.events.string` event
708
+ after all chunks are emitted.
709
+
710
+ * `options.recursive`:
711
+ For `bfj.match` only,
712
+ set this to `true`
713
+ if you wish to match against recursively JSON-parsed strings.
714
+
690
715
  * `options.numbers`:
691
716
  For `bfj.match` only,
692
717
  set this to `true`
@@ -821,6 +846,29 @@ const resume = emitter.pause();
821
846
  resume();
822
847
  ```
823
848
 
849
+ ## Can it break long strings into chunks?
850
+
851
+ Yes.
852
+ If you pass the `stringChunkSize` [option](#options-for-parsing-functions)
853
+ to `bfj.walk`,
854
+ it will emit a `bfj.events.stringChunk` event
855
+ for each chunk of the string.
856
+ The regular `bfj.events.string` event
857
+ will still be emitted
858
+ after all the chunks.
859
+
860
+ ## Can it recursively parse JSON nested inside a JSON string?
861
+
862
+ Yes.
863
+ If you pass the `recursive` [option](#options-for-parsing-functions)
864
+ to `bfj.match`,
865
+ it will recursively parse any string values
866
+ that satisfy the `selector` argument.
867
+ Note the same selector is applied
868
+ to every level of recursion,
869
+ so this works best in combination
870
+ with selectors that are predicate functions.
871
+
824
872
  ## Can it handle [newline-delimited JSON (NDJSON)](http://ndjson.org/)?
825
873
 
826
874
  Yes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfj",
3
- "version": "9.0.2",
3
+ "version": "9.1.0",
4
4
  "description": "Big-friendly JSON. Asynchronous streaming functions for large JSON data sets.",
5
5
  "homepage": "https://gitlab.com/philbooth/bfj",
6
6
  "bugs": "https://gitlab.com/philbooth/bfj/issues",
package/src/events.js CHANGED
@@ -5,6 +5,7 @@ module.exports = {
5
5
  object: 'obj',
6
6
  property: 'pro',
7
7
  string: 'str',
8
+ stringChunk: 'str-chunk',
8
9
  number: 'num',
9
10
  literal: 'lit',
10
11
  endPrefix: 'end-',
package/src/match.js CHANGED
@@ -5,6 +5,7 @@ const DataStream = require('./datastream')
5
5
  const events = require('./events')
6
6
  const Hoopy = require('hoopy')
7
7
  const jsonpath = require('jsonpath')
8
+ const { PassThrough } = require('node:stream')
8
9
  const walk = require('./walk')
9
10
 
10
11
  const DEFAULT_BUFFER_LENGTH = 256
@@ -39,6 +40,10 @@ module.exports = match
39
40
  * @option ndjson: Set this to true to parse newline-delimited JSON,
40
41
  * default is `false`.
41
42
  *
43
+ * @option recursive: Set this to true to recursively parse
44
+ * matched string values,
45
+ * default is `false`.
46
+ *
42
47
  * @option yieldRate: The number of data items to process per timeslice,
43
48
  * default is 1024.
44
49
  *
@@ -51,18 +56,17 @@ function match (stream, selector, options = {}) {
51
56
  const keys = []
52
57
  const scopes = []
53
58
  const properties = []
54
- const emitter = walk(stream, options)
55
59
  const matches = new Hoopy(options.bufferLength || DEFAULT_BUFFER_LENGTH)
56
60
  let streamOptions
57
61
  const { highWaterMark } = options
58
62
  if (highWaterMark) {
59
63
  streamOptions = { highWaterMark }
60
64
  }
61
- const results = new DataStream(read, streamOptions)
65
+ const results = options.results || new DataStream(read, streamOptions)
62
66
 
63
- let selectorFunction, selectorPath, selectorString, resume
67
+ let chunkStream, selectorFunction, selectorPath, selectorString, resume
64
68
  let coerceNumbers = false
65
- let awaitPush = true
69
+ let awaitPush = ! options.results
66
70
  let isEnded = false
67
71
  let length = 0
68
72
  let index = 0
@@ -70,6 +74,9 @@ function match (stream, selector, options = {}) {
70
74
  const minDepth = options.minDepth || 0
71
75
  check.assert.greaterOrEqual(minDepth, 0)
72
76
 
77
+ const recursive = options.recursive || false
78
+ check.assert.boolean(recursive)
79
+
73
80
  if (check.function(selector)) {
74
81
  selectorFunction = selector
75
82
  selector = null
@@ -98,6 +105,11 @@ function match (stream, selector, options = {}) {
98
105
  coerceNumbers = !! options.numbers
99
106
  }
100
107
 
108
+ const emitter = walk(stream, {
109
+ ...options,
110
+ streamChunkSize: recursive ? matches.length : null,
111
+ })
112
+
101
113
  emitter.on(events.array, array)
102
114
  emitter.on(events.object, object)
103
115
  emitter.on(events.property, property)
@@ -110,6 +122,10 @@ function match (stream, selector, options = {}) {
110
122
  emitter.on(events.error, error)
111
123
  emitter.on(events.dataError, dataError)
112
124
 
125
+ if (recursive) {
126
+ emitter.on(events.stringChunk, stringChunk)
127
+ }
128
+
113
129
  return results
114
130
 
115
131
  function read () {
@@ -190,12 +206,16 @@ function match (stream, selector, options = {}) {
190
206
  }
191
207
 
192
208
  function value (v) {
193
- let key
209
+ if (chunkStream) {
210
+ chunkStream = null
211
+ }
194
212
 
195
213
  if (scopes.length < minDepth) {
196
214
  return
197
215
  }
198
216
 
217
+ let key
218
+
199
219
  if (scopes.length > 0) {
200
220
  const scope = scopes[scopes.length - 1]
201
221
 
@@ -269,6 +289,19 @@ function match (stream, selector, options = {}) {
269
289
  after()
270
290
  }
271
291
 
292
+ function stringChunk (chunk) {
293
+ if (!chunkStream) {
294
+ chunkStream = new PassThrough(streamOptions)
295
+ match(
296
+ chunkStream,
297
+ selectorFunction || selectorPath || selectorString || selector,
298
+ { ...options, results },
299
+ )
300
+ }
301
+
302
+ chunkStream.write(chunk)
303
+ }
304
+
272
305
  function end () {
273
306
  isEnded = true
274
307
  endResults()
package/src/walk.js CHANGED
@@ -34,17 +34,23 @@ module.exports = initialise
34
34
  * emitting events as it encounters tokens. The event emitter is decorated
35
35
  * with a `pause` method that can be called to pause processing.
36
36
  *
37
- * @param stream: Readable instance representing the incoming JSON.
37
+ * @param stream: Readable instance representing the incoming JSON.
38
38
  *
39
- * @option ndjson: Set this to true to parse newline-delimited JSON.
39
+ * @option bufferLength: The length of the walk buffer, default is 256.
40
40
  *
41
- * @option yieldRate: The number of data items to process per timeslice,
42
- * default is 1024.
41
+ * @option ndjson: Set this to true to parse newline-delimited JSON.
43
42
  *
44
- * @option bufferLength: The length of the walk buffer, default is 256.
43
+ * @option stringChunkSize: The size at which to chunk long strings, emitting
44
+ * `string-chunk` events for each chunk followed by a
45
+ * regular `string` event when the complete string has
46
+ * been walked. Default is disabled.
47
+ *
48
+ * @option yieldRate: The number of data items to process per timeslice,
49
+ * default is 1024.
45
50
  **/
46
51
  function initialise (stream, options = {}) {
47
52
  check.assert.instanceStrict(stream, require('stream').Readable, 'Invalid stream argument')
53
+ check.assert.maybe.greater(options.stringChunkSize, 0, 'Invalid stringChunkSize option')
48
54
 
49
55
  const currentPosition = {
50
56
  line: 1,
@@ -62,6 +68,7 @@ function initialise (stream, options = {}) {
62
68
  const shouldHandleNdjson = !! options.ndjson
63
69
  const yieldRate = options.yieldRate || 1024
64
70
  const bufferLength = options.bufferLength || DEFAULT_BUFFER_LENGTH
71
+ const stringChunkSize = options.stringChunkSize || Number.POSITIVE_INFINITY
65
72
 
66
73
  let index = 0
67
74
  let isStreamEnded = false
@@ -71,6 +78,8 @@ function initialise (stream, options = {}) {
71
78
  let isWalkingString = false
72
79
  let hasEndedLine = true
73
80
  let count = 0
81
+ let stringChunkCount = 0
82
+ let stringChunkStart = 0
74
83
  let resumeFn
75
84
  let pause
76
85
  let cachedCharacter
@@ -433,6 +442,9 @@ function initialise (stream, options = {}) {
433
442
 
434
443
  if (isEscaping) {
435
444
  str.push(await escape(char))
445
+ if (++stringChunkCount >= stringChunkSize) {
446
+ await walkStringChunk(event, str)
447
+ }
436
448
  return walkStringContinue(event, str, false)
437
449
  }
438
450
 
@@ -442,13 +454,29 @@ function initialise (stream, options = {}) {
442
454
 
443
455
  if (char !== '"') {
444
456
  str.push(char)
457
+ if (++stringChunkCount >= stringChunkSize) {
458
+ await walkStringChunk(event, str)
459
+ }
445
460
  return walkStringContinue(event, str, isEscaping)
446
461
  }
447
462
 
448
463
  isWalkingString = false
464
+
465
+ await walkStringChunk(event, str)
466
+ stringChunkStart = 0
467
+
449
468
  return emit(event, str.join(''))
450
469
  }
451
470
 
471
+ function walkStringChunk (event, str) {
472
+ if (event === events.string) {
473
+ const chunk = str.slice(stringChunkStart).join('')
474
+ stringChunkStart = str.length
475
+ stringChunkCount = 0
476
+ return emit(events.stringChunk, chunk)
477
+ }
478
+ }
479
+
452
480
  async function escape (char) {
453
481
  if (escapes[char]) {
454
482
  return escapes[char]
@@ -303,6 +303,35 @@ suite('integration:', () => {
303
303
  })
304
304
  })
305
305
 
306
+ suite('match recursive:', () => {
307
+ let file, results, errors
308
+
309
+ setup(done => {
310
+ file = path.join(__dirname, 'data.json')
311
+ fs.writeFileSync(file, JSON.stringify({
312
+ foo: 'bar',
313
+ wibble: {
314
+ foo: 'baz',
315
+ wibble: JSON.stringify({ foo: 'qux' }),
316
+ },
317
+ }))
318
+ results = []
319
+ errors = []
320
+ const datastream = bfj.match(fs.createReadStream(file), (k) => k === 'foo', { recursive: true })
321
+ datastream.on('data', item => results.push(item))
322
+ datastream.on('error', error => errors.push(error))
323
+ datastream.on('end', done)
324
+ })
325
+
326
+ test('the correct properties were matched', () => {
327
+ assert.deepEqual([ 'bar', 'baz', 'qux' ], results)
328
+ })
329
+
330
+ test('no errors occurred', () => {
331
+ assert.deepEqual(errors, [])
332
+ })
333
+ })
334
+
306
335
  suite('parse request:', () => {
307
336
  let error, result
308
337
 
@@ -122,8 +122,11 @@ suite('match:', () => {
122
122
  assert.lengthOf(log.args.walk[0], 2)
123
123
  assert.strictEqual(log.args.walk[0][0], stream)
124
124
  assert.lengthOf(Object.keys(log.args.walk[0][0]), 0)
125
- assert.strictEqual(log.args.walk[0][1], options)
126
- assert.lengthOf(Object.keys(log.args.walk[0][1]), 2)
125
+ assert.deepEqual(log.args.walk[0][1], {
126
+ ...options,
127
+ streamChunkSize: null,
128
+ })
129
+ assert.lengthOf(Object.keys(log.args.walk[0][1]), 3)
127
130
  })
128
131
 
129
132
  test('EventEmitter.on was called eleven times', () => {
@@ -1253,5 +1256,72 @@ suite('match:', () => {
1253
1256
  })
1254
1257
  })
1255
1258
  })
1259
+
1260
+ suite('match recursive:', () => {
1261
+ let stream, predicate, options, result
1262
+
1263
+ setup(() => {
1264
+ stream = {}
1265
+ predicate = spooks.fn({ name: 'predicate', log, results: [ true ] })
1266
+ options = { recursive: true }
1267
+ result = match(stream, predicate, options)
1268
+ })
1269
+
1270
+ test('DataStream was called once', () => {
1271
+ assert.strictEqual(log.counts.DataStream, 1)
1272
+ })
1273
+
1274
+ test('walk was called once', () => {
1275
+ assert.strictEqual(log.counts.walk, 1)
1276
+ })
1277
+
1278
+ test('EventEmitter.on was called twelve times', () => {
1279
+ assert.strictEqual(log.counts.on, 12)
1280
+ })
1281
+
1282
+ test('EventEmitter.on was called correctly twelfth time', () => {
1283
+ assert.lengthOf(log.args.on[11], 2)
1284
+ assert.strictEqual(log.args.on[11][0], 'str-chunk')
1285
+ assert.isFunction(log.args.on[11][1])
1286
+ assert.strictEqual(log.these.on[11], results.walk[0])
1287
+ })
1288
+
1289
+ suite('array with string chunks:', () => {
1290
+ setup(() => {
1291
+ log.args.on[0][1]()
1292
+ log.args.on[11][1]('{"foo":"')
1293
+ log.args.on[11][1]('bar",')
1294
+ log.args.on[11][1]('"baz"')
1295
+ log.args.on[11][1](':"qux"')
1296
+ log.args.on[5][1]('{"foo":"bar","baz":"qux"}')
1297
+ log.args.on[3][1]()
1298
+ log.args.DataStream[0][0]()
1299
+ })
1300
+
1301
+ test('predicate was called twice', () => {
1302
+ assert.strictEqual(log.counts.predicate, 2)
1303
+ })
1304
+
1305
+ test('predicate was called correctly first time', () => {
1306
+ assert.strictEqual(log.args.predicate[0][0], 0)
1307
+ assert.strictEqual(log.args.predicate[0][1], '{"foo":"bar","baz":"qux"}')
1308
+ assert.strictEqual(log.args.predicate[0][2], 1)
1309
+ })
1310
+
1311
+ test('predicate was called correctly second time', () => {
1312
+ assert.isUndefined(log.args.predicate[1][0])
1313
+ assert.deepEqual(log.args.predicate[1][1], ['{"foo":"bar","baz":"qux"}'])
1314
+ assert.strictEqual(log.args.predicate[1][2], 0)
1315
+ })
1316
+
1317
+ test('results.push was not called', () => {
1318
+ assert.strictEqual(log.counts.push, 0)
1319
+ })
1320
+
1321
+ test('results.emit was not called', () => {
1322
+ assert.strictEqual(log.counts.emit, 0)
1323
+ })
1324
+ })
1325
+ })
1256
1326
  })
1257
1327
  })
package/test/unit/walk.js CHANGED
@@ -103,6 +103,10 @@ suite('walk:', () => {
103
103
  assert.strictEqual(log.counts.string, 0)
104
104
  })
105
105
 
106
+ test('stringChunk event did not occur', () => {
107
+ assert.strictEqual(log.counts.stringChunk, 0)
108
+ })
109
+
106
110
  test('number event did not occur', () => {
107
111
  assert.strictEqual(log.counts.number, 0)
108
112
  })
@@ -190,6 +194,10 @@ suite('walk:', () => {
190
194
  assert.strictEqual(log.counts.string, 0)
191
195
  })
192
196
 
197
+ test('stringChunk event did not occur', () => {
198
+ assert.strictEqual(log.counts.stringChunk, 0)
199
+ })
200
+
193
201
  test('number event did not occur', () => {
194
202
  assert.strictEqual(log.counts.number, 0)
195
203
  })
@@ -273,6 +281,10 @@ suite('walk:', () => {
273
281
  assert.strictEqual(log.counts.string, 0)
274
282
  })
275
283
 
284
+ test('stringChunk event did not occur', () => {
285
+ assert.strictEqual(log.counts.stringChunk, 0)
286
+ })
287
+
276
288
  test('number event did not occur', () => {
277
289
  assert.strictEqual(log.counts.number, 0)
278
290
  })
@@ -349,6 +361,109 @@ suite('walk:', () => {
349
361
  assert.strictEqual(log.counts.property, 0)
350
362
  })
351
363
 
364
+ test('stringChunk event occured once', () => {
365
+ assert.strictEqual(log.counts.stringChunk, 1)
366
+ })
367
+
368
+ test('number event did not occur', () => {
369
+ assert.strictEqual(log.counts.number, 0)
370
+ })
371
+
372
+ test('literal event did not occur', () => {
373
+ assert.strictEqual(log.counts.literal, 0)
374
+ })
375
+
376
+ test('endArray event did not occur', () => {
377
+ assert.strictEqual(log.counts.endArray, 0)
378
+ })
379
+
380
+ test('endObject event did not occur', () => {
381
+ assert.strictEqual(log.counts.endObject, 0)
382
+ })
383
+
384
+ test('error event did not occur', () => {
385
+ assert.strictEqual(log.counts.error, 0)
386
+ })
387
+
388
+ test('dataError event did not occur', () => {
389
+ assert.strictEqual(log.counts.dataError, 0)
390
+ })
391
+
392
+ test('endLine event did not occur', () => {
393
+ assert.strictEqual(log.counts.endLine, 0)
394
+ })
395
+
396
+ test('endPrefix event did not occur', () => {
397
+ assert.strictEqual(log.counts.endPrefix, 0)
398
+ })
399
+ })
400
+
401
+ suite('string (with chunk size):', () => {
402
+ let stream, emitter
403
+
404
+ setup(done => {
405
+ stream = new Readable()
406
+ stream._read = () => {}
407
+
408
+ emitter = walk(stream, { stringChunkSize: 8 })
409
+
410
+ stream.push('"\\"the quick brown fox\r\n\\tjumps\\u00a0over the lazy\\u1680dog\\""')
411
+ stream.push(null)
412
+
413
+ Object.entries(events).forEach(([ key, value ]) => {
414
+ emitter.on(value, spooks.fn({
415
+ name: key,
416
+ log: log
417
+ }))
418
+ })
419
+
420
+ emitter.on(events.end, done)
421
+ })
422
+
423
+ test('stringChunk event occurred seven times', () => {
424
+ assert.strictEqual(log.counts.stringChunk, 6)
425
+ })
426
+
427
+ test('stringChunk event was dispatched correctly', () => {
428
+ assert.lengthOf(log.args.stringChunk[0], 1)
429
+ assert.strictEqual(log.args.stringChunk[0][0], '"the qui')
430
+ assert.lengthOf(log.args.stringChunk[1], 1)
431
+ assert.strictEqual(log.args.stringChunk[1][0], 'ck brown')
432
+ assert.lengthOf(log.args.stringChunk[2], 1)
433
+ assert.strictEqual(log.args.stringChunk[2][0], ' fox\r\n\tj')
434
+ assert.lengthOf(log.args.stringChunk[3], 1)
435
+ assert.strictEqual(log.args.stringChunk[3][0], 'umps\u00a0ove')
436
+ assert.lengthOf(log.args.stringChunk[4], 1)
437
+ assert.strictEqual(log.args.stringChunk[4][0], 'r the la')
438
+ assert.lengthOf(log.args.stringChunk[5], 1)
439
+ assert.strictEqual(log.args.stringChunk[5][0], 'zy\u1680dog"')
440
+ })
441
+
442
+ test('string event occurred once', () => {
443
+ assert.strictEqual(log.counts.string, 1)
444
+ })
445
+
446
+ test('string event was dispatched correctly', () => {
447
+ assert.lengthOf(log.args.string[0], 1)
448
+ assert.strictEqual(log.args.string[0][0], '"the quick brown fox\r\n\tjumps\u00a0over the lazy\u1680dog"')
449
+ })
450
+
451
+ test('end event occurred once', () => {
452
+ assert.strictEqual(log.counts.end, 1)
453
+ })
454
+
455
+ test('array event did not occur', () => {
456
+ assert.strictEqual(log.counts.array, 0)
457
+ })
458
+
459
+ test('object event did not occur', () => {
460
+ assert.strictEqual(log.counts.object, 0)
461
+ })
462
+
463
+ test('property event did not occur', () => {
464
+ assert.strictEqual(log.counts.property, 0)
465
+ })
466
+
352
467
  test('number event did not occur', () => {
353
468
  assert.strictEqual(log.counts.number, 0)
354
469
  })
@@ -433,6 +548,10 @@ suite('walk:', () => {
433
548
  assert.strictEqual(log.counts.string, 0)
434
549
  })
435
550
 
551
+ test('stringChunk event did not occur', () => {
552
+ assert.strictEqual(log.counts.stringChunk, 0)
553
+ })
554
+
436
555
  test('literal event did not occur', () => {
437
556
  assert.strictEqual(log.counts.literal, 0)
438
557
  })
@@ -513,6 +632,10 @@ suite('walk:', () => {
513
632
  assert.strictEqual(log.counts.string, 0)
514
633
  })
515
634
 
635
+ test('stringChunk event did not occur', () => {
636
+ assert.strictEqual(log.counts.stringChunk, 0)
637
+ })
638
+
516
639
  test('number event did not occur', () => {
517
640
  assert.strictEqual(log.counts.number, 0)
518
641
  })
@@ -592,6 +715,10 @@ suite('walk:', () => {
592
715
  assert.strictEqual(log.counts.string, 0)
593
716
  })
594
717
 
718
+ test('stringChunk event did not occur', () => {
719
+ assert.strictEqual(log.counts.stringChunk, 0)
720
+ })
721
+
595
722
  test('number event did not occur', () => {
596
723
  assert.strictEqual(log.counts.number, 0)
597
724
  })
@@ -671,6 +798,10 @@ suite('walk:', () => {
671
798
  assert.strictEqual(log.counts.string, 0)
672
799
  })
673
800
 
801
+ test('stringChunk event did not occur', () => {
802
+ assert.strictEqual(log.counts.stringChunk, 0)
803
+ })
804
+
674
805
  test('number event did not occur', () => {
675
806
  assert.strictEqual(log.counts.number, 0)
676
807
  })
@@ -2892,8 +3023,8 @@ suite('walk:', () => {
2892
3023
  assert.strictEqual(log.counts.endObject, 1)
2893
3024
  })
2894
3025
 
2895
- test('error event occurred eleven times', () => {
2896
- assert.strictEqual(log.counts.error, 11)
3026
+ test('error event occurred thirteen times', () => {
3027
+ assert.strictEqual(log.counts.error, 13)
2897
3028
  })
2898
3029
 
2899
3030
  test('end event occurred once', () => {