Port from Tasty to Sydtest

There are some drawbacks:
- No properly lazy golden tests. This would in principle be nice when e.g. using `pretty-simple`.
- Because tests can be created dynamically, they can't be listed up front without running them. This presumably makes filtering slightly more annoying to use in practice.
- Terminal output is less compact than tasty, both horizontally and vertically. There appears to be no way to change this.
- We end up defining an orphan `Monoid (TestDefM '[] () ())` instance, to avoid changing much downstream code. Note though that this is not strictly necessary, and could potentially be contributed upstream.
- There's a warning about threads in GHCI which we can't seem to disable.
- The license forbids use in commercial projects without sponsoring. Thankfully that doesn't apply here.

Anyway, it's generally very impressive. It simplifies a few things for us, and will particularly help when we come to want to specify dependencies between tests.
This commit is contained in:
George Thomas 2025-12-30 17:23:45 +00:00
parent 5b24dc1238
commit 450d7e5240
4 changed files with 59 additions and 75 deletions

View File

@ -2,9 +2,8 @@ module Main (main) where
import Pre import Pre
import Data.Text qualified as T
import Data.Text.IO qualified as T import Data.Text.IO qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Encoding qualified as TL
import Puzzles.Day1 qualified as Day1 import Puzzles.Day1 qualified as Day1
import Puzzles.Day10 qualified as Day10 import Puzzles.Day10 qualified as Day10
import Puzzles.Day2 qualified as Day2 import Puzzles.Day2 qualified as Day2
@ -15,15 +14,11 @@ import Puzzles.Day6 qualified as Day6
import Puzzles.Day7 qualified as Day7 import Puzzles.Day7 qualified as Day7
import Puzzles.Day8 qualified as Day8 import Puzzles.Day8 qualified as Day8
import Puzzles.Day9 qualified as Day9 import Puzzles.Day9 qualified as Day9
import Test.Tasty.Ingredients.ConsoleReporter
main :: IO () main :: IO ()
main = main =
defaultMain sydTest $ doNotRandomiseExecutionOrder $ for_ enumerate \isRealData@(bool "examples" "real" -> t) ->
. localOption (Always :: UseColor) describe t $ for_
. testGroup "tests"
$ enumerate <&> \isRealData@(bool "examples" "real" -> t) ->
testGroup t $
[ Day1.puzzle [ Day1.puzzle
, Day2.puzzle , Day2.puzzle
, Day3.puzzle , Day3.puzzle
@ -35,7 +30,7 @@ main =
, Day9.puzzle , Day9.puzzle
, Day10.puzzle , Day10.puzzle
] ]
<&> \Puzzle{number, parser, parts, extraTests} -> \Puzzle{number, parser, parts, extraTests} ->
let let
pt = show number pt = show number
parseFile fp = parseFile fp =
@ -43,10 +38,9 @@ main =
. runParser (parser isRealData <* eof) fp . runParser (parser isRealData <* eof) fp
=<< T.readFile fp =<< T.readFile fp
in in
withResource (parseFile $ "../inputs/" <> t <> "/" <> pt) mempty \input -> describe pt do
testGroup pt $ input <- liftIO $ parseFile $ "../inputs/" <> t <> "/" <> pt
( flip mapWithIndexOutputParameterisedFunctionList parts \(show . succ -> n) pp -> sequence_ $ flip mapWithIndexOutputParameterisedFunctionList parts \(show . succ -> n) pp ->
goldenVsStringDiff n diffCommand ("../outputs/" <> t <> "/" <> pt <> "/" <> n) $ it n . pureGoldenTextFile ("../outputs/" <> t <> "/" <> pt <> "/" <> n) $
TL.encodeUtf8 . (<> "\n") . TL.show . pp <$> input T.show (pp input) <> "\n"
) describe "extra" $ extraTests isRealData ("../outputs/" <> t <> "/" <> pt <> "/extra/") input
<> [testGroup "extra" $ extraTests isRealData ("../outputs/" <> t <> "/" <> pt <> "/extra/") input]

View File

@ -1,4 +1,5 @@
{-# LANGUAGE PackageImports #-} {-# LANGUAGE PackageImports #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Pre ( module Pre (
module BasePrelude, module BasePrelude,
@ -28,9 +29,7 @@ module Pre (
module Data.Word, module Data.Word,
module Linear, module Linear,
module Safe, module Safe,
module Test.Tasty, module Test.Syd,
module Test.Tasty.Golden,
module Test.Tasty.HUnit,
module Text.Megaparsec, module Text.Megaparsec,
module Text.Megaparsec.Char, module Text.Megaparsec.Char,
module Text.Megaparsec.Char.Lexer, module Text.Megaparsec.Char.Lexer,
@ -41,7 +40,6 @@ module Pre (
allUnorderedPairs, allUnorderedPairs,
adjacentPairs, adjacentPairs,
sortPair, sortPair,
diffCommand,
OutputParameterisedFunctionList, OutputParameterisedFunctionList,
mapOutputParameterisedFunctionList, mapOutputParameterisedFunctionList,
mapWithIndexOutputParameterisedFunctionList, mapWithIndexOutputParameterisedFunctionList,
@ -90,9 +88,7 @@ import Data.Void
import Data.Word import Data.Word
import Linear (V2 (..)) import Linear (V2 (..))
import Safe import Safe
import Test.Tasty import Test.Syd
import Test.Tasty.Golden hiding (Always)
import Test.Tasty.HUnit
import Text.Megaparsec hiding (Pos, State, Stream, many, some) import Text.Megaparsec hiding (Pos, State, Stream, many, some)
import Text.Megaparsec.Char import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer (decimal) import Text.Megaparsec.Char.Lexer (decimal)
@ -101,7 +97,7 @@ data Puzzle = forall input outputs. Puzzle
{ number :: Word { number :: Word
, parser :: Bool -> Parsec Void Text input , parser :: Bool -> Parsec Void Text input
, parts :: OutputParameterisedFunctionList Show input outputs , parts :: OutputParameterisedFunctionList Show input outputs
, extraTests :: Bool -> FilePath -> IO input -> [TestTree] , extraTests :: Bool -> FilePath -> input -> Spec
} }
digit :: (Token s ~ Char, Num b, MonadParsec e s f) => f b digit :: (Token s ~ Char, Num b, MonadParsec e s f) => f b
@ -129,9 +125,6 @@ adjacentPairs = \case
sortPair :: (Ord a) => (a, a) -> (a, a) sortPair :: (Ord a) => (a, a) -> (a, a)
sortPair (a, b) = if a <= b then (a, b) else (b, a) sortPair (a, b) = if a <= b then (a, b) else (b, a)
diffCommand :: FilePath -> FilePath -> [String]
diffCommand a b = ["diff", "--color=always", a, b]
infixr 9 /\ infixr 9 /\
(/\) :: (c output) => (input -> output) -> OutputParameterisedFunctionList c input outputs -> OutputParameterisedFunctionList c input (output : outputs) (/\) :: (c output) => (input -> output) -> OutputParameterisedFunctionList c input outputs -> OutputParameterisedFunctionList c input (output : outputs)
(/\) = OutputParameterisedFunctionListCons (/\) = OutputParameterisedFunctionListCons
@ -163,3 +156,8 @@ mapWithIndexOutputParameterisedFunctionList f = go 0
go i = \case go i = \case
OutputParameterisedFunctionListNil -> [] OutputParameterisedFunctionListNil -> []
OutputParameterisedFunctionListCons x xs -> f i x : go (i + 1) xs OutputParameterisedFunctionListCons x xs -> f i x : go (i + 1) xs
instance Semigroup (TestDefM '[] () ()) where
(<>) = (>>)
instance Monoid (TestDefM '[] () ()) where
mempty = pure ()

View File

@ -4,9 +4,8 @@ import Pre
import Data.Sequence qualified as Seq import Data.Sequence qualified as Seq
import Data.Stream.Infinite qualified as S import Data.Stream.Infinite qualified as S
import Data.Text.Lazy qualified as TL import Data.Text qualified as T
import Data.Text.Lazy.Encoding qualified as TL import Data.Text.IO qualified as T
import Data.Text.Lazy.IO qualified as TL
puzzle :: Puzzle puzzle :: Puzzle
puzzle = puzzle =
@ -21,31 +20,23 @@ puzzle =
. mkGrid . mkGrid
) )
/\ nil /\ nil
, extraTests = \isRealData path input -> , extraTests = \isRealData path input -> do
[ testCase "round trip" do it "round trip" do
t <- TL.readFile if isRealData then "../inputs/real/4" else "../inputs/examples/4" t <- T.readFile if isRealData then "../inputs/real/4" else "../inputs/examples/4"
input' <- input drawGrid (mkGrid input <&> \case InEmpty -> OutEmpty; InRoll -> OutRoll) `shouldBe` t
t @=? drawGrid (mkGrid input' <&> \case InEmpty -> OutEmpty; InRoll -> OutRoll) describe "frames" do
, withResource let frames = Seq.fromList . takeUntil noneAccessible . fmap snd . generateFrames $ mkGrid input
(Seq.fromList . takeUntil noneAccessible . fmap snd . generateFrames . mkGrid <$> input) let nFrames = Seq.length frames - 1
mempty for_ [0 .. nFrames] \n ->
\frames -> it (show n) . pureGoldenTextFile (path <> "frames/" <> show n) $
testGroup maybe "frame list too short!" drawGrid (Seq.lookup n frames)
"frames" it "end" do
let nFrames = if isRealData then 58 else 9 Just g <- pure $ Seq.lookup nFrames frames
in ( [0 .. nFrames] <&> \n -> (g `shouldSatisfyNamed` "accessible tile found") noneAccessible
goldenVsStringDiff (show n) diffCommand (path <> "frames/" <> show n) $
TL.encodeUtf8 . maybe "frame list too short!" drawGrid . Seq.lookup n <$> frames
)
<> [ testCase "end" do
Just g <- Seq.lookup nFrames <$> frames
assertBool "accessible tile found" $ noneAccessible g
]
]
} }
newtype Grid a = Grid (Seq (Seq (V2 Int, a))) newtype Grid a = Grid (Seq (Seq (V2 Int, a)))
deriving (Functor) deriving (Functor, Show)
data InTile data InTile
= InEmpty = InEmpty
@ -67,8 +58,8 @@ outToChar = \case
OutRoll -> inToChar InRoll OutRoll -> inToChar InRoll
OutAccessible -> 'x' OutAccessible -> 'x'
drawGrid :: Grid OutTile -> TL.Text drawGrid :: Grid OutTile -> T.Text
drawGrid (Grid g) = TL.unlines . toList . fmap (TL.pack . toList . fmap outToChar) $ snd <<$>> g drawGrid (Grid g) = T.unlines . toList . fmap (T.pack . toList . fmap outToChar) $ snd <<$>> g
mkGrid :: [[a]] -> Grid a mkGrid :: [[a]] -> Grid a
mkGrid = Grid . Seq.fromList . map Seq.fromList . zipWith (map . first . V2) [0 ..] . map (zip [0 ..]) mkGrid = Grid . Seq.fromList . map Seq.fromList . zipWith (map . first . V2) [0 ..] . map (zip [0 ..])

View File

@ -39,6 +39,9 @@ executable aoc
ghc-options: ghc-options:
-Wall -Wall
-fdefer-type-errors -fdefer-type-errors
-threaded
-rtsopts
-with-rtsopts=-N
build-depends: build-depends:
base >= 4.14, base >= 4.14,
async, async,
@ -60,11 +63,9 @@ executable aoc
mtl, mtl,
pretty-simple, pretty-simple,
safe, safe,
sydtest,
stm, stm,
streams, streams,
tasty-golden,
tasty-hunit,
tasty,
text, text,
time, time,
transformers, transformers,