packages feed

rando-0.0.0.2: test/Test.hs

-- | It's obviously difficult to test nondeterministic functions.
--   Some of these methods are \"uncommon\" but they do the job.

{-# LANGUAGE ViewPatterns #-}

import Control.Monad
import qualified Data.List as L
import Data.Set (Set)
import qualified Data.Set as Set
import Test.Microspec hiding (shuffle)

import Rando

main :: IO ()
main = microspec $ do
   describe "shuffle" $ do

      -- This tests that there's no position in the list (e.g. first or last
      --   element) that's "stuck" (never changes position):
      it "at least sometimes, all elements change position" $ \(NonEmpty l) ->
         length (L.nub l) > 1 ==>
            monadicIO $ prop_sometimesAllElemsChange $ NonEmpty l

      -- This is apparently a very common symptom of off-by-one errors with
      --   this algorithm:
      it "sometimes has elements which don't change position" $
         monadicIO . prop_sometimesElemsDontChange

      it "produces roughly equal distributions" $
         monadicIO $ prop_roughlyEqualShuffleDistributions

   describe "pickOne" $ do
      it "draws from all elements of the list" $
         monadicIO . prop_pickOne_picksFromAllElems

-- L.nub so we're not confused by dupes:
prop_sometimesAllElemsChange :: NonEmptyList Int -> PropertyM IO Bool
prop_sometimesAllElemsChange (NonEmpty (L.nub -> l_u)) = do
   l_s <- run $ shuffle l_u
   if (and::[Bool]->Bool) $ zipWith (/=) l_s l_u
      then pure True
      else prop_sometimesAllElemsChange $ NonEmpty l_u

prop_sometimesElemsDontChange :: NonEmptyList Int -> PropertyM IO Bool
prop_sometimesElemsDontChange (NonEmpty (L.nub -> l_u)) = do
   l_s <- run $ shuffle l_u
   if (or::[Bool]->Bool) $ zipWith (==) l_s l_u
      then pure True
      else prop_sometimesElemsDontChange $ NonEmpty l_u

prop_roughlyEqualShuffleDistributions :: PropertyM IO Bool
prop_roughlyEqualShuffleDistributions = do
   allRuns <- run $ replicateM numRuns (shuffle ([1..9] :: [Int]))
   -- TODO: this weirdly passed when 'shuffle' was undefined! Why??
   let thingWereReallyTesting = (and::[Bool]->Bool) $
          map (closeTo5 . average) $
             L.transpose allRuns
       -- TODO: it passes even with this!!:
       forceMore =
          let (a:b:_) = allRuns
          in a /= b
   -- run $ print $ take 2 allRuns
   pure $ thingWereReallyTesting && forceMore
 where
   numRuns :: Int
   numRuns = 10000

   average :: [Int] -> Double
   average l = toEnum (sum l) / toEnum numRuns

   closeTo5 :: Double -> Bool
   closeTo5 n = abs (n - 5) < 0.02

prop_pickOne_picksFromAllElems :: Set Int -> PropertyM IO Bool
prop_pickOne_picksFromAllElems ourList =
   pickFromTest' Set.empty
 where
   pickFromTest' :: Set Int -> PropertyM IO Bool
   pickFromTest' onesWeveSeenSoFar = do
      if ourList == onesWeveSeenSoFar
         then pure True
         else do
            nextVal <- run $ pickOne $ Set.toList ourList
            pickFromTest' (Set.insert nextVal onesWeveSeenSoFar)