Sun calculation js library which is fully based on formula from http://aa.quae.nl/en/reken/zonpositie.html
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
974 lines
26 KiB
974 lines
26 KiB
'use strict'
|
|
|
|
// Transforms a stream of TAP into a stream of result objects
|
|
// and string comments. Emits "results" event with summary.
|
|
const MiniPass = require('minipass')
|
|
|
|
const yaml = require('js-yaml')
|
|
const util = require('util')
|
|
const assert = require('assert')
|
|
|
|
// every line outside of a yaml block is one of these things, or
|
|
// a comment, or garbage.
|
|
const lineTypes = {
|
|
testPoint: /^(not )?ok(?: ([0-9]+))?(?:(?: -)?( .*?))?(\{?)\n$/,
|
|
pragma: /^pragma ([+-])([a-z]+)\n$/,
|
|
bailout: /^bail out!(.*)\n$/i,
|
|
version: /^TAP version ([0-9]+)\n$/i,
|
|
childVersion: /^( )+TAP version ([0-9]+)\n$/i,
|
|
plan: /^([0-9]+)\.\.([0-9]+)(?:\s+(?:#\s*(.*)))?\n$/,
|
|
subtest: /^# Subtest(?:: (.*))?\n$/,
|
|
subtestIndent: /^ # Subtest(?:: (.*))?\n$/,
|
|
comment: /^\s*#.*\n$/
|
|
}
|
|
|
|
const lineTypeNames = Object.keys(lineTypes)
|
|
|
|
const lineType = line => {
|
|
for (let t in lineTypes) {
|
|
const match = line.match(lineTypes[t])
|
|
if (match)
|
|
return [t, match]
|
|
}
|
|
return null
|
|
}
|
|
|
|
const parseDirective = line => {
|
|
if (!line.trim())
|
|
return false
|
|
|
|
line = line.replace(/\{\s*$/, '').trim()
|
|
const time = line.match(/^time=((?:[1-9][0-9]*|0)(?:\.[0-9]+)?)(ms|s)$/i)
|
|
if (time) {
|
|
let n = +time[1]
|
|
if (time[2] === 's') {
|
|
// JS does weird things with floats. Round it off a bit.
|
|
n *= 1000000
|
|
n = Math.round(n)
|
|
n /= 1000
|
|
}
|
|
return [ 'time', n ]
|
|
}
|
|
|
|
const type = line.match(/^(todo|skip)\b/i)
|
|
if (!type)
|
|
return false
|
|
|
|
return [ type[1].toLowerCase(), line.substr(type[1].length).trim() || true ]
|
|
}
|
|
|
|
class Result {
|
|
constructor (parsed, count) {
|
|
const ok = !parsed[1]
|
|
const id = +(parsed[2] || count + 1)
|
|
let buffered = parsed[4]
|
|
this.ok = ok
|
|
this.id = id
|
|
|
|
let rest = parsed[3] || ''
|
|
let name
|
|
rest = rest.replace(/([^\\]|^)((?:\\\\)*)#/g, '$1\n$2').split('\n')
|
|
name = rest.shift()
|
|
rest = rest.filter(r => r.trim()).join('#')
|
|
|
|
// now, let's see if there's a directive in there.
|
|
const dir = parseDirective(rest.trim())
|
|
if (!dir)
|
|
name += (rest ? '#' + rest : '') + buffered
|
|
else {
|
|
// handle buffered subtests with todo/skip on them, like
|
|
// ok 1 - bar # todo foo {\n
|
|
const dirKey = dir[0]
|
|
const dirValue = dir[1]
|
|
this[dirKey] = dirValue
|
|
}
|
|
|
|
if (/\{\s*$/.test(name)) {
|
|
name = name.replace(/\{\s*$/, '')
|
|
buffered = '{'
|
|
}
|
|
|
|
if (buffered === '{')
|
|
this.buffered = true
|
|
|
|
if (name)
|
|
this.name = name.trim()
|
|
}
|
|
}
|
|
|
|
class Parser extends MiniPass {
|
|
constructor (options, onComplete) {
|
|
if (typeof options === 'function') {
|
|
onComplete = options
|
|
options = {}
|
|
}
|
|
|
|
options = options || {}
|
|
super(options)
|
|
this.resume()
|
|
|
|
if (onComplete)
|
|
this.on('complete', onComplete)
|
|
|
|
this.comments = []
|
|
this.results = null
|
|
this.braceLevel = null
|
|
this.parent = options.parent || null
|
|
this.failures = []
|
|
if (options.passes)
|
|
this.passes = []
|
|
this.level = options.level || 0
|
|
|
|
this.buffer = ''
|
|
this.bail = !!options.bail
|
|
this.bailingOut = false
|
|
this.bailedOut = false
|
|
this.syntheticBailout = false
|
|
this.syntheticPlan = false
|
|
this.omitVersion = !!options.omitVersion
|
|
this.planStart = -1
|
|
this.planEnd = -1
|
|
this.planComment = ''
|
|
this.yamlish = ''
|
|
this.yind = ''
|
|
this.child = null
|
|
this.current = null
|
|
this.maybeSubtest = null
|
|
this.extraQueue = []
|
|
this.buffered = options.buffered || null
|
|
this.aborted = false
|
|
this.preserveWhitespace = options.preserveWhitespace || false
|
|
|
|
this.count = 0
|
|
this.pass = 0
|
|
this.fail = 0
|
|
this.todo = 0
|
|
this.skip = 0
|
|
this.ok = true
|
|
|
|
this.strict = options.strict || false
|
|
this.pragmas = { strict: this.strict }
|
|
|
|
this.postPlan = false
|
|
}
|
|
|
|
tapError (error, line) {
|
|
if (line)
|
|
this.emit('line', line)
|
|
this.ok = false
|
|
this.fail ++
|
|
if (typeof error === 'string') {
|
|
error = {
|
|
tapError: error
|
|
}
|
|
}
|
|
this.failures.push(error)
|
|
}
|
|
|
|
parseTestPoint (testPoint, line) {
|
|
this.emitResult()
|
|
if (this.bailedOut)
|
|
return
|
|
|
|
this.emit('line', line)
|
|
const res = new Result(testPoint, this.count)
|
|
if (this.planStart !== -1) {
|
|
const lessThanStart = +res.id < this.planStart
|
|
const greaterThanEnd = +res.id > this.planEnd
|
|
if (lessThanStart || greaterThanEnd) {
|
|
if (lessThanStart)
|
|
res.tapError = 'id less than plan start'
|
|
else
|
|
res.tapError = 'id greater than plan end'
|
|
res.plan = { start: this.planStart, end: this.planEnd }
|
|
this.tapError(res)
|
|
}
|
|
}
|
|
|
|
if (res.id) {
|
|
if (!this.first || res.id < this.first)
|
|
this.first = res.id
|
|
if (!this.last || res.id > this.last)
|
|
this.last = res.id
|
|
}
|
|
|
|
if (!res.skip && !res.todo)
|
|
this.ok = this.ok && res.ok
|
|
|
|
// hold onto it, because we might get yamlish diagnostics
|
|
this.current = res
|
|
}
|
|
|
|
nonTap (data, didLine) {
|
|
if (this.bailingOut && /^( {4})*\}\n$/.test(data))
|
|
return
|
|
|
|
if (this.strict) {
|
|
const err = {
|
|
tapError: 'Non-TAP data encountered in strict mode',
|
|
data: data
|
|
}
|
|
this.tapError(err)
|
|
if (this.parent)
|
|
this.parent.tapError(err)
|
|
}
|
|
|
|
// emit each line, then the extra as a whole
|
|
if (!didLine)
|
|
data.split('\n').slice(0, -1).forEach(line => {
|
|
line += '\n'
|
|
if (this.current || this.extraQueue.length)
|
|
this.extraQueue.push(['line', line])
|
|
else
|
|
this.emit('line', line)
|
|
})
|
|
|
|
if (this.current || this.extraQueue.length)
|
|
this.extraQueue.push(['extra', data])
|
|
else
|
|
this.emit('extra', data)
|
|
}
|
|
|
|
plan (start, end, comment, line) {
|
|
// not allowed to have more than one plan
|
|
if (this.planStart !== -1) {
|
|
this.nonTap(line)
|
|
return
|
|
}
|
|
|
|
// can't put a plan in a child.
|
|
if (this.child || this.yind) {
|
|
this.nonTap(line)
|
|
return
|
|
}
|
|
|
|
this.emitResult()
|
|
if (this.bailedOut)
|
|
return
|
|
|
|
// 1..0 is a special case. Otherwise, end must be >= start
|
|
if (end < start && end !== 0 && start !== 1) {
|
|
if (this.strict)
|
|
this.tapError({
|
|
tapError: 'plan end cannot be less than plan start',
|
|
plan: {
|
|
start: start,
|
|
end: end
|
|
}
|
|
}, line)
|
|
else
|
|
this.nonTap(line)
|
|
return
|
|
}
|
|
|
|
this.planStart = start
|
|
this.planEnd = end
|
|
const p = { start: start, end: end }
|
|
if (comment)
|
|
this.planComment = p.comment = comment
|
|
|
|
// This means that the plan is coming at the END of all the tests
|
|
// Plans MUST be either at the beginning or the very end. We treat
|
|
// plans like '1..0' the same, since they indicate that no tests
|
|
// will be coming.
|
|
if (this.count !== 0 || this.planEnd === 0)
|
|
this.postPlan = true
|
|
|
|
this.emit('line', line)
|
|
this.emit('plan', p)
|
|
}
|
|
|
|
resetYamlish () {
|
|
this.yind = ''
|
|
this.yamlish = ''
|
|
}
|
|
|
|
// that moment when you realize it's not what you thought it was
|
|
yamlGarbage () {
|
|
const yamlGarbage = this.yind + '---\n' + this.yamlish
|
|
this.emitResult()
|
|
if (this.bailedOut)
|
|
return
|
|
this.nonTap(yamlGarbage, true)
|
|
}
|
|
|
|
yamlishLine (line) {
|
|
if (line === this.yind + '...\n') {
|
|
// end the yaml block
|
|
this.processYamlish()
|
|
} else {
|
|
this.yamlish += line
|
|
}
|
|
}
|
|
|
|
processYamlish () {
|
|
const yamlish = this.yamlish
|
|
this.resetYamlish()
|
|
|
|
let diags
|
|
try {
|
|
diags = yaml.safeLoad(yamlish)
|
|
} catch (er) {
|
|
this.nonTap(this.yind + '---\n' + yamlish + this.yind + '...\n', true)
|
|
return
|
|
}
|
|
|
|
this.current.diag = diags
|
|
// we still don't emit the result here yet, to support diags
|
|
// that come ahead of buffered subtests.
|
|
}
|
|
|
|
write (chunk, encoding, cb) {
|
|
if (this.aborted)
|
|
return
|
|
|
|
if (typeof encoding === 'string' && encoding !== 'utf8')
|
|
chunk = new Buffer(chunk, encoding)
|
|
|
|
if (Buffer.isBuffer(chunk))
|
|
chunk += ''
|
|
|
|
if (typeof encoding === 'function') {
|
|
cb = encoding
|
|
encoding = null
|
|
}
|
|
|
|
this.buffer += chunk
|
|
do {
|
|
const match = this.buffer.match(/^.*\r?\n/)
|
|
if (!match)
|
|
break
|
|
|
|
this.buffer = this.buffer.substr(match[0].length)
|
|
this.parse(match[0])
|
|
} while (this.buffer.length)
|
|
|
|
if (cb)
|
|
process.nextTick(cb)
|
|
|
|
return true
|
|
}
|
|
|
|
end (chunk, encoding, cb) {
|
|
if (chunk) {
|
|
if (typeof encoding === 'function') {
|
|
cb = encoding
|
|
encoding = null
|
|
}
|
|
this.write(chunk, encoding)
|
|
}
|
|
|
|
if (this.buffer)
|
|
this.write('\n')
|
|
|
|
// if we have yamlish, means we didn't finish with a ...
|
|
if (this.yamlish)
|
|
this.yamlGarbage()
|
|
|
|
this.emitResult()
|
|
|
|
if (this.syntheticBailout && this.level === 0) {
|
|
this.syntheticBailout = false
|
|
let reason = this.bailedOut
|
|
if (reason === true)
|
|
reason = ''
|
|
else
|
|
reason = ' ' + reason
|
|
this.emit('line', 'Bail out!' + reason + '\n')
|
|
}
|
|
|
|
let skipAll
|
|
|
|
if (this.planEnd === 0 && this.planStart === 1) {
|
|
skipAll = true
|
|
if (this.count === 0) {
|
|
this.ok = true
|
|
} else {
|
|
this.tapError('Plan of 1..0, but test points encountered')
|
|
}
|
|
} else if (!this.bailedOut && this.planStart === -1) {
|
|
if (this.count === 0 && !this.syntheticPlan) {
|
|
this.syntheticPlan = true
|
|
if (this.buffered) {
|
|
this.planStart = 1
|
|
this.planEnd = 0
|
|
} else
|
|
this.plan(1, 0, 'no tests found', '1..0 # no tests found\n')
|
|
skipAll = true
|
|
} else {
|
|
this.tapError('no plan')
|
|
}
|
|
} else if (this.ok && this.count !== (this.planEnd - this.planStart + 1)) {
|
|
this.tapError('incorrect number of tests')
|
|
}
|
|
|
|
if (this.ok && !skipAll && this.first !== this.planStart) {
|
|
this.tapError('first test id does not match plan start')
|
|
}
|
|
|
|
if (this.ok && !skipAll && this.last !== this.planEnd) {
|
|
this.tapError('last test id does not match plan end')
|
|
}
|
|
|
|
this.emitComplete(skipAll)
|
|
if (cb)
|
|
process.nextTick(cb)
|
|
}
|
|
|
|
emitComplete (skipAll) {
|
|
if (!this.results) {
|
|
const res = this.results = new FinalResults(!!skipAll, this)
|
|
|
|
if (!res.bailout) {
|
|
// comment a bit at the end so we know what happened.
|
|
// but don't repeat these comments if they're already present.
|
|
if (res.plan.end !== res.count)
|
|
this.emitComment('test count(' + res.count +
|
|
') != plan(' + res.plan.end + ')', false, true)
|
|
|
|
if (res.fail > 0 && !res.ok)
|
|
this.emitComment('failed ' + res.fail +
|
|
(res.count > 1 ? ' of ' + res.count + ' tests'
|
|
: ' test'),
|
|
false, true)
|
|
|
|
if (res.todo > 0)
|
|
this.emitComment('todo: ' + res.todo, false, true)
|
|
|
|
if (res.skip > 0)
|
|
this.emitComment('skip: ' + res.skip, false, true)
|
|
}
|
|
|
|
this.emit('complete', this.results)
|
|
}
|
|
}
|
|
|
|
version (version, line) {
|
|
// If version is specified, must be at the very beginning.
|
|
if (version >= 13 &&
|
|
this.planStart === -1 &&
|
|
this.count === 0 &&
|
|
!this.current) {
|
|
this.emit('line', line)
|
|
this.emit('version', version)
|
|
} else
|
|
this.nonTap(line)
|
|
}
|
|
|
|
pragma (key, value, line) {
|
|
// can't put a pragma in a child or yaml block
|
|
if (this.child) {
|
|
this.nonTap(line)
|
|
return
|
|
}
|
|
|
|
this.emitResult()
|
|
if (this.bailedOut)
|
|
return
|
|
// only the 'strict' pragma is currently relevant
|
|
if (key === 'strict') {
|
|
this.strict = value
|
|
}
|
|
this.pragmas[key] = value
|
|
this.emit('line', line)
|
|
this.emit('pragma', key, value)
|
|
}
|
|
|
|
bailout (reason, synthetic) {
|
|
this.syntheticBailout = synthetic
|
|
|
|
if (this.bailingOut)
|
|
return
|
|
|
|
// Guard because emitting a result can trigger a forced bailout
|
|
// if the harness decides that failures should be bailouts.
|
|
this.bailingOut = reason || true
|
|
|
|
if (!synthetic)
|
|
this.emitResult()
|
|
else
|
|
this.current = null
|
|
|
|
this.bailedOut = this.bailingOut
|
|
this.ok = false
|
|
if (!synthetic) {
|
|
// synthetic bailouts get emitted on end
|
|
let line = 'Bail out!'
|
|
if (reason)
|
|
line += ' ' + reason
|
|
this.emit('line', line + '\n')
|
|
}
|
|
this.emit('bailout', reason)
|
|
if (this.parent) {
|
|
this.end()
|
|
this.parent.bailout(reason, true)
|
|
}
|
|
}
|
|
|
|
clearExtraQueue () {
|
|
for (let c = 0; c < this.extraQueue.length; c++) {
|
|
this.emit(this.extraQueue[c][0], this.extraQueue[c][1])
|
|
}
|
|
this.extraQueue.length = 0
|
|
}
|
|
|
|
endChild () {
|
|
if (this.child && (!this.bailingOut || this.child.count)) {
|
|
this.child.end()
|
|
this.child = null
|
|
}
|
|
}
|
|
|
|
emitResult () {
|
|
if (this.bailedOut)
|
|
return
|
|
|
|
this.endChild()
|
|
this.resetYamlish()
|
|
|
|
if (!this.current)
|
|
return this.clearExtraQueue()
|
|
|
|
const res = this.current
|
|
this.current = null
|
|
|
|
this.count++
|
|
if (res.ok) {
|
|
this.pass++
|
|
if (this.passes)
|
|
this.passes.push(res)
|
|
} else {
|
|
this.fail++
|
|
if (!res.todo && !res.skip) {
|
|
this.ok = false
|
|
this.failures.push(res)
|
|
}
|
|
}
|
|
|
|
if (res.skip)
|
|
this.skip++
|
|
|
|
if (res.todo)
|
|
this.todo++
|
|
|
|
this.emit('assert', res)
|
|
if (this.bail && !res.ok && !res.todo && !res.skip && !this.bailingOut) {
|
|
this.maybeChild = null
|
|
const ind = new Array(this.level + 1).join(' ')
|
|
let p
|
|
for (p = this; p.parent; p = p.parent);
|
|
const bailName = res.name ? ' # ' + res.name : ''
|
|
p.parse(ind + 'Bail out!' + bailName + '\n')
|
|
}
|
|
this.clearExtraQueue()
|
|
}
|
|
|
|
// TODO: We COULD say that any "relevant tap" line that's indented
|
|
// by 4 spaces starts a child test, and just call it 'unnamed' if
|
|
// it does not have a prefix comment. In that case, any number of
|
|
// 4-space indents can be plucked off to try to find a relevant
|
|
// TAP line type, and if so, start the unnamed child.
|
|
startChild (line) {
|
|
const maybeBuffered = this.current && this.current.buffered
|
|
const unindentStream = !maybeBuffered && this.maybeChild
|
|
const indentStream = !maybeBuffered && !unindentStream &&
|
|
lineTypes.subtestIndent.test(line)
|
|
const unnamed = !maybeBuffered && !unindentStream && !indentStream
|
|
|
|
// If we have any other result waiting in the wings, we need to emit
|
|
// that now. A buffered test emits its test point at the *end* of
|
|
// the child subtest block, so as to match streamed test semantics.
|
|
if (!maybeBuffered)
|
|
this.emitResult()
|
|
|
|
if (this.bailedOut)
|
|
return
|
|
|
|
this.child = new Parser({
|
|
bail: this.bail,
|
|
parent: this,
|
|
level: this.level + 1,
|
|
buffered: maybeBuffered,
|
|
preserveWhitespace: this.preserveWhitespace,
|
|
omitVersion: true,
|
|
strict: this.strict
|
|
})
|
|
|
|
this.child.on('complete', results => {
|
|
if (!results.ok)
|
|
this.ok = false
|
|
})
|
|
|
|
this.child.on('line', l => {
|
|
if (l.trim() || this.preserveWhitespace)
|
|
l = ' ' + l
|
|
this.emit('line', l)
|
|
})
|
|
|
|
// Canonicalize the parsing result of any kind of subtest
|
|
// if it's a buffered subtest or a non-indented Subtest directive,
|
|
// then synthetically emit the Subtest comment
|
|
line = line.substr(4)
|
|
let subtestComment
|
|
if (indentStream) {
|
|
subtestComment = line
|
|
line = null
|
|
} else if (maybeBuffered) {
|
|
subtestComment = '# Subtest: ' + this.current.name + '\n'
|
|
} else {
|
|
subtestComment = this.maybeChild || '# Subtest\n'
|
|
}
|
|
|
|
this.maybeChild = null
|
|
this.child.name = subtestComment.substr('# Subtest: '.length).trim()
|
|
|
|
// at some point, we may wish to move 100% to preferring
|
|
// the Subtest comment on the parent level. If so, uncomment
|
|
// this line, and remove the child.emitComment below.
|
|
// this.emit('comment', subtestComment)
|
|
if (!this.child.buffered)
|
|
this.emit('line', subtestComment)
|
|
this.emit('child', this.child)
|
|
this.child.emitComment(subtestComment, true)
|
|
if (line)
|
|
this.child.parse(line)
|
|
}
|
|
|
|
abort (message, extra) {
|
|
if (this.child) {
|
|
const b = this.child.buffered
|
|
this.child.abort(message, extra)
|
|
extra = null
|
|
if (b)
|
|
this.write('\n}\n')
|
|
}
|
|
|
|
let dump
|
|
if (extra && Object.keys(extra).length) {
|
|
try {
|
|
dump = yaml.safeDump(extra).trimRight()
|
|
} catch (er) {}
|
|
}
|
|
|
|
let y
|
|
if (dump)
|
|
y = ' ---\n ' + dump.split('\n').join('\n ') + '\n ...\n'
|
|
else
|
|
y = '\n'
|
|
let n = (this.count || 0) + 1
|
|
if (this.current)
|
|
n += 1
|
|
|
|
if (this.planEnd !== -1 && this.planEnd < n && this.parent) {
|
|
// skip it, let the parent do this.
|
|
this.aborted = true
|
|
return
|
|
}
|
|
|
|
let ind = '' // new Array(this.level + 1).join(' ')
|
|
message = message.replace(/[\n\r\s\t]/g, ' ')
|
|
let point = '\nnot ok ' + n + ' - ' + message + '\n' + y
|
|
|
|
if (this.planEnd === -1)
|
|
point += '1..' + n + '\n'
|
|
|
|
this.write(point)
|
|
this.aborted = true
|
|
this.end()
|
|
}
|
|
|
|
emitComment (line, skipLine, noDuplicate) {
|
|
if (line.trim().charAt(0) !== '#')
|
|
line = '# ' + line
|
|
|
|
if (line.slice(-1) !== '\n')
|
|
line += '\n'
|
|
|
|
if (noDuplicate && this.comments.indexOf(line) !== -1)
|
|
return
|
|
|
|
this.comments.push(line)
|
|
if (this.current || this.extraQueue.length) {
|
|
// no way to get here with skipLine being true
|
|
this.extraQueue.push(['line', line])
|
|
this.extraQueue.push(['comment', line])
|
|
} else {
|
|
if (!skipLine)
|
|
this.emit('line', line)
|
|
this.emit('comment', line)
|
|
}
|
|
}
|
|
|
|
parse (line) {
|
|
// normalize line endings
|
|
line = line.replace(/\r\n$/, '\n')
|
|
|
|
// sometimes empty lines get trimmed, but are still part of
|
|
// a subtest or a yaml block. Otherwise, nothing to parse!
|
|
if (line === '\n') {
|
|
if (this.child)
|
|
line = ' ' + line
|
|
else if (this.yind)
|
|
line = this.yind + line
|
|
}
|
|
|
|
// If we're bailing out, then the only thing we want to see is the
|
|
// end of a buffered child test. Anything else should be ignored.
|
|
// But! if we're bailing out a nested child, and ANOTHER nested child
|
|
// comes after that one, then we don't want the second child's } to
|
|
// also show up, or it looks weird.
|
|
if (this.bailingOut) {
|
|
if (!/^\s*}\n$/.test(line))
|
|
return
|
|
else if (!this.braceLevel || line.length < this.braceLevel)
|
|
this.braceLevel = line.length
|
|
else
|
|
return
|
|
}
|
|
|
|
// This allows omitting even parsing the version if the test is
|
|
// an indented child test. Several parsers get upset when they
|
|
// see an indented version field.
|
|
if (this.omitVersion && lineTypes.version.test(line) && !this.yind)
|
|
return
|
|
|
|
// check to see if the line is indented.
|
|
// if it is, then it's either a subtest, yaml, or garbage.
|
|
const indent = line.match(/^[ \t]*/)[0]
|
|
if (indent) {
|
|
this.parseIndent(line, indent)
|
|
return
|
|
}
|
|
|
|
// In any case where we're going to emitResult, that can trigger
|
|
// a bailout, so we need to only emit the line once we know that
|
|
// isn't happening, to prevent cases where there's a bailout, and
|
|
// then one more line of output. That'll also prevent the case
|
|
// where the test point is emitted AFTER the line that follows it.
|
|
|
|
// buffered subtests must end with a }
|
|
if (this.child && this.child.buffered && line === '}\n') {
|
|
this.endChild()
|
|
this.emit('line', line)
|
|
this.emitResult()
|
|
return
|
|
}
|
|
|
|
// just a \n, emit only if we care about whitespace
|
|
const validLine = this.preserveWhitespace || line.trim() || this.yind
|
|
if (line === '\n')
|
|
return validLine && this.emit('line', line)
|
|
|
|
// buffered subtest with diagnostics
|
|
if (this.current && line === '{\n' &&
|
|
!this.current.buffered &&
|
|
!this.child) {
|
|
this.emit('line', line)
|
|
this.current.buffered = true
|
|
return
|
|
}
|
|
|
|
// now we know it's not indented, so if it's either valid tap
|
|
// or garbage. Get the type of line.
|
|
const type = lineType(line)
|
|
if (!type) {
|
|
this.nonTap(line)
|
|
return
|
|
}
|
|
|
|
if (type[0] === 'comment') {
|
|
this.emitComment(line)
|
|
return
|
|
}
|
|
|
|
// if we have any yamlish, it's garbage now. We tolerate non-TAP and
|
|
// comments in the midst of yaml (though, perhaps, that's questionable
|
|
// behavior), but any actual TAP means that the yaml block was just
|
|
// not valid.
|
|
if (this.yind)
|
|
this.yamlGarbage()
|
|
|
|
// If it's anything other than a comment or garbage, then any
|
|
// maybeChild is just an unsatisfied promise.
|
|
if (this.maybeChild) {
|
|
this.emitComment(this.maybeChild)
|
|
this.maybeChild = null
|
|
}
|
|
|
|
// nothing but comments can come after a trailing plan
|
|
if (this.postPlan) {
|
|
this.nonTap(line)
|
|
return
|
|
}
|
|
|
|
// ok, now it's maybe a thing
|
|
if (type[0] === 'bailout') {
|
|
this.bailout(type[1][1].trim(), false)
|
|
return
|
|
}
|
|
|
|
if (type[0] === 'pragma') {
|
|
const pragma = type[1]
|
|
this.pragma(pragma[2], pragma[1] === '+', line)
|
|
return
|
|
}
|
|
|
|
if (type[0] === 'version') {
|
|
const version = type[1]
|
|
this.version(parseInt(version[1], 10), line)
|
|
return
|
|
}
|
|
|
|
if (type[0] === 'plan') {
|
|
const plan = type[1]
|
|
this.plan(+plan[1], +plan[2], (plan[3] || '').trim(), line)
|
|
return
|
|
}
|
|
|
|
// streamed subtests will end when this test point is emitted
|
|
if (type[0] === 'testPoint') {
|
|
// note: it's weird, but possible, to have a testpoint ending in
|
|
// { before a streamed subtest which ends with a test point
|
|
// instead of a }. In this case, the parser gets confused, but
|
|
// also, even beginning to handle that means doing a much more
|
|
// involved multi-line parse. By that point, the subtest block
|
|
// has already been emitted as a 'child' event, so it's too late
|
|
// to really do the optimal thing. The only way around would be
|
|
// to buffer up everything and do a multi-line parse. This is
|
|
// rare and weird, and a multi-line parse would be a bigger
|
|
// rewrite, so I'm allowing it as it currently is.
|
|
this.parseTestPoint(type[1], line)
|
|
return
|
|
}
|
|
|
|
// We already detected nontap up above, so the only case left
|
|
// should be a `# Subtest:` comment. Ignore for coverage, but
|
|
// include the error here just for good measure.
|
|
/* istanbul ignore else */
|
|
if (type[0] === 'subtest') {
|
|
// this is potentially a subtest. Not indented.
|
|
// hold until later.
|
|
this.maybeChild = line
|
|
} else {
|
|
throw new Error('Unhandled case: ' + type[0])
|
|
}
|
|
}
|
|
|
|
parseIndent (line, indent) {
|
|
// still belongs to the child, so pass it along.
|
|
if (this.child && line.substr(0, 4) === ' ') {
|
|
line = line.substr(4)
|
|
this.child.write(line)
|
|
return
|
|
}
|
|
|
|
// one of:
|
|
// - continuing yaml block
|
|
// - starting yaml block
|
|
// - ending yaml block
|
|
// - body of a new child subtest that was previously introduced
|
|
// - An indented subtest directive
|
|
// - A comment, or garbage
|
|
|
|
// continuing/ending yaml block
|
|
if (this.yind) {
|
|
if (line.indexOf(this.yind) === 0) {
|
|
this.emit('line', line)
|
|
this.yamlishLine(line)
|
|
return
|
|
} else {
|
|
// oops! that was not actually yamlish, I guess.
|
|
// this is a case where the indent is shortened mid-yamlish block
|
|
// treat existing yaml as garbage, continue parsing this line
|
|
this.yamlGarbage()
|
|
}
|
|
}
|
|
|
|
|
|
// start a yaml block under a test point
|
|
if (this.current && !this.yind && line === indent + '---\n') {
|
|
this.yind = indent
|
|
this.emit('line', line)
|
|
return
|
|
}
|
|
|
|
// at this point, not yamlish, and not an existing child test.
|
|
// We may have already seen an unindented Subtest directive, or
|
|
// a test point that ended in { indicating a buffered subtest
|
|
// Child tests are always indented 4 spaces.
|
|
if (line.substr(0, 4) === ' ') {
|
|
if (this.maybeChild ||
|
|
this.current && this.current.buffered ||
|
|
lineTypes.subtestIndent.test(line)) {
|
|
this.startChild(line)
|
|
return
|
|
}
|
|
|
|
// It's _something_ indented, if the indentation is divisible by
|
|
// 4 spaces, and the result is actual TAP of some sort, then do
|
|
// a child subtest for it as well.
|
|
//
|
|
// This will lead to some ambiguity in cases where there are multiple
|
|
// levels of non-signaled subtests, but a Subtest comment in the
|
|
// middle of them, which may or may not be considered "indented"
|
|
// See the subtest-no-comment-mid-comment fixture for an example
|
|
// of this. As it happens, the preference is towards an indented
|
|
// Subtest comment as the interpretation, which is the only possible
|
|
// way to resolve this, since otherwise there's no way to distinguish
|
|
// between an anonymous subtest with a non-indented Subtest comment,
|
|
// and an indented Subtest comment.
|
|
const s = line.match(/( {4})+(.*\n)$/)
|
|
if (s[2].charAt(0) !== ' ') {
|
|
// integer number of indentations.
|
|
const type = lineType(s[2])
|
|
if (type) {
|
|
if (type[0] === 'comment') {
|
|
this.emit('line', line)
|
|
this.emitComment(line)
|
|
} else {
|
|
// it's relevant! start as an "unnamed" child subtest
|
|
this.startChild(line)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// at this point, it's either a non-subtest comment, or garbage.
|
|
|
|
if (lineTypes.comment.test(line)) {
|
|
this.emitComment(line)
|
|
return
|
|
}
|
|
|
|
this.nonTap(line)
|
|
}
|
|
}
|
|
|
|
class FinalResults {
|
|
constructor (skipAll, self) {
|
|
this.ok = self.ok
|
|
this.count = self.count
|
|
this.pass = self.pass
|
|
this.fail = self.fail || 0
|
|
this.bailout = self.bailedOut || false
|
|
this.todo = self.todo || 0
|
|
this.skip = skipAll ? self.count : self.skip || 0
|
|
this.plan = new FinalPlan(skipAll, self)
|
|
this.failures = self.failures
|
|
if (self.passes)
|
|
this.passes = self.passes
|
|
}
|
|
}
|
|
|
|
class FinalPlan {
|
|
constructor (skipAll, self) {
|
|
this.start = self.planStart === -1 ? null : self.planStart
|
|
this.end = self.planStart === -1 ? null : self.planEnd
|
|
this.skipAll = skipAll
|
|
this.skipReason = skipAll ? self.planComment : ''
|
|
this.comment = self.planComment || ''
|
|
}
|
|
}
|
|
|
|
module.exports = Parser
|
|
|