packages feed

symantic-parser-0.0.0.20210101: test/Golden.hs

{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UnboxedTuples #-}
module Golden where

import Control.Monad (Monad(..))
import Data.Char (Char)
import Data.Either (Either(..))
import Data.Function (($))
import Data.Semigroup (Semigroup(..))
import Data.String (String, IsString(..))
import Data.Text (Text)
import Data.Text.IO (readFile)
import System.IO (IO, FilePath)
import Test.Tasty
import Test.Tasty.Golden
import Text.Show (Show(..))
import qualified Data.ByteString.Lazy as BSL
import qualified Data.IORef as IORef
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import qualified Language.Haskell.TH.Syntax as TH

import qualified Symantic.Parser as P
import qualified Symantic.Parser.Haskell as H
import qualified Golden.Grammar as Grammar
--import Golden.Utils

goldensIO :: IO TestTree
goldensIO = return $ testGroup "Golden"
  [ goldensGrammar
  -- Commented-out for the release
  -- because resetTHNameCounter is not enough:
  -- TH names still change between runs
  -- with and without --accept
  -- , goldensMachine
  , goldensParser
  ]

goldensGrammar :: TestTree
goldensGrammar = testGroup "Grammar"
  [ testGroup "DumpComb" $ tests $ \name repr ->
    let file = "test/Golden/Grammar/"<>name<>".dump" in
    goldenVsStringDiff file diffGolden file $ do
      resetTHNameCounter
      return $ fromString $ show $
        P.dumpComb $ P.observeSharing repr
  , testGroup "OptimizeComb" $ tests $ \name repr ->
    let file = "test/Golden/Grammar/"<>name<>".opt.dump" in
    goldenVsStringDiff file diffGolden file $ do
      resetTHNameCounter
      return $ fromString $ show $
        P.dumpComb $ P.optimizeComb $ P.observeSharing repr
  ]
  where
  tests :: P.Grammar repr =>
           P.Satisfiable repr Char =>
           (forall a. String -> repr a -> TestTree) -> [TestTree]
  tests test =
    [ test "unit" $ P.unit
    , test "unit-unit" $ P.unit P.*> P.unit
    , test "app" $ P.pure (H.Haskell H.id) P.<*> P.unit
    , test "many-a" $ P.many (P.char 'a')
    , test "boom" $ Grammar.boom
    , test "brainfuck" $ Grammar.brainfuck
    , test "many-char-eof" $ P.many (P.char 'r') P.<* P.eof
    , test "eof" $ P.eof
    ]

goldensMachine :: TestTree
goldensMachine = testGroup "Machine"
  [ testGroup "DumpInstr" $ tests $ \name repr ->
    let file = "test/Golden/Machine/"<>name<>".dump" in
    goldenVsStringDiff file diffGolden file $ do
      resetTHNameCounter
      return $ fromString $ show $
        P.dumpInstr $ {-P.machine @() $ -}repr
  ]
  where
  tests ::
    P.Executable repr =>
    P.Readable repr Char =>
    (forall vs es ret. String -> repr Text vs es ret -> TestTree) -> [TestTree]
  tests test =
    [ test "unit" $ P.machine $ P.unit
    , test "unit-unit" $ P.machine $ P.unit P.*> P.unit
    , test "a-or-b" $ P.machine $ P.char 'a' P.<|> P.char 'b'
    , test "app" $ P.machine $ P.pure (H.Haskell H.id) P.<*> P.unit
    , test "many-a" $ P.machine $ P.many (P.char 'a')
    , test "boom" $ P.machine $ Grammar.boom
    , test "brainfuck" $ P.machine $ Grammar.brainfuck
    , test "many-char-eof" $ P.machine $ P.many (P.char 'r') P.<* P.eof
    , test "eof" $ P.machine $ P.eof
    , test "many-char-fail" $ P.machine $ P.many (P.char 'a') P.<* P.char 'b'
    ]

goldensParser :: TestTree
goldensParser = testGroup "Parser"
  [ testGroup "runParser" $ tests $ \name p ->
    let file = "test/Golden/Parser/"<>name in
    goldenVsStringDiff (file<>".txt") diffGolden (file<>".dump") $ do
      input :: Text <- readFile (file<>".txt")
      return $ fromString $
        case p input of
          Left err -> show err
          Right a -> show a
  ]
  where
  tests :: (forall a. Show a => String -> (Text -> Either (P.ParsingError Text) a) -> TestTree) -> [TestTree]
  tests test =
    [ test "char" $$(P.runParser $ P.char 'a')
    , test "string" $$(P.runParser $ P.string "ab")
    , test "many-char" $$(P.runParser $ P.many (P.char 'a'))
    , test "alt-right" $$(P.runParser $ P.string "aa" P.<|> P.string "ab")
    , test "alt-right-try" $$(P.runParser $ P.try (P.string "aa") P.<|> P.string "ab")
    , test "alt-left" $$(P.runParser $ P.string "aa" P.<|> P.string "ab")
    , test "many-char-eof" $$(P.runParser $ P.many (P.char 'r') P.<* P.eof)
    , test "eof" $$(P.runParser $ P.eof)
    , test "eof-fail" $$(P.runParser $ P.eof)
    -- , test "alt-char-fail" $$(P.runParser $ P.char 'a' P.<|> P.char 'b')
    -- , test "alt-char-fail" $$(P.runParser $ P.some (P.char 'a') P.<|> P.string "b")
    , test "many-char-fail" $$(P.runParser $ P.many (P.char 'a') P.<* P.char 'b')
    -- , test "alt-char-try-fail" $$(P.runParser $ P.try (P.char 'a') P.<|> P.char 'b')
    ]

-- | Resetting 'TH.counter' makes 'makeLetName' deterministic,
-- except when profiling is enabled, in this case those tests may fail
-- due to a different numbering of the 'def' and 'ref' combinators.
resetTHNameCounter :: IO ()
resetTHNameCounter = IORef.writeIORef TH.counter 0

-- * Golden testing utilities

diffGolden :: FilePath -> FilePath -> [String]
diffGolden ref new = ["diff", "-u", ref, new]

unLeft :: Either String BSL.ByteString -> IO BSL.ByteString
unLeft = \case
  Left err -> return $ TL.encodeUtf8 $ TL.pack err
  Right a  -> return a