UNPKG

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