From a3dd63f26c8882bc2c8dbc94495332532c3b87f8 Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Sat, 12 Sep 2026 20:18:13 +0300 Subject: [PATCH] Update tests --- src/AFRP.hs | 3 +- test/AFRPLawsSpec.hs | 8 ++++-- test/AFRPSpec.hs | 67 ++++++++++++++++---------------------------- test/Support.hs | 14 ++++----- 4 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/AFRP.hs b/src/AFRP.hs index d60434c..a395b8f 100644 --- a/src/AFRP.hs +++ b/src/AFRP.hs @@ -23,6 +23,7 @@ module AFRP , lMerge , Pair(..) , Request(..) + , SerializeUTCTime(..) , edge , duration , tag @@ -308,7 +309,7 @@ instance Monoid (Event a) where mempty = Tick hold :: (Serialize a, Eq a) => a -> Mealy m (Event a) a -hold def = preMapAccum step def id +hold def = mapAccum step def id where step prev = \case Tick -> prev diff --git a/test/AFRPLawsSpec.hs b/test/AFRPLawsSpec.hs index ee93dd4..fa38617 100644 --- a/test/AFRPLawsSpec.hs +++ b/test/AFRPLawsSpec.hs @@ -17,9 +17,11 @@ import Test.Hspec.Hedgehog (forAll, forAllWith, hedgehog, (===)) -- sides on generated inputs. runPure :: Mealy Identity a b -> [a] -> [b] -runPure _ [] = [] -runPure m (a : as) = case runIdentity (runMealy m id fakeRequest a) of - Pair b m' -> b : runPure m' as +runPure m = go (runMealy m id) + where + go _ [] = [] + go w (a : as) = case runIdentity (stepAuto w fakeRequest a) of + (b, w') -> b : go w' as -- | Machines wrap functions and have no Show; name them for forAll instead. forAllMealy :: Gen (Mealy Identity a b) -> PropertyT IO (Mealy Identity a b) diff --git a/test/AFRPSpec.hs b/test/AFRPSpec.hs index c5f9ee5..dd9f567 100644 --- a/test/AFRPSpec.hs +++ b/test/AFRPSpec.hs @@ -25,17 +25,24 @@ fakeRequest = Request (sec 0) utc nil sec :: Integer -> UTCTime sec n = UTCTime (toEnum 0) (fromIntegral n) +untime :: SerializeUTCTime -> UTCTime +untime (SerializeUTCTime t) = t + runPure :: Mealy Identity a b -> [a] -> [b] -runPure _ [] = [] -runPure m (a : as) = case runIdentity (AFRP.runMealy m id fakeRequest a) of - Pair b m' -> b : runPure m' as +runPure m = go (runMealy m id) + where + go _ [] = [] + go w (a : as) = case runIdentity (stepAuto w fakeRequest a) of + (b, w') -> b : go w' 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 - Pair b m' -> b : runTimed m' as +runTimed m = go (runMealy m id) + where + go _ [] = [] + go w ((s, a) : as) = + case runIdentity (stepAuto w (Request (sec s) utc nil) a) of + (b, w') -> b : go w' as -- | A minimal State monad for observing effectful arrows (e.g. whenA gating). newtype St a = St { unSt :: Int -> (a, Int) } @@ -54,12 +61,11 @@ 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 +runStEff m = go (runMealy m id) where go _ s [] = ([], s) - go m' s (a : rest) = - case unSt (AFRP.runMealy m' id fakeRequest a) s of - (Pair b m'', s') -> let (bs, s'') = go m'' s' rest in (b : bs, s'') + go w s (a : rest) = case unSt (stepAuto w fakeRequest a) s of + ((b, w'), s') -> let (bs, s'') = go w' s' rest in (b : bs, s'') spec :: Spec spec = describe "AFRP" $ do @@ -81,7 +87,6 @@ spec = describe "AFRP" $ do debounceSpec rollupSpec effSpec - switchSpec mapAccumRequestSpec preMapAccumRequestSpec whenASpec @@ -411,41 +416,17 @@ effSpec = describe "eff" $ do 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) + runTimed (mapAccumRequest (\req s _ -> s ++ [SerializeUTCTime (requestTime req)]) [] (map untime)) [(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) + let out = runTimed (mapAccumRequest (\req s _ -> s ++ [SerializeUTCTime (requestTime req)]) [] (map untime)) [(fromIntegral s, ()) | s <- secs'] expected = [ map (sec . fromIntegral) (take (i + 1) secs') | i <- [0 .. length secs' - 1] ] out === expected @@ -453,14 +434,14 @@ mapAccumRequestSpec = describe "mapAccumRequest" $ do preMapAccumRequestSpec :: Spec preMapAccumRequestSpec = describe "preMapAccumRequest" $ do it "accumulates request times, pre-state extraction" $ - runTimed (preMapAccumRequest (\req s _ -> s ++ [requestTime req]) [] id) + runTimed (preMapAccumRequest (\req s _ -> s ++ [SerializeUTCTime (requestTime req)]) [] (map untime)) [(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) + let out = runTimed (preMapAccumRequest (\req s _ -> s ++ [SerializeUTCTime (requestTime req)]) [] (map untime)) [(fromIntegral s, ()) | s <- secs'] expected = [ map (sec . fromIntegral) (take i secs') | i <- [0 .. length secs' - 1] ] out === expected @@ -524,11 +505,11 @@ sampleSpec = describe "sample" $ do -- | 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 (Pair a (subscribed ents)) +subscribed ents = Mealy ents $ \_ -> Fun $ \_ a -> a -- | 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 (Pair (a +) (subscribedF ents)) +subscribedF ents = Mealy ents $ \_ -> Fun $ \_ a -> (a +) entitiesSpec :: Spec entitiesSpec = describe "entities" $ do @@ -545,7 +526,7 @@ entitiesSpec = describe "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 + entities (sliding (3 :: Int) :: Mealy Identity (Event Int) [Int]) `shouldBe` S.empty it "Category (.) unions entity sets" $ entities (subscribed (S.singleton "a") >>> subscribed (S.singleton "b")) diff --git a/test/Support.hs b/test/Support.hs index 1f34a25..ff09e2b 100644 --- a/test/Support.hs +++ b/test/Support.hs @@ -16,9 +16,8 @@ import Data.Aeson (Value, object, (.=)) import qualified Data.Text as T import Data.Time (UTCTime (..), utc) import Data.UUID (nil) -import AFRP (Mealy (..), Pair (..), Request (..)) +import AFRP (Mealy (..), Request (..), stepAuto) import HomeAssistant.Controller (HASSEff (..), Service) -import AFRP (stepAuto) fakeRequest :: Request fakeRequest = Request (sec 0) utc nil @@ -50,11 +49,12 @@ interp (Trace _ _) = pure () -- | Run a HASS arrow over a list of inputs, collecting per-step emitted services. runHASS :: Mealy HASSEff a b -> [a] -> [(b, [Service])] -runHASS _ [] = [] -runHASS m (a : as) = - let w = runMealy m interp - in case runAcc (stepAuto w fakeRequest a) [] of - (Pair b m', svcs) -> (b, svcs) : runHASS m' as +runHASS m = go (runMealy m interp) + where + go _ [] = [] + go w (a : as) = + case runAcc (stepAuto w fakeRequest a) [] of + ((b, w'), svcs) -> (b, svcs) : go w' as services :: [(b, [Service])] -> [[Service]] services = map snd