content: module Test.Pos.Util.TimerSpec
      ( spec) where
  
  import           Control.Concurrent.Async (async)
  import           Control.Concurrent.STM (newTVarIO, readTVar, retry, writeTVar)
  import           Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)
  import           Data.Time.Units (Microsecond, fromMicroseconds, toMicroseconds)
  import           Test.Hspec (Spec, describe, it)
  import           Test.QuickCheck (Gen, Property, choose, property)
  import           Test.QuickCheck.Monadic (assert, forAllM, monadicIO, run)
  import           Universum hiding (newTVarIO)
  
  import           Pos.Util.Timer
  
  nominalDiffTimeToMicroseconds :: POSIXTime -> Microsecond
  nominalDiffTimeToMicroseconds = fromMicroseconds . round . (* 1000000)
  
  chooseMsc :: (Microsecond, Microsecond) -> Gen Microsecond
  chooseMsc (a, b) = fromMicroseconds <$> choose (toMicroseconds a, toMicroseconds b)
  
  -- | A timer should wait at least given interval.
  prop_wait :: Property
  prop_wait =
      let tMIN = 250 :: Microsecond
          tMAX = 500 :: Microsecond
      in monadicIO
      $ forAllM (chooseMsc (tMIN, tMAX))
      $ \t -> do
          timer <- newTimer
          startTime <- run getPOSIXTime
          _ <- startTimer t timer
          _ <- atomically $ waitTimer timer
          endTime <- run getPOSIXTime
          let diffTime = nominalDiffTimeToMicroseconds (endTime - startTime)
          assert $ t <= diffTime
  
  -- | A timer should be additive: (re)-starting a timer @t1@ after @t0@
  -- microseconds should wait at least @t0 + t1@ microseconds.
  prop_additive :: Property
  prop_additive =
      let tMIN = 250 :: Microsecond
          tMAX = 500 :: Microsecond
      in monadicIO
      $ forAllM (chooseMsc (tMIN, tMAX))
      $ \tmax -> forAllM (chooseMsc (tMIN `div` 2 , tmax `div` 2))
      $ \tmin -> do
          timerMin <- newTimer
          timerMax <- newTimer
          hasBeenReset <- run $ newTVarIO True
          startTime <- run $
                 startTimer tmin timerMin
              >> startTimer tmax timerMax
              >> getPOSIXTime
          -- Set the 'timerMax' once the 'timerMin' expires.
          _ <- run $ async $
                 atomically (waitTimer timerMin)
              >> startTimer tmax timerMax
              >> atomically (writeTVar hasBeenReset True)
          -- Wait on the 'timerMax', and retry if the 'timerMin' has not expired
          -- and the 'timerMax' reset, to avoid the case in which the
          -- 'timerMax' finishes before it's reset by the above thread, causing
          -- a test failure.
          endTime <- run $
                 atomically (waitTimer timerMax >> readTVar hasBeenReset >>= flip unless retry)
              >> getPOSIXTime
          let diffTime = nominalDiffTimeToMicroseconds (endTime - startTime)
          assert $ tmax + tmin <= diffTime
  
  spec :: Spec
  spec = describe "Timer" $ do
      it "should wait" $ property prop_wait
      it "should be additive" $ property prop_additive
  