aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/scala/org/perl8/test/tap/Consumer.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/scala/org/perl8/test/tap/Consumer.scala')
-rw-r--r--src/main/scala/org/perl8/test/tap/Consumer.scala390
1 files changed, 313 insertions, 77 deletions
diff --git a/src/main/scala/org/perl8/test/tap/Consumer.scala b/src/main/scala/org/perl8/test/tap/Consumer.scala
index 8d64503..0ad74fb 100644
--- a/src/main/scala/org/perl8/test/tap/Consumer.scala
+++ b/src/main/scala/org/perl8/test/tap/Consumer.scala
@@ -1,123 +1,352 @@
package org.perl8.test.tap
-import org.perl8.test._
+import java.io.{ByteArrayInputStream,InputStream,OutputStream}
+import scala.annotation.tailrec
+import scala.util.parsing.combinator._
+import scala.util.parsing.input.{Position,Reader}
+
+import org.perl8.test.{Plan,NumericPlan,SkipAll}
object Consumer {
- import java.io.OutputStream
- import scala.util.parsing.combinator._
+ def parse (input: InputStream, cb: TAPEvent => Unit): TAPResult =
+ consumer(cb).parse(input)
+
+ def parse (input: InputStream): TAPResult =
+ consumer().parse(input)
+
+ def parse (input: String, cb: TAPEvent => Unit): TAPResult =
+ consumer(cb).parse(new ByteArrayInputStream(input.getBytes))
+
+ def parse (input: String): TAPResult =
+ consumer().parse(new ByteArrayInputStream(input.getBytes))
+
+ // XXX should be able to make a streaming input stream out of this
+ def parse (input: OutputStream, cb: TAPEvent => Unit): TAPResult =
+ consumer(cb).parse(new ByteArrayInputStream(input.toString.getBytes))
+
+ def parse (input: OutputStream): TAPResult =
+ consumer().parse(new ByteArrayInputStream(input.toString.getBytes))
+
+ private def consumer (cb: TAPEvent => Unit = e => ()) =
+ new Consumer(cb)
+}
- def parse (input: String): TAPResult = {
- import TAPParser.{parseAll,tap,Success,NoSuccess}
+class Consumer (cb: TAPEvent => Unit) {
+ def parse (input: InputStream): TAPResult = {
+ import TAPParser.{tap,Success,NoSuccess}
- parseAll(tap(), input) match {
+ tap(new LineReader(input)) match {
case Success(result, _) => result
case failure: NoSuccess => throw new ParseException(failure.msg)
}
}
- def parse (input: OutputStream): TAPResult =
- parse(input.toString)
+ private object TAPParser extends Parsers {
+ type Elem = Line
+
+ def tap: Parser[TAPResult] =
+ planFirst | planLast
- private object TAPParser extends RegexParsers {
- def tap (indent: String = ""): Parser[TAPResult] =
- planFirst(indent) | planLast(indent)
+ private def planFirst: Parser[TAPResult] =
+ plan ~ rep(result) ^^ { case plan ~ results =>
+ new TAPResult(plan, results)
+ }
- def planFirst (indent: String): Parser[TAPResult] =
- (line(plan, indent) ~ rep(line(result(indent), indent))) ^^ {
- case plan ~ results => new TAPResult(plan, results)
+ private def planLast: Parser[TAPResult] =
+ rep(result) ~ plan ^^ { case results ~ plan =>
+ new TAPResult(plan, results)
}
- def planLast (indent: String): Parser[TAPResult] =
- (rep(line(result(indent), indent)) ~ line(plan, indent)) ^^ {
- case results ~ plan => new TAPResult(plan, results)
+ private def plan: Parser[Plan] =
+ planLine ^^ { line =>
+ line.plan
}
- def comment: Parser[String] =
- """#[^\n]*""".r
+ private def result: Parser[TestResult] =
+ simpleResult | subtestResult
+
+ private def simpleResult: Parser[TestResult] =
+ resultLine ^^ { line =>
+ line.result
+ }
- def plan: Parser[Plan] =
- (planValue <~ ws) ~ opt(planDirective) ^^ {
- case planValue ~ Some(SkipDirective(d)) => SkipAll(d)
- case planValue ~ None => NumericPlan(planValue)
+ private def subtestResult: Parser[TestResult] = new Parser[TestResult] {
+ def apply (in: Input) = {
+ if (in.atEnd) {
+ Failure("Subtest expected, but end of input found", in)
+ }
+ else {
+ val firstLine = in.first
+ if (firstLine.indent.length <= expectedIndent.length) {
+ Failure(
+ "Subtest expected, but '" + firstLine + "' is not indented",
+ in
+ )
+ }
+ else {
+ parseSubtest(in)
+ }
+ }
}
- def result (indent: String): Parser[TestResult] =
- simpleResult | subtestResult(indent)
+ private def parseSubtest (in: Input) = {
+ val oldIndent = expectedIndent
+ val newIndent = in.first.indent
- def simpleResult: Parser[TestResult] =
- (ok <~ ws) ~
- (testNumber <~ ws) ~
- (testDescription <~ ws) ~
- opt(testDirective) ^^ {
- case ok ~ testNumber ~ testDescription ~ testDirective =>
- new TestResult(ok, testNumber, testDescription, testDirective, None)
+ val subtestParseResult = try {
+ expectedIndent = newIndent
+ tap(in)
+ }
+ finally {
+ expectedIndent = oldIndent
+ }
+
+ subtestParseResult match {
+ case Success(subtestResult, rest) => {
+ simpleResult(rest) match {
+ case Success(summaryResult, rest) => {
+ val testResult = new TestResult(
+ summaryResult.passed,
+ summaryResult.number,
+ summaryResult.description,
+ summaryResult.directive,
+ Some(subtestParseResult.get)
+ )
+ Success(testResult, rest)
+ }
+ case Failure(_, rest) => {
+ Failure(
+ "Subtest summary test result expected, but " +
+ "'" + subtestParseResult.next.first + "' found",
+ rest
+ )
+ }
+ case e: Error => e
+ }
+ }
+ case Failure(_, rest) => {
+ Failure("Subtest expected, but '" + in.first + "' found", in)
+ }
+ case e: Error => e
+ }
}
+ }
- def subtestResult (indent: String): Parser[TestResult] =
- (new Parser[TAPResult] {
- def apply (in: Input) = {
- val source = in.source
- val offset = in.offset
- val str = source.subSequence(offset, source.length)
- val newIndent = ws.findPrefixMatchOf(str).getOrElse("").toString
- newIndent match {
- case "" => Failure("subtests must be indented", in)
- case _ => {
- tap(indent + newIndent)(in.drop(-indent.length))
+ private def planLine: Parser[PlanLine] = new Parser[PlanLine] {
+ def apply (in: Input) = {
+ if (in.atEnd) {
+ Failure("Plan line expected, but end of input found", in)
+ }
+ else {
+ val line = in.first
+ if (line.indent == expectedIndent) {
+ 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] = new Parser[ResultLine] {
+ def apply (in: Input) = {
+ if (in.atEnd) {
+ Failure("Result line expected, but end of input found", in)
+ }
+ else {
+ val line = in.first
+ if (line.indent == expectedIndent) {
+ 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
+ )
+ }
}
- } <~ indent <~ rep(comment ~ "\n" ~ indent)) ~ simpleResult ^^ {
- case tapResult ~ testResult =>
- new TestResult(
- testResult.passed,
- testResult.number,
- testResult.description,
- testResult.directive,
- Some(tapResult)
- )
}
+ }
- def planValue: Parser[Int] =
- "1.." ~> """\d+""".r ^^ { _.toInt }
+ private var expectedIndent = ""
+ }
- def planDirective: Parser[Directive] =
- skipDirective
+ private sealed trait Line {
+ def contents: String
+ def indent: String
+ override def toString: String =
+ indent + contents
+ }
- def ok: Parser[Boolean] =
- opt("not ") <~ "ok" ^^ { _.isEmpty }
+ private object Line {
+ def apply (line: String): Line = {
+ commentRx.findFirstMatchIn(line).map { m =>
+ m.subgroups match {
+ case Seq(indent, text) => new CommentLine(text, indent)
+ }
+ }.getOrElse {
+ planRx.findFirstMatchIn(line).map { m =>
+ m.subgroups match {
+ case Seq(indent, p, null) =>
+ new PlanLine(NumericPlan(p.toInt), indent)
+ case Seq(indent, _, skip) =>
+ new PlanLine(SkipAll(skip), indent)
+ }
+ }.getOrElse {
+ resultRx.findFirstMatchIn(line).map { m =>
+ val indent = m.group(1)
+ val passed = m.group(2) == null
+ val number = m.group(3).toInt
+ val description = m.group(4) match {
+ case null => ""
+ case s => s.trim
+ }
+ val directive = (m.group(5), m.group(6)) match {
+ case (null, null) => None
+ case (d, r) => {
+ val reason = if (r == null) "" else r
+ """(?i:skip)""".r.findFirstIn(d) match {
+ case Some(_) => Some(new SkipDirective(Some(reason)))
+ case None => Some(new TodoDirective(Some(reason)))
+ }
+ }
+ }
+ val result = new TestResult(
+ passed,
+ number,
+ description,
+ directive,
+ None
+ )
+ new ResultLine(result, indent)
+ }.getOrElse {
+ throw ParseException("Couldn't parse line: " + line)
+ }
+ }
+ }
+ }
- def testNumber: Parser[Int] =
- """\d+""".r ^^ { _.toInt }
+ private val commentRx = """^(\s*)#\s*(.*)""".r
+ private val planRx = """^(\s*)1..(\d+)\s*(?:# SKIP (.*))?""".r
+ private val resultRx =
+ """^(\s*)(not )?ok (\d+)\s*([^#]+)?(?:#\s*(?i:(skip|todo))\s+(.*))?""".r
+ }
- def testDescription: Parser[String] =
- """[^#\n]*""".r ^^ { _.trim }
+ private case class CommentLine (
+ val text: String,
+ override val indent: String
+ ) extends Line {
+ def contents = "# " + text
+ }
- def testDirective: Parser[Directive] =
- todoDirective | skipDirective
+ private case class PlanLine (
+ val plan: Plan,
+ override val indent: String
+ ) extends Line {
+ def contents = {
+ val count = plan.plan
+ val comment = plan match {
+ case SkipAll(m) => " # SKIP " + m
+ case _ => ""
+ }
+ indent + "1.." + count + comment
+ }
+ }
- def skipDirective: Parser[Directive] =
- "#" ~> ws ~> """(?i:skip)""".r ~> ws ~> opt("""[^\n]*""".r) ^^ {
- case desc => new SkipDirective(desc.map(s => s.trim))
+ private case class ResultLine(
+ val result: TestResult,
+ override val indent: String
+ ) extends Line {
+ def contents = {
+ val success = (if (result.passed) "ok" else "not ok") + " "
+ val number = result.number + " "
+ val description = result.description match {
+ case "" => ""
+ case s => s + " "
+ }
+ val directive = result.directive match {
+ case Some(TodoDirective(m)) => "# TODO " + m
+ case Some(SkipDirective(m)) => "# skip " + m
+ case None => ""
}
+ indent + success + number + description + directive
+ }
+ }
+
+ private class LineReader (
+ in: InputStream,
+ lineNum: Int = 1
+ ) extends Reader[Line] {
+ 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(""))
- def todoDirective: Parser[Directive] =
- "#" ~> ws ~> """(?i:todo)""".r ~> ws ~> opt("""[^\n]*""".r) ^^ {
- case desc => new TodoDirective(desc.map(s => s.trim))
+ def rest: Reader[Line] =
+ new LineReader(in, lineNum + 1)
+
+ private lazy val nextLine: Option[Line] =
+ readNextLine
+
+ private def readNextLine: Option[Line] = {
+ val buf = new StringBuilder
+
+ @tailrec
+ def read {
+ val byte = in.read
+ if (byte != -1) {
+ buf += byte.toChar
+ if (byte != '\n') {
+ read
+ }
+ }
}
- def line[T] (p: => Parser[T], indent: String): Parser[T] =
- rep(indent ~ comment ~ "\n") ~>
- indent ~> p <~ "\n" <~
- rep(indent ~ comment ~ "\n")
+ read
+
+ val line = buf.toString match {
+ case "" => None
+ case s => Some(Line(s.init))
+ }
- override def skipWhitespace = false
+ line.flatMap { l =>
+ l match {
+ case CommentLine(_, _) => readNextLine
+ case other => Some(other)
+ }
+ }
+ }
- val ws = """[ \t]*""".r
+ class LinePosition (
+ override val line: Int,
+ override val lineContents: String
+ ) extends Position {
+ def column: Int = 1
+ }
}
}
-trait Directive {
+sealed trait Directive {
val message: Option[String]
}
case class SkipDirective (
@@ -162,6 +391,13 @@ class TAPResult (val plan: Plan, val results: Seq[TestResult]) {
}
}
+trait TAPEvent
+case class ResultEvent (result: TestResult) extends TAPEvent
+case class PlanEvent (plan: Plan) extends TAPEvent
+case object SubtestStartEvent extends TAPEvent
+case class SubtestEndEvent (result: TestResult) extends TAPEvent
+case class CommentEvent (text: String) extends TAPEvent
+
case class ParseException (
val message: String
) extends RuntimeException(message)