packages feed

alarmclock-0.6.0.1: test/Spec.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import           Test.Hspec

import           Control.Concurrent
import           Control.Concurrent.AlarmClock
import           Control.Concurrent.AlarmClock.TimeScale
import           Control.Concurrent.STM
import           Control.Monad
import           Data.IORef
import           Data.Proxy
import           Data.Time

main :: IO ()
main = hspec $ describe "Control.Concurrent.AlarmClock" $ do

  describe "Timescale" $ do
    let beforeTimescaleTest :: TimeScale t => IO (t,t)
        beforeTimescaleTest = do
          putStrLn "getting first time"
          t0 <- getAbsoluteTime
          threadDelay 500000
          putStrLn "getting second time"
          t1 <- getAbsoluteTime
          putStrLn "got times"
          return (t0, t1)

        timescaleSpec :: (TimeScale t, Eq t, Show t) => Proxy t -> SpecWith (t,t)
        timescaleSpec _ = do
          describe "earlierOf" $ do
            it "picks the first if earlier"  $ \(t0,t1) -> earlierOf t0 t1 `shouldBe` t0
            it "picks the second if earlier" $ \(t0,t1) -> earlierOf t1 t0 `shouldBe` t0

          describe "microsecondsDiff" $ do
            it "finds a difference of at least 500ms"
              $ \(t0,t1) -> microsecondsDiff t1 t0 `shouldSatisfy` (>= 500000)
            it "finds a difference of no more than 600ms"
              $ \(t0,t1) -> microsecondsDiff t1 t0 `shouldSatisfy` (<= 600000)
            it "finds a negative difference if args reversed"
              $ \(t0,t1) -> microsecondsDiff t0 t1 `shouldSatisfy` (<= (-500000))

    describe "UTCTime"   $ beforeAll beforeTimescaleTest $ timescaleSpec (Proxy :: Proxy UTCTime)
    describe "Monotonic" $ beforeAll beforeTimescaleTest $ timescaleSpec (Proxy :: Proxy MonotonicTime)

  let makeLog :: IO (a -> IO (), IO [a])
      makeLog = do
        logVar <- newIORef []
        return (modifyIORef logVar . (:), reverse <$> readIORef logVar)

  describe "AlarmClock" $ do
    let waitUntilUnset ac = do
          atomically $ guard . not =<< isAlarmSetSTM ac
          threadDelay 50000 -- alarm becomes unset before action has completed

    it "wakes up immediately on setAlarmNow" $ do
      (writeLog, readLog) <- makeLog
      withAlarmClock (\_ _ -> writeLog "alarm went off") $ \ac -> do
        setAlarmNow (ac :: AlarmClock UTCTime)
        threadDelay 100000
      readLog `shouldReturn` ["alarm went off"]

    it "wakes up a bit later using setAlarm" $ do
      (writeLog, readLog) <- makeLog
      withAlarmClock (\_ _ -> writeLog "alarm went off") $ \ac -> do
        setAlarm ac . addUTCTime 0.2 =<< getCurrentTime
        writeLog "waiting"
        threadDelay 100000
        writeLog "still waiting"
        threadDelay 200000
        writeLog "should have gone off by now"
      readLog `shouldReturn`
        [ "waiting"
        , "still waiting"
        , "alarm went off"
        , "should have gone off by now"
        ]

    it "wakes up at the earliest set time and no others" $ do
      (writeLog, readLog) <- makeLog
      withAlarmClock (\_ _ -> writeLog "alarm went off") $ \ac -> do
        setAlarm ac . addUTCTime 0.3 =<< getCurrentTime
        setAlarm ac . addUTCTime 0.2 =<< getCurrentTime
        writeLog "waiting"
        threadDelay 100000
        writeLog "still waiting"
        threadDelay 300000
        writeLog "should have gone off once by now"
      readLog `shouldReturn`
        [ "waiting"
        , "still waiting"
        , "alarm went off"
        , "should have gone off once by now"
        ]

    it "reports whether it is set or not" $ do
      (writeLog, readLog) <- makeLog
      withAlarmClock (\_ _ -> writeLog "alarm went off") $ \ac -> do
        let logIfSet = do
              currentlySet <- isAlarmSet ac
              writeLog $ if currentlySet then "alarm is set" else "alarm is not set"
        logIfSet
        setAlarm ac . addUTCTime 0.2 =<< getCurrentTime
        logIfSet
        writeLog "waiting"
        threadDelay 100000
        writeLog "still waiting"
        logIfSet
        threadDelay 200000
        writeLog "should have gone off by now"
        logIfSet
      readLog `shouldReturn`
        [ "alarm is not set"
        , "alarm is set"
        , "waiting"
        , "still waiting"
        , "alarm is set"
        , "alarm went off"
        , "should have gone off by now"
        , "alarm is not set"
        ]

    it "works within the STM monad" $ do
      (writeLog, readLog) <- makeLog
      withAlarmClock (\_ _ -> writeLog "alarm went off") $ \ac -> do
        now <- getCurrentTime
        atomically $ do
          guard . not =<< isAlarmSetSTM ac
          setAlarmSTM ac $ addUTCTime 0.1 now
          guard =<< isAlarmSetSTM ac
        writeLog "alarm is set"
        waitUntilUnset ac
        writeLog "alarm now not set again"
      readLog `shouldReturn`
        [ "alarm is set"
        , "alarm went off"
        , "alarm now not set again"
        ]

    it "can be set again once it goes off" $ do
      (writeLog, readLog) <- makeLog
      withAlarmClock (\_ _ -> writeLog "alarm went off") $ \ac -> do
        startTime <- getCurrentTime
        setAlarm ac $ addUTCTime 0.1 startTime
        writeLog "alarm is set"
        waitUntilUnset ac
        writeLog "alarm is not set"
        setAlarm ac $ addUTCTime 0.2 startTime
        writeLog "alarm is set"
        waitUntilUnset ac
        writeLog "alarm is not set"
      readLog `shouldReturn`
        [ "alarm is set"
        , "alarm went off"
        , "alarm is not set"
        , "alarm is set"
        , "alarm went off"
        , "alarm is not set"
        ]

    it "goes off immediately if set to go off in the past" $ do
      (writeLog, readLog) <- makeLog
      withAlarmClock (\_ _ -> writeLog "alarm went off") $ \ac -> do
        startTime <- getCurrentTime
        setAlarm ac $ addUTCTime (-0.1) startTime
        writeLog "alarm is set"
        waitUntilUnset ac
        writeLog "done"
        endTime <- getCurrentTime
        diffUTCTime endTime startTime `shouldSatisfy` (\t -> 0.0 <= t && t < 0.1)
      readLog `shouldReturn`
        [ "alarm is set"
        , "alarm went off"
        , "done"
        ]

    it "blocks destruction if the alarm is going off" $ do
      (writeLog, readLog) <- makeLog
      let alarmAction _ _ = do
            writeLog "alarm going off"
            threadDelay 200000
            writeLog "alarm finished going off"

      withAlarmClock alarmAction $ \ac -> do
        setAlarmNow (ac :: AlarmClock UTCTime)
        threadDelay 100000
        writeLog "destroying alarm clock"
      writeLog "alarm clock destroyed"
      readLog `shouldReturn`
        [ "alarm going off"
        , "destroying alarm clock"
        , "alarm finished going off"
        , "alarm clock destroyed"
        ]

    it "re-checks the time before going off" $
      withAlarmClock (\_ _ -> return ()) $ \ac -> do
        startTime <- getCurrentTime
        setAlarm ac $ AdjustingClock $ addUTCTime 0.3 startTime
        atomically $ guard . not =<< isAlarmSetSTM ac
        endTime <- getCurrentTime
        diffUTCTime endTime startTime `shouldSatisfy` (\t -> 0.3 <= t && t <= 0.5)

newtype AdjustingClock = AdjustingClock UTCTime deriving (Show, Eq, Ord)

instance TimeScale AdjustingClock where
  getAbsoluteTime        = AdjustingClock <$> getCurrentTime
  earlierOf              = min
  microsecondsDiff (AdjustingClock t1) (AdjustingClock t2) = 10000 + microsecondsDiff t1 t2 `div` 2