Files

622 lines
22 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
module AFRPSpec (spec) where
import Control.Arrow (arr, (&&&), first, left)
import Control.Category ((>>>))
import Control.Monad.Fix (MonadFix (..))
import Data.Foldable (for_)
import AFRP
import Data.Functor.Identity (Identity (..))
import Data.List (sort)
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Time (NominalDiffTime, UTCTime (..), utc)
import Data.UUID (nil)
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Test.Hspec
import Test.Hspec.Hedgehog
fakeRequest :: Request
fakeRequest = Request (sec 0) utc nil
sec :: Integer -> UTCTime
sec n = UTCTime (toEnum 0) (fromIntegral n)
runPure :: Mealy Identity a b -> [a] -> [b]
runPure _ [] = []
runPure m (a : as) = case runIdentity (AFRP.runMealy m id fakeRequest a) of
(b, m') -> b : runPure m' as
-- | Run a Mealy with a per-step wall clock (seconds since the day-0 epoch).
runTimed :: Mealy Identity a b -> [(Integer, a)] -> [b]
runTimed _ [] = []
runTimed m ((s, a) : as) =
case runIdentity (AFRP.runMealy m id (Request (sec s) utc nil) a) of
(b, m') -> b : runTimed m' as
-- | A minimal State monad for observing effectful arrows (e.g. whenA gating).
newtype St a = St { unSt :: Int -> (a, Int) }
instance Functor St where
fmap f (St g) = St $ \s -> let (a, s') = g s in (f a, s')
instance Applicative St where
pure a = St (\s -> (a, s))
St f <*> St x = St $ \s -> let (f', s') = f s; (a, s'') = x s' in (f' a, s'')
instance Monad St where
St m >>= k = St $ \s -> let (a, s') = m s; (b, s'') = unSt (k a) s' in (b, s'')
instance MonadFix St where
mfix f = St $ \s -> let (a, s') = unSt (f a) s in (a, s')
runStEff :: Mealy St a b -> Int -> [a] -> ([b], Int)
runStEff m s0 as = go m s0 as
where
go _ s [] = ([], s)
go m' s (a : rest) =
case unSt (AFRP.runMealy m' id fakeRequest a) s of
((b, m''), s') -> let (bs, s'') = go m'' s' rest in (b : bs, s'')
spec :: Spec
spec = describe "AFRP" $ do
entitiesSpec
holdSpec
eventsSpec
isEventSpec
tagSpec
toEventSpec
lMergeSpec
changesSpec
edgeSpec
dropFirstSpec
filterASpec
slidingSpec
mapAccumSpec
preMapAccumSpec
durationSpec
delayEventSpec
debounceSpec
rollupSpec
fixedSpec
effSpec
switchSpec
mapAccumRequestSpec
preMapAccumRequestSpec
whenASpec
thenASpec
sampleSpec
holdSpec :: Spec
holdSpec = describe "hold" $ do
it "holds initial value until an Event arrives" $
runPure (hold 'a') [Tick, Event 'b', Tick, Event 'c']
`shouldBe` ['a', 'b', 'b', 'c']
it "never changes on Tick" $
runPure (hold (0 :: Int)) (replicate 5 Tick) `shouldBe` replicate 5 (0 :: Int)
eventsSpec :: Spec
eventsSpec = describe "events" $ do
it "converts Tick to Left () and Event a to Right a" $
runPure events [Tick, Event 'a', Tick, Event 'b']
`shouldBe` [Left (), Right 'a', Left (), Right 'b']
isEventSpec :: Spec
isEventSpec = describe "isEvent" $ do
it "returns False for Tick" $
isEvent Tick `shouldBe` False
it "returns True for Event x" $
isEvent (Event ()) `shouldBe` True
tagSpec :: Spec
tagSpec = describe "tag" $ do
it "replaces value preserving structure" $ do
tag 'b' Tick `shouldBe` Tick
tag 'b' (Event 'a') `shouldBe` Event 'b'
toEventSpec :: Spec
toEventSpec = describe "toEvent" $ do
it "round-trips through events" $
runPure toEvent [Left (), Right 'a', Left ()]
`shouldBe` [Tick, Event 'a', Tick]
it "is inverse of events modulo Event/Either" $
runPure (events >>> toEvent) [Tick, Event 'a', Event 'b']
`shouldBe` [Tick, Event 'a', Event 'b']
lMergeSpec :: Spec
lMergeSpec = describe "lMerge" $ do
it "both Tick gives Tick" $
lMerge (Tick :: Event Int) (Tick :: Event Int) `shouldBe` Tick
it "prefers left Event" $
lMerge (Event (1 :: Int)) (Event (2 :: Int)) `shouldBe` Event (1 :: Int)
it "prefers right Event if left is Tick" $
lMerge Tick (Event (2 :: Int)) `shouldBe` Event (2 :: Int)
it "Tick is a left identity" $
hedgehog $ do
e <- forAll eventGen
lMerge Tick e === e
it "Tick is a right identity" $
hedgehog $ do
e <- forAll eventGen
lMerge e Tick === e
it "is associative" $
hedgehog $ do
a <- forAll eventGen
b <- forAll eventGen
c <- forAll eventGen
lMerge a (lMerge b c) === lMerge (lMerge a b) c
changesSpec :: Spec
changesSpec = describe "changes" $ do
it "first output is always Tick" $
runPure changes "hello" !! 0 `shouldBe` Tick
it "outputs Event only on value change" $
runPure changes "aaaabbbcca"
`shouldBe` [Tick, Tick, Tick, Tick
, Event 'b', Tick, Tick
, Event 'c', Tick
, Event 'a'
]
it "first output is Tick, subsequent outputs are Event iff value changed" $
hedgehog $ do
xs <- forAll $ Gen.list (Range.linear 0 100) Gen.alpha
let out = runPure changes xs
length out === length xs
case out of
[] -> pure ()
(Tick : rest) -> do
let triples = zip3 xs (drop 1 xs) rest
for_ triples $ \(prev, curr, o) ->
if prev /= curr
then o === Event curr
else o === Tick
_ -> failure
edgeSpec :: Spec
edgeSpec = describe "edge" $ do
it "emits Event () only on rising edge" $
runPure edge [False, True, True, False, True]
`shouldBe` [Tick, Event (), Tick, Tick, Event ()]
it "starts from False, so first True is a rising edge" $
runPure edge [True, False, True]
`shouldBe` [Event (), Tick, Event ()]
it "Event () only on False -> True transition" $
hedgehog $ do
bs <- forAll $ Gen.list (Range.linear 0 50) Gen.bool
let out = runPure edge bs
length out === length bs
case (bs, out) of
([], []) -> pure ()
(b : _, o : _) -> do
if b then o === Event () else o === Tick
let triples = zip3 bs (drop 1 bs) (drop 1 out)
for_ triples $ \(prev, curr, o') ->
if not prev && curr
then o' === Event ()
else o' === Tick
_ -> failure
dropFirstSpec :: Spec
dropFirstSpec = describe "dropFirst" $ do
it "drops the first Event, passes the rest" $
runPure dropFirst [Tick, Event 1, Event 2, Event 3]
`shouldBe` [Tick, Tick, Event 2, Event 3 :: Event Int]
it "passes Tick through untouched before first Event" $
runPure dropFirst [Tick, Tick, Tick :: Event Int]
`shouldBe` [Tick, Tick, Tick :: Event Int]
it "drops only the first Event, Ticks before it are inert" $
runPure dropFirst [Tick, Tick, Event 'a', Tick, Event 'b']
`shouldBe` [Tick, Tick, Tick, Tick, Event 'b' :: Event Char]
filterASpec :: Spec
filterASpec = describe "filterA" $ do
it "lets through values matching predicate" $
runPure (filterA (even @Int)) [1, 2, 3, 4]
`shouldBe` [Left (), Right 2, Left (), Right 4]
it "output is Right a iff predicate holds" $
hedgehog $ do
threshold <- forAll $ Gen.int (Range.linear (-10) 10)
let p = (> threshold)
xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-20) 20))
let out = runPure (filterA p) xs
length out === length xs
for_ (zip xs out) $ \(x, o) ->
if p x
then o === Right x
else o === Left ()
slidingSpec :: Spec
slidingSpec = describe "sliding" $ do
it "accumulates events up to the window size" $
runPure (sliding (3 :: Int)) [Event (1 :: Int), Event 2, Event 3, Event 4]
`shouldBe` [[1], [1, 2], [1, 2, 3], [2, 3, 4]]
it "Ticks don't change the accumulator" $
runPure (sliding 2) [Event (1 :: Int), Tick, Event 2]
`shouldBe` [[1], [1], [1, 2]]
it "empty list stays empty" $
runPure (sliding (5 :: Int)) ([] :: [Event Int]) `shouldBe` []
it "output length never exceeds window size" $
hedgehog $ do
n <- forAll $ Gen.int (Range.constant 1 10)
evs <- forAll $ Gen.list (Range.linear 0 20) (Gen.frequency
[(3, Event <$> Gen.alpha), (1, pure Tick)])
let out = runPure (sliding n) evs
for_ out $ \xs -> assert (length xs <= n)
mapAccumSpec :: Spec
mapAccumSpec = describe "mapAccum" $ do
it "running sum" $
runPure (mapAccum (+) (0 :: Int) id) [1, 2, 3]
`shouldBe` [1, 3, 6]
it "post-state extraction: output uses state after applying f" $
runPure (mapAccum (\s x -> s ++ [x]) ([] :: [Int]) id) [1, 2, 3]
`shouldBe` [[1], [1, 2], [1, 2, 3]]
it "output equals running sum of all inputs so far" $
hedgehog $ do
xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
let out = runPure (mapAccum (+) (0 :: Int) id) xs
length out === length xs
for_ (zip3 [0 ..] xs out) $ \(i, _x, cur) ->
cur === sum (take (i + 1) xs)
preMapAccumSpec :: Spec
preMapAccumSpec = describe "preMapAccum" $ do
it "running sum with pre-state extraction" $
runPure (preMapAccum (+) (0 :: Int) id) [1, 2, 3]
`shouldBe` [0, 1, 3]
it "pre-state extraction: output uses state before applying f" $
runPure (preMapAccum (\s x -> s ++ [x]) ([] :: [Int]) id) [1, 2, 3]
`shouldBe` [[], [1], [1, 2]]
it "output equals running sum before current input" $
hedgehog $ do
xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
let out = runPure (preMapAccum (+) (0 :: Int) id) xs
length out === length xs
for_ (zip3 [0 ..] xs out) $ \(i, _x, cur) ->
cur === sum (take i xs)
durationSpec :: Spec
durationSpec = describe "duration" $ do
it "first sample is 0, then elapsed time since first sample" $
runTimed duration [(0, 'a'), (5, 'b'), (10, 'c')]
`shouldBe` [0, 5, 10]
it "measures from the first observation, not the most recent" $
runTimed duration [(2, 'a'), (3, 'b'), (7, 'c')]
`shouldBe` [0, 1, 5]
it "output i equals times[i] - times[0]" $
hedgehog $ do
secs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear 0 1000))
let out = runTimed duration [(fromIntegral s, ()) | s <- secs]
length out === length secs
case secs of
[] -> pure ()
(t0 : _) -> for_ (zip secs out) $ \(s, d) ->
d === fromIntegral (s - t0)
delayEventSpec :: Spec
delayEventSpec = describe "delayEvent" $ do
let delay = 5 :: NominalDiffTime
it "emits a queued event once the delay has elapsed" $
runTimed (delayEvent delay)
[(0, Event 'a'), (3, Tick), (6, Tick)]
`shouldBe` [Tick, Tick, Event 'a']
it "preserves order when multiple events are queued" $
runTimed (delayEvent delay)
[(0, Event 'a'), (1, Event 'b'), (10, Tick), (12, Tick)]
`shouldBe` [Tick, Tick, Event 'a', Event 'b']
it "emits nothing on pure Tick input" $
runTimed (delayEvent delay) [(0, Tick :: Event Char), (10, Tick)]
`shouldBe` [Tick, Tick]
it "emits exactly one output Event per input Event" $
hedgehog $ do
evs <- forAll $ Gen.list (Range.linear 0 30) eventGen
let evs' = evs ++ replicate 3 Tick
n = length [() | Event _ <- evs]
out = runTimed (delayEvent (1 :: NominalDiffTime))
(zip [0, 2 ..] evs')
length [() | Event _ <- out] === n
debounceSpec :: Spec
debounceSpec = describe "debounce" $ do
let delay = 5 :: NominalDiffTime
it "fires the last event after the quiet period" $
runTimed (debounce delay)
[(0, Event 'a'), (3, Tick), (6, Tick)]
`shouldBe` [Tick, Tick, Event 'a']
it "a newer event before firing resets the timer" $
runTimed (debounce delay)
[(0, Event 'a'), (3, Event 'b'), (6, Tick), (8, Tick)]
`shouldBe` [Tick, Tick, Tick, Event 'b']
it "collapses a burst into a single emission" $
runTimed (debounce delay)
[(0, Event 'a'), (1, Event 'b'), (2, Event 'c'), (10, Tick)]
`shouldBe` [Tick, Tick, Tick, Event 'c']
it "emits no more output Events than input Events" $
hedgehog $ do
evs <- forAll $ Gen.list (Range.linear 0 30) eventGen
let nIn = length [() | Event _ <- evs]
out = runTimed (debounce (1 :: NominalDiffTime))
(zip [0, 2 ..] evs)
nOut = length [() | Event _ <- out]
assert (nOut <= nIn)
rollupSpec :: Spec
rollupSpec = describe "rollup" $ do
it "passes the first `limit` events through immediately, then bursts" $
runTimed (rollup 2 10)
[ (0, Event 'a'), (1, Event 'b')
, (2, Event 'c'), (3, Event 'd')
, (12, Tick)
]
`shouldBe` [ Event ['a'], Event ['b']
, Tick, Tick
, Event ['c', 'd']
]
it "flushes the accumulator when the window ends on a Tick" $
runTimed (rollup 1 10)
[(0, Event 'a'), (1, Event 'b'), (2, Tick), (12, Tick)]
`shouldBe` [Event ['a'], Tick, Tick, Event ['b']]
it "is idle (Tick) until the first Event" $
runTimed (rollup 2 10) [(0, Tick :: Event Char), (1, Tick)]
`shouldBe` [Tick, Tick]
it "every input Event appears exactly once across the outputs" $
hedgehog $ do
limit <- forAll $ Gen.int (Range.constant 1 5)
evs <- forAll $ Gen.list (Range.linear 0 30) eventGen
let evs' = evs ++ replicate 10 Tick
times = [0 ..]
out = runTimed (rollup limit 5) (zip times evs')
emitted = concat [xs | Event xs <- out]
sort emitted === sort [x | Event x <- evs]
fixedSpec :: Spec
fixedSpec = describe "fixed" $ do
it "accumulates events within a window and rolls over on expiry" $
runTimed (fixed 10)
[ (0, Event 'a'), (1, Event 'b'), (2, Tick)
, (12, Event 'c'), (13, Tick)
]
`shouldBe` [ ['a'], ['a', 'b'], ['a', 'b']
, ['c'], ['c']
]
it "starts a window even on a leading Tick" $
runTimed (fixed 10)
[ (0, Tick), (1, Event 'a')
, (12, Tick), (13, Tick)
, (25, Event 'b')
]
`shouldBe` [ [], ['a'], [], [], ['b'] ]
it "output is always the current window's accumulated list" $
hedgehog $ do
w <- forAll $ Gen.int (Range.constant 1 10)
evs <- forAll $ Gen.list (Range.linear 0 30) eventGen
let out = runTimed (fixed w) (zip [0 ..] evs)
expected =
[ [ x | Event x <- take (i - lo + 1) (drop lo evs) ]
| i <- [0 .. length evs - 1]
, let lo = (i `div` w) * w
]
out === expected
eventGen :: Gen (Event Char)
eventGen = Gen.frequency
[ (3, Event <$> Gen.alpha)
, (1, pure Tick)
]
effSpec :: Spec
effSpec = describe "eff" $ do
it "lifts a pure effect function into a stateless Mealy" $
runPure (eff (\_ x -> Identity (x + 1))) [1, 2, 3]
`shouldBe` [2 :: Int, 3, 4]
it "output equals f(input) for every step" $
hedgehog $ do
xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
let out = runPure (eff (\_ x -> Identity (x * 2))) xs
out === map (* 2) xs
switchSpec :: Spec
switchSpec = describe "switch" $ do
it "switches to the continuation at the first Event" $
runPure (switch (arr (\x -> (x, if x >= (3 :: Int) then Event () else Tick)))
(const (arr (const 99))))
[1, 2, 3, 4, 5]
`shouldBe` [1, 2, 99, 99, 99]
it "never switches if no Event is emitted" $
runPure (switch (arr (\x -> (x, Tick :: Event ())))
(const (arr (const 99))))
[1, 2, 3]
`shouldBe` [1, 2, 3 :: Int]
it "prefix outputs come from the first arrow, suffix from the continuation" $
hedgehog $ do
threshold <- forAll $ Gen.int (Range.linear (-20) 20)
xs <- forAll $ Gen.list (Range.linear 0 30) (Gen.int (Range.linear (-20) 20))
let firstArr = arr (\x -> (x, if x >= threshold then Event () else Tick))
out = runPure (switch firstArr (const (arr (const 99)))) xs
(pre, _post) = break (>= threshold) xs
take (length pre) out === pre
drop (length pre) out === replicate (length xs - length pre) 99
mapAccumRequestSpec :: Spec
mapAccumRequestSpec = describe "mapAccumRequest" $ do
it "accumulates request times, post-state extraction" $
runTimed (mapAccumRequest (\req s _ -> s ++ [requestTime req]) [] id)
[(0, 'a'), (5, 'b'), (10, 'c')]
`shouldBe` [ [sec 0], [sec 0, sec 5], [sec 0, sec 5, sec 10] ]
it "output i is every request time seen so far" $
hedgehog $ do
secs' <- forAll $ Gen.list (Range.linear 0 30) (Gen.int (Range.linear 0 100))
let out = runTimed (mapAccumRequest (\req s _ -> s ++ [requestTime req]) [] id)
[(fromIntegral s, ()) | s <- secs']
expected = [ map (sec . fromIntegral) (take (i + 1) secs') | i <- [0 .. length secs' - 1] ]
out === expected
preMapAccumRequestSpec :: Spec
preMapAccumRequestSpec = describe "preMapAccumRequest" $ do
it "accumulates request times, pre-state extraction" $
runTimed (preMapAccumRequest (\req s _ -> s ++ [requestTime req]) [] id)
[(0, 'a'), (5, 'b'), (10, 'c')]
`shouldBe` [ [], [sec 0], [sec 0, sec 5] ]
it "output i is every request time before the current step" $
hedgehog $ do
secs' <- forAll $ Gen.list (Range.linear 0 30) (Gen.int (Range.linear 0 100))
let out = runTimed (preMapAccumRequest (\req s _ -> s ++ [requestTime req]) [] id)
[(fromIntegral s, ()) | s <- secs']
expected = [ map (sec . fromIntegral) (take i secs') | i <- [0 .. length secs' - 1] ]
out === expected
whenASpec :: Spec
whenASpec = describe "whenA" $ do
let counter = eff (\_ (_ :: Int) -> St (\s -> ((), s + 1)))
it "output is always () regardless of the predicate" $
fst (runStEff (whenA (> 5) counter) 0 [1, 6, 2, 7])
`shouldBe` [(), (), (), ()]
it "runs the inner arrow only when the predicate holds" $
snd (runStEff (whenA (> 5) counter) 0 [1, 6, 2, 7])
`shouldBe` 2
it "never runs the inner arrow when the predicate is always false" $
snd (runStEff (whenA (const False) counter) 0 [1, 6, 2, 7])
`shouldBe` 0
it "runs the inner arrow on every input when the predicate is always true" $
snd (runStEff (whenA (const True) counter) 0 [1, 6, 2, 7])
`shouldBe` 4
thenASpec :: Spec
thenASpec = describe "thenA" $ do
it "short-circuits on Left and continues on Right" $
runPure (filterA (even @Int) `thenA` filterA (> 3)) [1..6]
`shouldBe` [Left (), Left (), Left (), Right 4, Left (), Right 6]
it "(>>|) is an infix alias for thenA" $
runPure (filterA (even @Int) >>| filterA (> 3)) [1..6]
`shouldBe` [Left (), Left (), Left (), Right 4, Left (), Right 6]
it "second stage runs only when the first produces Right" $
hedgehog $ do
xs <- forAll $ Gen.list (Range.linear 0 30) (Gen.int (Range.linear (-20) 20))
let out = runPure (filterA (even @Int) >>| filterA (> 0)) xs
expected =
[ if odd x then Left ()
else if x > 0 then Right x
else Left ()
| x <- xs ]
out === expected
sampleSpec :: Spec
sampleSpec = describe "sample" $ do
it "tags the current value onto the Event structure" $
runPure sample [(1, Tick), (2, Event 'a'), (3, Tick)]
`shouldBe` [Tick, Event @Int 2, Tick]
it "output is Event a iff the input event is present" $
hedgehog $ do
vals <- forAll $ Gen.list (Range.linear 0 30) (Gen.int (Range.linear 0 100))
evs <- forAll $ Gen.list (Range.linear 0 30) eventGen
let n = min (length vals) (length evs)
ps = take n $ zip vals evs
out = runPure sample ps
for_ (zip ps out) $ \((v, ev), o) ->
o === tag v ev
-- | A stateless arrow carrying a fixed entity set, for testing propagation.
subscribed :: S.Set T.Text -> Mealy Identity Int Int
subscribed ents = Mealy ents $ \_ _ a -> pure (a, subscribed ents)
-- | Same as 'subscribed' but yields a function, for testing '<*>'.
subscribedF :: S.Set T.Text -> Mealy Identity Int (Int -> Int)
subscribedF ents = Mealy ents $ \_ _ a -> pure ((a +), subscribedF ents)
entitiesSpec :: Spec
entitiesSpec = describe "entities" $ do
it "id carries no entities" $
entities (arr id :: Mealy Identity Int Int) `shouldBe` S.empty
it "arr carries no entities" $
entities (arr (+ 1) :: Mealy Identity Int Int) `shouldBe` S.empty
it "eff carries no entities" $
entities (eff (\_ x -> Identity (x + 1 :: Int))) `shouldBe` S.empty
it "primitive combinators carry no entities" $ do
entities (hold 'a') `shouldBe` S.empty
entities (changes @Int) `shouldBe` S.empty
entities edge `shouldBe` S.empty
entities (sliding (3 :: Int)) `shouldBe` S.empty
it "Category (.) unions entity sets" $
entities (subscribed (S.singleton "a") >>> subscribed (S.singleton "b"))
`shouldBe` S.fromList ["a", "b"]
it "Applicative (<*>) unions entity sets" $
entities (subscribedF (S.singleton "a") <*> subscribed (S.singleton "b"))
`shouldBe` S.fromList ["a", "b"]
it "Arrow (&&&) unions entity sets" $
entities (subscribed (S.singleton "a") &&& subscribed (S.singleton "b"))
`shouldBe` S.fromList ["a", "b"]
it "(>>>) unions entity sets" $
entities (subscribed (S.singleton "a") >>> arr id >>> subscribed (S.singleton "b"))
`shouldBe` S.fromList ["a", "b"]
it "left preserves the entity set" $
entities (left (subscribed (S.singleton "a")) :: Mealy Identity (Either Int Int) (Either Int Int))
`shouldBe` S.singleton "a"
it "first preserves the entity set" $
entities (first (subscribed (S.singleton "a")) :: Mealy Identity (Int, Int) (Int, Int))
`shouldBe` S.singleton "a"
it "fmap preserves the entity set" $
entities (fmap (+ 1) (subscribed (S.singleton "a")))
`shouldBe` S.singleton "a"