packages feed

data-findcycle-0.1.0.0: test/Main.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

import Control.Applicative ((<*>))
import Data.FindCycle
import Data.Foldable (Foldable, find, foldMap, toList)
import Data.Functor ((<$>))
import Data.Maybe
import Data.Numbers.Primes
import Test.Tasty
import Test.Tasty.QuickCheck
import Prelude hiding (Foldable, foldMap, (<$>), (<*>))

data Cycle = Cycle
    { cycMu, cycLambda :: Int
    , cycF :: Integer -> Integer
    , cycX0 :: Integer
    }

instance Show Cycle where
    show Cycle{..} = unwords ["Cycle", show cycMu, show cycLambda]

instance Arbitrary Cycle where
    arbitrary = do
        (Positive muPlusLambda) <- scaled smooth arbitrary
        mu <- choose (0, muPlusLambda - 1)
        let m = fromJust $ find (> fromIntegral muPlusLambda) primes
        -- TODO: just pick using Large suchThat mod m /= 0?
        let nonZeroModM = choose (1, m - 1)
        (f, x0) <- mkF mu (muPlusLambda - mu) m <$> nonZeroModM <*> nonZeroModM
        return (Cycle mu (muPlusLambda - mu) f x0)
      where
        scaled f g = sized $ \x -> resize (f x) g
        smooth s = max 1 (round $ (10 ** 5 :: Double) ** (fromIntegral s / 100))
        mkF mu lambda m a b = (f, g 0)
          where
            n = fromIntegral $ mu + lambda
            g i = (a * i + b) `mod` m
            f x
                | i < n - 1 = g (i + 1)
                | otherwise = g (fromIntegral mu)
              where
                i = (modInv a m * ((x - b) `mod` m)) `mod` m
        modInv 1 _ = 1
        modInv x y = (a * y + 1) `div` x
          where
            a = x - modInv (y `mod` x) x

data AlgClass a = AlgClass {algDef :: a, algAlt :: [a]}
instance Foldable AlgClass where
    foldMap f AlgClass{..} = foldMap f (algDef : algAlt)

data Labeled a = Labeled String a

totalAlgs :: AlgClass (Labeled (CycleFinder Integer))
totalAlgs =
    AlgClass
        (Labeled "naiveHashable" naiveHashable)
        [Labeled "naiveOrd" naiveOrd]

partialAlgs :: AlgClass (Labeled (CycleFinder Integer))
partialAlgs =
    AlgClass
        (Labeled "nivash" nivash)
        [ Labeled "brent" brent
        , Labeled "floyd" floyd
        ]

defAlg :: CycleFinder Integer
defAlg = alg
  where
    AlgClass (Labeled _ alg) _ = partialAlgs

type Runner r = Cycle -> r
type Extractor = Runner (Int, Int, ([Integer], [Integer]))

extractors :: AlgClass (Labeled (CycleFinder Integer -> Extractor))
extractors =
    AlgClass
        (Labeled "findCycleExtract" findExtract)
        [Labeled "unsafeFindCycleFromList" unsafeFromList]
  where
    findExtract alg Cycle{..} = findCycleExtract alg cycF cycX0
    unsafeFromList alg Cycle{..} =
        unsafeFindCycleFromList alg (iterate cycF cycX0)

defExtractor :: CycleFinder Integer -> Extractor
defExtractor = alg
  where
    AlgClass (Labeled _ alg) _ = extractors

discardExtract :: Runner (a, a, b) -> Runner (a, a)
discardExtract f c = (mu, lambda)
  where
    (mu, lambda, _) = f c

prop_algCorrect :: CycleFinder Integer -> Cycle -> Property
prop_algCorrect alg Cycle{..} = findCycle alg cycF cycX0 === (cycMu, cycLambda)

prop_algsAgree :: CycleFinder Integer -> CycleFinder Integer -> Cycle -> Property
prop_algsAgree a b Cycle{..} =
    findCycleExtract a cycF cycX0 === findCycleExtract b cycF cycX0

prop_runnersAgree :: (Eq r, Show r) => Runner r -> Runner r -> Cycle -> Property
prop_runnersAgree a b c = a c === b c

prop_muUpperBound :: CycleFinder Integer -> Cycle -> Property
prop_muUpperBound alg Cycle{..} =
    counterexample ("Should be at least " ++ show cycMu) $
        fst (findCycle alg cycF cycX0) >= cycMu

prop_extract :: CycleFinder Integer -> Cycle -> Property
prop_extract alg Cycle{..} =
    (length pre, length cyc, pre ++ cyc ++ cyc)
        === (mu, lambda, take (mu + 2 * lambda) (iterate cycF cycX0))
  where
    (mu, lambda, (pre, cyc)) = findCycleExtract alg cycF cycX0

prop_cycleExp :: CycleFinder Integer -> Cycle -> Property
prop_cycleExp alg Cycle{..} =
    property $ \(NonNegative n) ->
        counterexample ("n=" ++ show n) $
            g (fromIntegral n)
                === xs !! (n - max 0 (n - cycMu - ((n - cycMu) `mod` cycLambda)))
  where
    g = cycleExp alg cycF cycX0
    xs = iterate cycF cycX0

prop_cycleExpsAgree :: CycleFinder Integer -> Cycle -> Property
prop_cycleExpsAgree alg Cycle{..} =
    property $ \(NonNegative (n :: Int)) ->
        counterexample ("n=" ++ show n) $
            g (fromIntegral n) === g' (fromIntegral n)
  where
    g = cycleExp alg cycF cycX0
    g' = cycleExp' alg cycF cycX0

prop_finite :: CycleFinder Integer -> Cycle -> Property
prop_finite alg Cycle{..} =
    unsafeFindCycleFromList (minimalMu alg) (take cycMu xs)
        === (cycMu, 0, (take cycMu xs, []))
  where
    xs = iterate cycF cycX0

tests :: TestTree
tests =
    testGroup
        "Data.FindCycle minimal tests"
        [ let AlgClass (Labeled name alg) _ = totalAlgs
          in testProperty
                (name ++ " is correct")
                (prop_algCorrect alg)
        , testGroup
            "All total algorithms agree"
            [ testProperty (refName ++ " ~ " ++ name) (prop_algsAgree refAlg alg)
            | let AlgClass (Labeled refName refAlg) algs = totalAlgs
            , Labeled name alg <- algs
            ]
        , testGroup
            "Total agrees with minimalMu of partial"
            [ testProperty
                (refName ++ " ~ minimalMu " ++ algName)
                (prop_algsAgree refAlg (minimalMu alg))
            | let AlgClass (Labeled refName refAlg) _ = totalAlgs
            , Labeled algName alg <- toList partialAlgs
            ]
        , testGroup
            "mu upper bound for partial"
            [ testProperty name (prop_muUpperBound alg)
            | Labeled name alg <- toList partialAlgs
            ]
        , testProperty
            "minimalMu is idempotent"
            (prop_algsAgree (minimalMu defAlg) (minimalMu (minimalMu defAlg)))
        , testGroup
            "Extractors agree"
            [ testProperty
                (refName ++ " ~ " ++ rName)
                (prop_runnersAgree (refR defAlg) (r defAlg))
            | let AlgClass (Labeled refName refR) rs = extractors
            , Labeled rName r <- rs
            ]
        , testProperty
            "findCycle agrees with extractors"
            ( prop_runnersAgree
                (discardExtract (defExtractor defAlg))
                (\Cycle{..} -> findCycle defAlg cycF cycX0)
            )
        , testProperty "extraction is correct" (prop_extract defAlg)
        , testProperty "cycleExp matches naive iteration" (prop_cycleExp defAlg)
        , testProperty "cycleExp' agrees with cycleExp " (prop_cycleExpsAgree defAlg)
        , testGroup
            "finite lists"
            [ testProperty name (prop_finite alg)
            | Labeled name alg <- toList totalAlgs ++ toList partialAlgs
            ]
        ]

main :: IO ()
main = defaultMain tests