packages feed

these-0.7.6: test/Tests.hs

{-# LANGUAGE CPP                 #-}
{-# LANGUAGE DeriveFoldable      #-}
{-# LANGUAGE DeriveFunctor       #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE MonoLocalBinds      #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections       #-}
module Main (main) where

import Prelude ()
import Prelude.Compat

import Control.Applicative       (ZipList (..))
import Control.Lens              (folded, toListOf)
import Control.Monad             (join)
import Data.Bifunctor            (bimap)
import Data.Foldable             (toList)
import Data.Functor.Compose      (Compose (..))
import Data.Functor.Identity     (Identity (..))
import Data.HashMap.Strict       (HashMap)
import Data.IntMap               (IntMap)
import Data.List                 (nub)
import Data.Map                  (Map)
import Data.Maybe                (mapMaybe)
import Data.Semigroup            (Semigroup (..))
import Data.Sequence             (Seq)
import Data.Traversable          (fmapDefault, foldMapDefault)
import Test.QuickCheck
       (Arbitrary (..), CoArbitrary (..), Property, elements, once, (.&&.),
       (===))
import Test.QuickCheck.Function  (Fun (..))
import Test.QuickCheck.Instances ()
import Test.Tasty                (TestTree, defaultMain, testGroup)
import Test.Tasty.QuickCheck     (testProperty)

import qualified Data.Aeson            as Aeson
import qualified Data.Binary           as Binary
import qualified Data.Functor.Product  as P
import qualified Data.IntMap           as IntMap
import qualified Data.Map              as Map
import qualified Data.Vector           as V
import qualified Test.Tasty.QuickCheck as QC

import Data.Align
import Data.Align.Indexed
import Data.Align.Key
import Data.These

-- For old GHC to work
data Proxy (a :: * -> *) = Proxy

main :: IO ()
main = defaultMain tests

tests :: TestTree
tests = testGroup "Tests"
    [ theseProps
    , alignProps
    , alignWithKeyProps
    , crosswalkProps
    , testGroup "Semigroup"
        [ semigroupLaws "These" (These "x" "y")
        , semigroupLaws "SearchResult" (ScannedAndFound "x" "y")
        , monoidLaws "List" "x" -- to disallow
        ]
    ]

theseProps :: TestTree
theseProps = testGroup "These"
    [ functorProps
    , traversableProps
    , testProperty "Map value laziness property" mapStrictnessProp
    , testProperty "IntMap value laziness property" intmapStrictnessProp
    , aesonProps
    , binaryProps
    ]

crosswalkProps :: TestTree
crosswalkProps = testGroup "Crosswalk"
    [ crosswalkLaws "[]" (Proxy :: Proxy [])
    -- , crosswalkLaws "Identity" (Proxy :: Proxy Identity)
    , crosswalkLaws "Maybe" (Proxy :: Proxy Maybe)
    , crosswalkLaws "These" (Proxy :: Proxy (These Int))
    , crosswalkLaws "Seq" (Proxy :: Proxy Seq)
    , crosswalkLaws "Vector" (Proxy :: Proxy V.Vector)
    , crosswalkLaws "(,) Int" (Proxy :: Proxy ((,) Int))
    , crosswalkLaws "Compose [] []" (Proxy :: Proxy (Compose [] []))
    ]

alignProps :: TestTree
alignProps = testGroup "Align"
    [ dataAlignLaws "[]" (Proxy :: Proxy [])
    , dataAlignLaws "HashMap String" (Proxy :: Proxy (HashMap String))
    , dataAlignLaws "IntMap" (Proxy :: Proxy IntMap)
    , dataAlignLaws "Map Char" (Proxy :: Proxy (Map Char))
    , dataAlignLaws "Maybe" (Proxy :: Proxy Maybe)
    , dataAlignLaws "Product [] Maybe" (Proxy :: Proxy (P.Product [] Maybe))
    , dataAlignLaws "Seq" (Proxy :: Proxy Seq)
    , dataAlignLaws "Vector" (Proxy :: Proxy V.Vector)
    , dataAlignLaws "ZipList" (Proxy :: Proxy ZipList)
    -- , dataAlignLaws "WrongMap" (Proxy :: Proxy (WrongMap Char))
    -- weird objects:
    -- , dataAlignLaws "Const String" (Proxy :: Proxy (Const String))
    , dataAlignLaws "R" (Proxy :: Proxy R)
    -- , dataAlignLaws "Weirdmap" (Proxy :: Proxy (WeirdMap Char))
    ]

alignWithKeyProps :: TestTree
alignWithKeyProps = testGroup "AlignWithKey / AlignWithIndex"
    [ testProperty "example" $ once $ exampleK
    , testProperty "example" $ once $ exampleI
    ]
  where
    exampleK = alignWithKey (,) "foo" "quux" === exampleV
    exampleI = ialign (,) "foo" "quux" === exampleV

    exampleV =
        [ (0, These 'f' 'q')
        , (1, These 'o' 'u')
        , (2, These 'o' 'u')
        , (3, That 'x')
        ]

-- Even the `align` is/was defined using strict combinators, this will still work:
mapStrictnessProp :: [Int] -> [Int] -> Bool
mapStrictnessProp lkeys rkeys = length (nub lkeys) <= Map.size (lhs `align` rhs)
  where lhs  = Map.fromList $ fmap (,loop) lkeys
        rhs  = Map.fromList $ fmap (,loop) rkeys

        loop :: Int
        loop = error "break"

intmapStrictnessProp :: [Int] -> [Int] -> Bool
intmapStrictnessProp lkeys rkeys = length (nub lkeys) <= IntMap.size (lhs `align` rhs)
  where lhs  = IntMap.fromList $ fmap (,loop) lkeys
        rhs  = IntMap.fromList $ fmap (,loop) rkeys

        loop :: Int
        loop = error "break"

functorIdentityProp :: (Functor f, Eq (f a), Show (f a)) => f a -> Property
functorIdentityProp x = fmap id x === x

functorCompositionProp :: (Functor f, Show (f c), Eq (f c)) => f a -> Fun a b -> Fun b c -> Property
functorCompositionProp x (Fun _ f) (Fun _ g) = fmap g (fmap f x) === fmap (g . f) x

functorProps :: TestTree
functorProps = testGroup "Functor"
  [ QC.testProperty "identity" (functorIdentityProp :: These Int Bool -> Property)
  , QC.testProperty "composition" (functorCompositionProp :: These Int Int -> Fun Int Int -> Fun Int Int -> Property)
  ]

traversableIdentityProp :: (Traversable t, Eq (t a), Show (t a)) => t a -> Property
traversableIdentityProp x = traverse Identity x === Identity x

traversableCompositionProp :: (Traversable t, Applicative g, Applicative f, Show (Compose f g (t b)), Eq (Compose f g (t b)))
                           => t a1 -> Fun a1 (f a) -> Fun a (g b) -> Property
traversableCompositionProp x (Fun _ f) (Fun _ g) = traverse (Compose . fmap g . f) x === (Compose . fmap (traverse g) . traverse f $ x)

traversableFunctorProp :: (Traversable f, Show (f b), Eq (f b)) => f a -> Fun a b -> Property
traversableFunctorProp x (Fun _ f) = fmap f x === fmapDefault f x

traversableFoldableProp :: (Monoid m, Traversable t, Show m, Eq m) => t a -> Fun a m -> Property
traversableFoldableProp x (Fun _ f) = foldMap f x === foldMapDefault f x

traversableProps :: TestTree
traversableProps = testGroup "Traversable"
  [ QC.testProperty "identity" (traversableIdentityProp :: These Int Bool -> Property)
  , QC.testProperty "composition" (traversableCompositionProp :: These Bool Int -> Fun Int (Maybe Int) -> Fun Int (Either Bool Int) -> Property)
  , QC.testProperty "functor" (traversableFunctorProp :: These Bool Int -> (Fun Int Int) -> Property)
  , QC.testProperty "foldable" (traversableFoldableProp :: These Bool Int -> (Fun Int [Bool]) -> Property)
  ]

-------------------------------------------------------------------------------
-- Align laws
-------------------------------------------------------------------------------

-- Data.Align

-- (\`align` nil) = fmap This
-- (nil \`align`) = fmap That
-- join align = fmap (join These)
-- align (f \<$> x) (g \<$> y) = bimap f g \<$> align x y
-- alignWith f a b = f \<$> align a b
--
-- We also require a sixth property, when f is Foldable.
dataAlignLaws :: forall (f :: * -> *). ( Align f, Foldable f
                                       , Eq (f (These Int Int))
                                       , Show (f (These Int Int))
                                       , Eq (f (These (These Int Int) Int))
                                       , Show (f (These (These Int Int) Int))
                                       , CoArbitrary (These Int Int)
                                       , Arbitrary (f Int)
                                       , Eq (f Int)
                                       , Show (f Int))
              => String
              -> Proxy f
              -> TestTree
dataAlignLaws name _ = testGroup ("Data.Align laws: " <> name)
    [ QC.testProperty "right identity" rightIdentityProp
    , QC.testProperty "left identity" leftIdentityProp
    , QC.testProperty "join" joinProp
    , QC.testProperty "bimap" bimapProp
    , QC.testProperty "alignWith" alignWithProp
    , QC.testProperty "assoc" assocProp
    , QC.testProperty "alignToList" alignToListProp
    ]
  where
    rightIdentityProp :: f Int -> Property
    rightIdentityProp xs = (xs `align` (nil :: f Int)) === fmap This xs

    leftIdentityProp :: f Int -> Property
    leftIdentityProp xs = ((nil :: f Int) `align` xs) === fmap That xs

    joinProp :: f Int -> Property
    joinProp xs = join align xs === fmap (join These) xs

    bimapProp :: f Int -> f Int -> Fun Int Int -> Fun Int Int -> Property
    bimapProp xs ys (Fun _ f) (Fun _ g) =
      align (f <$> xs) (g <$> ys) === (bimap f g <$> align xs ys)

    alignWithProp :: f Int -> f Int -> Fun (These Int Int) Int -> Property
    alignWithProp xs ys (Fun _ f) =
      alignWith f xs ys === (f <$> align xs ys)

    assocProp :: f Int -> f Int -> f Int -> Property
    assocProp xs ys zs = rhs === lhs
      where
        rhs = (xs `align` ys) `align` zs
        lhs = fmap assoc $ xs `align` (ys `align` zs)

    alignToListProp :: f Int -> f Int -> Property
    alignToListProp xs ys =
        toList xs === toListOf (folded . here) xys
        .&&.
        toList xs === mapMaybe justHere (toList xys)
        .&&.
        toList ys === toListOf (folded . there) xys
      where
        xys = align xs ys

---------------------------------------------------------------------------
-- WrongMap doesn't satisfy Align laws
-------------------------------------------------------------------------------

newtype WrongMap k v = WM (Map k v) deriving (Eq, Ord, Show, Functor, Foldable)

instance (Arbitrary k, Arbitrary v, Ord k) => Arbitrary (WrongMap k v) where
    arbitrary = WM <$> arbitrary
    shrink (WM m) = WM <$> shrink m

instance Ord k => Align (WrongMap k) where
    nil = WM Map.empty
    align (WM x) (WM y)
       | Map.null y = WM $ This <$> x
       | Map.null x = WM $ That <$> y
       | otherwise  = WM $ Map.intersectionWith These x y

-------------------------------------------------------------------------------
-- WeirdMap
-------------------------------------------------------------------------------

-- | Sequence-like __invalid__ 'Align' instance for Map.
--
-- Satisfies first five laws;
-- Doesn't satisfy /assoc/ or /toList/ laws.
--
newtype WeirdMap k v = WeirdMap (Map k v)
  deriving (Eq, Ord, Show, Functor, Foldable)

instance (Arbitrary k, Arbitrary v, Ord k) => Arbitrary (WeirdMap k v) where
    arbitrary = WeirdMap <$> arbitrary
    shrink (WeirdMap m) = WeirdMap <$> shrink m

instance Ord k => Align (WeirdMap k) where
    nil = WeirdMap Map.empty

    alignWith f (WeirdMap x) (WeirdMap y) = WeirdMap $ Map.fromList $
        alignWith g (Map.toList x) (Map.toList y)
      where
        g (This (k, a))         = (k, f (This a))
        g (That (k, a))         = (k, f (That a))
        g (These (k, a) (_, b)) = (k, f (These a b))

-------------------------------------------------------------------------------
-- Const is invalid Align with Monoid, we need Idemporent monoid!
-------------------------------------------------------------------------------

{-
instance Monoid a => Align (Const a) where
    nil = Const mempty
    align (Const a) (Const b) = Const (mappend a b)
-}

-------------------------------------------------------------------------------
-- R does satisfy Align laws, though is weird
-- https://github.com/isomorphism/these/issues/96
-------------------------------------------------------------------------------

newtype R a = Nest [[a]]
  deriving (Show, Eq, Ord, Functor, Foldable)

instance Align R where
    nil = Nest []

    align (Nest ass) (Nest bss)
        | null ass                = That <$> Nest bss
        | null bss                = This <$> Nest ass
        | shape ass == shape bss  = Nest $ zipWith (zipWith These) ass bss
        | otherwise               = Nest [align (concat ass) (concat bss)]
      where
        shape = fmap (() <$)

instance Arbitrary a => Arbitrary (R a) where
    arbitrary = Nest <$> arbitrary
    shrink (Nest xss) = Nest <$> shrink xss

data Index = I1 | I2 | I3 | I4
  deriving (Eq, Ord, Show, Enum, Bounded)

instance Arbitrary Index where
    arbitrary = elements [minBound .. maxBound]
    shrink I1 = []
    shrink I2 = [I1]
    shrink I3 = [I1, I2]
    shrink I4 = [I1, I2, I3]

-------------------------------------------------------------------------------
-- Crosswalk laws
-------------------------------------------------------------------------------

crosswalkLaws
    :: forall (t :: * -> *).
       ( Crosswalk t
       , Arbitrary (t Int)
       , Eq (t Int), Show (t Int)
       )
    => String
    -> Proxy t
    -> TestTree
crosswalkLaws name _ = testGroup ("Data.CrossWalk laws: " <> name)
  [ QC.testProperty "crosswalk (const nil) = const nil" firstLaw
  , QC.testProperty "crosswalk f = sequenceL . fmap f" secondLaw
  ]
  where
    -- f = Map Index
    -- a, b = Int
    firstLaw :: t Int -> Property
    firstLaw x = lhs === rhs
      where
        lhs = crosswalk (const nil) x
        rhs = const nil x :: Map Index (t Int)

    secondLaw :: Fun Int (Map Index Int) -> t Int -> Property
    secondLaw (Fun _ f) x = lhs === rhs
      where
        lhs = crosswalk f x
        rhs = sequenceL . fmap f $ x

-------------------------------------------------------------------------------
-- aeson
-------------------------------------------------------------------------------

aesonProps :: TestTree
aesonProps = testGroup "aeson"
    [ testProperty "roundtrip / direct" prop1
    , testProperty "roundtrip / toJSON" prop2
    ]
  where
    prop1 :: These Int String -> Property
    prop1 x = Just x === Aeson.decode (Aeson.encode x)

    prop2 :: These Int String -> Property
    prop2 x = Just x === Aeson.decode (Aeson.encode $ Aeson.toJSON x)

-------------------------------------------------------------------------------
-- binary
-------------------------------------------------------------------------------

binaryProps :: TestTree
binaryProps = testProperty "binary / roundtrip" prop
  where
    prop :: These Int String -> Property
    prop x = x === Binary.decode (Binary.encode x)

-------------------------------------------------------------------------------
-- SearchResult
-------------------------------------------------------------------------------

semigroupLaws
    :: forall a. (Semigroup a, Show a, Eq a, Arbitrary a)
    => String -> a -> TestTree
semigroupLaws name _ = testGroup ("Semigroup: " ++ name)
    [ QC.testProperty "associativity" assocProp
    ]
  where
    assocProp :: a -> a -> a -> Property
    assocProp x y z = (x <> y) <> z === x <> (y <> z)

monoidLaws
    :: forall a. (Monoid a, Show a, Eq a, Arbitrary a)
    => String -> a -> TestTree
monoidLaws name _ = testGroup ("Monoid: " ++ name)
    [ QC.testProperty "associativity" assocProp
    , QC.testProperty "left-identity" idLeftProp
    , QC.testProperty "right-identity" idRightProp
    ]
  where
    assocProp :: a -> a -> a -> Property
    assocProp x y z = (x `mappend` y) `mappend` z === x `mappend` (y `mappend` z)

    idLeftProp :: a -> Property
    idLeftProp x = mappend mempty x === x

    idRightProp :: a -> Property
    idRightProp x = mappend x mempty === x

-- | Either a, or b, or both a and b
--
-- See https://github.com/isomorphism/these/issues/80
data SearchResult a b = Scanned a | Found b | ScannedAndFound a b
  deriving (Eq, Ord, Show)

instance (Arbitrary a, Arbitrary b) => Arbitrary (SearchResult a b) where
    arbitrary = srFromThese <$> arbitrary

srFromThese :: These a b -> SearchResult a b
srFromThese (This a)    = Scanned a
srFromThese (That b)    = Found b
srFromThese (These a b) = ScannedAndFound a b

-- | Accumulate 'a's from left to right, until one 'b' is found
instance Semigroup a => Semigroup (SearchResult a b) where
    ScannedAndFound a b <> _ = ScannedAndFound a b
    Found b <> _ = Found b
    Scanned a <> Scanned a' = Scanned (a <> a')
    Scanned a <> Found b = ScannedAndFound a b
    Scanned a <> ScannedAndFound a' b = ScannedAndFound (a <> a') b

{-
-- almost lawful
instance Monoid a => Monoid (SearchResult a b) where
    mappend = (<>)
    mempty = Scanned mempty
-}