packages feed

symantic-parser-0.1.0.20210201: test/Golden.hs

{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UnboxedTuples #-}
{-# OPTIONS_GHC -Wno-unused-local-binds #-} -- For TH splices
{-# OPTIONS_GHC -Wno-unused-matches #-} -- For TH splices
module Golden where

import Data.Bool (Bool(..))
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 Parser
--import qualified Golden.Splice

goldensIO :: IO TestTree
goldensIO = return $ testGroup "Golden"
  [ goldensGrammar
  , goldensMachine
  , goldensParser
  -- TODO: this will need cabal-install-3.4 to compile under GHC9.
  --, Golden.Splice.goldens
  ]

goldensGrammar :: TestTree
goldensGrammar = testGroup "Grammar"
  [ testGroup "ViewGrammar" $ tests $ \name repr ->
    let file = "test/Golden/Grammar/"<>name<>".dump" in
    goldenVsStringDiff file diffGolden file $ do
      resetTHNameCounter
      return $ fromString $ show $
        P.viewGrammar @'False $
        P.observeSharing repr
  , testGroup "OptimizeGrammar" $ tests $ \name repr ->
    let file = "test/Golden/Grammar/"<>name<>".opt.dump" in
    goldenVsStringDiff file diffGolden file $ do
      resetTHNameCounter
      return $ fromString $ P.showGrammar @'False repr
  ]
  where
  tests :: P.Grammar Char repr =>
           (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.id P.<*> P.unit
    , test "string" $ P.string "abcd"
    , test "tokens" $ P.tokens "abcd"
    , test "many-a" $ P.many (P.char 'a')
    , test "boom" $ Parser.boom
    , test "brainfuck" $ Parser.brainfuck
    , test "many-char-eof" $ P.many (P.char 'r') P.<* P.eof
    , test "eof" $ P.eof
    , test "nandlang" $ Parser.nandlang
    ]

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.viewMachine @'False repr
  ]
  where
  tests ::
    P.Machine Char repr =>
    (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 "string" $ P.machine $ P.string "abcd"
    , test "a-or-b" $ P.machine $ P.char 'a' P.<|> P.char 'b'
    , test "app" $ P.machine $ P.pure H.id P.<*> P.unit
    , test "many-a" $ P.machine $ P.many (P.char 'a')
    , test "some-string" $ P.machine $ P.some (P.string "abcd")
    , test "boom" $ P.machine $ Parser.boom
    , test "brainfuck" $ P.machine $ Parser.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'
    , test "nandlang" $ P.machine $ Parser.nandlang
    ]

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 "abc")
    , test "string-fail-horizon" $$(P.runParser $ P.string "abc")
    , test "many-char" $$(P.runParser $ P.many (P.char 'a'))
    , test "some-string" $$(P.runParser $ P.some (P.string "abcd"))
    , test "some-string-fail" $$(P.runParser $ P.some (P.string "abcd"))
    , test "some-string-eof-fail" $$(P.runParser $ P.some (P.string "abcd") P.<* P.eof)
    , test "alt-right-notry" $$(P.runParser $ P.traverse P.char "aa" P.<|> P.traverse P.char "ab")
    , test "alt-right-try" $$(P.runParser $ 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 "many-char-fail" $$(P.runParser $ P.many (P.char 'a') P.<* P.char 'b')
    , test "many-oneOf" $$(P.runParser $ P.many (P.oneOf ['a', 'b', 'c', 'd']) P.<* P.eof)
    ]

-- | Resetting 'TH.counter' makes 'makeLetName' deterministic,
-- except when GHC or executable flags change, like profiling
-- or even --accept unfortunately,
-- in those case the 'goldensMachine' tests may fail
-- due to a different numbering of the 'def' and 'ref' combinators.
-- Hence 'ShowLetName' is used with 'False'.
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