diff options
Diffstat (limited to 'src/main/scala/com/iinteractive/test/tap/Parser.scala')
-rw-r--r-- | src/main/scala/com/iinteractive/test/tap/Parser.scala | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/src/main/scala/com/iinteractive/test/tap/Parser.scala b/src/main/scala/com/iinteractive/test/tap/Parser.scala new file mode 100644 index 0000000..7bc44d3 --- /dev/null +++ b/src/main/scala/com/iinteractive/test/tap/Parser.scala @@ -0,0 +1,232 @@ +package com.iinteractive.test.tap + +import java.io.{ByteArrayInputStream,InputStream,OutputStream} +import scala.annotation.tailrec +import scala.io.Source +import scala.util.parsing.combinator._ +import scala.util.parsing.input.{Position,Reader} + +import com.iinteractive.test.Plan +import com.iinteractive.test.tap.Consumer._ + +/** This class parses a TAP stream. It can either parse it all at once (from a + * string), or it can be used as a streaming parser, where TAP events are + * emitted through a given callback. + */ +class Parser private ( + cb: TAPEvent => Unit, + indent: String +) { + /** Creates a parser instance. + * @param cb The event handler callback. It will be called after each + * meaningful line of TAP, with a + * [[com.iinteractive.test.tap.TAPEvent TAPEvent]] instance + * representing the event that was just parsed. + */ + def this (cb: TAPEvent => Unit = e => ()) = + this(cb, "") + + private def this (indent: String) = + this(e => (), indent) + + /** Parses TAP from an input stream. This variant will actually parse lines + * as they are available to read from the input stream, so this can be used + * as a streaming parser. + */ + def parse (input: InputStream): TAPResult = { + import parser._ + + cb(StartEvent) + tap(new LineReader(input)) match { + case Success(result, _) => { + cb(EndEvent(result)) + result + } + case failure: NoSuccess => throw new ParseException(failure.msg) + } + } + + /** Parses TAP contained in a string. This isn't useful for incremental + * parsing, because the entire input string must be created before + * parsing can begin. + */ + def parse (input: String): TAPResult = + parse(new ByteArrayInputStream(input.getBytes)) + + /** Parses TAP from an output stream. + * + * @todo Doesn't currently work as a streaming parser, since it just + * collects the entire output as a string and feeds it to the parser + * for strings. This could likely be improved. + */ + def parse (input: OutputStream): TAPResult = + parse(input.toString) + + private val parser = new TAPParser(cb, indent) + + private class TAPParser ( + cb: TAPEvent => Unit, + indent: String + ) extends Parsers { + type Elem = Line + + def tap: Parser[TAPResult] = + planFirst | planLast + + private def planFirst: Parser[TAPResult] = + plan ~ rep(result) ^^ { case plan ~ results => + new TAPResult(plan, results) + } + + private def planLast: Parser[TAPResult] = + rep(result) ~ plan ^^ { case results ~ plan => + new TAPResult(plan, results) + } + + private def plan: Parser[Plan] = + planLine ^^ { p => + cb(PlanEvent(p.plan)) + p.plan + } + + private def result: Parser[TestResult] = + simpleResult | subtestResult + + private def simpleResult: Parser[TestResult] = + resultLine ^^ { r => + cb(ResultEvent(r.result)) + r.result + } + + private def subtestResult: Parser[TestResult] = + subtest ~ simpleResult ^^ { case subtest ~ simpleResult => + new TestResult( + simpleResult.passed, + simpleResult.number, + simpleResult.description, + simpleResult.directive, + Some(subtest) + ) + } + + private def subtest: Parser[TAPResult] = + LineParser("subtest") { in => + // can't just return the result directly, because it's of a different + // type (the path dependent type associated with the new Parser + // instance we create here, rather than the path dependent type + // associated with this) + val subParser = new TAPParser( + e => (), + in.first.indent + ) + subParser.tap(in) match { + case subParser.Success(p, rest) => Success(p, rest) + case subParser.Failure(m, rest) => Failure(m, rest) + case subParser.Error(m, rest) => Error(m, rest) + } + } + + private def planLine: Parser[PlanLine] = LineParser("plan") { in => + val line = in.first + if (line.indent == indent) { + line match { + case p: PlanLine => + Success(p, in.rest) + case _ => + Failure("Plan line expected, but '" + line + "' found", in) + } + } + else { + Failure( + "Plan line expected, but " + + "'" + line + "' has incorrect indentation", + in + ) + } + } + + private def resultLine: Parser[ResultLine] = LineParser("result") { in => + val line = in.first + if (line.indent == indent) { + line match { + case p: ResultLine => + Success(p, in.rest) + case _ => + Failure("Result line expected, but '" + line + "' found", in) + } + } + else { + Failure( + "Result line expected, but " + + "'" + line + "' has incorrect indentation", + in + ) + } + } + + private def LineParser[T] (lineType: String)( + body: Input => ParseResult[T] + ): Parser[T] = Parser { in => + if (in.atEnd) { + Failure(lineType + " line expected, but end of input found", in) + } + else { + body(in) + } + } + } + + private class LineReader ( + in: Iterator[Char], + lineNum: Int + ) extends Reader[Line] { + def this (in: InputStream) = + this(Source.fromInputStream(in), 1) + + def atEnd: Boolean = + nextLine.isEmpty + + def first: Line = + nextLine.getOrElse(throw new RuntimeException("read from empty input")) + + lazy val pos = + new LinePosition(lineNum, nextLine.map(_.toString).getOrElse("")) + + lazy val rest: Reader[Line] = + new LineReader(remainingStream, lineNum + 1) + + private def nextLine: Option[Line] = + state._1 + + private def remainingStream: Iterator[Char] = + state._2 + + private lazy val state: (Option[Line], Iterator[Char]) = + readNextLine(in) + + @tailrec + private def readNextLine ( + stream: Iterator[Char] + ): (Option[Line], Iterator[Char]) = { + if (stream.hasNext) { + val (line, rest) = stream.span(_ != '\n') match { + case (l, r) => (parseLine(l.mkString), r.drop(1)) + } + line match { + case _: CommentLine => readNextLine(rest) + case other => (Some(other), rest) + } + } + else { + (None, in) + } + } + } + + private case class LinePosition ( + line: Int, + lineContents: String + ) extends Position { + def column: Int = 1 + } +} |