packages feed

mangrove-cli-0.5.0.0: test/Mangrove/ParseTreeSpec.hs

{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications  #-}

module Mangrove.ParseTreeSpec (spec) where

import           Control.Applicative
import           Data.Text             (Text)
import qualified Data.Text             as T
import           Test.Hspec
import           Test.Hspec.QuickCheck
import           Test.QuickCheck       hiding (Result (..))

import           Mangrove
import           Mangrove.ParseTree
import           Mangrove.Render
import           Mangrove.Scheme.Unix
import           Mangrove.Valency

import           Arbitrary
import           StructureEq
import           TestParsers

--------------------------------------------------------------------------------
-- Functor Laws

prop_fmapIdLaw :: UnixParser Int -> Bool
prop_fmapIdLaw tree = structEq tree (fmap id tree)

prop_fmapComLaw :: UnixParser Int -> Bool
prop_fmapComLaw tree =
  fmap (inc . dbl) tree
  `structEq`
  (fmap inc . fmap dbl) tree
  where
    inc = (1+)
    dbl = (2*)

--------------------------------------------------------------------------------
-- Applicative Laws

prop_applicativeIdLaw
  :: ParseTree UnixScheme Int
  -> ArgList
  -> Bool
prop_applicativeIdLaw m (ArgList args) =
  runArgumentParser (pure id <*> m) args
  ==
  runArgumentParser m args

prop_applicativeHomLaw
  :: Fun Int Int
  -> Int
  -> ArgList
  -> Bool
prop_applicativeHomLaw (Fn f) x (ArgList args) =
  runArgumentParser (pure f <*> pure x :: UnixParser Int) args
  ==
  runArgumentParser (pure (f x) :: UnixParser Int) args

prop_applicativeIntLaw
  :: ParseTree UnixScheme (Int -> Int)
  -> Int
  -> ArgList
  -> Bool
prop_applicativeIntLaw u y (ArgList args) =
  runArgumentParser (u <*> pure y) args
  ==
  runArgumentParser (pure ($ y) <*> u) args

prop_applicativeComLaw
  :: ParseTree UnixScheme (Int -> Int)
  -> ParseTree UnixScheme (Int -> Int)
  -> ParseTree UnixScheme Int
  -> ArgList
  -> Bool
prop_applicativeComLaw u v w (ArgList args) =
  runArgumentParser (pure (.) <*> u <*> v <*> w) args
  ==
  runArgumentParser (u <*> (v <*> w)) args

--------------------------------------------------------------------------------

prop_valencyPositive
  :: UnixParser Int
  -> Bool
prop_valencyPositive p =
  all (>= 0) (valency p)

prop_liftA2AddsValencies
  :: UnixParser Int
  -> UnixParser Int
  -> Bool
prop_liftA2AddsValencies l r =
  valency (liftA2 (+) l r) == liftA2 (+) (valency l) (valency r)

prop_altMaxesValency
  :: UnixParser Int
  -> UnixParser Int
  -> Bool
prop_altMaxesValency l r =
  valency (l <|> r) == liftA2 (max) (valency l) (valency r)

prop_altPicksOne
  :: ParseTree UnixScheme Int
  -> ParseTree UnixScheme Int
  -> ArgList
  -> Bool
prop_altPicksOne l r (ArgList args) =
  resultSum == resultL || resultSum == resultR
  where
    resultL = runArgumentParser l args
    resultR = runArgumentParser r args
    resultSum = runArgumentParser (l <|> r) args

prop_altEmptyIdentity
  :: (ParseTree UnixScheme Int -> ParseTree UnixScheme Int)
  -> ParseTree UnixScheme Int
  -> ArgList
  -> Bool
prop_altEmptyIdentity append tree (ArgList args) =
  runArgumentParser tree args == runArgumentParser (append tree) args

--------------------------------------------------------------------------------

prop_usageNullForNullaryNodes
  :: UnixParser Int
  -> Bool
prop_usageNullForNullaryNodes tree =
  nullary tree == (usage tree == "")

prop_bracketsOptionals
  :: UnixParser Int
  -> Bool
prop_bracketsOptionals tree =
  not (isOptional tree) || (isBracketed . renderText . usage) tree
  where
    isBracketed text = or $ do
      (start, _) <- T.uncons text
      (_, end)   <- T.unsnoc text

      pure $ start == '[' && end == ']'

prop_balancedBrackets
  :: UnixParser Int
  -> Bool
prop_balancedBrackets = testBalance 0 [] [] . renderText . usage
  where
    testBalance :: Int -> [Int] -> [Int] -> Text -> Bool
    testBalance index openBraces openBrackets text =
      case T.uncons text of
        Nothing -> null openBraces && null openBrackets
        Just ('{', text') ->
          testBalance (index + 1) (index : openBraces) openBrackets text'
        Just ('[', text') ->
          testBalance (index + 1) openBraces (index : openBrackets) text'
        Just ('}', text') ->
          case openBraces of
            [] -> False
            (match : openBraces')
              | match <= foldr const (-1) openBrackets -> False
              | otherwise ->
                testBalance (index + 1) openBraces' openBrackets text'
        Just (']', text') ->
          case openBrackets of
            [] -> False
            (match : openBrackets')
              | match <= foldr const (-1) openBraces -> False
              | otherwise ->
                testBalance (index + 1) openBraces openBrackets' text'
        Just (_, text') ->
          testBalance (index + 1) openBraces openBrackets text'

prop_noRedundantBrackets
  :: UnixParser Int
  -> Bool
prop_noRedundantBrackets =
  testBrackets 0 [] Nothing . renderText . usage
  where
    testBrackets :: Int -> [Int] -> Maybe (Int, Int) -> Text -> Bool
    testBrackets index opens lastSub text =
      case (T.uncons text, opens) of
        (Nothing, _) -> True
        (Just (c, text'), openIndex : opens')
          | c == '}' || c == ']' ->
            if lastSub == Just (openIndex+1, index-1)
            then False
            else testBrackets (index + 1) opens' (Just (openIndex, index)) text'
        (Just (c, text'), _)
          | c == '{' || c == '[' ->
            testBrackets (index + 1) (index : opens) lastSub text'
          | otherwise -> testBrackets (index + 1) opens lastSub text'

--------------------------------------------------------------------------------

spec :: Spec
spec = do
  describe "Functor Instance" $ do
    prop "satisfies identity law"
      prop_fmapIdLaw
    prop "satisfies composition law"
      prop_fmapComLaw

  describe "Applicative Instance" $ do
    prop "satisfies identity law"
      prop_applicativeIdLaw
    prop "satisfies homomorphism law"
      prop_applicativeHomLaw
    prop "satisfies interchange law"
      prop_applicativeIntLaw
    prop "satisfies composition law"
      prop_applicativeComLaw

  describe "Valency Instance" $ do
    prop "valency is always positive"
      prop_valencyPositive

  describe "pure" $ do
    it "resolves to the given value" $ do
      runArgumentParser (ValueNode 'a' :: ParseTree UnixScheme Char) []
        `shouldBe` Success [] 'a'

  describe "liftA2" $ do
    it "combines two pure values" $ do
      runArgumentParser (liftA2 (+) (pure 1) (pure 2) :: ParseTree UnixScheme Int) []
        `shouldBe` Success [] 3

      -- should be equivalent
      runArgumentParser ((+) <$> pure 1 <*> pure 2 :: ParseTree UnixScheme Int) []
        `shouldBe` Success [] 3

    prop "adds valencies"
      prop_liftA2AddsValencies

  describe "empty" $ do
    it "doesn't resolve to any value" $ do
      runArgumentParser (empty :: ParseTree UnixScheme Char) []
        `shouldBe` Failure "empty"

    it "has valency zero" $ do
      valency (empty :: ParseTree UnixScheme Char)
        `shouldBe` Just 0

  describe "(<|>)" $ do
    prop "valency equals the max valency between its children"
      prop_altMaxesValency
    prop "yields the left or the right result"
      prop_altPicksOne
    prop "empty is left identity" $
      prop_altEmptyIdentity (empty <|>)
    prop "empty is right identity" $
      prop_altEmptyIdentity (<|> empty)

    context "when the left child is resolvable" $ do
      it "resolves as the left child" $ do
        runArgumentParser (pure "asdf" <|> opt_e_param) []
          `shouldBe` Success [] "asdf"

        -- When the right child is also resolvable, it should be
        -- ignored.
        runArgumentParser (pure "asdf" <|> pure "qwer" :: ParseTree UnixScheme Text) []
          `shouldBe` Success [] "asdf"

    context "when the left child is unresolvable" $ do
      it "resolves as the right child" $ do
        runArgumentParser (opt_e_param <|> pure "asdf") []
          `shouldBe` Success [] "asdf"

    context "when one child is triggered" $ do
      it "prunes the other child" $ do
        runArgumentParser (opt_e_unit <|> opt_f_unit) ["-e", "-f"]
          `shouldBe` Success ["-f"] ()
        runArgumentParser (opt_e_unit <|> opt_f_unit) ["-f", "-e"]
          `shouldBe` Success ["-e"] ()

  describe "many" $ do
    it "parses multiple instances" $ do
      runArgumentParser (many opt_e_param) ["-e", "asdf", "-e", "qwer", "-e", "zxcv"]
        `shouldBe` Success [] ["asdf", "qwer", "zxcv"]
    it "parses zero instances" $ do
      runArgumentParser (many opt_e_param) ["blah"]
        `shouldBe` Success ["blah"] []

    it "handles compound trees" $ do
      let tree = (opt_f_unit *> opt_e_param) <|> opt_example_param
      runArgumentParser (many tree) ["-f", "-e", "asdf", "--example", "qwer"]
        `shouldBe` Success [] ["asdf", "qwer"]

    it "doesn't swallow arguments" $ do
      runArgumentParser (many $ opt_f_unit *> opt_e_param) ["-f", "-e", "asdf", "-f"]
        `shouldBe` Failure "expected: -e"
        -- Some attempts at implementing many/some resulted in
        -- arguments being silently swallowed if they were consumed by
        -- a parser inside a ManyNode which didn't receive enough
        -- input to resolve. In some cases this didn't occur until the
        -- second instance of the subtree was triggered. The expected
        -- behavior in this case is to fail with a message about what
        -- input was missing.

  describe "some" $ do
    it "parses multiple instances" $ do
      runArgumentParser (some opt_e_param) ["-e", "asdf", "-e", "qwer", "-e", "zxcv"]
        `shouldBe` Success [] ["asdf", "qwer", "zxcv"]
    it "requires at least one instance" $ do
      runArgumentParser (some opt_e_param) ["blah"]
        `shouldBe` Failure "unexpected blah"

    it "handles compound trees" $ do
      let tree = (opt_f_unit *> opt_e_param) <|> opt_example_param
      runArgumentParser (some tree) ["-f", "-e", "asdf", "--example", "qwer"]
        `shouldBe` Success [] ["asdf", "qwer"]

    it "doesn't swallow arguments" $ do
      runArgumentParser (some $ opt_f_unit *> opt_e_param) ["-f", "-e", "asdf", "-f"]
        `shouldBe` Failure "expected: -e"

  describe "optional" $ do
    it "parses exactly one instance" $ do
      runArgumentParser (optional opt_e_param) ["-e", "asdf", "-e", "qwer", "-e", "zxcv"]
        `shouldBe` Success [ "-e", "qwer", "-e", "zxcv"] (Just "asdf")
    it "parses zero instances" $ do
      runArgumentParser (optional opt_e_param) ["blah"]
        `shouldBe` Success ["blah"] Nothing

  describe "Usage" $ do
    prop "usage is empty iff the tree is nullary"
      prop_usageNullForNullaryNodes

    prop "optional trees are bracketed"
      prop_bracketsOptionals

    prop "bracketing is balanced"
      prop_balancedBrackets

    prop "has no redundant bracketing"
      prop_noRedundantBrackets

    it "separates alternatives with pipes" $ do
      usage (opt_e_param <|> opt_example_param <|> cmd_example_tree)
        `shouldBe` "-eSTRING|--example=STRING|example asdf"

    it "separates series with the scheme delimiter" $ do
      usage ((,,) <$> opt_e_param <*> opt_example_param <*> cmd_example_tree)
        `shouldBe` "-eSTRING --example=STRING example asdf"

    it "brackets products inside sums" $ do
      usage ( (,) <$> opt_e_unit <*> opt_e_unit
              <|>
              (,) <$> opt_f_unit <*> opt_f_unit
            )
        `shouldBe` "{-e -e}|{-f -f}"

    it "brackets sums inside products" $ do
      usage ( (,)
              <$> (opt_e_unit <|> opt_f_unit)
              <*> (opt_example_param <|> opt_e_param)
            )
        `shouldBe` "{-e|-f} {--example=STRING|-eSTRING}"

    it "omits empty nodes from alternative lists" $ do
      usage (empty <|> empty :: UnixParser Int)
        `shouldBe` ""

      usage (empty <|> pure 0 <|> empty :: UnixParser Int)
        `shouldBe` ""

      usage ( empty
              <|> opt_e_param
              <|> empty
              <|> opt_example_param
              <|> cmd_example_tree
              <|> empty
            )
        `shouldBe` "-eSTRING|--example=STRING|example asdf"

    it "omits static nodes from series lists" $ do
      usage ((,) <$> empty <*> pure 0 :: UnixParser (Int, Int))
        `shouldBe` ""

      usage ( (,,,,,)
              <$> empty
              <*> opt_e_param
              <*> pure 'a'
              <*> opt_example_param
              <*> cmd_example_tree
              <*> empty
            )
        `shouldBe` "-eSTRING --example=STRING example asdf"

    it "repeatable inputs are followed by \"...\"" $ do
      usage (some opt_e_unit)
        `shouldBe` "-e..."

      usage opt_example_intList
        `shouldBe` "--example=INT..."

      usage (some opt_example_param)
        `shouldBe` "{--example=STRING}..."

      usage (some $ (,) <$> opt_e_param <*> opt_f_unit)
        `shouldBe` "{-eSTRING -f}..."

      usage (some $ opt_e_unit <|> opt_f_unit)
        `shouldBe` "{-e|-f}..."