fradrive/test/Handler/Utils/ExamSpec.hs
Wolfgang Witt 48ee67f6d6 chore(test): allow valid nullResults
ExamRoomMatriculation sometimes shows incorrect ranges
2021-03-15 10:45:37 +00:00

253 lines
15 KiB
Haskell

{-# OPTIONS_GHC -Wwarn #-}
module Handler.Utils.ExamSpec (spec) where
import TestImport
import Data.Universe (Universe, Finite, universeF)
import ModelSpec () -- instance Arbitrary User
import Test.Hspec.QuickCheck (prop)
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.CaseInsensitive as CI
import Handler.Utils.Exam
instance Arbitrary ExamOccurrence where
arbitrary = ExamOccurrence
<$> arbitrary -- examOccurrenceExam
<*> arbitrary -- examOccurrenceName
<*> arbitrary -- examOccurrenceRoom
<*> arbitrary -- examOccurrenceRoomHidden
<*> frequency [(let d = fromIntegral i in ceiling $ 100 * exp(- d*d / 50), pure i) | i <- [10 ..1000]] -- examOccurrenceCapacity
<*> arbitrary -- examOccurrenceStart
<*> arbitrary -- examOccurrenceEnd
<*> arbitrary -- examOccurrenceDescription
data Preselection = NoPreselection | SomePreselection
deriving stock (Show, Bounded, Enum)
deriving anyclass (Universe, Finite)
data Nudges = NoNudges | SmallNudges | LargeNudges
deriving stock (Show, Bounded, Enum)
deriving anyclass (Universe, Finite)
uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
uncurry3 f (a, b, c) = f a b c
uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
uncurry4 f (a, b, c, d) = f a b c d
-- | Kopie der User-Datenstruktur beschränkt auf interessante Felder (besser verständliche Show-Instanz)
data UserProperties = UserProperties {pSurname :: Text, pMatrikelnummer :: Maybe Text}
deriving (Show)
extractProperties :: User -> UserProperties
extractProperties User {userSurname, userMatrikelnummer} = UserProperties userSurname userMatrikelnummer
-- function Handler.Utils.examAutoOccurrence
-- examAutoOccurrence :: forall seed.
-- Hashable seed
-- => seed
-- -> ExamOccurrenceRule
-- -> ExamAutoOccurrenceConfig
-- -> Map ExamOccurrenceId Natural
-- -> Map UserId (User, Maybe ExamOccurrenceId)
-- -> (Maybe (ExamOccurrenceMapping ExamOccurrenceId), Map UserId (Maybe ExamOccurrenceId))
-- examAutoOccurrence (hash -> seed) rule ExamAutoOccurrenceConfig{..} occurrences users
spec :: Spec
spec = do
describe "examAutoOccurrence" $ do
describe "Surname" $ do
let rule :: ExamOccurrenceRule
rule = ExamRoomSurname
forM_ universeF $ \nudges -> describe (show nudges) $
forM_ universeF $ \preselection ->
prop (show preselection) $ propertyTest rule nudges preselection
describe "Matriculation" $ do
let rule :: ExamOccurrenceRule
rule = ExamRoomMatriculation
forM_ universeF $ \nudges -> describe (show nudges) $
forM_ universeF $ \preselection ->
prop (show preselection) $ propertyTest rule nudges preselection
-- TODO test with ExamRoomManual, ExamRoomFifo, (ExamRoomSurname), ExamRoomMatriculation, ExamRoomRandom
where
seed :: ()
seed = ()
propertyTest :: ExamOccurrenceRule -> Nudges -> Preselection -> Gen Property
propertyTest rule nudges preselection = do
(users, occurrences) <- genUsersWithOccurrences preselection
eaocNudge <- case nudges of
NoNudges -> pure Map.empty
SmallNudges -> let nudgeFrequency = [(10, 0), (5, 1), (5, -1), (3, 2), (3, -2), (1, 3), (1, -3)]
in foldM (genNudge nudgeFrequency) Map.empty $ Map.keys occurrences
LargeNudges -> let nudgeFrequency = [(7, 0), (5, 3), (5, -3), (3, 6), (3, -6), (2, 9), (2, -9),
(2, 11), (2, -11), (1, 15), (1,-15), (1, 17), (1, -17)]
in foldM (genNudge nudgeFrequency) Map.empty $ Map.keys occurrences
let config :: ExamAutoOccurrenceConfig
config = def {eaocNudge}
(maybeMapping, userMap) = examAutoOccurrence seed rule config occurrences users
pure $ ioProperty $ do
-- every (relevant) user got assigned a room
shouldBe (length userMap) (length users)
let foldFn :: (UserId, Maybe ExamOccurrenceId) -> Bool -> Bool
foldFn _userMapping False = False
foldFn (_userId, Just _occurrenceId) True = True
foldFn (userId, Nothing) True
= (rule == ExamRoomMatriculation)
-- every user with a userMatrikelnummer got a room
-- fail on unknown user
|| (fromMaybe False $ isNothing . userMatrikelnummer . fst <$> Map.lookup userId users)
shouldSatisfy userMap $ foldr foldFn True . Map.toList
-- no room is overfull
let userProperties :: Map UserId (UserProperties, Maybe ExamOccurrenceId)
userProperties = Map.map (first extractProperties) users
shouldSatisfy (userProperties, occurrences, userMap) $ uncurry3 fitsInRooms
case maybeMapping of
-- all users match the shown ranges
(Just occurrenceMapping)
-> shouldSatisfy (rule, userProperties, occurrenceMapping, userMap) $ uncurry4 showsCorrectRanges
-- is a nullResult justified?
Nothing -> shouldSatisfy (rule, userProperties, occurrences) $ uncurry3 isNullResultJustified
-- | generate users without any pre-assigned rooms
genUsersWithOccurrences :: Preselection -> Gen (Map UserId (User, Maybe ExamOccurrenceId), Map ExamOccurrenceId Natural)
genUsersWithOccurrences preselection = do
rawUsers <- scale (50 *) $ listOf1 $ Entity <$> arbitrary <*> arbitrary
occurrences <- genOccurrences $ length rawUsers
-- user surnames anpassen, sodass interessante instanz
users <- fmap Map.fromList $ forM rawUsers $ \Entity {entityKey, entityVal} -> do
userSurname <- elements surnames
assignedRoom <- case preselection of
NoPreselection -> pure Nothing
SomePreselection -> frequency [(97, pure Nothing), (3, elements $ map Just $ Map.keys occurrences)]
pure (entityKey, (entityVal {userSurname}, assignedRoom))
pure (users, occurrences)
genOccurrences :: Int -> Gen (Map ExamOccurrenceId Natural)
genOccurrences numUsers = do
-- TODO is this realistic?
-- extra space to get nice borders
extraSpace <- elements [numUsers `div` 5 .. numUsers `div` 2]
let totalSpaceRequirement = fromIntegral $ numUsers + extraSpace
createOccurrences acc
| sum (map snd acc) < totalSpaceRequirement = do
Entity {entityKey, entityVal} <- Entity <$> arbitrary <*> arbitrary
createOccurrences $ (entityKey, examOccurrenceCapacity entityVal) : acc
| otherwise = pure acc
Map.fromList <$> createOccurrences []
genNudge :: [(Int, Integer)] -> Map ExamOccurrenceId Integer -> ExamOccurrenceId -> Gen (Map ExamOccurrenceId Integer)
genNudge nudgesList acc occurrenceId
= fmap appendNonZero $ frequency $ map (second pure) nudgesList
where
appendNonZero :: Integer -> Map ExamOccurrenceId Integer
appendNonZero 0 = acc
appendNonZero nudge = Map.insert occurrenceId nudge acc
-- name list copied from test/Database/Fill.hs
surnames :: [Text]
surnames = [ "Smith", "Johnson", "Williams", "Brown"
, "Jones", "Miller", "Davis", "Garcia"
, "Rodriguez", "Wilson", "Martinez", "Anderson"
, "Taylor", "Thomas", "Hernandez", "Moore"
, "Martin", "Jackson", "Thompson", "White"
, "Lopez", "Lee", "Gonzalez", "Harris"
, "Clark", "Lewis", "Robinson", "Walker"
, "Perez", "Hall", "Young", "Allen"
]
occurrenceMap :: Map UserId (Maybe ExamOccurrenceId) -> Map ExamOccurrenceId [UserId]
occurrenceMap userMap = foldl' (\acc (userId, maybeOccurrenceId) -> appendJust maybeOccurrenceId userId acc)
Map.empty $ Map.toAscList userMap
where
appendJust :: Maybe ExamOccurrenceId -> UserId -> Map ExamOccurrenceId [UserId] -> Map ExamOccurrenceId [UserId]
appendJust Nothing _userId = id
appendJust (Just occurrenceId) userId = Map.insertWith (++) occurrenceId [userId]
-- | Are all rooms large enough to hold all assigned Users?
fitsInRooms :: Map UserId (UserProperties, Maybe ExamOccurrenceId)
-> Map ExamOccurrenceId Natural
-> Map UserId (Maybe ExamOccurrenceId)
-> Bool
fitsInRooms userProperties occurrences userMap
= all roomIsBigEnough $ Map.toAscList $ occurrenceMap userMap
where
roomIsBigEnough :: (ExamOccurrenceId, [UserId]) -> Bool
roomIsBigEnough (roomId, userIds) = case lookup roomId occurrences of
Nothing -> False
(Just capacity) -> length userIds <= fromIntegral capacity
|| all (isJust . snd) (Map.restrictKeys userProperties $ Set.fromList userIds)
-- | Does the (currently surname) User fit to the displayed ranges?
-- Users with a previously assigned room are checked if the assignment stays the same, regardless of the ranges.
showsCorrectRanges :: ExamOccurrenceRule
-> Map UserId (UserProperties, Maybe ExamOccurrenceId)
-> ExamOccurrenceMapping ExamOccurrenceId
-> Map UserId (Maybe ExamOccurrenceId)
-> Bool
showsCorrectRanges rule userProperties (examOccurrenceMappingMapping -> mappingRanges) userMap
= all userFitsInRange $ Map.toAscList $ occurrenceMap userMap
where
userFitsInRange :: (ExamOccurrenceId, [UserId]) -> Bool
userFitsInRange (roomId, userIds) = flip all userIds $ \userId ->
case (Map.lookup roomId mappingRanges, Map.lookup userId userProperties) of
(_maybeRanges, Just (_userProperty, Just fixedRoomId))
-> roomId == fixedRoomId
(Just ranges, Just (UserProperties {pSurname, pMatrikelnummer}, Nothing))
-> any fitsInRange ranges
where
ciTag :: Maybe [CI Char]
ciTag = map CI.mk . Text.unpack <$> case rule of
ExamRoomSurname -> Just pSurname
ExamRoomMatriculation -> pMatrikelnummer
_rule -> Nothing
fitsInRange :: ExamOccurrenceMappingDescription -> Bool
fitsInRange ExamOccurrenceMappingRange {eaomrStart, eaomrEnd} = case ciTag of
Nothing -> True
(Just tag) -> eaomrStart <= tag && (take (length eaomrEnd) tag <= eaomrEnd)
fitsInRange ExamOccurrenceMappingSpecial {}
= True -- FIXME what is the meaning of special?
_otherwise -> False
-- | Is mapping impossible?
isNullResultJustified :: ExamOccurrenceRule
-> Map UserId (UserProperties, Maybe ExamOccurrenceId)
-> Map ExamOccurrenceId Natural -> Bool
isNullResultJustified rule userProperties occurrences
= noRelevantUsers rule userProperties || mappingImpossible rule userProperties occurrences
noRelevantUsers :: ExamOccurrenceRule -> Map UserId (UserProperties, Maybe ExamOccurrenceId) -> Bool
noRelevantUsers rule = null . Map.filter (isRelevantUser rule)
isRelevantUser :: ExamOccurrenceRule -> (UserProperties, Maybe ExamOccurrenceId) -> Bool
isRelevantUser _rule (_user, Just _assignedRoom) = False
isRelevantUser rule (UserProperties {pSurname, pMatrikelnummer}, Nothing) = case rule of
ExamRoomSurname -> not $ null pSurname
ExamRoomMatriculation -> maybe False (not . null) pMatrikelnummer
_rule -> False
mappingImpossible :: ExamOccurrenceRule -> Map UserId (UserProperties, Maybe ExamOccurrenceId) -> Map ExamOccurrenceId Natural -> Bool
mappingImpossible
rule
userProperties@(sort . map (ruleProperty rule . fst) . Map.elems . Map.filter (isRelevantUser rule) -> relevantUsers)
(map snd . Map.toList . adjustOccurrences userProperties -> occurrences') = go relevantUsers occurrences'
where
go :: [Maybe Text] -> [Natural] -> Bool
go [] _occurrences = False
go _remainingUsers [] = True
go remainingUsers (0:t) = go remainingUsers t
go remainingUsers@(h:_t) (firstOccurrence:laterOccurrences)
| nextUsers <= firstOccurrence = go remainingUsers' $ firstOccurrence - nextUsers : laterOccurrences
| otherwise = go remainingUsers laterOccurrences
where
(fromIntegral . length -> nextUsers, remainingUsers') = span (== h) remainingUsers
ruleProperty :: ExamOccurrenceRule -> UserProperties -> Maybe Text
ruleProperty rule = case rule of
ExamRoomSurname -> Just . pSurname
ExamRoomMatriculation -> pMatrikelnummer
_rule -> const Nothing
-- copied and adjusted from Hander.Utils.Exam
adjustOccurrences :: Map UserId (UserProperties, Maybe ExamOccurrenceId) -> Map ExamOccurrenceId Natural -> Map ExamOccurrenceId Natural
-- ^ reduce room capacity for every pre-assigned user by 1
adjustOccurrences userProperties occurrences = foldl' (flip $ Map.update predToPositive) occurrences $ Map.mapMaybe snd userProperties
-- FIXME what about capacity-0 in occurrences?
-- what if the first word is too big for the first room?
predToPositive :: Natural -> Maybe Natural
predToPositive 0 = Nothing
predToPositive 1 = Nothing
predToPositive n = Just $ pred n