tests/testthat/test-expected.R

# Tests for expected element functionality in error messages

test_that("'literal' parser includes expected in error marker", {
  reset_LNR()
  r <- literal("A")("B")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'A'")
})

test_that("'eof' parser includes expected in error marker", {
  reset_LNR()
  r <- eof()("A")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "end of file")
})

test_that("'satisfy' parser includes expected in error marker", {
  reset_LNR()
  starts_with_a <- function(x) grepl("^a", x)
  r <- satisfy(starts_with_a)("B")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "matching input")
})

test_that("'satisfy' parser can have custom expected message", {
  reset_LNR()
  is_number <- function(x) grepl("^\\d+$", x)
  r <- satisfy(is_number, expected = "numeric value")("abc")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "numeric value")
})

test_that("'%or%' combinator merges expected values when both fail at same position", {
  reset_LNR()
  r <- (literal("A") %or% literal("B"))("C")
  expect_true(failed(r))
  expected <- marker_expected(r)
  expect_true("literal 'A'" %in% expected)
  expect_true("literal 'B'" %in% expected)
  expect_equal(length(expected), 2)
})

test_that("'%or%' combinator returns expected from furthest failure", {
  reset_LNR()
  p1 <- literal("A") %then% literal("B")
  p2 <- literal("A") %then% literal("C")
  r <- (p1 %or% p2)(c("A", "D"))
  expect_true(failed(r))
  expected <- marker_expected(r)
  # Both parsers fail on line 2, so both expectations should be included
  expect_true("literal 'B'" %in% expected)
  expect_true("literal 'C'" %in% expected)
})

test_that("'%or%' combinator with three alternatives merges all expected at same position", {
  reset_LNR()
  r <- (literal("A") %or% literal("B") %or% literal("C"))("D")
  expect_true(failed(r))
  expected <- marker_expected(r)
  expect_true("literal 'A'" %in% expected)
  expect_true("literal 'B'" %in% expected)
  expect_true("literal 'C'" %in% expected)
})

test_that("'%then%' combinator propagates expected from first parser", {
  reset_LNR()
  r <- (literal("A") %then% literal("B"))("C")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'A'")
})

test_that("'%then%' combinator propagates expected from second parser", {
  reset_LNR()
  r <- (literal("A") %then% literal("B"))(c("A", "C"))
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'B'")
})

test_that("'%xthen%' combinator propagates expected information", {
  reset_LNR()
  r <- (literal("A") %xthen% literal("B"))(c("A", "C"))
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'B'")
})

test_that("'%thenx%' combinator propagates expected information", {
  reset_LNR()
  r <- (literal("A") %thenx% literal("B"))(c("A", "C"))
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'B'")
})

test_that("'named' parser adds semantic name to error marker", {
  reset_LNR()
  nucleotide <- named(
    satisfy(function(x) grepl("^[GATC]+$", x)),
    "nucleotide sequence"
  )
  r <- nucleotide("XYZ")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "nucleotide sequence")
})

test_that("'named' parser replaces underlying expected value", {
  reset_LNR()
  # literal would normally say "literal 'A'", but named overrides it
  named_parser <- named(literal("A"), "start token")
  r <- named_parser("B")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "start token")
})

test_that("'named' parsers combine nicely with %or%", {
  reset_LNR()
  p1 <- named(literal("A"), "token A")
  p2 <- named(literal("B"), "token B")
  r <- (p1 %or% p2)("C")
  expect_true(failed(r))
  expected <- marker_expected(r)
  expect_true("token A" %in% expected)
  expect_true("token B" %in% expected)
})

test_that("'reporter' includes expected in error message for single expectation", {
  reset_LNR()
  expect_error(
    reporter(literal("A"))("B"),
    regexp = "Expected: literal 'A'"
  )
})

test_that("'reporter' includes expected in error message for multiple expectations", {
  reset_LNR()
  expect_error(
    reporter(literal("A") %or% literal("B"))("C"),
    regexp = "Expected one of:.*literal 'A'.*literal 'B'"
  )
})

test_that("'reporter' error message shows correct line number and expected", {
  reset_LNR()
  p <- literal("A") %then% literal("B") %then% literal("C")
  err <- tryCatch(
    reporter(p)(c("A", "B", "D")),
    error = function(e) e
  )
  expect_match(err$message, "line 3")
  expect_match(err$message, "Expected: literal 'C'")
})

test_that("'reporter' with %or% shows combined expectations at failure point", {
  reset_LNR()
  nucleotide <- named(
    satisfy(function(x) grepl("^[GATC]+$", x)),
    "nucleotide sequence"
  )
  protein <- named(
    satisfy(function(x) grepl("^[ARNDCQEGHILKMFPSTWYV]+$", x)),
    "protein sequence"
  )
  p <- nucleotide %or% protein
  err <- tryCatch(
    reporter(p %then% eof())(c("12345")),
    error = function(e) e
  )
  expect_match(err$message, "Expected one of:")
  expect_match(err$message, "nucleotide sequence")
  expect_match(err$message, "protein sequence")
})

test_that("'reporter' error object contains expected attribute", {
  reset_LNR()
  err <- tryCatch(
    reporter(literal("A") %or% literal("B"))("C"),
    error = function(e) e
  )
  expect_true(!is.null(err$expected))
  expect_true("literal 'A'" %in% err$expected)
  expect_true("literal 'B'" %in% err$expected)
})

test_that("complex parser with nested %or% produces meaningful expectations", {
  reset_LNR()
  # Simulating a fasta header or sequence line
  header <- named(
    satisfy(function(x) grepl("^>", x)),
    "header line (starting with '>')"
  )
  nucl_seq <- named(
    satisfy(function(x) grepl("^[GATC]+$", x)),
    "nucleotide sequence"
  )
  prot_seq <- named(
    satisfy(function(x) grepl("^[ARNDCQEGHILKMFPSTWYV]+$", x)),
    "protein sequence"
  )

  fasta_line <- header %or% nucl_seq %or% prot_seq

  err <- tryCatch(
    reporter(fasta_line %then% eof())("12345"),
    error = function(e) e
  )

  expect_match(err$message, "Expected one of:")
  expect_match(err$message, "header line")
  expect_match(err$message, "nucleotide sequence")
  expect_match(err$message, "protein sequence")
})

test_that("'one_or_more' propagates expected information", {
  reset_LNR()
  # one_or_more requires at least one match
  r <- one_or_more(literal("A"))("B")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'A'")
})

test_that("'match_n' propagates expected information", {
  reset_LNR()
  r <- match_n(2, literal("A"))(c("A", "B"))
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'A'")
})

test_that("'match_s' with custom stringparser maintains error reporting", {
  reset_LNR()
  get_numbers <- function(x) {
    m <- gregexpr("[[:digit:]]+", x)
    matches <- regmatches(x, m)[[1]]
    if (length(matches) == 0) {
      return(list())
    } else {
      return(as.numeric(matches))
    }
  }

  r <- match_s(get_numbers)("no numbers here")
  expect_true(failed(r))
  # match_s doesn't add expected by default, but the error still occurs
})

test_that("empty input produces expected information", {
  reset_LNR()
  r <- literal("A")(character(0))
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'A'")
})

test_that("'%using%' does not alter expected information", {
  reset_LNR()
  r <- (literal("A") %using% toupper)("B")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'A'")
})

test_that("'%ret%' does not alter expected information", {
  reset_LNR()
  r <- (literal("A") %ret% "custom value")("B")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "literal 'A'")
})

test_that("'exactly' propagates expected information", {
  reset_LNR()
  r <- exactly(2, literal("A"))(c("A", "A", "A"))
  expect_true(failed(r))
  # exactly fails when there are too many matches
})

test_that("'zero_or_one' propagates expected information on failure", {
  reset_LNR()
  r <- zero_or_one(literal("A"))(c("A", "A"))
  expect_true(failed(r))
  # Fails because there are two A's, not zero or one
})

test_that("EmptyLine and Spacer return expected messages on failure", {
  reset_LNR()
  r <- EmptyLine()("text content")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "Emptyline")
  reset_LNR()
  r <- Spacer()("text content")
  expect_true(failed(r))
  expect_identical(marker_expected(r), "Spacer")
})

test_that("complex realistic parser produces helpful error messages", {
  reset_LNR()

  # Build a simple CSV-like parser
  number <- named(
    satisfy(function(x) grepl("^\\d+$", x)),
    "number"
  )
  text <- named(
    satisfy(function(x) grepl("^[a-zA-Z]+$", x)),
    "text"
  )

  csv_value <- number %or% text

  # Test with invalid input
  err <- tryCatch(
    reporter(csv_value %then% eof())("###"),
    error = function(e) e
  )

  expect_match(err$message, "line 1")
  expect_match(err$message, "Expected one of:")
  expect_match(err$message, "number")
  expect_match(err$message, "text")
})

Try the parcr package in your browser

Any scripts or data that you put into this service are public.

parcr documentation built on Feb. 15, 2026, 5:07 p.m.