packages feed

bricks-parsec-0.0.0.4: test/cases.hs

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}

-- Bricks
import Bricks.Expression
import Bricks.Parsec
import Bricks.Rendering

-- Bricks internal
import           Bricks.Internal.Prelude
import qualified Bricks.Internal.Seq     as Seq
import           Bricks.Internal.Text    (Text)
import qualified Bricks.Internal.Text    as Text

-- Bricks test
import Bricks.Test.Hedgehog
import Bricks.Test.QQ

-- Parsec
import qualified Text.Parsec      as P
import           Text.Parsec.Text (Parser)

-- Hedgehog
import           Hedgehog (Property, property, withTests, (===))
import qualified Hedgehog

-- Base
import System.IO (IO)
import Text.Show (show)

main :: IO ()
main = runTests $$(Hedgehog.discover)

{- | We'll use the @parseTest@ function a lot to test parsers. It's a bit like
'P.parseTest' from the Parsec library, but it works on parsers of type 'Text'
rather than @'Show' a => a@. It also prints the unparsed input so we can verify
that our parser consumes the right amount of input, and it prints a message if
the parser fails and consumes input. -}
parseTest :: Parser Text -> Text -> Text
parseTest p input =
  Text.intercalate "\n" $
  catMaybes

    [ Just $ case P.parse p "" input of
        Left err ->
          "Parse error at " <>
          Text.replace "\n" "\n - " (Text.pack (show err))
        Right x -> x

    , let
        p' = do
          _ <- p
          r <- P.many1 P.anyChar
          pure $ "Remaining input: " <> Text.pack (show r)
      in
        case P.parse p' "" input of
          Left _  -> Nothing
          Right x -> Just x

    , case P.parse (void p <|> pure ()) "" input of
        Left _  -> Just "Parser failed and consumed input"
        Right _ -> Nothing

    ]

prop_parse_str_unquoted :: Property
prop_parse_str_unquoted = withTests 1 $ property $ do

  let test = parseTest $ fmap var'text $ parse'var

  test "-ab_c" === [text|-ab_c|]

  test ""      === [text|┃Parse error at (line 1, column 1):
                         ┃ - unexpected end of input|]

  test "a\"b"  === [text|┃a
                         ┃Remaining input: "\"b"|]

  test "a b"   === [text|┃a
                         ┃Remaining input: "b"|]

  -- The unquoted string parser doesn't backtrack.
  -- Note how in this example it fails and consumes input.
  test "rec { }" === [text|┃Parse error at (line 1, column 4):
                           ┃ - unexpected " "
                           ┃Parser failed and consumed input|]

prop_parse_expression_dictKey :: Property
prop_parse_expression_dictKey = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (render'expression renderContext'default) $
      parse'expression'dictKey

  test "a"      === [text|"a"|]

  test "\"a\""  === [text|"a"|]

  test "a b"    === [text|┃"a"
                          ┃Remaining input: "b"|]

  test "${a.b}" === [text|a.b|]

prop_parse_strDynamic :: Property
prop_parse_strDynamic = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (render'strDynamic'quoted renderContext'default) $
      parse'str'dynamic

  test "\"a\""        === [text|"a"|]

  test "\"a\" x"      === [text|┃"a"
                                ┃Remaining input: "x"|]

  test "\"a ${b} c\"" === [text|"a ${b} c"|]

  test "\"a${ b }c\"" === [text|"a${b}c"|]

  test "\"$\""        === [text|"$"|]

  test "\"a$\""       === [text|"a$"|]

  test "\"\\${\""     === [text|"\${"|]

  test "\"a\\${\""    === [text|"a\${"|]

prop_parse_strIndented :: Property
prop_parse_strIndented = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $ fmap render $
      P.spaces *> parse'inStr

    render :: InStr -> Text
    render =
      Text.intercalateMap "\n" render'1 . inStr'toSeq

    render'1 :: InStr'1 -> Text
    render'1 x =
      Text.show (inStr'1'level x) <> " [" <>
      Text.intercalateMap ", "
        (\s ->
          render'strDynamic'quoted renderContext'default $
          Str'Dynamic (Seq.singleton s) Nothing
        )
        (inStr'1'str x) <>
      "] " <> Text.show (isJust (inStr'1'lineBreak x))

  test "''hello''x"  === [text|┃0 ["hello"] False
                               ┃Remaining input: "x"|]

  test "''hello'' x" === [text|┃0 ["hello"] False
                               ┃Remaining input: "x"|]

  test [text|┃  ''
             ┃    one
             ┃    two
             ┃  ''x|] === [text|┃0 [] True
                                ┃4 ["one"] True
                                ┃4 ["two"] True
                                ┃2 [] False
                                ┃Remaining input: "x"|]

  test [text|┃  ''
             ┃    one
             ┃
             ┃    two
             ┃  ''x|] === [text|┃0 [] True
                                ┃4 ["one"] True
                                ┃0 [] True
                                ┃4 ["two"] True
                                ┃2 [] False
                                ┃Remaining input: "x"|]

  test "'''' x"   === [text|┃0 [] False
                            ┃Remaining input: "x"|]

  test "''abc''"  === [text|0 ["abc"] False|]

  test [text|┃''
             ┃''|]
    === [text|┃0 [] True
              ┃0 [] False|]

  test [text|┃''   abc
             ┃def''|]
    === [text|┃3 ["abc"] True
              ┃0 ["def"] False|]

prop_parse_dict_pattern_start :: Property
prop_parse_dict_pattern_start = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      (P.try parse'dictPattern'start $> "yes") <|> pure "no"

  test "{a, b}:"     === [text|┃yes
                               ┃Remaining input: " b}:"|]

  test "{a ? 4, b}:" === [text|┃yes
                               ┃Remaining input: " 4, b}:"|]

  test "{ }: x"      === [text|┃yes
                               ┃Remaining input: " x"|]

  -- { } is not enough to determine whether we're parsing a dict param, because
  -- if it isn't followed by a colon, then it's actually an empty dict literal.
  test "{ } x"       === [text|┃no
                               ┃Remaining input: "{ } x"|]

  test "{ ... }:"    === [text|┃yes
                               ┃Remaining input: " }:"|]

prop_parse_dict_pattern :: Property
prop_parse_dict_pattern = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (render'dictPattern renderContext'default) $
      parse'dictPattern

  test "{}"               === [text|{ }|]
  test "{...}"            === [text|{ ... }|]
  test "{a}"              === [text|{ a }|]
  test "{a , b}"          === [text|{ a, b }|]
  test "{a , b ? c, ...}" === [text|{ a, b ? c, ... }|]

prop_parse_dot_rhs_chain :: Property
prop_parse_dot_rhs_chain = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (
        Text.intercalate "\n" .
        fmap (render'expression renderContext'default)
      ) $
      parse'dot'rhs'chain

  -- The dots parser /does/ match the empty string.
  test ""         === [text||]

  -- The simplest nonempty dot list
  test ".a"       === [text|"a"|]

  -- The dots parser consumes any trailing whitespace beyond the dot list.
  test ".a "      === [text|"a"|]

  -- Dot attributes are usually unquoted strings, but they may also be quoted.
  test ".\"a\""   === [text|"a"|]
  test ". \"a\""  === [text|"a"|]

  -- Here we throw some extra whitespace into the middle, which makes no
  -- difference, and some extra stuff onto the end, which does not get consumed.
  test ".a . b c" === [text|┃"a"
                            ┃"b"
                            ┃Remaining input: "c"|]

  -- Another example of a quoted dot, this time following an unquoted dot
  test ".a.\"b\"" === [text|┃"a"
                            ┃"b"|]

  -- If quotes or braces are involved, the stuff that follows a dot expression
  -- can directly abut it with no whitespace in between.
  test [text|.a."b"x|] === [text|┃"a"
                                 ┃"b"
                                 ┃Remaining input: "x"|]

  test [text|.a.b"x"|] === [text|┃"a"
                                 ┃"b"
                                 ┃Remaining input: "\"x\""|]

  test ".a.b(x)"   === [text|┃"a"
                             ┃"b"
                             ┃Remaining input: "(x)"|]

  test ". a . b"   === [text|┃"a"
                             ┃"b"|]

  test ". \"a\".b" === [text|┃"a"
                             ┃"b"|]

  test ". \"a\".${b}" === [text|┃"a"
                                ┃b|]

prop_parse_expression :: Property
prop_parse_expression = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (render'expression renderContext'default) $
      parse'expression

  -- The empty string is /not/ a valid expression.
  test "" === [text|┃Parse error at (line 1, column 1):
                    ┃ - unexpected end of input
                    ┃ - expecting expression|]

  -- A very simple expression: a one-letter variable
  test "a"         === [text|a|]

  -- Parsing an expression consumes any subsequent whitespace.
  test "a "        === [text|a|]

  -- When there are multiple expressions, that is parsed as a function call.
  test "f x"       === [text|f x|]

  -- Expressions can directly abut each other, so it's important that the
  -- expression parser is also able to read an expression even when another
  -- expression directly follows it.
  test "f[x y]"    === [text|┃f [
                             ┃  x
                             ┃  y
                             ┃]|]

  -- A simple example of parsing a dot expression
  test "a.b"       === [text|a.b|]

  -- Dot parsing also consumes trailing whitespace.
  test "a.b "      === [text|a.b|]

  -- It looks odd when a subsequent expression appears after a dot expression
  -- with no whitespace, but it is permitted.
  test "a.b\"c\""  === [text|a.b "c"|]

  -- A simple list example
  test "[ a b ]"   === [text|┃[
                             ┃  a
                             ┃  b
                             ┃]|]

  -- A list with trailing whitespace that get consumed
  test "[ a b ] "  === [text|┃[
                             ┃  a
                             ┃  b
                             ┃]|]

  -- A list that is in the left-hand side of a function call. This will fail at
  -- runtime if the call is evaluated, because a list is not a function, but it
  -- should /parse/ successfully.
  test "[ a b ] x" === [text|┃[
                             ┃  a
                             ┃  b
                             ┃] x|]

  -- The same thing with other weird stuff on the left-hand side of a function
  -- call.
  test "{ a = b; } x" === [text|┃{
                                ┃  a = b;
                                ┃} x|]

  test "{ } x"        === [text|{ } x|]

  -- Note that the case where an empty dict is on the left-hand side of a
  -- function call looks very similar to the case where a function expression
  -- using dict deconstruction with no bindings. The only difference is the
  -- colon.
  test "{ }: x"      === [text|┃{ }:
                               ┃x|]

  -- A list with a function call inside
  test "[ (f x) ]"   === [text|┃[
                               ┃  (f x)
                               ┃]|]

  test "[ a (f x) ]" === [text|┃[
                               ┃  a
                               ┃  (f x)
                               ┃]|]

  -- A minimal dict literal
  test "{ x = y; }"  === [text|┃{
                               ┃  x = y;
                               ┃}|]

  -- The left-hand side of a dict binding is allowed to be any expression.
  test "{ \"a b\" = y; }"  === [text|┃{
                                     ┃  "a b" = y;
                                     ┃}|]
  test "{ ${x} = y; }"     === [text|┃{
                                     ┃  ${x} = y;
                                     ┃}|]

  -- It may even be the empty string.
  test "{ \"\" = y; }"     === [text|┃{
                                     ┃  "" = y;
                                     ┃}|]

  -- None of the conventional whitespace within a dict literal is mandatory.
  test "{x=y;}"            === [text|┃{
                                     ┃  x = y;
                                     ┃}|]

  -- A simple dict literal with two bindings
  test "{ x = y; a = b; }" === [text|┃{
                                     ┃  x = y;
                                     ┃  a = b;
                                     ┃}|]

  -- The same thing without any whitespace
  test "{x=y;a=b;}" === [text|┃{
                              ┃  x = y;
                              ┃  a = b;
                              ┃}|]

  -- Dicts with 'inherit' bindings
  test "{ inherit a; }"       === [text|┃{
                                        ┃  inherit a;
                                        ┃}|]
  test "{ inherit a b; }"     === [text|┃{
                                        ┃  inherit a b;
                                        ┃}|]
  test "{ inherit (x) a b; }" === [text|┃{
                                        ┃  inherit (x) a b;
                                        ┃}|]

  -- An inherit binding can be empty, although it is weird.
  test "{ inherit; }"   === [text|┃{
                                  ┃  inherit;
                                  ┃}|]

  -- A simple function
  test "x : y" === [text|┃x:
                         ┃y|]

  -- Whitespace before the colon is unconventional, but allowed.
  test "x : y" === [text|┃x:
                         ┃y|]

  -- The space after the colon is not mandatory. (In Nix, this example would be
  -- parsed as the string "x:y", but here we do not support URI literals.)
  test "x:y"   === [text|┃x:
                         ┃y|]

  -- A slightly bigger example where we're starting to nest more things
  test "[ \"abc\" f { x = y; } ]"
    === [text|┃[
              ┃  "abc"
              ┃  f
              ┃  {
              ┃    x = y;
              ┃  }
              ┃]|]

  test "[ \"abc\" (f { x = y; }) ]"
    === [text|┃[
              ┃  "abc"
              ┃  (f {
              ┃    x = y;
              ┃  })
              ┃]|]

  -- This is not valid a expression (though the first bit of it is).
  test "a b: c"   === [text|┃a b
                            ┃Remaining input: ": c"|]

  -- This is not a valid expression.
  test "(a b: c)" === [text|┃Parse error at (line 1, column 5):
                            ┃ - unexpected ":"
                            ┃ - expecting expression list item or ")"
                            ┃Parser failed and consumed input|]

  -- Some functions that use dict deconstruction
  test "{ a, b, c ? x, ... }: g b (f a c)"
    === [text|┃{ a, b, c ? x, ... }:
              ┃g b (f a c)|]

  test "{ x, ... }: f x"    === [text|┃{ x, ... }:
                                      ┃f x|]
  test "{ x?\"abc\" }: x"   === [text|┃{ x ? "abc" }:
                                      ┃x|]
  test "{ ... }: x"         === [text|┃{ ... }:
                                      ┃x|]
  test "a@{ x, ... }: f x"  === [text|┃a@{ x, ... }:
                                      ┃f x|]
  test "a@{ x?\"abc\" }: x" === [text|┃a@{ x ? "abc" }:
                                      ┃x|]
  test "a@{ ... }: x"       === [text|┃a@{ ... }:
                                      ┃x|]

  -- A let expression
  test "let f = x: plus one x; in f seven"
    === [text|┃let
              ┃  f = x:
              ┃    plus one x;
              ┃in
              ┃  f seven|]

  -- A let binding list may be empty, although it is silly.
  test "let in f x"
    === [text|┃let
              ┃in
              ┃  f x|]

  -- Indented strings do not support any escape sequences.
  test [text|┃''
             ┃  There \ is \n no \$ escape.
             ┃''|]
    === [text|┃''
              ┃  There \\ is \\n no \\$ escape.
              ┃''|]

  -- Therefore if you want to include something like '' in an indented string,
  -- you have to put it inside an antiquote.
  test [text|┃''
             ┃  Isn't it
             ┃  ${"''"}interesting
             ┃''|]
    === [text|┃''
              ┃  Isn't it
              ┃  ${"''"}interesting
              ┃''|]

  -- Comments
  test [text|┃let                -- hi
             ┃  x {- ! -} = "a"; -- yep
             ┃ in                -- lol
             ┃   f x
             ┃|]
    === [text|┃let
              ┃  x = "a";
              ┃in
              ┃  f x|]

  -- Nested block comments
  test [text|┃f{- a
             ┃  -- b
             ┃    {- c {- d
             ┃    -}-} e
             ┃  -}x|]
    === "f x"

prop_parse_expression_list :: Property
prop_parse_expression_list = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (
        Text.intercalate "\n" .
        fmap (render'expression renderContext'default)
      ) $
      parse'expressionList

  test ""                       === [text||]

  test "x y z"                  === [text|┃x
                                          ┃y
                                          ┃z|]


  test "(a)b c(d)"              === [text|┃a
                                          ┃b
                                          ┃c
                                          ┃d|]

  test "a.\"b\"c"               === [text|┃a.b
                                          ┃c|]

  test "\"abc\" (f { x = y; })" === [text|┃"abc"
                                          ┃f {
                                          ┃  x = y;
                                          ┃}|]

  -- Parsing lists of variables that are similar to keywords
  test "r re reck"              === [text|┃r
                                          ┃re
                                          ┃reck|]
  test "r re rec { } reck"      === [text|┃r
                                          ┃re
                                          ┃rec { }
                                          ┃reck|]
  test "l le lets"              === [text|┃l
                                          ┃le
                                          ┃lets|]
  test "i ins"                  === [text|┃i
                                          ┃ins|]
  test "inheri inherits"        === [text|┃inheri
                                          ┃inherits|]

prop_parse_expression_list_item :: Property
prop_parse_expression_list_item = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (render'expression renderContext'default) $
      parse'expressionList'1

  test "abc def"  === [text|┃abc
                            ┃Remaining input: "def"|]
  test "a.b c"    === [text|┃a.b
                            ┃Remaining input: "c"|]
  test "a.\"b\"c" === [text|┃a.b
                            ┃Remaining input: "c"|]
  test "(a.b)c"   === [text|┃a.b
                            ┃Remaining input: "c"|]
  test "a.b(c)"   === [text|┃a.b
                            ┃Remaining input: "(c)"|]
  test "[ a b ]c" === [text|┃[
                            ┃  a
                            ┃  b
                            ┃]
                            ┃Remaining input: "c"|]
  test "a[ b c ]" === [text|┃a
                            ┃Remaining input: "[ b c ]"|]
  test "\"a\"b"   === [text|┃"a"
                            ┃Remaining input: "b"|]

prop_parse_expression_list_item_no_dot :: Property
prop_parse_expression_list_item_no_dot = withTests 1 $ property $ do

  let
    test :: Text -> Text
    test =
      parseTest $
      fmap (render'expression renderContext'default) $
      parse'expressionList'1'noDot

  test "a.b c" === [text|┃a
                         ┃Remaining input: ".b c"|]