{-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TupleSections #-} module Test.Run ( runEntries ) where import Control.Carrier.Reader import Control.Carrier.State.Church import Control.Exception (SomeException) import Control.Monad (guard, when) import Control.Monad.IO.Class import Data.Bool (bool) import Data.Foldable (for_, toList) import qualified Data.IntMap as IntMap import Data.List (elemIndex, intercalate, intersperse, sortBy) import qualified Data.Map as Map import Data.Maybe (fromMaybe) import Data.Monoid (Ap(..)) import Data.Ord (comparing) import Fresnel.Tropical import GHC.Exception.Type (Exception(displayException)) import Numeric (showFFloatAlt) import System.Console.ANSI import System.IO import Test.Group as Group import Test.Options import Test.Print import Test.QuickCheck (Args(..), isSuccess, quickCheckWithResult) import qualified Test.QuickCheck as QC runEntries :: [Entry] -> Options -> IO Bool runEntries groups (Options es args) = runReader w . runReader args . runReader stdout . runState (const . pure . not . hasFailures) mempty $ do t <- getAp (foldMap Ap (intersperse (mempty <$ blank <* blank) (map runEntry (matching ((==) . entryName) es groups)))) when (hasSuccesses t || hasFailures t) (blank *> topIndent end *> runTally t) where blank = topIndent vline <* nl w = fromMaybe zero (getTropical (maxWidths groups)) matching _ [] = id matching f fs = filter (\ g -> foldr ((||) . f g) False fs) runEntry :: (Has (Reader Args) sig m, Has (Reader Handle) sig m, Has (Reader Width) sig m, Has (State Tally) sig m, MonadIO m) => Entry -> m Tally runEntry = \case Group name entries -> runGroup name entries Prop name loc prop -> runProp name loc prop runGroup :: (Has (Reader Args) sig m, Has (Reader Handle) sig m, Has (Reader Width) sig m, Has (State Tally) sig m, MonadIO m) => String -> [Entry] -> m Tally runGroup groupName entries = do topIndent vline withSGR [SetConsoleIntensity BoldIntensity] (putS (space ++ groupName) *> nl) w <- ask t <- local (<> Width 2) (section Nothing (getAp (foldMap Ap (intersperse (mempty <$ line <* nl) (map (local (const (w :: Width)) . runEntry) entries))))) when (hasSuccesses t || hasFailures t) $ do if hasFailures t then failure' (putS (headingN <> gtally)) else topIndent vline *> putS space runTally t pure t runProp :: (Has (Reader Args) sig m, Has (Reader Handle) sig m, Has (Reader Width) sig m, Has (State Tally) sig m, MonadIO m) => String -> Loc -> QC.Property -> m Tally runProp name loc property = runPropWith (ask >>= \ args -> fromQC <$> liftIO (quickCheckWithResult args property)) name loc runPropWith :: (Has (Reader Args) sig m, Has (Reader Handle) sig m, Has (Reader Width) sig m, Has (State Tally) sig m, MonadIO m) => m Result -> String -> Loc -> m Tally runPropWith run name Loc{ path, lineNumber } = withHandle $ \ h -> do isTerminal <- liftIO (hIsTerminalDevice h) when isTerminal (title Pass False) failedPreviously <- gets hasFailures Result{ stats, status, failed } <- run modify (<> unit status) when isTerminal $ do liftIO (hClearFromCursorToLineBeginning h) liftIO (hSetCursorColumn h 0) title status failedPreviously putS " " *> stat (success (putS "Success")) (failure (putS "Failure")) status *> nl maxSuccess <- asks maxSuccess let details = numTests stats == maxSuccess && not (null (classes stats)) labels = runLabels status stats ln b = line *> stat success failure status (putS vline) *> b *> nl output | details || status == Fail || not (null labels) = section (Just status) | otherwise = id unit status <$ output (sepBy_ (ln (pure ())) (concat [ [ ln (sepBy_ (putS " ") (runStats maxSuccess stats ++ runClasses stats)) | details ] , do Failure{ seed, reason, exception, testCase } <- toList failed let len = length testCase concat [ [ do ln (putS (path ++ ':' : show lineNumber ++ ':' : ' ' : reason)) for_ exception (ln . putS . displayException) ] , [ for_ (enumerate (init testCase)) (\ (i, s) -> ln (putS (i ++ s))) | len >= 2 ] , [ ln (failure (putS (last testCase))) | len >= 1 ] , [ ln (putS ("--replay '" ++ seed ++ "'")) ] ] , labels , runTables stats ])) where title s failedPreviously = do topIndent (stat vline (bool heading1 headingN failedPreviously) s) stat (putS vline) (failure' (putS (hline ++ arrow))) s w <- asks width withSGR (SetConsoleIntensity BoldIntensity:stat [] [ SetColor Foreground Vivid Red ] s) (putS (bullet ++ name ++ replicate (w - length name) ' ')) withHandle (liftIO . hFlush) data Result = Result { stats :: Stats , status :: Status , failed :: Maybe Failure } data Failure = Failure { seed :: String , reason :: String , exception :: Maybe SomeException , testCase :: [String] } data Stats = Stats { numTests :: Int , numDiscarded :: Int , numShrinks :: Int , labels :: Map.Map [String] Int , classes :: Map.Map String Int , tables :: Map.Map String (Map.Map String Int) } defaultStats :: Stats defaultStats = Stats { numTests = 0 , numDiscarded = 0 , numShrinks = 0 , labels = Map.empty , classes = Map.empty , tables = Map.empty } fromQC :: QC.Result -> Result fromQC r = Result (resultStats r) (bool Fail Pass (isSuccess r)) $ case r of QC.Failure{ usedSeed, usedSize, reason, theException, failingTestCase } -> Just Failure{ seed = show (usedSeed, usedSize), reason, exception = theException, testCase = failingTestCase } _ -> Nothing resultStats :: QC.Result -> Stats resultStats = \case QC.Success{ numTests, numDiscarded, labels, classes, tables } -> defaultStats{ numTests, numDiscarded, labels, classes, tables } QC.GaveUp{ numTests, numDiscarded, labels, classes, tables } -> defaultStats{ numTests, numDiscarded, labels, classes, tables } QC.Failure{ numTests, numDiscarded, numShrinks, failingLabels, failingClasses } -> defaultStats{ numTests, numDiscarded, numShrinks, labels = Map.fromList (map ((, numTests) . pure) failingLabels), classes = Map.fromList (map (,numTests) (toList failingClasses)) } QC.NoExpectedFailure{ numTests, numDiscarded, labels, classes, tables } -> defaultStats{ numTests, numDiscarded, labels, classes, tables } runStats :: (Has (Reader Handle) sig m, MonadIO m) => Int -> Stats -> [m ()] runStats maxSuccess Stats{ numTests, numDiscarded, numShrinks } = [ sepBy_ (putS ", ") entries *> putS "." | not (null entries) ] where entries = concat [ [ putS (show numTests ++ " test" ++ singular numTests) | numTests > 0, numTests /= maxSuccess ] , [ putS (show numDiscarded ++ " discarded") | numDiscarded > 0 ] , [ putS (show numShrinks ++ " shrink" ++ singular numShrinks) | numShrinks > 0 ] ] runLabels :: (Has (Reader Handle) sig m, Has (State Tally) sig m, MonadIO m) => Status -> Stats -> [m ()] runLabels s Stats{ numTests, labels } | null labels = [] | otherwise = IntMap.elems numberedLabels >>= \ m -> let sorted = sortBy (flip (comparing snd) <> flip (comparing fst)) (Map.toList m) scaled = map (fmap (\ v -> realToFrac v / n * 100)) sorted sparked = sparkify (Map.elems m) ln l = line *> stat success failure s (putS vline) *> putS l *> nl in [ for_ (enumerate scaled) $ \ (i, (key, v)) -> ln (i ++ (' ' <$ guard (v < 10)) ++ showFFloatAlt (Just 1) v "" ++ "% " ++ key) , do ln [ c | e <- sparked, c <- [e, e, e] ] ln [ c | k <- Map.keys m, i <- maybe [] (pure . succ) (elemIndex k (map fst sorted)), c <- ' ':show i ++ " " ] ] where numberedLabels = IntMap.fromListWith (Map.unionWith (+)) [ (i, Map.singleton l n) | (labels, n) <- Map.toList labels , (i, l) <- zip [(0 :: Int)..] labels ] n = realToFrac numTests :: Double enumerate :: [a] -> [(String, a)] enumerate as = foldr (\ (i, a) as -> (show (i :: Int) ++ ". " ++ replicate (maxDigits - digits i) ' ', a):as) [] (zip [1 :: Int ..] as) where digits i = ceiling (logBase 10 (realToFrac (i + 1) :: Double)) maxDigits = digits (length as) runClasses :: (Has (Reader Handle) sig m, MonadIO m) => Stats -> [m ()] runClasses Stats{ numTests = n, classes } = [ putS (intercalate ", " (map (uncurry (class_ n)) (Map.toList classes)) ++ ".") | not (null classes) ] where class_ n label n' = if n == n' then label else showFFloatAlt (Just 1) (fromIntegral n' / fromIntegral n * 100 :: Double) ('%':' ':label) runTables :: Stats -> [m ()] runTables _ = [] plural :: Int -> a -> a -> a plural 1 s _ = s plural _ _ p = p singular :: Int -> String singular 1 = "s" singular _ = "" runTally :: (Has (Reader Handle) sig m, MonadIO m) => Tally -> m () runTally t = sepBy_ (putS ", " ) (map success (tally "✓" "success" "successes" (successes t)) ++ map failure (tally "✗" "failure" "failures" (failures t))) *> putS "." *> nl where tally prefix s p n = [ sepBy_ (putS " ") [ putS prefix, putS (show n), putS (plural n s p) ] | n /= 0 ]