Skip to content

Commit ff24f8d

Browse files
committed
New thread-safe golden test functions
1 parent fae2316 commit ff24f8d

File tree

2 files changed

+174
-0
lines changed

2 files changed

+174
-0
lines changed

hedgehog-extras.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ library
141141
Hedgehog.Extras.Test.Concurrent
142142
Hedgehog.Extras.Test.File
143143
Hedgehog.Extras.Test.Golden
144+
Hedgehog.Extras.Test.New.Golden
144145
Hedgehog.Extras.Test.New.Monad
145146
Hedgehog.Extras.Test.New.Monad.PropertyIO
146147
Hedgehog.Extras.Test.New.Monad.UnitIO
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
{-# LANGUAGE MultiWayIf #-}
2+
3+
module Hedgehog.Extras.Test.New.Golden
4+
( diffVsGoldenFile,
5+
diffFileVsGoldenFile,
6+
) where
7+
8+
import Control.Applicative
9+
import Control.Monad
10+
import Control.Monad.Catch
11+
import Control.Monad.IO.Class (MonadIO (liftIO))
12+
import Data.Algorithm.Diff (PolyDiff (Both), getGroupedDiff)
13+
import Data.Algorithm.DiffOutput (ppDiff)
14+
import Data.Bool
15+
import Data.Eq
16+
import Data.Function
17+
import Data.Maybe
18+
import Data.Monoid
19+
import Data.String
20+
import GHC.Stack (HasCallStack, callStack)
21+
import Hedgehog (MonadTest)
22+
import Hedgehog.Extras.Test.Base (failMessage)
23+
import System.FilePath (takeDirectory)
24+
import System.IO (FilePath)
25+
26+
import qualified Control.Concurrent.QSem as IO
27+
import qualified Data.List as List
28+
import qualified GHC.Stack as GHC
29+
import qualified Hedgehog.Extras.Test as H
30+
import qualified Hedgehog.Internal.Property as H
31+
import qualified System.Directory as IO
32+
import qualified System.Environment as IO
33+
import qualified System.IO as IO
34+
import qualified System.IO.Unsafe as IO
35+
36+
sem :: IO.QSem
37+
sem = IO.unsafePerformIO $ IO.newQSem 1
38+
{-# NOINLINE sem #-}
39+
40+
semBracket :: ()
41+
=> MonadIO m
42+
=> MonadMask m
43+
=> m a
44+
-> m a
45+
semBracket =
46+
bracket_
47+
(liftIO (IO.waitQSem sem))
48+
(liftIO (IO.signalQSem sem))
49+
50+
-- | The file to log whenever a golden file is referenced.
51+
mGoldenFileLogFile :: Maybe FilePath
52+
mGoldenFileLogFile = IO.unsafePerformIO $
53+
IO.lookupEnv "GOLDEN_FILE_LOG_FILE"
54+
55+
-- | Whether the test should create the golden files if the files do not exist.
56+
createGoldenFiles :: Bool
57+
createGoldenFiles = IO.unsafePerformIO $ do
58+
value <- IO.lookupEnv "CREATE_GOLDEN_FILES"
59+
return $ value == Just "1"
60+
61+
-- | Whether the test should recreate the golden files if the files already exist.
62+
recreateGoldenFiles :: Bool
63+
recreateGoldenFiles = IO.unsafePerformIO $ do
64+
value <- IO.lookupEnv "RECREATE_GOLDEN_FILES"
65+
return $ value == Just "1"
66+
67+
writeGoldenFile :: ()
68+
=> HasCallStack
69+
=> MonadIO m
70+
=> MonadTest m
71+
=> FilePath
72+
-> String
73+
-> m ()
74+
writeGoldenFile goldenFile actualContent = GHC.withFrozenCallStack $ do
75+
H.note_ $ "Creating golden file " <> goldenFile
76+
H.createDirectoryIfMissing_ (takeDirectory goldenFile)
77+
H.writeFile goldenFile actualContent
78+
79+
reportGoldenFileMissing :: ()
80+
=> HasCallStack
81+
=> MonadIO m
82+
=> MonadTest m
83+
=> FilePath
84+
-> m ()
85+
reportGoldenFileMissing goldenFile = GHC.withFrozenCallStack $ do
86+
H.note_ $ unlines
87+
[ "Golden file " <> goldenFile <> " does not exist."
88+
, "To create it, run with CREATE_GOLDEN_FILES=1."
89+
, "To recreate it, run with RECREATE_GOLDEN_FILES=1."
90+
]
91+
H.failure
92+
93+
checkAgainstGoldenFile :: ()
94+
=> HasCallStack
95+
=> MonadIO m
96+
=> MonadTest m
97+
=> FilePath
98+
-> [String]
99+
-> m ()
100+
checkAgainstGoldenFile goldenFile actualLines = GHC.withFrozenCallStack $ do
101+
referenceLines <- List.lines <$> H.readFile goldenFile
102+
let difference = getGroupedDiff actualLines referenceLines
103+
case difference of
104+
[] -> pure ()
105+
[Both{}] -> pure ()
106+
_ -> do
107+
H.note_ $ unlines
108+
[ "Golden test failed against the golden file."
109+
, "To recreate golden file, run with RECREATE_GOLDEN_FILES=1."
110+
]
111+
failMessage callStack $ ppDiff difference
112+
113+
-- | Diff contents against the golden file. If CREATE_GOLDEN_FILES environment is
114+
-- set to "1", then should the golden file not exist it would be created. If
115+
-- RECREATE_GOLDEN_FILES is set to "1", then should the golden file exist it would
116+
-- be recreated. If GOLDEN_FILE_LOG_FILE is set to a filename, then the golden file
117+
-- path will be logged to the specified file.
118+
--
119+
-- Set the environment variable when you intend to generate or re-generate the golden
120+
-- file for example when running the test for the first time or if the golden file
121+
-- genuinely needs to change.
122+
--
123+
-- To re-generate a golden file you must also delete the golden file because golden
124+
-- files are never overwritten.
125+
--
126+
-- TODO: Improve the help output by saying the difference of
127+
-- each input.
128+
diffVsGoldenFile
129+
:: HasCallStack
130+
=> MonadIO m
131+
=> MonadMask m
132+
=> MonadTest m
133+
=> String -- ^ Actual content
134+
-> FilePath -- ^ Reference file
135+
-> m ()
136+
diffVsGoldenFile actualContent goldenFile =
137+
GHC.withFrozenCallStack $ semBracket $ do
138+
forM_ mGoldenFileLogFile $ \logFile ->
139+
liftIO $ IO.appendFile logFile $ goldenFile <> "\n"
140+
141+
fileExists <- liftIO $ IO.doesFileExist goldenFile
142+
143+
if
144+
| recreateGoldenFiles -> writeGoldenFile goldenFile actualContent
145+
| fileExists -> checkAgainstGoldenFile goldenFile actualLines
146+
| createGoldenFiles -> writeGoldenFile goldenFile actualContent
147+
| otherwise -> reportGoldenFileMissing goldenFile
148+
149+
where
150+
actualLines = List.lines actualContent
151+
152+
-- | Diff file against the golden file. If CREATE_GOLDEN_FILES environment is
153+
-- set to "1", then should the gold file not exist it would be created. If
154+
-- GOLDEN_FILE_LOG_FILE is set to a filename, then the golden file path will be
155+
-- logged to the specified file.
156+
--
157+
-- Set the environment variable when you intend to generate or re-generate the golden
158+
-- file for example when running the test for the first time or if the golden file
159+
-- genuinely needs to change.
160+
--
161+
-- To re-generate a golden file you must also delete the golden file because golden
162+
-- files are never overwritten.
163+
diffFileVsGoldenFile
164+
:: HasCallStack
165+
=> MonadIO m
166+
=> MonadMask m
167+
=> MonadTest m
168+
=> FilePath -- ^ Actual file
169+
-> FilePath -- ^ Reference file
170+
-> m ()
171+
diffFileVsGoldenFile actualFile referenceFile = GHC.withFrozenCallStack $ do
172+
contents <- H.readFile actualFile
173+
diffVsGoldenFile contents referenceFile

0 commit comments

Comments
 (0)