anitomata-0.1.0.0: test-suite/Test/AnitomataSpec.hs
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
module Test.AnitomataSpec
( spec
) where
import Anitomata
import Control.Exception (ErrorCall(..), evaluate, try)
import Data.Function ((&), on)
import Data.Functor ((<&>))
import Data.Kind (Type)
import Data.Maybe (isJust, isNothing)
import Data.Proxy (Proxy(Proxy))
import Data.Semigroup (Semigroup(sconcat))
import GHC.TypeLits (KnownNat, Nat, natVal)
import Prelude
import Test.Hspec (Spec, describe, parallel)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
( Arbitrary(arbitrary), NonNegative(getNonNegative), Positive(Positive, getPositive), (.&&.)
, (===), (==>), Gen, chooseInt, frequency, ioProperty, listOf1, mapSize, oneof, vectorOf
)
import Data.List qualified as L
import Data.List.NonEmpty qualified as NonEmpty
import Data.Vector qualified as V
import Data.Vector.Generic qualified as G
spec :: Spec
spec = parallel do
describe "Foo.Data.Anim" do
prop "Initial source rect is correct" \case
FiniteTestAnim (TestAnim a) ->
animFrame a === case animSlice a of
AnimSlice { animSliceDir = dir, animSliceFrames = frames }
| dir == AnimDirForward -> V.head frames
| otherwise -> V.last frames
prop "Finite animations can be stepped to completion" \case
FiniteTestAnim (TestAnim a) ->
-- Use the computed total duration as the delta time so that we can
-- get through the animation quickly.
iterateAnim dur a
& fmap fromSteppedAnim
& L.lookup AnimStatusFinished
& isJust
where
Just AnimMeta { animMetaTotalDur = dur } = animMeta a
prop "No frames are skipped if delta time is small enough" $ mapSize (`div` 2) \case
FiniteTestAnimBuilder (TestAnimBuilder ab) ->
expectedFrames `L.isSubsequenceOf` realFrames
.&&. length expectedFrames === totalFrameCount
.&&. iterDur - totalDur >= 0
.&&. iterDur - totalDur < dt
where
expectedFrames = getExpectedFrames a0
realFrames = getRealFrames $ iterateAnim dt a0
iterDur = dt * fromIntegral (length realFrames)
a0 = buildAnim AnimDurationDefault ab
dt = minFrameDur / 2
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCount
, animMetaTotalDur = totalDur
, animMetaMinFrameDur = minFrameDur
} = animMeta a0
prop "Some frames are skipped if delta time is large enough" \case
FiniteTestAnimBuilder (TestAnimBuilder ab) ->
length expectedFrames >= length realFrames
.&&. iterDur - totalDur >= 0
.&&. iterDur - totalDur < dt
where
expectedFrames = getExpectedFrames a0
realFrames = getRealFrames $ iterateAnim dt a0
iterDur = dt * fromIntegral (length realFrames)
a0 = buildAnim AnimDurationDefault ab
dt = maxFrameDur * 2
Just AnimMeta
{ animMetaTotalDur = totalDur
, animMetaMaxFrameDur = maxFrameDur
} = animMeta a0
prop "Duration of finite animations can be scaled" \case
(FiniteTestAnimBuilder (TestAnimBuilder ab), Positive scaleFactor) ->
scaleFactor * iterDurDefault === iterDurScaled
.&&. scaleFactor * totalDurDefault === totalDurScaled
.&&. scaleFactor * minFrameDurDefault === minFrameDurScaled
.&&. scaleFactor * maxFrameDurDefault === maxFrameDurScaled
.&&. totalFrameCountDefault === totalFrameCountScaled
where
stepCountScaled = length $ iterateAnim dtScaled aScaled
iterDurScaled = dtScaled * fromIntegral stepCountScaled
aScaled = buildAnim (AnimDurationScaled scaleFactor) ab
dtScaled = maxFrameDurScaled
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCountScaled
, animMetaTotalDur = totalDurScaled
, animMetaMinFrameDur = minFrameDurScaled
, animMetaMaxFrameDur = maxFrameDurScaled
} = animMeta aScaled
stepCountDefault = length $ iterateAnim dtDefault aDefault
iterDurDefault = dtDefault * fromIntegral stepCountDefault
aDefault = buildAnim AnimDurationDefault ab
dtDefault = maxFrameDurDefault
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCountDefault
, animMetaTotalDur = totalDurDefault
, animMetaMinFrameDur = minFrameDurDefault
, animMetaMaxFrameDur = maxFrameDurDefault
} = animMeta aDefault
prop "Total duration of finite animations can be set while respecting relative frame timings" \case
(FiniteTestAnimBuilder (TestAnimBuilder ab), Positive newTotalDur) ->
totalDur === newTotalDur
.&&. iterDur - newTotalDur >= 0
.&&. iterDur - newTotalDur < dt
.&&. scaleFactor * minFrameDurDefault === minFrameDur
.&&. scaleFactor * maxFrameDurDefault === maxFrameDur
.&&. totalFrameCountDefault === totalFrameCount
where
stepCount = length $ iterateAnim dt a'
iterDur = dt * fromIntegral stepCount
a' = buildAnim (AnimDurationTotal newTotalDur) ab
dt = maxFrameDur
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCount
, animMetaTotalDur = totalDur
, animMetaMinFrameDur = minFrameDur
, animMetaMaxFrameDur = maxFrameDur
} = animMeta a'
scaleFactor = newTotalDur / totalDurDefault
a = buildAnim AnimDurationDefault ab
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCountDefault
, animMetaTotalDur = totalDurDefault
, animMetaMinFrameDur = minFrameDurDefault
, animMetaMaxFrameDur = maxFrameDurDefault
} = animMeta a
prop "Constant frame duration of finite animations can be set" \case
(FiniteTestAnimBuilder (TestAnimBuilder ab), Positive frameDur) ->
iterDur - frameDur * fromIntegral totalFrameCount >= 0
.&&. iterDur - frameDur * fromIntegral totalFrameCount < dt
.&&. minFrameDur === frameDur
.&&. maxFrameDur === frameDur
.&&. totalFrameCountDefault === totalFrameCount
where
stepCount = length $ iterateAnim dt a'
iterDur = dt * fromIntegral stepCount
a' = buildAnim (AnimDurationEachFrame frameDur) ab
dt = maxFrameDur
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCount
, animMetaMinFrameDur = minFrameDur
, animMetaMaxFrameDur = maxFrameDur
} = animMeta a'
a = buildAnim AnimDurationDefault ab
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCountDefault
} = animMeta a
prop "Total duration of finite animations can be set via constant frame timing" \case
(FiniteTestAnimBuilder (TestAnimBuilder ab), Positive newTotalDur) ->
totalDur === newTotalDur
.&&. iterDur - newTotalDur >= 0
.&&. iterDur - newTotalDur < dt
.&&. minFrameDur === frameDur
.&&. maxFrameDur === frameDur
.&&. totalFrameCountDefault === totalFrameCount
where
stepCount = length $ iterateAnim dt a'
iterDur = dt * fromIntegral stepCount
a' = buildAnim (AnimDurationEachFrameFromTotal newTotalDur) ab
dt = maxFrameDur
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCount
, animMetaTotalDur = totalDur
, animMetaMinFrameDur = minFrameDur
, animMetaMaxFrameDur = maxFrameDur
} = animMeta a'
frameDur = newTotalDur / fromIntegral totalFrameCount
a = buildAnim AnimDurationDefault ab
Just AnimMeta
{ animMetaTotalFrameCount = totalFrameCountDefault
} = animMeta a
prop "Stepping a completed animation is idempotent" \case
(FiniteTestAnim (TestAnim a), Positive dt) ->
((==) `on` animFrame . steppedAnimValue) sa $ stepAnim dt $ steppedAnimValue sa
where
sa = last $ iterateAnim dt a
prop "Infinite animations do not have countable metadata" \case
(InfiniteTestAnimBuilder (TestAnimBuilder ab), TestAnimDuration ad) ->
validDur ad ==> isNothing $ animMeta $ buildAnim ad ab
prop "Infinite animations cannot override total duration" \case
(InfiniteTestAnimBuilder (TestAnimBuilder ab), TestAnimDuration ad) ->
invalidInfiniteDur ad ==> ioProperty do
try (evaluate $ buildAnim ad ab) >>= \case
Left (ErrorCall msg) -> pure $ "buildAnim: " `L.isPrefixOf` msg
Right {} -> pure False
prop "Repeating a finite animation some finite number of times" \case
(TestAnimSlice as, (TestAnimRepeat ar) :: TestAnimRepeat 1 0, TestAnimDuration ad) ->
getExpectedFrames a === take (succ n * sliceLen) (cycle $ getSliceFrames as)
where
a = buildAnim ad ab
ab = repeatAnim ar $ fromAnimSlice as
sliceLen = V.length $ animSliceFrames as
n = case ar of
AnimRepeatCount x -> x
AnimRepeatForever -> error "impossible"
prop "An infinite animation is \"infinite\"" \case
(TestAnimSlice as, TestAnimDuration ad, Positive n) ->
validDur ad && n > sliceLen ==>
take (n * sliceLen) (getExpectedFrames a) ===
take (n * sliceLen) (cycle $ getSliceFrames as)
where
a = buildAnim ad ab
ab = repeatAnim AnimRepeatForever $ fromAnimSlice as
sliceLen = V.length $ animSliceFrames as
prop "An infinite pingponging animation is \"infinite\"" \case
(TestAnimSlice as, TestAnimDuration ad, Positive n) ->
validDur ad ==>
take (n * sliceLen) (getExpectedFrames a) ===
take (n * sliceLen) (cycle $ sliceFrames <> reverse sliceFrames)
where
sliceFrames = getSliceFrames as
a = buildAnim ad ab
ab = repeatAnim AnimRepeatForever $ pingpongAnimSlice as
sliceLen = V.length $ animSliceFrames as
getExpectedFrames :: G.Vector v f => Anim_ v t f -> [f]
getExpectedFrames a0 = foldMap getSliceFrames $ animSequence a0
getSliceFrames :: G.Vector v f => AnimSlice_ v t f -> [f]
getSliceFrames AnimSlice { animSliceDir = dir, animSliceFrames = frames }
| AnimDirForward <- dir = G.toList frames
| otherwise = G.toList $ G.reverse frames
getRealFrames :: G.Vector v f => [SteppedAnim_ v t f] -> [f]
getRealFrames = fmap \case
SteppedAnim { steppedAnimValue = a } -> animFrame a
validDur :: AnimDuration_ t -> Bool
validDur = \case
AnimDurationDefault -> True
AnimDurationScaled {} -> True
AnimDurationTotal {} -> False
AnimDurationEachFrame {} -> True
AnimDurationEachFrameFromTotal {} -> False
invalidInfiniteDur :: AnimDuration_ t -> Bool
invalidInfiniteDur = \case
AnimDurationDefault -> False
AnimDurationScaled {} -> False
AnimDurationTotal {} -> True
AnimDurationEachFrame {} -> False
AnimDurationEachFrameFromTotal {} -> True
type FiniteTestAnim :: Type
newtype FiniteTestAnim = FiniteTestAnim (TestAnim 1 0)
deriving newtype (Arbitrary, Show)
type InfiniteTestAnim :: Type
newtype InfiniteTestAnim = InfiniteTestAnim (TestAnim 0 1)
deriving newtype (Arbitrary, Show)
type TestAnim :: Nat -> Nat -> Type
newtype TestAnim nf ni = TestAnim (Anim_ V.Vector Rational AnimFrame)
deriving newtype (Show)
instance (KnownNat nf, KnownNat ni) => Arbitrary (TestAnim nf ni) where
arbitrary :: Gen (TestAnim nf ni)
arbitrary = do
TestAnimDuration dur <- arbitrary
TestAnimBuilder builder <- arbitrary @(TestAnimBuilder nf ni)
pure $ TestAnim $ buildAnim dur builder
type FiniteTestAnimBuilder :: Type
newtype FiniteTestAnimBuilder = FiniteTestAnimBuilder (TestAnimBuilder 1 0)
deriving newtype (Arbitrary, Show)
type InfiniteTestAnimBuilder :: Type
newtype InfiniteTestAnimBuilder = InfiniteTestAnimBuilder (TestAnimBuilder 0 1)
deriving newtype (Arbitrary, Show)
type TestAnimBuilder :: Nat -> Nat -> Type
newtype TestAnimBuilder nf ni = TestAnimBuilder (AnimBuilder_ V.Vector Rational AnimFrame)
deriving newtype (Semigroup, Show)
instance (KnownNat nf, KnownNat ni) => Arbitrary (TestAnimBuilder nf ni) where
arbitrary :: Gen (TestAnimBuilder nf ni)
arbitrary = do
sconcat . NonEmpty.fromList <$> listOf1 do
mkBuilder <- genMkAnimBuilder
TestAnimSlice slice <- arbitrary
TestAnimRepeat rep <- arbitrary @(TestAnimRepeat nf ni)
pure $ TestAnimBuilder $ repeatAnim rep $ mkBuilder slice
type TestAnimSlice :: Type
newtype TestAnimSlice = TestAnimSlice (AnimSlice_ V.Vector Rational AnimFrame)
deriving newtype (Show)
instance Arbitrary TestAnimSlice where
arbitrary :: Gen TestAnimSlice
arbitrary = do
TestAnimDir dir <- arbitrary
len <- chooseInt (1, 3)
frameDurs <- do
positiveDurs <- fmap getPositive <$> vectorOf len arbitrary
pure $ V.fromListN len positiveDurs
frames <- do
rects <- vectorOf len genAnimFrame
pure $ V.fromListN len rects
pure $ TestAnimSlice AnimSlice
{ animSliceDir = dir
, animSliceFrameDurs = frameDurs
, animSliceFrames = frames
}
type TestAnimDuration :: Type
newtype TestAnimDuration = TestAnimDuration (AnimDuration_ Rational)
deriving stock (Show)
instance Arbitrary TestAnimDuration where
arbitrary :: Gen TestAnimDuration
arbitrary =
TestAnimDuration <$> frequency
[ (4, pure AnimDurationDefault)
, (1, AnimDurationScaled . getNonNegative <$> arbitrary)
, (1, AnimDurationTotal . getPositive <$> arbitrary)
, (1, AnimDurationEachFrame . getNonNegative <$> arbitrary)
, (1, AnimDurationEachFrameFromTotal . getPositive <$> arbitrary)
]
type TestAnimDir :: Type
newtype TestAnimDir = TestAnimDir AnimDir
instance Arbitrary TestAnimDir where
arbitrary :: Gen TestAnimDir
arbitrary = TestAnimDir <$> oneof [pure AnimDirForward, pure AnimDirBackward]
type TestAnimRepeat :: Nat -> Nat -> Type
newtype TestAnimRepeat nf ni = TestAnimRepeat AnimRepeat
deriving newtype (Show)
instance (KnownNat nf, KnownNat ni) => Arbitrary (TestAnimRepeat nf ni) where
arbitrary :: Gen (TestAnimRepeat nf ni)
arbitrary =
TestAnimRepeat <$> frequency
[ ( fromInteger $ natVal $ Proxy @nf
, AnimRepeatCount . getNonNegative <$> arbitrary
)
, ( fromInteger $ natVal $ Proxy @ni
, pure AnimRepeatForever
)
]
genMkAnimBuilder :: Gen (AnimSlice_ V.Vector Rational AnimFrame -> AnimBuilder_ V.Vector Rational AnimFrame)
genMkAnimBuilder = arbitrary <&> \case
False -> fromAnimSlice
True -> pingpongAnimSlice
genAnimFrame :: Arbitrary a => Gen (AnimFrame_ a)
genAnimFrame = AnimFrame <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
fromSteppedAnim :: SteppedAnim_ v t f -> (AnimStatus, Anim_ v t f)
fromSteppedAnim SteppedAnim { steppedAnimStatus = as, steppedAnimValue = a } = (as, a)