UNPKG

26.7 kBJavaScriptView Raw
1'use strict'
2
3// Transforms a stream of TAP into a stream of result objects
4// and string comments. Emits "results" event with summary.
5const MiniPass = require('minipass')
6
7const yaml = require('js-yaml')
8const util = require('util')
9const assert = require('assert')
10
11// every line outside of a yaml block is one of these things, or
12// a comment, or garbage.
13const lineTypes = {
14 testPoint: /^(not )?ok(?: ([0-9]+))?(?:(?: -)?( .*?))?(\{?)\n$/,
15 pragma: /^pragma ([+-])([a-z]+)\n$/,
16 bailout: /^bail out!(.*)\n$/i,
17 version: /^TAP version ([0-9]+)\n$/i,
18 childVersion: /^( )+TAP version ([0-9]+)\n$/i,
19 plan: /^([0-9]+)\.\.([0-9]+)(?:\s+(?:#\s*(.*)))?\n$/,
20 subtest: /^# Subtest(?:: (.*))?\n$/,
21 subtestIndent: /^ # Subtest(?:: (.*))?\n$/,
22 comment: /^\s*#.*\n$/
23}
24
25const lineTypeNames = Object.keys(lineTypes)
26
27const lineType = line => {
28 for (let t in lineTypes) {
29 const match = line.match(lineTypes[t])
30 if (match)
31 return [t, match]
32 }
33 return null
34}
35
36const parseDirective = line => {
37 if (!line.trim())
38 return false
39
40 line = line.replace(/\{\s*$/, '').trim()
41 const time = line.match(/^time=((?:[1-9][0-9]*|0)(?:\.[0-9]+)?)(ms|s)$/i)
42 if (time) {
43 let n = +time[1]
44 if (time[2] === 's') {
45 // JS does weird things with floats. Round it off a bit.
46 n *= 1000000
47 n = Math.round(n)
48 n /= 1000
49 }
50 return [ 'time', n ]
51 }
52
53 const type = line.match(/^(todo|skip)\b/i)
54 if (!type)
55 return false
56
57 return [ type[1].toLowerCase(), line.substr(type[1].length).trim() || true ]
58}
59
60class Result {
61 constructor (parsed, count) {
62 const ok = !parsed[1]
63 const id = +(parsed[2] || count + 1)
64 let buffered = parsed[4]
65 this.ok = ok
66 this.id = id
67
68 let rest = parsed[3] || ''
69 let name
70 rest = rest.replace(/([^\\]|^)((?:\\\\)*)#/g, '$1\n$2').split('\n')
71 name = rest.shift()
72 rest = rest.filter(r => r.trim()).join('#')
73
74 // now, let's see if there's a directive in there.
75 const dir = parseDirective(rest.trim())
76 if (!dir)
77 name += (rest ? '#' + rest : '') + buffered
78 else {
79 // handle buffered subtests with todo/skip on them, like
80 // ok 1 - bar # todo foo {\n
81 const dirKey = dir[0]
82 const dirValue = dir[1]
83 this[dirKey] = dirValue
84 }
85
86 if (/\{\s*$/.test(name)) {
87 name = name.replace(/\{\s*$/, '')
88 buffered = '{'
89 }
90
91 if (buffered === '{')
92 this.buffered = true
93
94 if (name)
95 this.name = name.trim()
96 }
97}
98
99class Parser extends MiniPass {
100 constructor (options, onComplete) {
101 if (typeof options === 'function') {
102 onComplete = options
103 options = {}
104 }
105
106 options = options || {}
107 super(options)
108 this.resume()
109
110 if (onComplete)
111 this.on('complete', onComplete)
112
113 this.comments = []
114 this.results = null
115 this.braceLevel = null
116 this.parent = options.parent || null
117 this.failures = []
118 if (options.passes)
119 this.passes = []
120 this.level = options.level || 0
121
122 this.buffer = ''
123 this.bail = !!options.bail
124 this.bailingOut = false
125 this.bailedOut = false
126 this.syntheticBailout = false
127 this.syntheticPlan = false
128 this.omitVersion = !!options.omitVersion
129 this.planStart = -1
130 this.planEnd = -1
131 this.planComment = ''
132 this.yamlish = ''
133 this.yind = ''
134 this.child = null
135 this.current = null
136 this.maybeSubtest = null
137 this.extraQueue = []
138 this.buffered = options.buffered || null
139 this.aborted = false
140 this.preserveWhitespace = options.preserveWhitespace || false
141
142 this.count = 0
143 this.pass = 0
144 this.fail = 0
145 this.todo = 0
146 this.skip = 0
147 this.ok = true
148
149 this.strict = options.strict || false
150 this.pragmas = { strict: this.strict }
151
152 this.postPlan = false
153 }
154
155 tapError (error, line) {
156 if (line)
157 this.emit('line', line)
158 this.ok = false
159 this.fail ++
160 if (typeof error === 'string') {
161 error = {
162 tapError: error
163 }
164 }
165 this.failures.push(error)
166 }
167
168 parseTestPoint (testPoint, line) {
169 this.emitResult()
170 if (this.bailedOut)
171 return
172
173 this.emit('line', line)
174 const res = new Result(testPoint, this.count)
175 if (this.planStart !== -1) {
176 const lessThanStart = +res.id < this.planStart
177 const greaterThanEnd = +res.id > this.planEnd
178 if (lessThanStart || greaterThanEnd) {
179 if (lessThanStart)
180 res.tapError = 'id less than plan start'
181 else
182 res.tapError = 'id greater than plan end'
183 res.plan = { start: this.planStart, end: this.planEnd }
184 this.tapError(res)
185 }
186 }
187
188 if (res.id) {
189 if (!this.first || res.id < this.first)
190 this.first = res.id
191 if (!this.last || res.id > this.last)
192 this.last = res.id
193 }
194
195 if (!res.skip && !res.todo)
196 this.ok = this.ok && res.ok
197
198 // hold onto it, because we might get yamlish diagnostics
199 this.current = res
200 }
201
202 nonTap (data, didLine) {
203 if (this.bailingOut && /^( {4})*\}\n$/.test(data))
204 return
205
206 if (this.strict) {
207 const err = {
208 tapError: 'Non-TAP data encountered in strict mode',
209 data: data
210 }
211 this.tapError(err)
212 if (this.parent)
213 this.parent.tapError(err)
214 }
215
216 // emit each line, then the extra as a whole
217 if (!didLine)
218 data.split('\n').slice(0, -1).forEach(line => {
219 line += '\n'
220 if (this.current || this.extraQueue.length)
221 this.extraQueue.push(['line', line])
222 else
223 this.emit('line', line)
224 })
225
226 if (this.current || this.extraQueue.length)
227 this.extraQueue.push(['extra', data])
228 else
229 this.emit('extra', data)
230 }
231
232 plan (start, end, comment, line) {
233 // not allowed to have more than one plan
234 if (this.planStart !== -1) {
235 this.nonTap(line)
236 return
237 }
238
239 // can't put a plan in a child.
240 if (this.child || this.yind) {
241 this.nonTap(line)
242 return
243 }
244
245 this.emitResult()
246 if (this.bailedOut)
247 return
248
249 // 1..0 is a special case. Otherwise, end must be >= start
250 if (end < start && end !== 0 && start !== 1) {
251 if (this.strict)
252 this.tapError({
253 tapError: 'plan end cannot be less than plan start',
254 plan: {
255 start: start,
256 end: end
257 }
258 }, line)
259 else
260 this.nonTap(line)
261 return
262 }
263
264 this.planStart = start
265 this.planEnd = end
266 const p = { start: start, end: end }
267 if (comment)
268 this.planComment = p.comment = comment
269
270 // This means that the plan is coming at the END of all the tests
271 // Plans MUST be either at the beginning or the very end. We treat
272 // plans like '1..0' the same, since they indicate that no tests
273 // will be coming.
274 if (this.count !== 0 || this.planEnd === 0)
275 this.postPlan = true
276
277 this.emit('line', line)
278 this.emit('plan', p)
279 }
280
281 resetYamlish () {
282 this.yind = ''
283 this.yamlish = ''
284 }
285
286 // that moment when you realize it's not what you thought it was
287 yamlGarbage () {
288 const yamlGarbage = this.yind + '---\n' + this.yamlish
289 this.emitResult()
290 if (this.bailedOut)
291 return
292 this.nonTap(yamlGarbage, true)
293 }
294
295 yamlishLine (line) {
296 if (line === this.yind + '...\n') {
297 // end the yaml block
298 this.processYamlish()
299 } else {
300 this.yamlish += line
301 }
302 }
303
304 processYamlish () {
305 const yamlish = this.yamlish
306 this.resetYamlish()
307
308 let diags
309 try {
310 diags = yaml.safeLoad(yamlish)
311 } catch (er) {
312 this.nonTap(this.yind + '---\n' + yamlish + this.yind + '...\n', true)
313 return
314 }
315
316 this.current.diag = diags
317 // we still don't emit the result here yet, to support diags
318 // that come ahead of buffered subtests.
319 }
320
321 write (chunk, encoding, cb) {
322 if (this.aborted)
323 return
324
325 if (typeof encoding === 'string' && encoding !== 'utf8')
326 chunk = new Buffer(chunk, encoding)
327
328 if (Buffer.isBuffer(chunk))
329 chunk += ''
330
331 if (typeof encoding === 'function') {
332 cb = encoding
333 encoding = null
334 }
335
336 this.buffer += chunk
337 do {
338 const match = this.buffer.match(/^.*\r?\n/)
339 if (!match)
340 break
341
342 this.buffer = this.buffer.substr(match[0].length)
343 this.parse(match[0])
344 } while (this.buffer.length)
345
346 if (cb)
347 process.nextTick(cb)
348
349 return true
350 }
351
352 end (chunk, encoding, cb) {
353 if (chunk) {
354 if (typeof encoding === 'function') {
355 cb = encoding
356 encoding = null
357 }
358 this.write(chunk, encoding)
359 }
360
361 if (this.buffer)
362 this.write('\n')
363
364 // if we have yamlish, means we didn't finish with a ...
365 if (this.yamlish)
366 this.yamlGarbage()
367
368 this.emitResult()
369
370 if (this.syntheticBailout && this.level === 0) {
371 this.syntheticBailout = false
372 let reason = this.bailedOut
373 if (reason === true)
374 reason = ''
375 else
376 reason = ' ' + reason
377 this.emit('line', 'Bail out!' + reason + '\n')
378 }
379
380 let skipAll
381
382 if (this.planEnd === 0 && this.planStart === 1) {
383 skipAll = true
384 if (this.count === 0) {
385 this.ok = true
386 } else {
387 this.tapError('Plan of 1..0, but test points encountered')
388 }
389 } else if (!this.bailedOut && this.planStart === -1) {
390 if (this.count === 0 && !this.syntheticPlan) {
391 this.syntheticPlan = true
392 if (this.buffered) {
393 this.planStart = 1
394 this.planEnd = 0
395 } else
396 this.plan(1, 0, 'no tests found', '1..0 # no tests found\n')
397 skipAll = true
398 } else {
399 this.tapError('no plan')
400 }
401 } else if (this.ok && this.count !== (this.planEnd - this.planStart + 1)) {
402 this.tapError('incorrect number of tests')
403 }
404
405 if (this.ok && !skipAll && this.first !== this.planStart) {
406 this.tapError('first test id does not match plan start')
407 }
408
409 if (this.ok && !skipAll && this.last !== this.planEnd) {
410 this.tapError('last test id does not match plan end')
411 }
412
413 this.emitComplete(skipAll)
414 if (cb)
415 process.nextTick(cb)
416 }
417
418 emitComplete (skipAll) {
419 if (!this.results) {
420 const res = this.results = new FinalResults(!!skipAll, this)
421
422 if (!res.bailout) {
423 // comment a bit at the end so we know what happened.
424 // but don't repeat these comments if they're already present.
425 if (res.plan.end !== res.count)
426 this.emitComment('test count(' + res.count +
427 ') != plan(' + res.plan.end + ')', false, true)
428
429 if (res.fail > 0 && !res.ok)
430 this.emitComment('failed ' + res.fail +
431 (res.count > 1 ? ' of ' + res.count + ' tests'
432 : ' test'),
433 false, true)
434
435 if (res.todo > 0)
436 this.emitComment('todo: ' + res.todo, false, true)
437
438 if (res.skip > 0)
439 this.emitComment('skip: ' + res.skip, false, true)
440 }
441
442 this.emit('complete', this.results)
443 }
444 }
445
446 version (version, line) {
447 // If version is specified, must be at the very beginning.
448 if (version >= 13 &&
449 this.planStart === -1 &&
450 this.count === 0 &&
451 !this.current) {
452 this.emit('line', line)
453 this.emit('version', version)
454 } else
455 this.nonTap(line)
456 }
457
458 pragma (key, value, line) {
459 // can't put a pragma in a child or yaml block
460 if (this.child) {
461 this.nonTap(line)
462 return
463 }
464
465 this.emitResult()
466 if (this.bailedOut)
467 return
468 // only the 'strict' pragma is currently relevant
469 if (key === 'strict') {
470 this.strict = value
471 }
472 this.pragmas[key] = value
473 this.emit('line', line)
474 this.emit('pragma', key, value)
475 }
476
477 bailout (reason, synthetic) {
478 this.syntheticBailout = synthetic
479
480 if (this.bailingOut)
481 return
482
483 // Guard because emitting a result can trigger a forced bailout
484 // if the harness decides that failures should be bailouts.
485 this.bailingOut = reason || true
486
487 if (!synthetic)
488 this.emitResult()
489 else
490 this.current = null
491
492 this.bailedOut = this.bailingOut
493 this.ok = false
494 if (!synthetic) {
495 // synthetic bailouts get emitted on end
496 let line = 'Bail out!'
497 if (reason)
498 line += ' ' + reason
499 this.emit('line', line + '\n')
500 }
501 this.emit('bailout', reason)
502 if (this.parent) {
503 this.end()
504 this.parent.bailout(reason, true)
505 }
506 }
507
508 clearExtraQueue () {
509 for (let c = 0; c < this.extraQueue.length; c++) {
510 this.emit(this.extraQueue[c][0], this.extraQueue[c][1])
511 }
512 this.extraQueue.length = 0
513 }
514
515 endChild () {
516 if (this.child && (!this.bailingOut || this.child.count)) {
517 this.child.end()
518 this.child = null
519 }
520 }
521
522 emitResult () {
523 if (this.bailedOut)
524 return
525
526 this.endChild()
527 this.resetYamlish()
528
529 if (!this.current)
530 return this.clearExtraQueue()
531
532 const res = this.current
533 this.current = null
534
535 this.count++
536 if (res.ok) {
537 this.pass++
538 if (this.passes)
539 this.passes.push(res)
540 } else {
541 this.fail++
542 if (!res.todo && !res.skip) {
543 this.ok = false
544 this.failures.push(res)
545 }
546 }
547
548 if (res.skip)
549 this.skip++
550
551 if (res.todo)
552 this.todo++
553
554 this.emit('assert', res)
555 if (this.bail && !res.ok && !res.todo && !res.skip && !this.bailingOut) {
556 this.maybeChild = null
557 const ind = new Array(this.level + 1).join(' ')
558 let p
559 for (p = this; p.parent; p = p.parent);
560 const bailName = res.name ? ' # ' + res.name : ''
561 p.parse(ind + 'Bail out!' + bailName + '\n')
562 }
563 this.clearExtraQueue()
564 }
565
566 // TODO: We COULD say that any "relevant tap" line that's indented
567 // by 4 spaces starts a child test, and just call it 'unnamed' if
568 // it does not have a prefix comment. In that case, any number of
569 // 4-space indents can be plucked off to try to find a relevant
570 // TAP line type, and if so, start the unnamed child.
571 startChild (line) {
572 const maybeBuffered = this.current && this.current.buffered
573 const unindentStream = !maybeBuffered && this.maybeChild
574 const indentStream = !maybeBuffered && !unindentStream &&
575 lineTypes.subtestIndent.test(line)
576 const unnamed = !maybeBuffered && !unindentStream && !indentStream
577
578 // If we have any other result waiting in the wings, we need to emit
579 // that now. A buffered test emits its test point at the *end* of
580 // the child subtest block, so as to match streamed test semantics.
581 if (!maybeBuffered)
582 this.emitResult()
583
584 if (this.bailedOut)
585 return
586
587 this.child = new Parser({
588 bail: this.bail,
589 parent: this,
590 level: this.level + 1,
591 buffered: maybeBuffered,
592 preserveWhitespace: this.preserveWhitespace,
593 omitVersion: true,
594 strict: this.strict
595 })
596
597 this.child.on('complete', results => {
598 if (!results.ok)
599 this.ok = false
600 })
601
602 this.child.on('line', l => {
603 if (l.trim() || this.preserveWhitespace)
604 l = ' ' + l
605 this.emit('line', l)
606 })
607
608 // Canonicalize the parsing result of any kind of subtest
609 // if it's a buffered subtest or a non-indented Subtest directive,
610 // then synthetically emit the Subtest comment
611 line = line.substr(4)
612 let subtestComment
613 if (indentStream) {
614 subtestComment = line
615 line = null
616 } else if (maybeBuffered) {
617 subtestComment = '# Subtest: ' + this.current.name + '\n'
618 } else {
619 subtestComment = this.maybeChild || '# Subtest\n'
620 }
621
622 this.maybeChild = null
623 this.child.name = subtestComment.substr('# Subtest: '.length).trim()
624
625 // at some point, we may wish to move 100% to preferring
626 // the Subtest comment on the parent level. If so, uncomment
627 // this line, and remove the child.emitComment below.
628 // this.emit('comment', subtestComment)
629 if (!this.child.buffered)
630 this.emit('line', subtestComment)
631 this.emit('child', this.child)
632 this.child.emitComment(subtestComment, true)
633 if (line)
634 this.child.parse(line)
635 }
636
637 abort (message, extra) {
638 if (this.child) {
639 const b = this.child.buffered
640 this.child.abort(message, extra)
641 extra = null
642 if (b)
643 this.write('\n}\n')
644 }
645
646 let dump
647 if (extra && Object.keys(extra).length) {
648 try {
649 dump = yaml.safeDump(extra).trimRight()
650 } catch (er) {}
651 }
652
653 let y
654 if (dump)
655 y = ' ---\n ' + dump.split('\n').join('\n ') + '\n ...\n'
656 else
657 y = '\n'
658 let n = (this.count || 0) + 1
659 if (this.current)
660 n += 1
661
662 if (this.planEnd !== -1 && this.planEnd < n && this.parent) {
663 // skip it, let the parent do this.
664 this.aborted = true
665 return
666 }
667
668 let ind = '' // new Array(this.level + 1).join(' ')
669 message = message.replace(/[\n\r\s\t]/g, ' ')
670 let point = '\nnot ok ' + n + ' - ' + message + '\n' + y
671
672 if (this.planEnd === -1)
673 point += '1..' + n + '\n'
674
675 this.write(point)
676 this.aborted = true
677 this.end()
678 }
679
680 emitComment (line, skipLine, noDuplicate) {
681 if (line.trim().charAt(0) !== '#')
682 line = '# ' + line
683
684 if (line.slice(-1) !== '\n')
685 line += '\n'
686
687 if (noDuplicate && this.comments.indexOf(line) !== -1)
688 return
689
690 this.comments.push(line)
691 if (this.current || this.extraQueue.length) {
692 // no way to get here with skipLine being true
693 this.extraQueue.push(['line', line])
694 this.extraQueue.push(['comment', line])
695 } else {
696 if (!skipLine)
697 this.emit('line', line)
698 this.emit('comment', line)
699 }
700 }
701
702 parse (line) {
703 // normalize line endings
704 line = line.replace(/\r\n$/, '\n')
705
706 // sometimes empty lines get trimmed, but are still part of
707 // a subtest or a yaml block. Otherwise, nothing to parse!
708 if (line === '\n') {
709 if (this.child)
710 line = ' ' + line
711 else if (this.yind)
712 line = this.yind + line
713 }
714
715 // If we're bailing out, then the only thing we want to see is the
716 // end of a buffered child test. Anything else should be ignored.
717 // But! if we're bailing out a nested child, and ANOTHER nested child
718 // comes after that one, then we don't want the second child's } to
719 // also show up, or it looks weird.
720 if (this.bailingOut) {
721 if (!/^\s*}\n$/.test(line))
722 return
723 else if (!this.braceLevel || line.length < this.braceLevel)
724 this.braceLevel = line.length
725 else
726 return
727 }
728
729 // This allows omitting even parsing the version if the test is
730 // an indented child test. Several parsers get upset when they
731 // see an indented version field.
732 if (this.omitVersion && lineTypes.version.test(line) && !this.yind)
733 return
734
735 // check to see if the line is indented.
736 // if it is, then it's either a subtest, yaml, or garbage.
737 const indent = line.match(/^[ \t]*/)[0]
738 if (indent) {
739 this.parseIndent(line, indent)
740 return
741 }
742
743 // In any case where we're going to emitResult, that can trigger
744 // a bailout, so we need to only emit the line once we know that
745 // isn't happening, to prevent cases where there's a bailout, and
746 // then one more line of output. That'll also prevent the case
747 // where the test point is emitted AFTER the line that follows it.
748
749 // buffered subtests must end with a }
750 if (this.child && this.child.buffered && line === '}\n') {
751 this.endChild()
752 this.emit('line', line)
753 this.emitResult()
754 return
755 }
756
757 // just a \n, emit only if we care about whitespace
758 const validLine = this.preserveWhitespace || line.trim() || this.yind
759 if (line === '\n')
760 return validLine && this.emit('line', line)
761
762 // buffered subtest with diagnostics
763 if (this.current && line === '{\n' &&
764 !this.current.buffered &&
765 !this.child) {
766 this.emit('line', line)
767 this.current.buffered = true
768 return
769 }
770
771 // now we know it's not indented, so if it's either valid tap
772 // or garbage. Get the type of line.
773 const type = lineType(line)
774 if (!type) {
775 this.nonTap(line)
776 return
777 }
778
779 if (type[0] === 'comment') {
780 this.emitComment(line)
781 return
782 }
783
784 // if we have any yamlish, it's garbage now. We tolerate non-TAP and
785 // comments in the midst of yaml (though, perhaps, that's questionable
786 // behavior), but any actual TAP means that the yaml block was just
787 // not valid.
788 if (this.yind)
789 this.yamlGarbage()
790
791 // If it's anything other than a comment or garbage, then any
792 // maybeChild is just an unsatisfied promise.
793 if (this.maybeChild) {
794 this.emitComment(this.maybeChild)
795 this.maybeChild = null
796 }
797
798 // nothing but comments can come after a trailing plan
799 if (this.postPlan) {
800 this.nonTap(line)
801 return
802 }
803
804 // ok, now it's maybe a thing
805 if (type[0] === 'bailout') {
806 this.bailout(type[1][1].trim(), false)
807 return
808 }
809
810 if (type[0] === 'pragma') {
811 const pragma = type[1]
812 this.pragma(pragma[2], pragma[1] === '+', line)
813 return
814 }
815
816 if (type[0] === 'version') {
817 const version = type[1]
818 this.version(parseInt(version[1], 10), line)
819 return
820 }
821
822 if (type[0] === 'plan') {
823 const plan = type[1]
824 this.plan(+plan[1], +plan[2], (plan[3] || '').trim(), line)
825 return
826 }
827
828 // streamed subtests will end when this test point is emitted
829 if (type[0] === 'testPoint') {
830 // note: it's weird, but possible, to have a testpoint ending in
831 // { before a streamed subtest which ends with a test point
832 // instead of a }. In this case, the parser gets confused, but
833 // also, even beginning to handle that means doing a much more
834 // involved multi-line parse. By that point, the subtest block
835 // has already been emitted as a 'child' event, so it's too late
836 // to really do the optimal thing. The only way around would be
837 // to buffer up everything and do a multi-line parse. This is
838 // rare and weird, and a multi-line parse would be a bigger
839 // rewrite, so I'm allowing it as it currently is.
840 this.parseTestPoint(type[1], line)
841 return
842 }
843
844 // We already detected nontap up above, so the only case left
845 // should be a `# Subtest:` comment. Ignore for coverage, but
846 // include the error here just for good measure.
847 /* istanbul ignore else */
848 if (type[0] === 'subtest') {
849 // this is potentially a subtest. Not indented.
850 // hold until later.
851 this.maybeChild = line
852 } else {
853 throw new Error('Unhandled case: ' + type[0])
854 }
855 }
856
857 parseIndent (line, indent) {
858 // still belongs to the child, so pass it along.
859 if (this.child && line.substr(0, 4) === ' ') {
860 line = line.substr(4)
861 this.child.write(line)
862 return
863 }
864
865 // one of:
866 // - continuing yaml block
867 // - starting yaml block
868 // - ending yaml block
869 // - body of a new child subtest that was previously introduced
870 // - An indented subtest directive
871 // - A comment, or garbage
872
873 // continuing/ending yaml block
874 if (this.yind) {
875 if (line.indexOf(this.yind) === 0) {
876 this.emit('line', line)
877 this.yamlishLine(line)
878 return
879 } else {
880 // oops! that was not actually yamlish, I guess.
881 // this is a case where the indent is shortened mid-yamlish block
882 // treat existing yaml as garbage, continue parsing this line
883 this.yamlGarbage()
884 }
885 }
886
887
888 // start a yaml block under a test point
889 if (this.current && !this.yind && line === indent + '---\n') {
890 this.yind = indent
891 this.emit('line', line)
892 return
893 }
894
895 // at this point, not yamlish, and not an existing child test.
896 // We may have already seen an unindented Subtest directive, or
897 // a test point that ended in { indicating a buffered subtest
898 // Child tests are always indented 4 spaces.
899 if (line.substr(0, 4) === ' ') {
900 if (this.maybeChild ||
901 this.current && this.current.buffered ||
902 lineTypes.subtestIndent.test(line)) {
903 this.startChild(line)
904 return
905 }
906
907 // It's _something_ indented, if the indentation is divisible by
908 // 4 spaces, and the result is actual TAP of some sort, then do
909 // a child subtest for it as well.
910 //
911 // This will lead to some ambiguity in cases where there are multiple
912 // levels of non-signaled subtests, but a Subtest comment in the
913 // middle of them, which may or may not be considered "indented"
914 // See the subtest-no-comment-mid-comment fixture for an example
915 // of this. As it happens, the preference is towards an indented
916 // Subtest comment as the interpretation, which is the only possible
917 // way to resolve this, since otherwise there's no way to distinguish
918 // between an anonymous subtest with a non-indented Subtest comment,
919 // and an indented Subtest comment.
920 const s = line.match(/( {4})+(.*\n)$/)
921 if (s[2].charAt(0) !== ' ') {
922 // integer number of indentations.
923 const type = lineType(s[2])
924 if (type) {
925 if (type[0] === 'comment') {
926 this.emit('line', line)
927 this.emitComment(line)
928 } else {
929 // it's relevant! start as an "unnamed" child subtest
930 this.startChild(line)
931 }
932 return
933 }
934 }
935 }
936
937 // at this point, it's either a non-subtest comment, or garbage.
938
939 if (lineTypes.comment.test(line)) {
940 this.emitComment(line)
941 return
942 }
943
944 this.nonTap(line)
945 }
946}
947
948class FinalResults {
949 constructor (skipAll, self) {
950 this.ok = self.ok
951 this.count = self.count
952 this.pass = self.pass
953 this.fail = self.fail || 0
954 this.bailout = self.bailedOut || false
955 this.todo = self.todo || 0
956 this.skip = skipAll ? self.count : self.skip || 0
957 this.plan = new FinalPlan(skipAll, self)
958 this.failures = self.failures
959 if (self.passes)
960 this.passes = self.passes
961 }
962}
963
964class FinalPlan {
965 constructor (skipAll, self) {
966 this.start = self.planStart === -1 ? null : self.planStart
967 this.end = self.planStart === -1 ? null : self.planEnd
968 this.skipAll = skipAll
969 this.skipReason = skipAll ? self.planComment : ''
970 this.comment = self.planComment || ''
971 }
972}
973
974module.exports = Parser