tadka-2.0.0.0: test/golden/Main.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Golden test suite: renders each graphical fixture and compares byte-for-byte
-- against a committed expected file. Set GEN_GOLDEN=1 to (re)generate the
-- expected files instead of checking.
module Main (main) where
import qualified Data.Aeson as A
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import Control.Monad (forM, unless)
import Data.Foldable (toList)
import Data.List (elemIndex, sortBy)
import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import GHC.IO.Encoding (setLocaleEncoding, utf8)
import Prettyprinter (LayoutOptions (..), PageWidth (Unbounded),
layoutPretty)
import Prettyprinter.Render.Text (renderStrict)
import System.Environment (lookupEnv)
import System.Exit (exitFailure)
import Fixtures (contextFixtures, fixtures, generatedSource, jsonFixtures,
narratableFixtures, single)
import Tadka
cfg :: Config
cfg = withColorMode ColorNever
. withUnicodeMode UnicodeAlways
. withTarget TGraphical
$ defaultConfig
renderFix :: SomeDiagnostic -> Text
cfgCtx :: Config
cfgCtx = withContextLines 1 cfg
renderCtx :: SomeDiagnostic -> Text
renderCtx (SomeDiagnostic e) = case selectRenderer cfgCtx of
SomeRenderer r@(Graphical _) -> renderStrict (layoutPretty (LayoutOptions Unbounded) (render r e))
_ -> ""
renderFix (SomeDiagnostic e) = case selectRenderer cfg of
SomeRenderer r@(Graphical _) ->
renderStrict (layoutPretty (LayoutOptions Unbounded) (render r e))
_ -> "<<not graphical>>"
-- Same as 'cfg' but with hyperlinks turned on, to pin the OSC 8 escape bytes
-- byte-for-byte (proves the wrap touches only the "= see:" line — everything
-- else must match "single-label" exactly).
hyperlinkCfg :: Config
hyperlinkCfg = withHyperlinkMode HyperlinkAlways cfg
renderHyperlink :: SomeDiagnostic -> Text
renderHyperlink (SomeDiagnostic e) = case selectRenderer hyperlinkCfg of
SomeRenderer r@(Graphical _) -> renderStrict (layoutPretty (LayoutOptions Unbounded) (render r e))
_ -> "<<not graphical>>"
-- Narratable fixtures render at a low depth limit so the truncation marker fires.
narrCfg :: Config
narrCfg = withRelatedDepthLimit 1 (withTarget TNarratable defaultConfig)
renderNarr :: SomeDiagnostic -> Text
renderNarr (SomeDiagnostic e) = case selectRenderer narrCfg of
SomeRenderer r@(Narratable _) -> render r e
_ -> "<<not narratable>>"
-- JSON fixtures render at depth 1 too, then serialize with a deterministic
-- ordered pretty-printer matching the vision's canonical layout.
jsonCfg :: Config
jsonCfg = withRelatedDepthLimit 1 (withTarget TJson defaultConfig)
renderJs :: SomeDiagnostic -> Text
renderJs (SomeDiagnostic e) = case selectRenderer jsonCfg of
SomeRenderer r@(Json _) -> prettyJSON (render r e)
_ -> "<<not json>>"
-- Deterministic pretty-printer for an 'A.Value': object keys in the canonical
-- field order, all-scalar objects inline, empty arrays inline, 2-space indent.
prettyJSON :: A.Value -> Text
prettyJSON = go 0
where
prefOrder =
[ "code","severity","message","labels","file","line","column","length","text"
, "primary","stale","help","url","related","causes","truncated","cycleOmitted" ]
go :: Int -> A.Value -> Text
go ind v = case v of
A.Object o -> renderObj ind o
A.Array a -> renderArr ind (toList a)
_ -> TL.toStrict (TLE.decodeUtf8 (A.encode v))
renderObj ind o
| KM.null o = "{}"
| inlineable = "{ " <> T.intercalate ", " (map field pairs) <> " }"
| otherwise = "{\n" <> T.intercalate ",\n" (map (\p -> pad (ind + 1) <> field p) pairs)
<> "\n" <> pad ind <> "}"
where
pairs = sortBy (comparing (rank . fst)) (KM.toList o)
inlineable = all (isSimple . snd) pairs
field (k, val) = "\"" <> K.toText k <> "\": " <> go (ind + 1) val
rank k = maybe (1 :: Int, 0) (\i -> (0, i)) (elemIndex (K.toText k) prefOrder)
renderArr _ [] = "[]"
renderArr ind xs = "[\n" <> T.intercalate ",\n" (map (\x -> pad (ind + 1) <> go (ind + 1) x) xs)
<> "\n" <> pad ind <> "]"
isSimple (A.Object o) = KM.null o
isSimple (A.Array a) = null (toList a)
isSimple _ = True
pad n = T.replicate (2 * n) " "
-- | Collapse GHC's base/ghc-internal module-qualification split so the
-- 'generatedSource' fixture (the only one containing TH-'pprint'd references
-- to base internals like @show@/@(.)@/@Just@) isn't tied to which GHC release
-- produced it. Pre-split GHC (9.4-9.8-ish) qualifies these as @GHC.Base@,
-- @GHC.Show@, @GHC.Maybe@; post-split GHC (9.10+) qualifies the same
-- identifiers as @GHC.Internal.Base@, @GHC.Internal.Show@,
-- @GHC.Internal.Maybe@ — and this is an ongoing, multi-release GHC-internal
-- migration (further reshuffling is expected at GHC 10.0), not a one-off
-- two-way difference. Collapsing @GHC.Internal.@ down to @GHC.@ normalizes
-- every release's spelling to one canonical form, so this fixture checks what
-- it's actually meant to check — that the derived method bodies are still
-- direct calls to the same functions — without re-pinning to base's internal
-- module layout every time GHC reorganizes it.
normalizeGhcInternal :: Text -> Text
normalizeGhcInternal = T.replace (T.pack "GHC.Internal.") (T.pack "GHC.")
-- (name, rendered output) across all three handlers.
allFixtures :: [(String, Text)]
allFixtures =
[ (n, renderFix d) | (n, d) <- fixtures ]
++ [ (n, renderNarr d) | (n, d) <- narratableFixtures ]
++ [ (n, renderJs d) | (n, d) <- jsonFixtures ]
++ [ (n, renderCtx d) | (n, d) <- contextFixtures ]
++ [ ("generated-parseerror", normalizeGhcInternal (T.pack generatedSource)) ]
++ [ ("single-label-hyperlink", renderHyperlink (SomeDiagnostic single)) ]
fixturePath :: String -> FilePath
fixturePath name = "test/golden/fixtures/" <> name <> ".txt"
main :: IO ()
main = do
setLocaleEncoding utf8
mode <- lookupEnv "GEN_GOLDEN"
case mode of
Just _ -> do
mapM_ (\(n, out) -> TIO.writeFile (fixturePath n) out) allFixtures
putStrLn "[golden] regenerated fixtures"
Nothing -> do
results <- forM allFixtures $ \(n, actual) -> do
expected <- TIO.readFile (fixturePath n)
if actual == expected
then putStrLn (" ok " <> n) >> pure True
else do
putStrLn (" FAIL " <> n <> "\n--- expected ---\n" <> T.unpack expected
<> "\n--- actual ---\n" <> T.unpack actual)
pure False
unless (and results) exitFailure