From 562d60a18ac4b0f9f8de012b913172f431f3f044 Mon Sep 17 00:00:00 2001 From: ilopatin Date: Sat, 30 Mar 2024 15:27:41 +0100 Subject: [PATCH] Add pretty errors --- .../parser/benchmarks/ParserBenchmark.scala | 4 +- .../lucene/LuceneQueryBenchmark.scala | 7 +- .../micro/CharParserMicroBenchmarks.scala | 11 +- .../zio/parser/caliban/CalibanParser.scala | 2 +- .../src/main/scala/zio/parser/Parser.scala | 99 +++++++++++++-- .../src/main/scala/zio/parser/Syntax.scala | 8 +- .../internal/stacksafe/CharParserImpl.scala | 8 +- .../scala/zio/parser/ParseFailureSpec.scala | 113 ++++++++++++++++++ .../test/scala/zio/parser/ParserSpec.scala | 8 +- .../test/scala/zio/parser/SyntaxSpec.scala | 2 +- .../parser/examples/ContextualExample.scala | 2 +- 11 files changed, 226 insertions(+), 38 deletions(-) create mode 100644 zio-parser/shared/src/test/scala/zio/parser/ParseFailureSpec.scala diff --git a/benchmarks/src/main/scala/zio/parser/benchmarks/ParserBenchmark.scala b/benchmarks/src/main/scala/zio/parser/benchmarks/ParserBenchmark.scala index 78fa485..df101b6 100644 --- a/benchmarks/src/main/scala/zio/parser/benchmarks/ParserBenchmark.scala +++ b/benchmarks/src/main/scala/zio/parser/benchmarks/ParserBenchmark.scala @@ -46,7 +46,7 @@ abstract class ParserBenchmark[T] { // } @Benchmark - def zioParserRecursive(): Either[zio.parser.Parser.ParserError[String], T] = { + def zioParserRecursive(): Either[zio.parser.ParseFailure[String], T] = { import zio.parser._ zioSyntax.parseString(value, ParserImplementation.Recursive) } @@ -58,7 +58,7 @@ abstract class ParserBenchmark[T] { } @Benchmark - def zioParserOpStack(): Either[zio.parser.Parser.ParserError[String], T] = { + def zioParserOpStack(): Either[zio.parser.ParseFailure[String], T] = { import zio.parser._ zioSyntax.parseString(value, ParserImplementation.StackSafe) } diff --git a/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/LuceneQueryBenchmark.scala b/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/LuceneQueryBenchmark.scala index 790721f..5e5a6dd 100644 --- a/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/LuceneQueryBenchmark.scala +++ b/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/LuceneQueryBenchmark.scala @@ -15,6 +15,7 @@ import org.openjdk.jmh.annotations.{ } import zio.Chunk import zio.parser.{ParserImplementation, Syntax} +import zio.parser.ParseFailure import zio.parser.Parser.ParserError import zio.parser.internal.Debug @@ -50,11 +51,11 @@ class LuceneQueryBenchmark { catsParser.query.parseAll(testQuery) @Benchmark - def zioParse(): Either[ParserError[String], Query] = + def zioParse(): Either[ParseFailure[String], Query] = zioParserQuery.parseString(testQuery) @Benchmark - def zioParseStrippedRecursive(): Either[ParserError[String], Query] = + def zioParseStrippedRecursive(): Either[ParseFailure[String], Query] = zioParserStrippedQuery.parseString(testQuery, ParserImplementation.Recursive) @Benchmark @@ -62,7 +63,7 @@ class LuceneQueryBenchmark { zioParserStrippedQuery.parseChunk(testQueryChunk) @Benchmark - def zioParseStrippedOpStack(): Either[ParserError[String], Query] = + def zioParseStrippedOpStack(): Either[ParseFailure[String], Query] = zioParserStrippedQuery.parseString(testQuery, ParserImplementation.StackSafe) // @Benchmark diff --git a/benchmarks/src/main/scala/zio/parser/benchmarks/micro/CharParserMicroBenchmarks.scala b/benchmarks/src/main/scala/zio/parser/benchmarks/micro/CharParserMicroBenchmarks.scala index 1a93078..021142a 100644 --- a/benchmarks/src/main/scala/zio/parser/benchmarks/micro/CharParserMicroBenchmarks.scala +++ b/benchmarks/src/main/scala/zio/parser/benchmarks/micro/CharParserMicroBenchmarks.scala @@ -13,6 +13,7 @@ import org.openjdk.jmh.annotations.{ Warmup } import zio.Chunk +import zio.parser.ParseFailure import zio.parser.Parser.ParserError import zio.parser.{Regex, Syntax} @@ -68,23 +69,23 @@ class CharParserMicroBenchmarks { } @Benchmark - def skipAndTransform(): Either[ParserError[Nothing], String] = + def skipAndTransform(): Either[ParseFailure[Nothing], String] = skipAndTransformSyntax.parseChars(hello) @Benchmark - def skipAndTransformOrElse(): Either[ParserError[String], String] = + def skipAndTransformOrElse(): Either[ParseFailure[String], String] = skipAndTransformOrElseSyntax.parseChars(world) @Benchmark - def skipAndTransformRepeat(): Either[ParserError[String], Chunk[String]] = + def skipAndTransformRepeat(): Either[ParseFailure[String], Chunk[String]] = skipAndTransformRepeatSyntax.parseChars(hellos) @Benchmark - def skipAndTransformZip(): Either[ParserError[String], String10] = + def skipAndTransformZip(): Either[ParseFailure[String], String10] = skipAndTransformZipSyntax.parseChars(hellos) @Benchmark - def repeatWithSep0(): Either[ParserError[String], Chunk[String]] = + def repeatWithSep0(): Either[ParseFailure[String], Chunk[String]] = repeatWithSep0Syntax.parseString(hellosSep) } diff --git a/zio-parser-caliban/src/main/scala/zio/parser/caliban/CalibanParser.scala b/zio-parser-caliban/src/main/scala/zio/parser/caliban/CalibanParser.scala index 36cf975..06a9906 100644 --- a/zio-parser-caliban/src/main/scala/zio/parser/caliban/CalibanParser.scala +++ b/zio-parser-caliban/src/main/scala/zio/parser/caliban/CalibanParser.scala @@ -911,7 +911,7 @@ object CalibanDemo extends ZIOAppDefault { (id: "1000", int: 3, float: 3.14, bool: true, nope: null, enum: YES, list: [1,2,3]) """.trim - val parsed: ZIO[Any, Nothing, Either[Parser.ParserError[String], StringValue]] = + val parsed: ZIO[Any, Nothing, Either[ParseFailure[String], StringValue]] = ZIO .succeed(CalibanParser.stringValue.parseString(query)) // val parsed = UIO(CalibanSyntax.stringValue.parse("\"\"\"hello\"\"\"")) diff --git a/zio-parser/shared/src/main/scala/zio/parser/Parser.scala b/zio-parser/shared/src/main/scala/zio/parser/Parser.scala index ed09e0f..896a7e7 100644 --- a/zio-parser/shared/src/main/scala/zio/parser/Parser.scala +++ b/zio-parser/shared/src/main/scala/zio/parser/Parser.scala @@ -272,38 +272,38 @@ sealed trait Parser[+Err, -In, +Result] extends VersionSpecificParser[Err, In, R // Execution /** Run this parser on the given 'input' string */ - final def parseString(input: String)(implicit ev: Char <:< In): Either[ParserError[Err], Result] = + final def parseString(input: String)(implicit ev: Char <:< In): Either[ParseFailure[Err], Result] = parseString(input, self.defaultImplementation) /** Run this parser on the given 'input' string using a specific parser implementation */ final def parseString(input: String, parserImplementation: ParserImplementation)(implicit ev: Char <:< In - ): Either[ParserError[Err], Result] = + ): Either[ParseFailure[Err], Result] = parserImplementation match { case ParserImplementation.StackSafe => val parser = new CharParserImpl[Err, Result]( self.compiledOpStack, input ) - parser.run() + parser.run().left.map(err => ParseFailure(input, err)) case ParserImplementation.Recursive => val state = recursive.ParserState.fromString(input) val result = self.optimized.parseRec(state.asInstanceOf[ParserState[In]]) if (state.error != null) - Left(state.error.asInstanceOf[ParserError[Err]]) + Left(ParseFailure(input, state.error.asInstanceOf[ParserError[Err]])) else Right(result) } /** Run this parser on the given 'input' chunk of characters */ - final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[ParserError[Err], Result] = + final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[ParseFailure[Err], Result] = parseChars(input, self.defaultImplementation) /** Run this parser on the given 'input' chunk of characters using a specific parser implementation */ final def parseChars(input: Chunk[Char], parserImplementation: ParserImplementation)(implicit ev: Char <:< In - ): Either[ParserError[Err], Result] = + ): Either[ParseFailure[Err], Result] = parseString(new String(input.toArray), parserImplementation) /** Run this parser on the given 'input' chunk */ @@ -505,7 +505,7 @@ object Parser { state.position += 1 result.asInstanceOf[D2] } else { - state.error = ParserError.UnexpectedEndOfInput + state.error = ParserError.UnexpectedEndOfInput(state.nameStack) null.asInstanceOf[D2] } @@ -527,7 +527,7 @@ object Parser { val position = state.position val result = state.regex(compiledRegex) if (result == Regex.NeedMoreInput) { - state.error = ParserError.UnexpectedEndOfInput + state.error = ParserError.UnexpectedEndOfInput(state.nameStack) } else if (result == Regex.NotMatched) { onFailure match { case Some(failure) => @@ -568,7 +568,7 @@ object Parser { val position = state.position val result = state.regex(compiledRegex) if (result == Regex.NeedMoreInput) { - state.error = ParserError.UnexpectedEndOfInput + state.error = ParserError.UnexpectedEndOfInput(state.nameStack) null.asInstanceOf[Chunk[Char]] } else if (result == Regex.NotMatched) { state.error = getFailure(position, state.nameStack) @@ -609,7 +609,7 @@ object Parser { val position = state.position val result = state.regex(compiledRegex) if (result == Regex.NeedMoreInput) { - state.error = ParserError.UnexpectedEndOfInput + state.error = ParserError.UnexpectedEndOfInput(state.nameStack) null.asInstanceOf[Char] } else if (result == Regex.NotMatched) { state.error = getFailure(position, state.nameStack) @@ -1145,7 +1145,7 @@ object Parser { } if (count < min && state.error == null) { - state.error = ParserError.UnexpectedEndOfInput + state.error = ParserError.UnexpectedEndOfInput(state.nameStack) } else { if (count >= min) { state.error = null @@ -1391,7 +1391,7 @@ object Parser { def map[Err2](f: Err => Err2): ParserError[Err2] = self match { case ParserError.Failure(nameStack, position, failure) => ParserError.Failure(nameStack, position, f(failure)) case ParserError.UnknownFailure(nameStack, position) => ParserError.UnknownFailure(nameStack, position) - case ParserError.UnexpectedEndOfInput => ParserError.UnexpectedEndOfInput + case ParserError.UnexpectedEndOfInput(nameStack) => ParserError.UnexpectedEndOfInput(nameStack) case ParserError.NotConsumedAll(nameStack, position) => ParserError.NotConsumedAll(nameStack, position) case ParserError.AllBranchesFailed(left, right) => ParserError.AllBranchesFailed(left.map(f), right.map(f)) } @@ -1423,7 +1423,7 @@ object Parser { final case class UnknownFailure(nameStack: List[String], position: Int) extends ParserError[Nothing] /** The input stream ended before the parser finished */ - case object UnexpectedEndOfInput extends ParserError[Nothing] + final case class UnexpectedEndOfInput(nameStack: List[String]) extends ParserError[Nothing] /** The parser was supposed to consume the full input but it did not. * @@ -1439,3 +1439,76 @@ object Parser { final case class AllBranchesFailed[Err](left: ParserError[Err], right: ParserError[Err]) extends ParserError[Err] } } + +final case class ParseFailure[+Err](input: String, error: ParserError[Err]) { + def pretty: String = { + val sb = new StringBuilder + + def errorBuilder(error: ParserError[Err]): Unit = + error match { + case ParserError.Failure(nameStack, position, failure) => + sb.append(s"Failure at position $position: $failure") + if (nameStack.nonEmpty) { + sb.append(" in ") + sb.append(nameStack.mkString(" -> ")) + } + case ParserError.UnknownFailure(nameStack, position) => + sb.append(s"Unknown failure at position $position") + if (nameStack.nonEmpty) { + sb.append(" in ") + sb.append(nameStack.mkString(" -> ")) + } + case ParserError.UnexpectedEndOfInput(nameStack) => + sb.append("Unexpected end of input") + if (nameStack.nonEmpty) { + sb.append(" in ") + sb.append(nameStack.mkString(" -> ")) + } + case ParserError.NotConsumedAll(nameStack, position) => + sb.append(s"Parser did not consume all input at position $position") + if (nameStack.nonEmpty) { + sb.append(" in ") + sb.append(nameStack.mkString(" -> ")) + } + case ParserError.AllBranchesFailed(left, right) => + sb.append("All branches failed: ") + errorBuilder(left) + sb.append(" and ") + errorBuilder(right) + } + + def printError(pos: Int) = { + val lines = input.split("\n") + val positions = lines.scanLeft(0)(_ + _.length + 1) // +1 for newline character + val errorLineIndex = positions.indexWhere(_ > pos) - 1 + + val contextRange = (math.max(errorLineIndex - 2, 0), math.min(errorLineIndex + 2, lines.length - 1)) + + if (contextRange._1 > 0) sb.append("...\n") + + lines.slice(contextRange._1, contextRange._2 + 1).zipWithIndex.foreach { case (line, index) => + val actualIndex = contextRange._1 + index + sb.append(line).append("\n") + if (actualIndex == errorLineIndex) { + val columnPos = pos - positions(errorLineIndex) + sb.append(" " * columnPos) + sb.append("^\n") + sb.append("error: ") + errorBuilder(error) + sb.append("\n") + } + } + + if (contextRange._2 < lines.length - 1) sb.append("...\n") + } + + error match { + case ParserError.Failure(_, position, _) => printError(position) + case ParserError.UnknownFailure(_, position) => printError(position) + case ParserError.NotConsumedAll(_, position) => printError(position) + case _ => errorBuilder(error) + } + + sb.toString() + } +} diff --git a/zio-parser/shared/src/main/scala/zio/parser/Syntax.scala b/zio-parser/shared/src/main/scala/zio/parser/Syntax.scala index 5b099c9..bf39830 100644 --- a/zio-parser/shared/src/main/scala/zio/parser/Syntax.scala +++ b/zio-parser/shared/src/main/scala/zio/parser/Syntax.scala @@ -384,23 +384,23 @@ class Syntax[+Err, -In, +Out, Value] private ( ) /** Run this parser on the given 'input' string */ - final def parseString(input: String)(implicit ev: Char <:< In): Either[ParserError[Err], Value] = + final def parseString(input: String)(implicit ev: Char <:< In): Either[ParseFailure[Err], Value] = asParser.parseString(input) /** Run this parser on the given 'input' string using a specific parser implementation */ final def parseString(input: String, parserImplementation: ParserImplementation)(implicit ev: Char <:< In - ): Either[ParserError[Err], Value] = + ): Either[ParseFailure[Err], Value] = asParser.parseString(input, parserImplementation) /** Run this parser on the given 'input' chunk of characters */ - final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[ParserError[Err], Value] = + final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[ParseFailure[Err], Value] = asParser.parseChars(input) /** Run this parser on the given 'input' chunk of characters using a specific parser implementation */ final def parseChars(input: Chunk[Char], parserImplementation: ParserImplementation)(implicit ev: Char <:< In - ): Either[ParserError[Err], Value] = asParser.parseChars(input, parserImplementation) + ): Either[ParseFailure[Err], Value] = asParser.parseChars(input, parserImplementation) /** Run this parser on the given 'input' chunk */ final def parseChunk[In0 <: In](input: Chunk[In0])(implicit diff --git a/zio-parser/shared/src/main/scala/zio/parser/internal/stacksafe/CharParserImpl.scala b/zio-parser/shared/src/main/scala/zio/parser/internal/stacksafe/CharParserImpl.scala index 6a0c122..cc59e92 100644 --- a/zio-parser/shared/src/main/scala/zio/parser/internal/stacksafe/CharParserImpl.scala +++ b/zio-parser/shared/src/main/scala/zio/parser/internal/stacksafe/CharParserImpl.scala @@ -138,7 +138,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) { position = position + 1 lastSuccess1 = source(position - 1).asInstanceOf[AnyRef] } else { - lastFailure1 = ParserError.UnexpectedEndOfInput + lastFailure1 = ParserError.UnexpectedEndOfInput(nameStack) } opStack.pop() @@ -157,7 +157,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) { ) } } else { - failure = ParserError.UnexpectedEndOfInput + failure = ParserError.UnexpectedEndOfInput(nameStack) } pos = pos + 1 } @@ -174,7 +174,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) { case MatchRegex(regex, pushAs, failAs) => val result = regex.test(position, source) if (result == Regex.NeedMoreInput) { - lastFailure1 = ParserError.UnexpectedEndOfInput + lastFailure1 = ParserError.UnexpectedEndOfInput(nameStack) } else if (result == Regex.NotMatched) { failAs match { case Some(failure) => @@ -355,7 +355,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) { val result = builder.result() if (result.length < min) { // not enough elements - lastFailure1 = ParserError.UnexpectedEndOfInput + lastFailure1 = ParserError.UnexpectedEndOfInput(nameStack) opStack.pop() } else { lastIgnoredError = lastFailure1 diff --git a/zio-parser/shared/src/test/scala/zio/parser/ParseFailureSpec.scala b/zio-parser/shared/src/test/scala/zio/parser/ParseFailureSpec.scala new file mode 100644 index 0000000..a9ec193 --- /dev/null +++ b/zio-parser/shared/src/test/scala/zio/parser/ParseFailureSpec.scala @@ -0,0 +1,113 @@ +package zio.parser + +import zio.test.Assertion._ +import zio.test._ + +object ParseFailureSpec extends ZIOSpecDefault { + override def spec = + suite("ParseFailure")( + failureTest( + "should pretty print a Failure error", + Syntax.char('y'), + "x", + """|x + |^ + |error: Failure at position 0: not 'y' + |""".stripMargin + ), + failureTest( + "should pretty print an UnexpectedEndOfInput error", + Syntax.char('y'), + "", + "Unexpected end of input" + ), + failureTest( + "should pretty print a NotConsumedAll error", + Syntax.char('y') <~ Syntax.end, + "yyz", + """|yyz + | ^ + |error: Parser did not consume all input at position 1 + |""".stripMargin + ), + failureTest( + "should pretty print an AllBranchesFailed error", + Syntax.char('y').orElse(Syntax.char('z')), + "x", + "All branches failed: Failure at position 0: not 'y' and Failure at position 0: not 'z'" + ), + failureTest( + "should pretty print an error with multiline input", + Syntax.char('y'), + "x\ny", + """|x + |^ + |error: Failure at position 0: not 'y' + |y + |""".stripMargin + ), + failureTest( + "should point to the correct position in the input", + Syntax.char('x').repeat <~ Syntax.end, + "xxxyxx", + """|xxxyxx + | ^ + |error: Parser did not consume all input at position 3 + |""".stripMargin + ), + failureTest( + "should replace the following lines with an ellipsis", + Syntax.char('y'), + "x\n" + "y\n" * 100, + """|x + |^ + |error: Failure at position 0: not 'y' + |y + |y + |... + |""".stripMargin + ), + failureTest( + "should replace the preceding and following lines with an ellipsis", + (Syntax.digit.orElse(Syntax.whitespace)).repeat <~ Syntax.end, + "1\n1\n1\n" + "y\n" + "1\n1\n1\n", + """|... + |1 + |1 + |y + |^ + |error: Parser did not consume all input at position 6 + |1 + |1 + |... + |""".stripMargin + ), + failureTest( + "should print the failed parser name", + (Syntax.digit.named("digit").orElse(Syntax.whitespace.named("whitespace"))).repeat <~ + Syntax.end.named("end"), + "1 1 1 1 y 1 1 1", + """|1 1 1 1 y 1 1 1 + | ^ + |error: Parser did not consume all input at position 8 in end + |""".stripMargin + ), + failureTest( + "should print the parser name stack", + ((Syntax.digit.named("digit") ~ Syntax.string("Keyword", ()).named("keyword")) + .named("combined")), + "5Keywor", + "Unexpected end of input in keyword -> combined" + ) + ) + + private def failureTest[E, T]( + name: String, + parser: Syntax[E, Char, Char, T], + input: String, + expectedErrorMessage: String + ): Spec[Any, TestFailure[Nothing]] = + test(name) { + assert(parser.parseString(input).left.map(_.pretty))(isLeft(equalTo(expectedErrorMessage))) + } +} diff --git a/zio-parser/shared/src/test/scala/zio/parser/ParserSpec.scala b/zio-parser/shared/src/test/scala/zio/parser/ParserSpec.scala index b887ca0..628f732 100644 --- a/zio-parser/shared/src/test/scala/zio/parser/ParserSpec.scala +++ b/zio-parser/shared/src/test/scala/zio/parser/ParserSpec.scala @@ -197,7 +197,7 @@ object ParserSpec extends ZIOSpecDefault { isLeft(equalTo(ParserError.Failure(Nil, 0, "not a"))) ) @@ TestAspect.ignore, // With compiling to Regex it fails with UnexpectedEndOfInput - to be discussed parserTest("repeat immediate end of stream", Syntax.char('a', "not a").repeat, "")( - isLeft(equalTo(ParserError.UnexpectedEndOfInput)) + isLeft(equalTo(ParserError.UnexpectedEndOfInput(Nil))) ), parserTest("repeat 1", charA.repeat, "abc")( isRight(equalTo(Chunk('a'))) @@ -412,7 +412,7 @@ object ParserSpec extends ZIOSpecDefault { isRight(equalTo("123")) ), parserTest("digits, failing", Syntax.digit.+.string, "abc123")( - isLeft(equalTo(ParserError.UnexpectedEndOfInput)) + isLeft(equalTo(ParserError.UnexpectedEndOfInput(Nil))) ) ), parserTest("Not, inner failing", Syntax.string("hello", ()).not("it was hello"), "world")( @@ -461,7 +461,7 @@ object ParserSpec extends ZIOSpecDefault { ): Spec[Any, Nothing] = test(name) { // Debug.printParserTree(syntax.asParser.optimized) - assert(syntax.parseString(input, implementation))(assertion) + assert(syntax.parseString(input, implementation).left.map(_.error))(assertion) } private def createParserTest_[E, T]( @@ -470,7 +470,7 @@ object ParserSpec extends ZIOSpecDefault { assertion: Assertion[Either[ParserError[E], T]] ): Spec[Any, Nothing] = test(name)( - assert(parser.parseString(input, implementation))(assertion) + assert(parser.parseString(input, implementation).left.map(_.error))(assertion) ) // test(name)(assert(syntax.parseString(input))(assertion)) } diff --git a/zio-parser/shared/src/test/scala/zio/parser/SyntaxSpec.scala b/zio-parser/shared/src/test/scala/zio/parser/SyntaxSpec.scala index 178368f..932746c 100644 --- a/zio-parser/shared/src/test/scala/zio/parser/SyntaxSpec.scala +++ b/zio-parser/shared/src/test/scala/zio/parser/SyntaxSpec.scala @@ -81,7 +81,7 @@ object SyntaxSpec extends ZIOSpecDefault { }, test("oneOf fails to parse garbage") { val parsed = WeekDay.weekDaySyntax.parseString("garbage") - assert(parsed)(isLeft(isSubtype[AllBranchesFailed[String]](anything))) + assert(parsed.left.map(_.error))(isLeft(isSubtype[AllBranchesFailed[String]](anything))) } ) } diff --git a/zio-parser/shared/src/test/scala/zio/parser/examples/ContextualExample.scala b/zio-parser/shared/src/test/scala/zio/parser/examples/ContextualExample.scala index 0d85abe..41793ed 100644 --- a/zio-parser/shared/src/test/scala/zio/parser/examples/ContextualExample.scala +++ b/zio-parser/shared/src/test/scala/zio/parser/examples/ContextualExample.scala @@ -75,7 +75,7 @@ object ContextualExample extends ZIOSpecDefault { ) }, test("parse wrong") { - assert(node.parseString(""))( + assert(node.parseString("").left.map(_.error))( isLeft(equalTo(ParserError.Failure(List("close tag (hello)"), 7, "Not ''"))) ) },