ac-library-hs-1.2.2.0: test/Tests/Extra/MultiSet.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
module Tests.Extra.MultiSet (tests) where
import AtCoder.Extra.MultiSet qualified as MS
import Control.Monad (foldM_, when)
import Control.Monad.Primitive (PrimMonad, PrimState)
import Control.Monad.ST (RealWorld)
import Data.IntMap qualified as IM
import Data.IntSet qualified as IS
import Data.Vector.Algorithms.Intro qualified as VAI
import Data.Vector.Unboxed qualified as VU
import System.IO.Unsafe (unsafePerformIO)
import Test.Hspec
import Test.QuickCheck.Monadic as QCM
import Test.Tasty
import Test.Tasty.Hspec
import Test.Tasty.QuickCheck as QC
spec_invalid :: IO TestTree
spec_invalid = testSpec "capacity limit" $ do
it "throws error 1" $ do
ms <- MS.new @_ 1
MS.inc ms 0
MS.inc ms 0
MS.inc ms 1 `shouldThrow` anyException
it "throws error 2" $ do
ms <- MS.new @_ 2
MS.inc ms 0
MS.inc ms 1
MS.inc ms 2 `shouldThrow` anyException
it "throws error 2" $ do
ms <- MS.new @_ 2
MS.inc ms 0
MS.inc ms 1
MS.delete ms 1
MS.inc ms 2 `shouldThrow` anyException
data Init = Init
{ n :: {-# UNPACK #-} !Int,
ref0 :: !(IM.IntMap Int),
msM :: !(IO (MS.MultiSet RealWorld))
}
instance Show Init where
show Init {..} = show (n, ref0)
instance QC.Arbitrary Init where
arbitrary = do
n <- QC.chooseInt (1, 10)
pure $ Init n IM.empty (MS.new n)
data Query
= Member Int
| NotMember Int
| Lookup Int
| Inc Int
| -- Safe* are performed to not create negative count
SafeDec Int
| SafeAdd Int Int
| SafeSub Int Int
| Insert Int Int
| Delete Int
deriving (Show)
-- | Arbitrary return type for the `Query` result.
data Result
= None
| B Bool
| I Int
| M (Maybe Int)
deriving (Show, Eq)
queryGen :: Int -> QC.Gen Query
queryGen n = do
QC.oneof
[ Member <$> keyGen,
NotMember <$> keyGen,
Lookup <$> keyGen,
Inc <$> keyGen,
SafeDec <$> keyGen,
SafeAdd <$> keyGen <*> deltaGen,
SafeSub <$> keyGen <*> deltaGen,
Insert <$> keyGen <*> insertValGen,
Delete <$> keyGen
]
where
keyGen = QC.chooseInt (-n, n)
deltaGen = QC.chooseInt (-n, n)
insertValGen = QC.chooseInt (1, n)
-- | containers. (referencial implementation)
handleRef :: IM.IntMap Int -> Query -> (IM.IntMap Int, Result)
handleRef im q = case q of
Member key -> (im, B $ IM.member key im)
NotMember key -> (im, B $ IM.notMember key im)
Lookup key -> (im, M $ IM.lookup key im)
Inc key -> (IM.insertWith (+) key 1 im, None)
SafeDec key -> case IM.lookup key im of
Just 1 -> (IM.delete key im, None)
Just n -> (IM.insert key (n - 1) im, None)
Nothing -> (im, None)
SafeAdd key dx -> case IM.lookup key im of
Just n | n + dx == 0 -> (IM.delete key im, None)
Just n | n + dx > 0 -> (IM.insert key (n + dx) im, None)
Nothing | dx > 0 -> (IM.insert key dx im, None)
_ -> (im, None)
SafeSub key dx -> case IM.lookup key im of
Just n | n - dx == 0 -> (IM.delete key im, None)
Just n | n - dx > 0 -> (IM.insert key (n - dx) im, None)
Nothing | -dx > 0 -> (IM.insert key (-dx) im, None)
_ -> (im, None)
Insert key val -> (IM.insert key val im, None)
Delete key -> (IM.delete key im, None)
-- | ac-library-hs.
handleAcl :: (HasCallStack, PrimMonad m) => MS.MultiSet (PrimState m) -> Query -> m Result
handleAcl ms q = case q of
Member key -> B <$> MS.member ms key
NotMember key -> B <$> MS.notMember ms key
Lookup key -> M <$> MS.lookup ms key
Inc key -> do
MS.inc ms key
pure None
SafeDec key -> do
b <- MS.member ms key
when b $ do
MS.dec ms key
pure None
SafeAdd key dx -> do
MS.lookup ms key >>= \case
Just n | n + dx >= 0 -> do
MS.add ms key dx
Nothing | dx >= 0 -> do
MS.add ms key dx
_ -> pure ()
pure None
SafeSub key dx -> do
MS.lookup ms key >>= \case
Just n | n - dx >= 0 -> do
MS.sub ms key dx
Nothing | -dx >= 0 -> do
MS.sub ms key dx
_ -> pure ()
pure None
Insert key val -> do
MS.insert ms key val
pure None
Delete key -> do
MS.delete ms key
pure None
-- | Ensures the capacity limit.
passQuery :: Int -> IS.IntSet -> Query -> Bool
passQuery limit is (Inc k) = IS.member k is || IS.size is < limit
passQuery limit is (SafeAdd k dx) = IS.member k is || IS.size is < limit || dx <= 0
passQuery limit is (SafeSub k dx) = IS.member k is || IS.size is < limit || dx >= 0
passQuery limit is (Insert k _) = IS.member k is || IS.size is < limit
passQuery _ _ _ = True
-- | Records used keys.
recordKey :: IS.IntSet -> Query -> IS.IntSet
recordKey is (Inc k) = IS.insert k is
recordKey is (SafeAdd k dx) | dx > 0 = IS.insert k is
recordKey is (SafeSub k dx) | dx < 0 = IS.insert k is
recordKey is (Insert k _) = IS.insert k is
recordKey is _ = is
-- TODO: record used key
prop_randomTest :: Init -> QC.Property
prop_randomTest Init {..} = QCM.monadicIO $ do
ms <- QCM.run msM
q <- QCM.pick $ QC.chooseInt (1, 5 * n)
qs <- QCM.pick $ QC.vectorOf q (queryGen n)
foldM_
( \(!ref, !keys) query -> do
if passQuery n keys query
then do
-- run the query
let (!ref', !expected) = handleRef ref query
res <- QCM.run $ handleAcl ms query
QCM.assertWith (expected == res) $ show (query, expected, res)
-- check the map contents:
let assocsE = VU.modify (VAI.sortBy compare) (VU.fromList (IM.assocs ref'))
assocs <- QCM.run $ VU.modify (VAI.sortBy compare) <$> MS.unsafeAssocs ms
QCM.assertWith (assocsE == assocs) $ show ("- assocs", assocsE, assocs)
let keysE = VU.modify (VAI.sortBy compare) (VU.fromList (IM.keys ref'))
keys_ <- QCM.run $ VU.modify (VAI.sortBy compare) <$> MS.unsafeKeys ms
QCM.assertWith (keysE == keys_) $ show ("- keys", keysE, keys_)
let elemsE = VU.modify (VAI.sortBy compare) (VU.fromList (IM.elems ref'))
elems <- QCM.run $ VU.modify (VAI.sortBy compare) <$> MS.unsafeElems ms
QCM.assertWith (elemsE == elems) $ show ("- elems", elemsE, elems)
let sizeE = IM.size ref'
size <- QCM.run $ MS.size ms
QCM.assertWith (sizeE == size) $ show ("- size", sizeE, size)
pure (ref', recordKey keys query)
else pure (ref, keys)
)
(ref0, IS.empty)
qs
-- TODO: test invariant
tests :: [TestTree]
tests =
[ unsafePerformIO spec_invalid,
QC.testProperty "random test" prop_randomTest
]