@tap-format/parser
Version:
A highly verbose parser for the Test Anything Protocol that exposes an observable and streaming interface
509 lines (392 loc) • 10.2 kB
JavaScript
'use strict'
var Rx = require('rx')
var RxNode = require('rx-node')
var R = require('ramda')
var PassThrough = require('readable-stream/passthrough')
var split = require('split')
var duplexer = require('duplexer')
var jsYaml = require('js-yaml')
var O = Rx.Observable
var TEST = 'TEST'
var ASSERTION = 'ASSERTION'
var PLAN = 'PLAN'
var VERSION = 'VERSION'
var COMMENT_BLOCK_START = 'COMMENT_BLOCK_START'
var COMMENT_BLOCK_END = 'COMMENT_BLOCK_END'
var COMMENT = 'COMMENT'
var RESULT = 'RESULT'
var TODO = 'TODO'
var COMMENT_BLOCK_PADDING_SIZE = 2
var REGEXES = {
test: /^#\s+(.+)/,
assertion: new RegExp('^(not )?ok\\b(?:(?:\\s+(\\d+))?(?:\\s+(?:(?:\\s*-\\s*)?(.*)))?)?'),
result: /^# (fail|tests|pass)\s+[0-9]+/,
plan: /^(\d+)\.\.(\d+)\b(?:\s+#\s+SKIP\s+(.*)$)?/,
version: /^TAP\s+version\s+(\d+)/i,
todo: /^(.*?)\s*#\s*TODO\s+(.*)$/,
skip: /^(.*?)\s*#\s*SKIP\s+(.*)$/
}
var removeCommentBlockPadding = R.map(R.drop(COMMENT_BLOCK_PADDING_SIZE))
var parseYamlBlock = R.pipe(
removeCommentBlockPadding,
R.join('\n'),
jsYaml.safeLoad
)
module.exports = {
stream: function () {
var input = new PassThrough()
var output = new PassThrough()
var returnStream = duplexer(input, output)
var tap$ = RxNode.fromStream(input.pipe(split()))
RxNode.writeToStream(parse$(tap$).map(JSON.stringify), output)
return returnStream
},
// TODO: not completely happy about this name for the method
observeStream: function (stream) {
var input$ = RxNode.fromStream(stream.pipe(split()))
return parse$(input$)
}
}
function parse$ (tap$) {
var plans$ = getPlans$(tap$)
var versions$ = getVerions$(tap$)
var tests$ = getTests$(tap$)
var assertions$ = getAssertions$(tap$)
var comments$ = getComments$(tap$)
var passingAssertions$ = assertions$.filter(function (a) { return a.ok })
var failingAssertions$ = assertions$.filter(function (a) { return !a.ok })
var results$ = O.merge(
getResult$('tests', assertions$),
getResult$('pass', passingAssertions$),
getResult$('fail', failingAssertions$)
)
var all$ = O
.merge(
tests$,
assertions$,
comments$,
plans$,
versions$,
results$
)
all$.tests$ = tests$
all$.assertions$ = assertions$
all$.plans$ = plans$
all$.versions$ = versions$
all$.comments$ = comments$
all$.results$ = results$
all$.passingAssertions$ = passingAssertions$
all$.failingAssertions$ = failingAssertions$
return all$
}
function getResult$ (name, input$) {
return input$
.scan(function (prev) {return prev + 1}, 0)
.last()
.map(function (count) {
return {
type: 'result',
name: name,
count: count,
raw: '# ' + name + ' ' + count
}
})
}
function getAssertions$ (input$) {
var formattedLines$ = getGroupedLines$(input$)
var tests$ = getTests$(input$)
var assertions$ = getRawAssertions$(formattedLines$)
var commentBlockStart$ = getCommentBlockStart$(formattedLines$)
var commentBlockEnd$ = getCommentBlockEnd$(formattedLines$)
var commentBlocks$ = getCommentBlocks$(formattedLines$, commentBlockStart$, commentBlockEnd$)
return getFormattedAssertions$(assertions$, commentBlocks$, tests$)
}
function getTests$ (input$) {
var formattedLines$ = getGroupedLines$(input$)
return getFormattedTests$(formattedLines$)
}
function getComments$ (input$) {
var parsingCommentBlock = false
var formattedLines$ = getGroupedLines$(input$)
var commentBlockStart$ = getCommentBlockStart$(formattedLines$)
var commentBlockEnd$ = getCommentBlockEnd$(formattedLines$)
commentBlockStart$.forEach(function () {parsingCommentBlock = true})
commentBlockEnd$.forEach(function () {parsingCommentBlock = false})
return formattedLines$
.filter(function (line) {
var raw = line.current.raw
if (parsingCommentBlock) {
return false
}
if (isTest(raw)) {
return false
}
if (isResult(raw)) {
return false
}
if (isAssertion(raw)) {
return false
}
if (isVersion(raw)) {
return false
}
if (isCommentBlockStart(raw)) {
return false
}
if (isCommentBlockEnd(raw)) {
return false
}
if (isPlan(raw)) {
return false
}
if (isOk(raw)) {
return false
}
if (raw === '') {
return false
}
return true
})
.map(formatCommentObject)
}
function getPlans$ (input$) {
return input$
.filter(isPlan)
.map(formatPlanObject)
}
function getVerions$ (input$) {
return input$
.filter(isVersion)
.map(formatVersionObject)
}
function getGroupedLines$ (input$) {
return input$
.pairwise()
.map(formatLinePair)
}
function getRawAssertions$ (input$) {
return input$
.filter(R.pipe(
R.path(['current', 'type']),
R.equals(ASSERTION)
))
.map(function (line, index) {
line.current.assertionNumber = index + 1
line.next.assertionNumber = index + 2
return line
})
}
function getCommentBlockStart$ (input$) {
return input$
.filter(R.pipe(
R.path(['current', 'type']),
R.equals(COMMENT_BLOCK_START)
))
}
function getCommentBlockEnd$ (input$) {
return input$
.filter(R.pipe(
R.path(['current', 'type']),
R.equals(COMMENT_BLOCK_END)
))
}
function getAssertionsWithComments (assertions$, blocks$) {
return assertions$
.filter(R.pipe(
R.path(['next', 'type']),
R.equals(COMMENT_BLOCK_START)
))
.flatMap(function (line) {
return blocks$.take(1)
.map(function (rawDiagnostic) {
return {
raw: line.current.raw,
lineNumber: line.current.number,
assertionNumber: line.current.assertionNumber,
diagnostic: parseYamlBlock(rawDiagnostic),
rawDiagnostic: rawDiagnostic
}
})
})
}
function getCommentBlocks$ (formattedLines$, start$, end$) {
var parsingCommentBlock = false
var currentCommentBlock = []
var formatBlock = R.pipe(
R.map(R.path(['current', 'raw'])),
R.flatten
)
formattedLines$
.forEach(function (line) {
if (parsingCommentBlock) {
currentCommentBlock.push(line)
}
else {
currentCommentBlock = []
}
})
start$
.forEach(function (line) {
currentCommentBlock = [line]
parsingCommentBlock = true
})
return end$
.map(function () {
parsingCommentBlock = false
return formatBlock(currentCommentBlock)
})
}
function getFormattedTests$ (input$) {
return input$
.filter(R.pipe(
R.path(['current', 'type']),
R.equals(TEST)
))
.map(function (line, index) {
return formatTestObject(line.current.raw, line.current.number, index + 1)
})
}
function getFormattedAssertions$ (assertions$, commentBlocks$, tests$) {
var currentTestNumber = 0
var assertionsWithComments$ = getAssertionsWithComments(assertions$, commentBlocks$)
tests$.forEach(function (line) {currentTestNumber = line.testNumber})
return assertions$
.filter(R.pipe(
R.path(['next', 'type']),
R.complement(R.equals(COMMENT_BLOCK_START))
))
.map(function (line) {
var formattedLine = R.pipe(
R.path(['current']),
R.pick(['raw']),
R.merge({
lineNumber: line.current.number,
assertionNumber: line.current.assertionNumber,
diagnostic: {}
})
)(line)
return formattedLine
})
.merge(assertionsWithComments$)
.map(function (line) {
return formatAssertionObject(line, currentTestNumber)
})
}
function formatLinePair (pair, index) {
return {
current: {
raw: pair[0],
type: getLineType(pair[0]),
number: index
},
next: {
raw: pair[1],
type: getLineType(pair[1]),
number: index + 1
}
}
}
function isTest (line) {
return REGEXES.test.test(line)
&& !isResult(line)
&& !isOk(line)
}
function isOk (line) {
return line === '# ok'
}
function isResult (line) {
return REGEXES.result.test(line)
}
function isPlan (line) {
return REGEXES.plan.test(line)
}
function isVersion (line) {
return REGEXES.version.test(line)
}
function isCommentBlockStart (line) {
if (line === null || line === undefined) {
return false
}
return line.indexOf(' ---') === 0
}
function isCommentBlockEnd (line) {
if (line === null || line === undefined) {
return false
}
return line.indexOf(' ...') === 0
}
function isAssertion (line) {
return REGEXES.assertion.test(line)
}
function getLineType (line) {
if (isTest(line)) {
return TEST
}
if (isAssertion(line)) {
return ASSERTION
}
if (isPlan(line)) {
return PLAN
}
if (isVersion(line)) {
return VERSION
}
if (isCommentBlockStart(line)) {
return COMMENT_BLOCK_START
}
if (isCommentBlockEnd(line)) {
return COMMENT_BLOCK_END
}
}
function formatCommentObject (line) {
var raw = line.current.raw
return {
raw: raw,
title: raw,
lineNumber: line.current.number,
type: 'comment'
}
}
function formatTestObject (line, lineNumber, testNumber) {
return {
raw: line,
type: 'test',
title: line.replace('# ', ''),
lineNumber: lineNumber,
testNumber: testNumber
}
}
function formatAssertionObject (line, testNumber) {
var m = REGEXES.assertion.exec(line.raw)
var rawDiagnostic = ''
if (line.rawDiagnostic) {
rawDiagnostic = line.rawDiagnostic.join('\n')
}
return {
type: 'assertion',
title: m[3],
raw: line.raw + '\n' + rawDiagnostic,
ok: !m[1],
diagnostic: line.diagnostic, // TODO: rename this to "diagnostic",
rawDiagnostic: rawDiagnostic,
lineNumber: line.lineNumber,
testNumber: testNumber,
assertionNumber: line.assertionNumber
}
}
function formatPlanObject (line) {
var m = REGEXES.plan.exec(line);
return {
type: 'plan',
raw: line,
from: m[1] && Number(m[1]),
to: m[2] && Number(m[2]),
skip: m[3]
}
}
function formatVersionObject (line) {
return {
raw: line,
type: 'version'
}
}