2025-12-08 12:48:49 +00:00
|
|
|
{-# LANGUAGE PackageImports #-}
|
2025-12-31 01:18:05 +00:00
|
|
|
{-# LANGUAGE TypeFamilies #-}
|
2026-01-05 18:31:34 +00:00
|
|
|
{-# LANGUAGE UndecidableInstances #-}
|
2025-12-08 12:48:49 +00:00
|
|
|
|
|
|
|
|
module Pre (
|
|
|
|
|
module BasePrelude,
|
|
|
|
|
module Control.Applicative,
|
2026-01-06 17:14:03 +00:00
|
|
|
module Control.DeepSeq,
|
2025-12-08 12:48:49 +00:00
|
|
|
module Control.Monad,
|
|
|
|
|
module Control.Monad.Loops,
|
|
|
|
|
module Control.Monad.State,
|
|
|
|
|
module Data.Bifunctor,
|
|
|
|
|
module Data.Bool,
|
|
|
|
|
module Data.Char,
|
|
|
|
|
module Data.Foldable,
|
|
|
|
|
module Data.Foldable1,
|
2025-12-08 22:48:29 +00:00
|
|
|
module Data.Function,
|
2025-12-08 12:48:49 +00:00
|
|
|
module Data.Functor,
|
2026-01-06 17:14:03 +00:00
|
|
|
module Data.Functor.Compose,
|
2025-12-08 12:48:49 +00:00
|
|
|
module Data.List,
|
|
|
|
|
module Data.List.Extra,
|
|
|
|
|
module Data.List.NonEmpty,
|
|
|
|
|
module Data.Maybe,
|
|
|
|
|
module Data.Ord,
|
|
|
|
|
module Data.Sequence,
|
|
|
|
|
module Data.Stream.Infinite,
|
|
|
|
|
module Data.Text,
|
|
|
|
|
module Data.Text.Encoding,
|
2025-12-08 16:34:30 +00:00
|
|
|
module Data.Traversable,
|
2026-01-06 18:54:40 +00:00
|
|
|
module Data.Tree,
|
2025-12-09 13:37:39 +00:00
|
|
|
module Data.Tuple.Extra,
|
2025-12-08 12:48:49 +00:00
|
|
|
module Data.Void,
|
|
|
|
|
module Data.Word,
|
2026-01-06 17:14:03 +00:00
|
|
|
module GHC.Generics,
|
2025-12-08 12:48:49 +00:00
|
|
|
module Linear,
|
2025-12-08 22:01:49 +00:00
|
|
|
module Safe,
|
2025-12-08 12:48:49 +00:00
|
|
|
module Text.Megaparsec,
|
|
|
|
|
module Text.Megaparsec.Char,
|
|
|
|
|
module Text.Megaparsec.Char.Lexer,
|
2026-01-06 19:44:50 +00:00
|
|
|
module Text.Pretty.Simple,
|
2025-12-08 12:48:49 +00:00
|
|
|
Puzzle (..),
|
2026-01-06 22:32:40 +00:00
|
|
|
mwhen,
|
2026-01-06 22:32:11 +00:00
|
|
|
(<<$>>),
|
|
|
|
|
(<<&>>),
|
|
|
|
|
takeUntil,
|
2025-12-08 23:38:48 +00:00
|
|
|
digit,
|
|
|
|
|
digitsToInt,
|
2025-12-09 00:02:54 +00:00
|
|
|
listIndex,
|
2025-12-09 10:47:32 +00:00
|
|
|
allUnorderedPairs,
|
2025-12-09 13:37:39 +00:00
|
|
|
adjacentPairs,
|
|
|
|
|
sortPair,
|
2026-01-07 00:55:46 +00:00
|
|
|
drawTree,
|
2026-01-05 14:37:25 +00:00
|
|
|
HList (..),
|
2026-01-06 17:14:03 +00:00
|
|
|
HListC (..),
|
2026-01-04 02:14:35 +00:00
|
|
|
HListF (..),
|
2026-01-04 02:39:33 +00:00
|
|
|
foldHListF,
|
2026-01-04 02:57:38 +00:00
|
|
|
foldHListF0,
|
2026-01-04 02:14:35 +00:00
|
|
|
mapHListF,
|
2026-01-05 18:31:34 +00:00
|
|
|
lookupHList,
|
2025-12-16 16:15:11 +00:00
|
|
|
(/\),
|
2025-12-31 01:18:05 +00:00
|
|
|
(/\\),
|
2025-12-16 16:15:11 +00:00
|
|
|
nil,
|
2026-01-06 17:14:03 +00:00
|
|
|
Constrained (..),
|
2026-01-06 17:36:37 +00:00
|
|
|
withConstrained,
|
2026-01-04 02:14:35 +00:00
|
|
|
Fanout (..),
|
2026-01-05 18:31:34 +00:00
|
|
|
Length,
|
2026-01-06 18:22:56 +00:00
|
|
|
TestTree,
|
2026-01-07 00:39:35 +00:00
|
|
|
Test,
|
2026-01-06 18:22:56 +00:00
|
|
|
test,
|
|
|
|
|
testLazy,
|
2026-01-05 14:37:25 +00:00
|
|
|
TestName,
|
|
|
|
|
getTestTree,
|
2026-01-06 22:38:34 +00:00
|
|
|
displayTestResultsConsole,
|
2026-01-08 01:25:24 +00:00
|
|
|
TestRunnerOpts (..),
|
2026-01-05 14:37:25 +00:00
|
|
|
runTests,
|
|
|
|
|
assertEqual,
|
|
|
|
|
assert,
|
2026-01-07 00:39:35 +00:00
|
|
|
assertFailure,
|
2026-01-05 14:37:25 +00:00
|
|
|
golden,
|
2025-12-08 12:48:49 +00:00
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
|
|
|
|
|
import "base" Prelude as BasePrelude hiding (
|
|
|
|
|
foldl1,
|
|
|
|
|
foldr1,
|
|
|
|
|
head,
|
|
|
|
|
init,
|
|
|
|
|
last,
|
|
|
|
|
maximum,
|
|
|
|
|
minimum,
|
|
|
|
|
tail,
|
|
|
|
|
unzip,
|
2025-12-09 00:02:54 +00:00
|
|
|
(!!),
|
2025-12-08 12:48:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
import Control.Applicative
|
2026-01-06 17:14:03 +00:00
|
|
|
import Control.DeepSeq (NFData, deepseq)
|
|
|
|
|
import Control.DeepSeq qualified as DeepSeq
|
|
|
|
|
import Control.Exception (SomeException, evaluate)
|
2025-12-08 12:48:49 +00:00
|
|
|
import Control.Monad
|
2026-01-07 00:39:35 +00:00
|
|
|
import Control.Monad.Catch (MonadCatch, MonadThrow, try)
|
|
|
|
|
import Control.Monad.Except
|
2026-01-08 01:25:24 +00:00
|
|
|
import Control.Monad.Loops hiding (firstM)
|
|
|
|
|
import Control.Monad.Reader
|
2025-12-08 12:48:49 +00:00
|
|
|
import Control.Monad.State
|
2026-01-08 01:25:24 +00:00
|
|
|
import Control.Monad.Writer
|
2025-12-08 12:48:49 +00:00
|
|
|
import Data.Bifunctor
|
|
|
|
|
import Data.Bool
|
|
|
|
|
import Data.Char
|
2026-01-05 18:31:34 +00:00
|
|
|
import Data.Finite
|
2026-01-06 22:38:34 +00:00
|
|
|
import Data.Fixed (Fixed (MkFixed))
|
2025-12-08 12:48:49 +00:00
|
|
|
import Data.Foldable hiding (foldl1, foldr1, maximum, maximumBy, minimum, minimumBy)
|
|
|
|
|
import Data.Foldable1
|
2025-12-08 22:48:29 +00:00
|
|
|
import Data.Function
|
2025-12-08 12:48:49 +00:00
|
|
|
import Data.Functor
|
2026-01-06 17:14:03 +00:00
|
|
|
import Data.Functor.Compose (Compose (Compose), getCompose)
|
2026-01-04 02:14:35 +00:00
|
|
|
import Data.Functor.Contravariant
|
2026-01-06 17:14:03 +00:00
|
|
|
import Data.Kind (Constraint, Type)
|
2026-01-06 22:38:34 +00:00
|
|
|
import Data.List (List, genericLength, sortOn, transpose)
|
2025-12-11 11:34:36 +00:00
|
|
|
import Data.List.Extra (dropEnd, enumerate, firstJust, notNull, splitOn)
|
2025-12-09 00:26:22 +00:00
|
|
|
import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty, some1, tail, tails)
|
2025-12-08 12:48:49 +00:00
|
|
|
import Data.Maybe
|
|
|
|
|
import Data.Ord
|
|
|
|
|
import Data.Sequence (Seq)
|
|
|
|
|
import Data.Stream.Infinite (Stream ((:>)))
|
|
|
|
|
import Data.Text (Text)
|
2025-12-31 01:18:05 +00:00
|
|
|
import Data.Text qualified as T
|
2025-12-08 12:48:49 +00:00
|
|
|
import Data.Text.Encoding (encodeUtf8)
|
2026-01-05 14:37:25 +00:00
|
|
|
import Data.Text.IO qualified as T
|
2026-01-06 22:38:34 +00:00
|
|
|
import Data.Text.Lazy qualified as TL
|
2026-01-06 17:14:03 +00:00
|
|
|
import Data.Time
|
2025-12-08 16:34:30 +00:00
|
|
|
import Data.Traversable
|
2026-01-07 00:55:46 +00:00
|
|
|
import Data.Tree hiding (drawTree)
|
2026-01-08 01:25:24 +00:00
|
|
|
import Data.Tuple.Extra (firstM, (&&&))
|
2025-12-08 12:48:49 +00:00
|
|
|
import Data.Void
|
|
|
|
|
import Data.Word
|
2026-01-06 17:14:03 +00:00
|
|
|
import GHC.Generics (Generic)
|
2026-01-05 18:31:34 +00:00
|
|
|
import GHC.TypeNats (KnownNat, Nat, type (+))
|
2025-12-08 12:48:49 +00:00
|
|
|
import Linear (V2 (..))
|
2025-12-08 22:01:49 +00:00
|
|
|
import Safe
|
2026-01-06 22:38:34 +00:00
|
|
|
import System.Console.ANSI
|
2026-01-08 01:25:24 +00:00
|
|
|
import System.Directory
|
|
|
|
|
import System.FilePath
|
2025-12-08 12:48:49 +00:00
|
|
|
import Text.Megaparsec hiding (Pos, State, Stream, many, some)
|
|
|
|
|
import Text.Megaparsec.Char
|
|
|
|
|
import Text.Megaparsec.Char.Lexer (decimal)
|
2026-01-06 19:44:50 +00:00
|
|
|
import Text.Pretty.Simple (pPrint, pPrintForceColor, pShow)
|
2025-12-08 12:48:49 +00:00
|
|
|
|
2026-01-06 17:14:03 +00:00
|
|
|
data Puzzle = forall input outputs. (KnownNat (Length outputs), NFData input) => Puzzle
|
2025-12-08 12:48:49 +00:00
|
|
|
{ number :: Word
|
2025-12-08 22:42:29 +00:00
|
|
|
, parser :: Bool -> Parsec Void Text input
|
2025-12-31 01:18:05 +00:00
|
|
|
, parts :: PuzzleParts input outputs
|
2026-01-06 18:22:56 +00:00
|
|
|
, extraTests :: Bool -> FilePath -> [TestTree IO (input, HList outputs)]
|
2025-12-08 12:48:49 +00:00
|
|
|
}
|
2025-12-08 23:38:48 +00:00
|
|
|
|
2026-01-06 22:32:11 +00:00
|
|
|
(<<$>>) :: (Functor f1, Functor f2) => (a -> b) -> f1 (f2 a) -> f1 (f2 b)
|
|
|
|
|
(<<$>>) = fmap . fmap
|
|
|
|
|
(<<&>>) :: (Functor f1, Functor f2) => f1 (f2 a) -> (a -> b) -> f1 (f2 b)
|
|
|
|
|
(<<&>>) = flip (<<$>>)
|
|
|
|
|
|
2026-01-06 22:32:40 +00:00
|
|
|
mwhen :: (Monoid p) => Bool -> p -> p
|
|
|
|
|
mwhen b x = if b then x else mempty
|
|
|
|
|
|
2026-01-06 22:32:11 +00:00
|
|
|
takeUntil :: (Foldable t) => (a -> Bool) -> t a -> [a]
|
|
|
|
|
takeUntil p = foldr (\x xs -> x : if p x then [] else xs) []
|
|
|
|
|
|
2025-12-08 23:38:48 +00:00
|
|
|
digit :: (Token s ~ Char, Num b, MonadParsec e s f) => f b
|
|
|
|
|
digit = fromIntegral . digitToInt <$> digitChar
|
|
|
|
|
|
|
|
|
|
digitsToInt :: (Integral a) => [a] -> Int
|
2025-12-13 11:24:46 +00:00
|
|
|
digitsToInt = foldl' (\acc d -> acc * 10 + fromIntegral d) 0
|
2025-12-09 00:02:54 +00:00
|
|
|
|
|
|
|
|
listIndex :: Int -> [a] -> Maybe a
|
|
|
|
|
listIndex n =
|
|
|
|
|
if n < 0
|
|
|
|
|
then const Nothing
|
|
|
|
|
else \case
|
|
|
|
|
[] -> Nothing
|
|
|
|
|
x : xs -> if n == 0 then Just x else listIndex (n - 1) xs
|
2025-12-09 10:47:32 +00:00
|
|
|
|
2025-12-09 11:00:01 +00:00
|
|
|
allUnorderedPairs :: Bool -> [a] -> [(a, a)]
|
|
|
|
|
allUnorderedPairs diagonals = concat . join (zipWith (flip $ map . (,)) . (bool tail toList diagonals) . tails)
|
2025-12-09 13:37:39 +00:00
|
|
|
|
|
|
|
|
adjacentPairs :: [b] -> [(b, b)]
|
|
|
|
|
adjacentPairs = \case
|
|
|
|
|
[] -> []
|
|
|
|
|
x : xs -> zip (x : xs) xs
|
|
|
|
|
|
|
|
|
|
sortPair :: (Ord a) => (a, a) -> (a, a)
|
|
|
|
|
sortPair (a, b) = if a <= b then (a, b) else (b, a)
|
2025-12-09 16:47:12 +00:00
|
|
|
|
2026-01-08 00:53:20 +00:00
|
|
|
{- | This is `Data.Tree.drawTree` with the ASCII characters replaced with Unicode box drawing characters,
|
|
|
|
|
and using `Text` instead of `String`.
|
|
|
|
|
-}
|
2026-01-08 00:53:14 +00:00
|
|
|
drawTree :: Tree Text -> Text
|
|
|
|
|
drawTree = T.unlines . draw
|
2026-01-07 00:55:46 +00:00
|
|
|
where
|
2026-01-08 00:53:14 +00:00
|
|
|
draw (Node x ts0) = T.lines x <> drawSubTrees ts0
|
2026-01-07 00:55:46 +00:00
|
|
|
where
|
|
|
|
|
drawSubTrees [] = []
|
|
|
|
|
drawSubTrees [t] =
|
|
|
|
|
"│" : shift_ "└─ " " " (draw t)
|
|
|
|
|
drawSubTrees (t : ts) =
|
2026-01-08 00:53:14 +00:00
|
|
|
"│" : shift_ "├─ " "│ " (draw t) <> drawSubTrees ts
|
|
|
|
|
shift_ first_ other = zipWith (<>) (first_ : repeat other)
|
2026-01-07 00:55:46 +00:00
|
|
|
|
2026-01-06 18:22:56 +00:00
|
|
|
type PuzzleParts input = HListF ((Fanout ((->) input) (Op Text)))
|
2025-12-31 01:18:05 +00:00
|
|
|
infixr 9 /\\
|
2026-01-06 18:22:56 +00:00
|
|
|
(/\\) :: (input -> output, output -> Text) -> PuzzleParts input outputs -> PuzzleParts input (output : outputs)
|
|
|
|
|
(/\\) (f, o) = HConsF $ Fanout (f, Op o)
|
2025-12-16 16:15:11 +00:00
|
|
|
infixr 9 /\
|
2026-01-06 18:22:56 +00:00
|
|
|
(/\) :: (Show output) => (input -> output) -> PuzzleParts input outputs -> PuzzleParts input (output : outputs)
|
|
|
|
|
(/\) f = HConsF $ Fanout (f, Op T.show)
|
2025-12-31 01:18:05 +00:00
|
|
|
nil :: PuzzleParts input '[]
|
2026-01-04 02:14:35 +00:00
|
|
|
nil = HNilF
|
|
|
|
|
|
2026-01-05 14:37:25 +00:00
|
|
|
data HList (as :: List Type) :: Type where
|
|
|
|
|
HNil :: HList '[]
|
|
|
|
|
HCons ::
|
|
|
|
|
a ->
|
|
|
|
|
HList as ->
|
|
|
|
|
HList (a ': as)
|
|
|
|
|
|
2026-01-06 17:14:03 +00:00
|
|
|
data HListC (c :: Type -> Constraint) (as :: List Type) :: Type where
|
|
|
|
|
HNilC :: HListC c '[]
|
|
|
|
|
HConsC ::
|
|
|
|
|
(c a) =>
|
|
|
|
|
a ->
|
|
|
|
|
HListC c as ->
|
|
|
|
|
HListC c (a ': as)
|
|
|
|
|
instance NFData (HListC NFData outputs) where
|
|
|
|
|
rnf = \case
|
|
|
|
|
HNilC -> ()
|
|
|
|
|
HConsC x xs -> deepseq x $ DeepSeq.rnf xs
|
|
|
|
|
|
2026-01-04 02:14:35 +00:00
|
|
|
data HListF (f :: Type -> Type) (as :: List Type) :: Type where
|
|
|
|
|
HNilF :: HListF f '[]
|
|
|
|
|
HConsF ::
|
|
|
|
|
f a ->
|
|
|
|
|
HListF f as ->
|
|
|
|
|
HListF f (a ': as)
|
2026-01-04 02:39:33 +00:00
|
|
|
foldHListF :: (forall x xs. f x -> r xs -> r (x ': xs)) -> r '[] -> HListF f as -> r as
|
|
|
|
|
foldHListF f e = \case
|
|
|
|
|
HNilF -> e
|
|
|
|
|
HConsF x xs -> f x $ foldHListF f e xs
|
2026-01-04 02:57:38 +00:00
|
|
|
foldHListF0 :: (forall x. f x -> r -> r) -> r -> HListF f as -> r
|
|
|
|
|
foldHListF0 f e = \case
|
|
|
|
|
HNilF -> e
|
|
|
|
|
HConsF x xs -> f x $ foldHListF0 f e xs
|
2026-01-04 02:14:35 +00:00
|
|
|
mapHListF :: (forall a. f a -> g a) -> HListF f as -> HListF g as
|
2026-01-04 02:39:33 +00:00
|
|
|
mapHListF t = foldHListF (\x r -> HConsF (t x) $ r) HNilF
|
2026-01-04 02:14:35 +00:00
|
|
|
|
2026-01-05 18:31:34 +00:00
|
|
|
lookupHList :: (forall a. f a -> r) -> HListF f as -> Finite (Length as) -> r
|
|
|
|
|
lookupHList f = \case
|
|
|
|
|
HNilF -> absurd . separateZero
|
|
|
|
|
HConsF x xs -> maybe (f x) (lookupHList f xs) . unshift
|
|
|
|
|
|
2026-01-06 17:14:03 +00:00
|
|
|
data Constrained c a where
|
2026-01-06 17:36:37 +00:00
|
|
|
Constrained :: (c a) => a -> Constrained c a
|
2026-01-06 17:39:18 +00:00
|
|
|
withConstrained :: ((c a) => a -> b) -> Constrained c a -> b
|
|
|
|
|
withConstrained f (Constrained x) = f x
|
2026-01-06 17:14:03 +00:00
|
|
|
|
2026-01-04 02:14:35 +00:00
|
|
|
newtype Fanout f g a = Fanout (f a, g a)
|
2026-01-05 14:37:25 +00:00
|
|
|
|
2026-01-05 18:31:34 +00:00
|
|
|
type family Length as :: Nat where
|
|
|
|
|
Length '[] = 0
|
|
|
|
|
Length (x ': xs) = Length xs + 1
|
|
|
|
|
|
2026-01-05 14:37:25 +00:00
|
|
|
data TestTree m input where
|
2026-01-06 18:22:56 +00:00
|
|
|
TestTree :: TestName -> TestCase m input output -> [TestTree m output] -> TestTree m input
|
|
|
|
|
|
|
|
|
|
data TestCase m input output where
|
2026-01-07 00:39:35 +00:00
|
|
|
TestCase :: (NFData output) => (input -> Test m output) -> TestCase m input output
|
|
|
|
|
TestCaseLazy :: (input -> Test m output) -> TestCase m input output
|
|
|
|
|
|
2026-01-08 01:25:24 +00:00
|
|
|
newtype Test m a
|
|
|
|
|
= Test
|
|
|
|
|
( ExceptT
|
|
|
|
|
TestFailure
|
|
|
|
|
( WriterT
|
|
|
|
|
[TestLogItem]
|
|
|
|
|
(ReaderT TestRunnerOpts m)
|
|
|
|
|
)
|
|
|
|
|
a
|
|
|
|
|
)
|
2026-01-07 00:39:35 +00:00
|
|
|
deriving newtype
|
|
|
|
|
( Functor
|
|
|
|
|
, Applicative
|
|
|
|
|
, Monad
|
|
|
|
|
, MonadIO
|
|
|
|
|
, MonadThrow
|
|
|
|
|
, MonadCatch
|
|
|
|
|
, MonadError TestFailure
|
2026-01-08 01:25:24 +00:00
|
|
|
, MonadWriter [TestLogItem]
|
|
|
|
|
, MonadReader TestRunnerOpts
|
2026-01-07 00:39:35 +00:00
|
|
|
)
|
2026-01-06 18:22:56 +00:00
|
|
|
|
|
|
|
|
-- | See `testLazy` for avoiding the `NFData` constraint.
|
2026-01-07 00:55:07 +00:00
|
|
|
test :: (NFData output) => Text -> (input -> Test m output) -> [TestTree m output] -> TestTree m input
|
|
|
|
|
test n f = TestTree (TestName n) $ TestCase f
|
2026-01-06 18:22:56 +00:00
|
|
|
|
|
|
|
|
{- | This is `test` without the `NFData` constraint.
|
|
|
|
|
It doesn't force the output before completion, which means that reported timings may be less accurate.
|
|
|
|
|
-}
|
2026-01-07 00:55:07 +00:00
|
|
|
testLazy :: Text -> (input -> Test m output) -> [TestTree m output] -> TestTree m input
|
|
|
|
|
testLazy n f = TestTree (TestName n) $ TestCaseLazy f
|
2026-01-05 14:37:25 +00:00
|
|
|
|
2026-01-08 01:25:24 +00:00
|
|
|
data TestResult = TestResult
|
|
|
|
|
{ name :: TestName
|
|
|
|
|
, logs :: [TestLogItem]
|
|
|
|
|
, result :: Either TestFailure (NominalDiffTime, [TestResult])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data TestLogItem
|
|
|
|
|
= LogRegeneratedGolden
|
2026-01-07 00:39:35 +00:00
|
|
|
|
|
|
|
|
data TestFailure
|
|
|
|
|
= ExceptionFailure SomeException
|
|
|
|
|
| AssertionFailure Text
|
2026-01-08 01:25:24 +00:00
|
|
|
| GoldenMissing
|
2026-01-07 00:39:35 +00:00
|
|
|
| GoldenFailure {expected :: Text, actual :: Text}
|
2026-01-05 14:37:25 +00:00
|
|
|
|
2026-01-07 00:43:51 +00:00
|
|
|
newtype TestName = TestName Text
|
2026-01-05 14:37:25 +00:00
|
|
|
|
2026-01-08 00:49:41 +00:00
|
|
|
getTestTree :: TestTree m r -> Tree Text
|
|
|
|
|
getTestTree (TestTree (TestName name) _ ts) = Node name $ map getTestTree ts
|
2026-01-05 14:37:25 +00:00
|
|
|
|
2026-01-06 22:52:43 +00:00
|
|
|
displayTestResultsConsole :: Maybe Int -> TestResult -> TL.Text
|
|
|
|
|
displayTestResultsConsole terminalWidth testResult =
|
2026-01-06 22:38:34 +00:00
|
|
|
displayResult 0 testResult <> TL.pack (setSGRCode [Reset])
|
|
|
|
|
where
|
|
|
|
|
displayResult indent =
|
2026-01-06 22:52:43 +00:00
|
|
|
(TL.replicate (fromIntegral indent) " " <>) . \case
|
2026-01-08 01:25:24 +00:00
|
|
|
TestResult{name = TestName name, logs, result} ->
|
|
|
|
|
case result of
|
|
|
|
|
Right (dt, children) ->
|
|
|
|
|
TL.fromStrict (header Green '✓' name indent (Just dt) <> displayLogs)
|
|
|
|
|
<> TL.concat (map (displayResult (indent + 1)) children)
|
|
|
|
|
Left e ->
|
|
|
|
|
TL.fromStrict $
|
|
|
|
|
header Red '✗' name indent Nothing
|
|
|
|
|
<> displayLogs
|
|
|
|
|
<> setColour Vivid Red
|
|
|
|
|
<> indentAllLines indent case e of
|
|
|
|
|
ExceptionFailure ex -> T.show ex
|
|
|
|
|
AssertionFailure t -> T.stripEnd t
|
|
|
|
|
GoldenMissing -> "Golden file missing"
|
|
|
|
|
GoldenFailure{expected, actual} ->
|
|
|
|
|
"Expected:\n" <> T.stripEnd expected <> "\nActual:\n" <> T.stripEnd actual
|
|
|
|
|
where
|
|
|
|
|
displayLogs =
|
|
|
|
|
setColour Dull Magenta
|
|
|
|
|
<> indentAllLines
|
|
|
|
|
indent
|
|
|
|
|
( flip foldMap logs \case
|
|
|
|
|
LogRegeneratedGolden -> "Created golden file"
|
|
|
|
|
)
|
|
|
|
|
<> setColour Dull Magenta
|
2026-01-06 22:52:43 +00:00
|
|
|
header colour icon name indent time =
|
2026-01-06 22:38:34 +00:00
|
|
|
setColour Vivid colour
|
|
|
|
|
<> T.singleton icon
|
|
|
|
|
<> " "
|
|
|
|
|
<> setColour Dull White
|
2026-01-07 00:43:51 +00:00
|
|
|
<> name
|
2026-01-06 22:38:34 +00:00
|
|
|
<> maybe
|
|
|
|
|
mempty
|
2026-01-11 23:26:49 +00:00
|
|
|
( \t@(showTime -> tt) ->
|
2026-01-06 22:52:43 +00:00
|
|
|
T.replicate
|
|
|
|
|
( fromIntegral $
|
|
|
|
|
maybe
|
|
|
|
|
3
|
2026-01-11 23:26:49 +00:00
|
|
|
(\n -> n - (2 * indent + T.length name + T.length tt + 4))
|
2026-01-06 22:52:43 +00:00
|
|
|
terminalWidth
|
|
|
|
|
)
|
|
|
|
|
" "
|
2026-01-07 01:02:31 +00:00
|
|
|
<> setColour Dull Blue
|
2026-01-06 22:38:34 +00:00
|
|
|
<> tt
|
2026-01-11 23:26:49 +00:00
|
|
|
<> " "
|
|
|
|
|
<> T.singleton (timeBarFunction t)
|
2026-01-06 22:38:34 +00:00
|
|
|
)
|
|
|
|
|
time
|
|
|
|
|
<> "\n"
|
2026-01-07 01:11:45 +00:00
|
|
|
paddedAllLines p = T.unlines . map (p <>) . T.lines
|
2026-01-08 01:25:24 +00:00
|
|
|
indentAllLines indent = paddedAllLines $ T.replicate (indent * 2) " "
|
2026-01-11 23:26:49 +00:00
|
|
|
timeBarFunction t
|
|
|
|
|
| t < 0.01 = ' '
|
|
|
|
|
| t < 0.03 = '▁'
|
|
|
|
|
| t < 0.1 = '▂'
|
|
|
|
|
| t < 0.3 = '▃'
|
|
|
|
|
| t < 1 = '▄'
|
|
|
|
|
| t < 3 = '▅'
|
|
|
|
|
| t < 10 = '▆'
|
|
|
|
|
| t < 30 = '▇'
|
|
|
|
|
| otherwise = '█'
|
2026-01-06 22:38:34 +00:00
|
|
|
showTime (nominalDiffTimeToSeconds -> MkFixed duration) =
|
|
|
|
|
-- SI prefixes, and always exactly 2 decimal places, or 3 if there's no prefix
|
|
|
|
|
T.show res
|
|
|
|
|
<> T.singleton '.'
|
|
|
|
|
<> T.take (if isNothing unit then 3 else 2) (T.show frac <> "000")
|
2026-01-06 22:47:41 +00:00
|
|
|
<> foldMap T.singleton unit
|
2026-01-06 22:38:34 +00:00
|
|
|
<> T.singleton 's'
|
|
|
|
|
where
|
|
|
|
|
(frac, res, unit) = case duration of
|
|
|
|
|
0 -> (0, 0, Nothing)
|
|
|
|
|
d -> go (0 :: Int) 0 d
|
|
|
|
|
go = \case
|
|
|
|
|
4 -> (,,Nothing)
|
|
|
|
|
iterations -> \carried n ->
|
|
|
|
|
case n `divMod` 1000 of
|
|
|
|
|
(0, r) ->
|
|
|
|
|
( carried
|
|
|
|
|
, r
|
|
|
|
|
, Just case iterations of
|
2026-01-06 22:47:41 +00:00
|
|
|
3 -> 'm'
|
|
|
|
|
2 -> 'μ'
|
|
|
|
|
1 -> 'n'
|
|
|
|
|
_ -> 'p'
|
2026-01-06 22:38:34 +00:00
|
|
|
)
|
|
|
|
|
(d, r) -> go (succ iterations) r d
|
|
|
|
|
sgr = T.pack . setSGRCode
|
|
|
|
|
setColour d c = sgr [SetColor Foreground d c]
|
|
|
|
|
|
2026-01-08 01:25:24 +00:00
|
|
|
data TestRunnerOpts = TestRunnerOpts
|
|
|
|
|
{ regenerateGoldenFiles :: Bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runTests :: (MonadIO m, MonadCatch m) => TestRunnerOpts -> a -> TestTree m a -> m TestResult
|
|
|
|
|
runTests opts r0 (TestTree name tc ts) =
|
2026-01-07 00:39:35 +00:00
|
|
|
let Test t = Control.Monad.Catch.try $ runTest tc
|
2026-01-08 01:25:24 +00:00
|
|
|
in runReaderT (runWriterT (runExceptT t)) opts
|
|
|
|
|
>>= fmap (\(result, logs) -> TestResult{name, logs, result}) . firstM \case
|
|
|
|
|
Left e ->
|
|
|
|
|
pure $ Left e
|
|
|
|
|
Right (Left e) ->
|
|
|
|
|
pure $ Left $ ExceptionFailure e
|
2026-01-11 23:52:19 +00:00
|
|
|
Right (Right (r, dt)) -> do
|
|
|
|
|
rs <- for ts $ runTests opts r
|
2026-01-11 23:53:24 +00:00
|
|
|
let childTimes = either (const 0) id $ fmap (sum . map fst) $ traverse (.result) rs
|
2026-01-11 23:52:19 +00:00
|
|
|
pure $ Right (dt + childTimes, rs)
|
2026-01-06 17:14:03 +00:00
|
|
|
where
|
2026-01-06 18:22:56 +00:00
|
|
|
runTest = \case
|
|
|
|
|
TestCase f -> timed (liftIO . evaluate . DeepSeq.force) $ f r0
|
|
|
|
|
TestCaseLazy f -> timed pure $ f r0
|
|
|
|
|
timed f x = do
|
2026-01-06 17:14:03 +00:00
|
|
|
t0 <- liftIO getCurrentTime
|
|
|
|
|
r <- x
|
2026-01-06 18:22:56 +00:00
|
|
|
rf <- f r
|
2026-01-06 17:14:03 +00:00
|
|
|
t1 <- liftIO getCurrentTime
|
|
|
|
|
pure (rf, diffUTCTime t1 t0)
|
2026-01-05 14:37:25 +00:00
|
|
|
|
2026-01-07 00:39:35 +00:00
|
|
|
assertEqual :: (Eq p, Monad m) => p -> p -> Test m ()
|
2026-01-05 14:37:25 +00:00
|
|
|
assertEqual expected actual = assert "not equal" (expected == actual)
|
2026-01-07 00:39:35 +00:00
|
|
|
assert :: (Monad m) => Text -> Bool -> Test m ()
|
|
|
|
|
assert s b = if b then pure () else assertFailure s
|
|
|
|
|
assertFailure :: (Monad m) => Text -> Test m a
|
|
|
|
|
assertFailure = throwError . AssertionFailure
|
|
|
|
|
golden :: (MonadIO m, MonadFail m) => FilePath -> Text -> Test m ()
|
2026-01-08 01:25:24 +00:00
|
|
|
golden file actual = do
|
|
|
|
|
TestRunnerOpts{..} <- ask
|
|
|
|
|
exists <- liftIO $ doesFileExist file
|
|
|
|
|
if exists
|
|
|
|
|
then do
|
|
|
|
|
expected <- liftIO $ T.readFile file
|
|
|
|
|
if expected == actual then pure () else throwError $ GoldenFailure{expected, actual}
|
|
|
|
|
else do
|
|
|
|
|
if regenerateGoldenFiles
|
|
|
|
|
then
|
|
|
|
|
let parents = dropWhile null $ scanl (</>) "" $ splitDirectories $ takeDirectory file
|
|
|
|
|
in tell [LogRegeneratedGolden] >> liftIO do
|
|
|
|
|
for_ parents \dir -> do
|
|
|
|
|
parentExists <- liftIO $ doesDirectoryExist dir
|
|
|
|
|
when (not parentExists) $ createDirectory dir
|
|
|
|
|
T.writeFile file actual
|
|
|
|
|
else
|
|
|
|
|
throwError GoldenMissing
|