{-# LANGUAGE MultiWayIf #-}
module Hedgehog.Extras.Test.Golden
( diffVsGoldenFile,
diffFileVsGoldenFile,
) where
import Control.Applicative
import Control.Exception (bracket_)
import Control.Monad
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Algorithm.Diff (PolyDiff (Both), getGroupedDiff)
import Data.Algorithm.DiffOutput (ppDiff)
import Data.Bool
import Data.Eq
import Data.Function
import Data.Maybe
import Data.Monoid
import Data.String
import GHC.Stack (HasCallStack, callStack)
import Hedgehog (MonadTest)
import Hedgehog.Extras.Test.Base (failMessage)
import System.FilePath (takeDirectory)
import System.IO (FilePath, IO)
import qualified Control.Concurrent.QSem as IO
import qualified Data.List as List
import qualified GHC.Stack as GHC
import qualified Hedgehog.Extras.Test as H
import qualified Hedgehog.Internal.Property as H
import qualified System.Directory as IO
import qualified System.Environment as IO
import qualified System.IO as IO
import qualified System.IO.Unsafe as IO
sem :: IO.QSem
sem :: QSem
sem = IO QSem -> QSem
forall a. IO a -> a
IO.unsafePerformIO (IO QSem -> QSem) -> IO QSem -> QSem
forall a b. (a -> b) -> a -> b
$ Int -> IO QSem
IO.newQSem Int
1
{-# NOINLINE sem #-}
semBracket :: IO a -> IO a
semBracket :: forall a. IO a -> IO a
semBracket = IO () -> IO () -> IO a -> IO a
forall a b c. IO a -> IO b -> IO c -> IO c
bracket_ (QSem -> IO ()
IO.waitQSem QSem
sem) (QSem -> IO ()
IO.signalQSem QSem
sem)
mGoldenFileLogFile :: Maybe FilePath
mGoldenFileLogFile :: Maybe String
mGoldenFileLogFile = IO (Maybe String) -> Maybe String
forall a. IO a -> a
IO.unsafePerformIO (IO (Maybe String) -> Maybe String)
-> IO (Maybe String) -> Maybe String
forall a b. (a -> b) -> a -> b
$
String -> IO (Maybe String)
IO.lookupEnv String
"GOLDEN_FILE_LOG_FILE"
createGoldenFiles :: Bool
createGoldenFiles :: Bool
createGoldenFiles = IO Bool -> Bool
forall a. IO a -> a
IO.unsafePerformIO (IO Bool -> Bool) -> IO Bool -> Bool
forall a b. (a -> b) -> a -> b
$ do
Maybe String
value <- String -> IO (Maybe String)
IO.lookupEnv String
"CREATE_GOLDEN_FILES"
Bool -> IO Bool
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Bool -> IO Bool) -> Bool -> IO Bool
forall a b. (a -> b) -> a -> b
$ Maybe String
value Maybe String -> Maybe String -> Bool
forall a. Eq a => a -> a -> Bool
== String -> Maybe String
forall a. a -> Maybe a
Just String
"1"
recreateGoldenFiles :: Bool
recreateGoldenFiles :: Bool
recreateGoldenFiles = IO Bool -> Bool
forall a. IO a -> a
IO.unsafePerformIO (IO Bool -> Bool) -> IO Bool -> Bool
forall a b. (a -> b) -> a -> b
$ do
Maybe String
value <- String -> IO (Maybe String)
IO.lookupEnv String
"RECREATE_GOLDEN_FILES"
Bool -> IO Bool
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Bool -> IO Bool) -> Bool -> IO Bool
forall a b. (a -> b) -> a -> b
$ Maybe String
value Maybe String -> Maybe String -> Bool
forall a. Eq a => a -> a -> Bool
== String -> Maybe String
forall a. a -> Maybe a
Just String
"1"
writeGoldenFile :: ()
=> MonadIO m
=> MonadTest m
=> FilePath
-> String
-> m ()
writeGoldenFile :: forall (m :: * -> *).
(MonadIO m, MonadTest m) =>
String -> String -> m ()
writeGoldenFile String
goldenFile String
actualContent = do
String -> m ()
forall (m :: * -> *). (MonadTest m, HasCallStack) => String -> m ()
H.note_ (String -> m ()) -> String -> m ()
forall a b. (a -> b) -> a -> b
$ String
"Creating golden file " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
goldenFile
String -> m ()
forall (m :: * -> *).
(MonadTest m, MonadIO m, HasCallStack) =>
String -> m ()
H.createDirectoryIfMissing_ (String -> String
takeDirectory String
goldenFile)
String -> String -> m ()
forall (m :: * -> *).
(MonadTest m, MonadIO m, HasCallStack) =>
String -> String -> m ()
H.writeFile String
goldenFile String
actualContent
reportGoldenFileMissing :: ()
=> MonadIO m
=> MonadTest m
=> FilePath
-> m ()
reportGoldenFileMissing :: forall (m :: * -> *). (MonadIO m, MonadTest m) => String -> m ()
reportGoldenFileMissing String
goldenFile = do
String -> m ()
forall (m :: * -> *). (MonadTest m, HasCallStack) => String -> m ()
H.note_ (String -> m ()) -> String -> m ()
forall a b. (a -> b) -> a -> b
$ [String] -> String
unlines
[ String
"Golden file " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
goldenFile String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
" does not exist."
, String
"To create it, run with CREATE_GOLDEN_FILES=1."
, String
"To recreate it, run with RECREATE_GOLDEN_FILES=1."
]
m ()
forall (m :: * -> *) a. (MonadTest m, HasCallStack) => m a
H.failure
checkAgainstGoldenFile :: ()
=> MonadIO m
=> MonadTest m
=> FilePath
-> [String]
-> m ()
checkAgainstGoldenFile :: forall (m :: * -> *).
(MonadIO m, MonadTest m) =>
String -> [String] -> m ()
checkAgainstGoldenFile String
goldenFile [String]
actualLines = do
[String]
referenceLines <- String -> [String]
List.lines (String -> [String]) -> m String -> m [String]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> m String
forall (m :: * -> *).
(MonadTest m, MonadIO m, HasCallStack) =>
String -> m String
H.readFile String
goldenFile
let difference :: [Diff [String]]
difference = [String] -> [String] -> [Diff [String]]
forall a. Eq a => [a] -> [a] -> [Diff [a]]
getGroupedDiff [String]
actualLines [String]
referenceLines
case [Diff [String]]
difference of
[] -> () -> m ()
forall a. a -> m a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
[Both{}] -> () -> m ()
forall a. a -> m a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
[Diff [String]]
_ -> do
String -> m ()
forall (m :: * -> *). (MonadTest m, HasCallStack) => String -> m ()
H.note_ (String -> m ()) -> String -> m ()
forall a b. (a -> b) -> a -> b
$ [String] -> String
unlines
[ String
"Golden test failed against golden file: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
goldenFile
, String
"To recreate golden file, run with RECREATE_GOLDEN_FILES=1."
]
CallStack -> String -> m ()
forall (m :: * -> *) a. MonadTest m => CallStack -> String -> m a
failMessage CallStack
HasCallStack => CallStack
callStack (String -> m ()) -> String -> m ()
forall a b. (a -> b) -> a -> b
$ [Diff [String]] -> String
ppDiff [Diff [String]]
difference
diffVsGoldenFile
:: HasCallStack
=> (MonadIO m, MonadTest m)
=> String
-> FilePath
-> m ()
diffVsGoldenFile :: forall (m :: * -> *).
(HasCallStack, MonadIO m, MonadTest m) =>
String -> String -> m ()
diffVsGoldenFile String
actualContent String
goldenFile = (HasCallStack => m ()) -> m ()
forall a. HasCallStack => (HasCallStack => a) -> a
GHC.withFrozenCallStack ((HasCallStack => m ()) -> m ()) -> (HasCallStack => m ()) -> m ()
forall a b. (a -> b) -> a -> b
$ do
Maybe String -> (String -> m ()) -> m ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
t a -> (a -> m b) -> m ()
forM_ Maybe String
mGoldenFileLogFile ((String -> m ()) -> m ()) -> (String -> m ()) -> m ()
forall a b. (a -> b) -> a -> b
$ \String
logFile ->
IO () -> m ()
forall a. IO a -> m a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> m ()) -> IO () -> m ()
forall a b. (a -> b) -> a -> b
$ IO () -> IO ()
forall a. IO a -> IO a
semBracket (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ String -> String -> IO ()
IO.appendFile String
logFile (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
goldenFile String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"\n"
Bool
fileExists <- IO Bool -> m Bool
forall a. IO a -> m a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> m Bool) -> IO Bool -> m Bool
forall a b. (a -> b) -> a -> b
$ String -> IO Bool
IO.doesFileExist String
goldenFile
if
| Bool
recreateGoldenFiles -> String -> String -> m ()
forall (m :: * -> *).
(MonadIO m, MonadTest m) =>
String -> String -> m ()
writeGoldenFile String
goldenFile String
actualContent
| Bool
fileExists -> String -> [String] -> m ()
forall (m :: * -> *).
(MonadIO m, MonadTest m) =>
String -> [String] -> m ()
checkAgainstGoldenFile String
goldenFile [String]
actualLines
| Bool
createGoldenFiles -> String -> String -> m ()
forall (m :: * -> *).
(MonadIO m, MonadTest m) =>
String -> String -> m ()
writeGoldenFile String
goldenFile String
actualContent
| Bool
otherwise -> String -> m ()
forall (m :: * -> *). (MonadIO m, MonadTest m) => String -> m ()
reportGoldenFileMissing String
goldenFile
where
actualLines :: [String]
actualLines = String -> [String]
List.lines String
actualContent
diffFileVsGoldenFile
:: HasCallStack
=> (MonadIO m, MonadTest m)
=> FilePath
-> FilePath
-> m ()
diffFileVsGoldenFile :: forall (m :: * -> *).
(HasCallStack, MonadIO m, MonadTest m) =>
String -> String -> m ()
diffFileVsGoldenFile String
actualFile String
referenceFile = (HasCallStack => m ()) -> m ()
forall a. HasCallStack => (HasCallStack => a) -> a
GHC.withFrozenCallStack ((HasCallStack => m ()) -> m ()) -> (HasCallStack => m ()) -> m ()
forall a b. (a -> b) -> a -> b
$ do
String
contents <- String -> m String
forall (m :: * -> *).
(MonadTest m, MonadIO m, HasCallStack) =>
String -> m String
H.readFile String
actualFile
String -> String -> m ()
forall (m :: * -> *).
(HasCallStack, MonadIO m, MonadTest m) =>
String -> String -> m ()
diffVsGoldenFile String
contents String
referenceFile