packages feed

fresnel-0.0.0.0: test/Test/Group.hs

{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TemplateHaskell #-}
module Test.Group
( Entry(..)
, mkGroup
, deriveGroup
, mkProp
, entryName
, Loc(..)
, here
, zero
, Semiring(..)
, Unital(..)
, horizontal
, sumWidths
, vertical
, maxWidths
, Width(..)
, HasWidth(..)
  -- * Statuses
, stat
, Status(..)
  -- * Tallies
, hasFailures
, hasSuccesses
, unit
, Tally(..)
) where

import Data.Char (isSpace)
import Fresnel.Tropical
import GHC.Stack
import Language.Haskell.TH.Lib
import Language.Haskell.TH.Syntax (Module(..), modString)
import Numeric (readDec)
import Test.QuickCheck (Property, allProperties)

data Entry
  = Group String [Entry]
  | Prop String Loc Property

mkGroup :: String -> [(String, Property)] -> Entry
mkGroup name = Group name . map (uncurry mkProp)

deriveGroup :: ExpQ
deriveGroup = [e| mkGroup $(thisModule >>= \ (Module _ name) -> stringE (modString name)) $allProperties |]

mkProp :: String -> Property -> Entry
mkProp s = Prop name Loc{ path, lineNumber }
  where
  (name, path, lineNumber) = case breaks [isSpace, not . isSpace, isSpace, not . isSpace, (== ':'), (/= ':')] s of
    [n, _, _, _, p, _, l] -> (unwords (filter (\ s -> s /= "_" && s /= "prop") (breakAll (== '_') n)), p, fst (head (readDec l)))
    _                     -> ("", "", 0)

entryName :: Entry -> String
entryName = \case
  Group name _  -> name
  Prop name _ _ -> name


data Loc = Loc { path :: FilePath, lineNumber :: Int }

here :: HasCallStack => Loc
here = Loc{ path = srcLocFile srcLoc, lineNumber = srcLocStartLine srcLoc }
  where
  (_, srcLoc) = head (getCallStack callStack)


breaks :: [a -> Bool] -> [a] -> [[a]]
breaks ps as = case ps of
  []   -> [as]
  p:ps -> let (h, t) = break p as in h : breaks ps t

breakAll :: (a -> Bool) -> [a] -> [[a]]
breakAll p = go False where
  go b = \case
    [] -> []
    as -> let (h, t) = break (if b then not . p else p) as in h : go (not b) t


zero :: Monoid s => s
zero = mempty

class Semigroup s => Semiring s where
  (><) :: s -> s -> s
  infixr 7 ><

instance (Semigroup a, Ord a) => Semiring (Tropical a) where
  Tropical a1 >< Tropical a2 = Tropical ((<>) <$> a1 <*> a2)

class (Monoid s, Semiring s) => Unital s where
  one :: s

instance (Monoid a, Ord a) => Unital (Tropical a) where
  one = finite zero


horizontal :: (Foldable t, Unital r) => (a -> r) -> t a -> r
horizontal f = foldr ((><) . f) one

sumWidths :: (Foldable t, HasWidth a) => t a -> Tropical Width
sumWidths = horizontal maxWidth

vertical :: (Foldable t, Semiring r, Monoid r) => (a -> r) -> t a -> r
vertical f = foldr ((<>) . f) zero

maxWidths :: (Foldable t, HasWidth a) => t a -> Tropical Width
maxWidths = vertical maxWidth

newtype Width = Width { width :: Int }
  deriving (Eq, Ord)

instance Semigroup Width where
  Width a1 <> Width a2 = Width (a1 + a2)

instance Monoid Width where
  mempty = Width 0

instance Semiring Width where
  Width a1 >< Width a2 = Width (a1 * a2)

instance Unital Width where
  one = Width 1


class HasWidth t where
  maxWidth :: t -> Tropical Width

instance HasWidth Char where
  maxWidth _ = finite one

instance HasWidth Entry where
  maxWidth = \case
    Group groupName entries -> sumWidths groupName <> maxWidths entries
    Prop name _ _           -> sumWidths name


-- Statuses

stat :: a -> a -> Status -> a
stat pass fail = \case{ Pass -> pass ; Fail -> fail }

data Status = Pass | Fail
  deriving (Eq)


-- Tallies

hasSuccesses :: Tally -> Bool
hasSuccesses = (/= 0) . successes

hasFailures :: Tally -> Bool
hasFailures = (/= 0) . failures

unit :: Status -> Tally
unit = stat (Tally 1 0) (Tally 0 1)

data Tally = Tally { successes :: Int, failures :: Int }

instance Semigroup Tally where
  Tally s1 f1 <> Tally s2 f2 = Tally (s1 + s2) (f1 + f2)

instance Monoid Tally where
  mempty = Tally 0 0