packages feed

haxl-2.4.0.0: tests/WriteTests.hs

-- Copyright (c) 2014-present, Facebook, Inc.
-- All rights reserved.
--
-- This source code is distributed under the terms of a BSD license,
-- found in the LICENSE file.

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
module WriteTests (tests) where

import Test.HUnit

import Control.Concurrent
import Data.Either
import Data.Foldable
import Data.Hashable
import Data.IORef
import qualified Data.Text as Text

import Haxl.Core.Monad (mapWrites, flattenWT)
import Haxl.Core
import Haxl.Prelude as Haxl

-- A fake data source
data SimpleDataSource a where
  GetNumber :: SimpleDataSource Int

deriving instance Eq (SimpleDataSource a)
deriving instance Show (SimpleDataSource a)
instance ShowP SimpleDataSource where showp = show

instance Hashable (SimpleDataSource a) where
  hashWithSalt s GetNumber = hashWithSalt s (0 :: Int)

instance StateKey SimpleDataSource where
  data State SimpleDataSource = DSState

instance DataSourceName SimpleDataSource where
  dataSourceName _ = "SimpleDataSource"

instance DataSource u SimpleDataSource where
  fetch _st _flags _usr  = SyncFetch $ Haxl.mapM_ fetch1
    where
    fetch1 :: BlockedFetch SimpleDataSource -> IO ()
    fetch1 (BlockedFetch GetNumber m) =
      threadDelay 1000 >> putSuccess m 37

newtype SimpleWrite = SimpleWrite Text
  deriving (Eq, Show, Ord)

assertEqualIgnoreOrder ::
  (Eq a, Show a, Ord a) => String -> [a] -> [a] -> Assertion
assertEqualIgnoreOrder msg lhs rhs =
  assertEqual msg (sort lhs) (sort rhs)

doInnerWrite :: GenHaxl u SimpleWrite Int
doInnerWrite = do
  tellWrite $ SimpleWrite "inner"
  return 0

doOuterWrite :: GenHaxl u SimpleWrite Int
doOuterWrite = do
  tellWrite $ SimpleWrite "outer1"

  doWriteMemo <- newMemoWith doInnerWrite
  let doMemoizedWrite = runMemo doWriteMemo
  _ <- doMemoizedWrite
  _ <- doMemoizedWrite

  tellWrite $ SimpleWrite "outer2"

  return 1

doNonMemoWrites :: GenHaxl u SimpleWrite Int
doNonMemoWrites = do
  tellWrite $ SimpleWrite "inner"
  tellWriteNoMemo $ SimpleWrite "inner not memo"
  return 0

writeSoundness :: Test
writeSoundness = TestCase $ do
  let numReps = 4

  -- do writes without memoization
  env1 <- emptyEnv ()
  (allRes, allWrites) <- runHaxlWithWrites env1 $
    Haxl.sequence (replicate numReps doInnerWrite)

  assertBool "Write Soundness 1" $
    allWrites == replicate numReps (SimpleWrite "inner")
  assertBool "Write Soundness 2" $ allRes == replicate numReps 0

  -- do writes with memoization
  env2 <- emptyEnv ()

  (memoRes, memoWrites) <- runHaxlWithWrites env2 $ do
    doWriteMemo <- newMemoWith doInnerWrite
    let memoizedWrite = runMemo doWriteMemo

    Haxl.sequence (replicate numReps memoizedWrite)

  assertBool "Write Soundness 3" $
    memoWrites == replicate numReps (SimpleWrite "inner")
  assertBool "Write Soundness 4" $ memoRes == replicate numReps 0

  -- do writes with interleaved memo
  env3 <- emptyEnv ()

  (ilRes, ilWrites) <- runHaxlWithWrites env3 $ do
    doWriteMemo <- newMemoWith doInnerWrite
    let memoizedWrite = runMemo doWriteMemo

    Haxl.sequence $ replicate numReps (doInnerWrite *> memoizedWrite)

  assertBool "Write Soundness 5" $
    ilWrites == replicate (2*numReps) (SimpleWrite "inner")
  assertBool "Write Soundness 6" $ ilRes == replicate numReps 0

  -- do writes with nested memo
  env4 <- emptyEnv ()

  (nestRes, nestWrites) <- runHaxlWithWrites env4 $ do
    doWriteMemo' <- newMemoWith doOuterWrite
    let memoizedWrite' = runMemo doWriteMemo'

    Haxl.sequence (replicate numReps memoizedWrite')

  let expWrites =
        [ SimpleWrite "outer1"
        , SimpleWrite "inner"
        , SimpleWrite "inner"
        , SimpleWrite "outer2"
        ]
  assertBool "Write Soundness 7" $
    nestWrites == fold (replicate numReps expWrites)
  assertBool "Write Soundness 8" $ nestRes == replicate numReps 1

  -- do both kinds of writes without memoization
  env5 <- emptyEnv ()
  (allRes, allWrites) <- runHaxlWithWrites env5 $
    Haxl.sequence (replicate numReps doNonMemoWrites)

  assertBool "Write Soundness 9" $
    allWrites == replicate numReps (SimpleWrite "inner") ++
      replicate numReps (SimpleWrite "inner not memo")
  assertBool "Write Soundness 10" $ allRes == replicate numReps 0

  -- do both kinds of writes with memoization
  env6 <- emptyEnv ()

  (memoRes, memoWrites) <- runHaxlWithWrites env6 $ do
    doWriteMemo <- newMemoWith doNonMemoWrites
    let memoizedWrite = runMemo doWriteMemo

    Haxl.sequence (replicate numReps memoizedWrite)

  -- "inner not memo" only appears once in this test
  assertBool "Write Soundness 11" $
    memoWrites == replicate numReps (SimpleWrite "inner") ++
      [SimpleWrite "inner not memo"]
  assertBool "Write Soundness 12" $ memoRes == replicate numReps 0

writeLogsCorrectnessTest :: Test
writeLogsCorrectnessTest = TestLabel "writeLogs_correctness" $ TestCase $ do
  e <- emptyEnv ()
  (_ , wrts) <- runHaxlWithWrites e doNonMemoWrites
  assertEqualIgnoreOrder "Expected writes" [SimpleWrite "inner",
    SimpleWrite "inner not memo"] wrts
  wrtsNoMemo <- readIORef $ writeLogsRefNoMemo e
  wrtsMemo <- readIORef $ writeLogsRef e
  assertEqualIgnoreOrder "WriteTree not empty" [] $ flattenWT wrtsNoMemo
  assertEqualIgnoreOrder "WriteTree not empty" [] $ flattenWT wrtsMemo

mapWritesTest :: Test
mapWritesTest = TestLabel "mapWrites" $ TestCase $ do
  let func (SimpleWrite s) = SimpleWrite $ Text.toUpper s
  env0 <- emptyEnv ()
  (res0, wrts0) <- runHaxlWithWrites env0 $ mapWrites func doNonMemoWrites
  assertEqual "Expected computation result" 0 res0
  assertEqualIgnoreOrder "Writes correctly transformed" [SimpleWrite "INNER",
    SimpleWrite "INNER NOT MEMO"] wrts0

  -- Writes should behave the same inside and outside mapWrites
  env1 <- emptyEnv ()
  (res1, wrts1) <- runHaxlWithWrites env1 $ do
    outer <- doOuterWrite
    outerMapped <- mapWrites func doOuterWrite
    return $ outer == outerMapped
  assertBool "Results are identical" res1
  assertEqualIgnoreOrder
    "Writes correctly transformed, non-transformed writes preserved"
    [ SimpleWrite "outer1", SimpleWrite "inner"
    , SimpleWrite "inner", SimpleWrite "outer2"
    , SimpleWrite "OUTER1", SimpleWrite "INNER"
    , SimpleWrite "INNER", SimpleWrite "OUTER2"
    ]
    wrts1

  -- Memoization behaviour should be unaffected
  env2 <- emptyEnv ()
  (_res2, wrts2) <- runHaxlWithWrites env2 $ do
    writeMemo <- newMemoWith doNonMemoWrites
    let doWriteMemo = runMemo writeMemo
    _ <- mapWrites func doWriteMemo
    _ <- doWriteMemo
    return ()
  -- "inner not memo" should appear only once
  assertEqualIgnoreOrder
    "Write correctly transformed under memoization"
    [ SimpleWrite "INNER"
    , SimpleWrite "inner"
    , SimpleWrite "INNER NOT MEMO"
    ]
    wrts2

  -- Same as previous, but the non-mapped computation is run first
  env3 <- emptyEnv ()
  (_res3, wrts3) <- runHaxlWithWrites env3 $ do
    writeMemo <- newMemoWith doNonMemoWrites
    let doWriteMemo = runMemo writeMemo
    _ <- doWriteMemo
    _ <- mapWrites func doWriteMemo
    return ()
  -- "inner not memo" should appear only once
  assertEqualIgnoreOrder
    "Flipped: Write correctly transformed under memoization"
    [ SimpleWrite "inner"
    , SimpleWrite "INNER"
    , SimpleWrite "inner not memo"
    ]
    wrts3

  -- inner computation performs no writes
  env4 <- emptyEnv ()
  (res4, wrts4) <- runHaxlWithWrites env4 $
    mapWrites func (return (0 :: Int))
  assertEqual "No Writes: Expected computation result" 0 res4
  assertEqualIgnoreOrder "No writes" [] wrts4

  -- inner computation throws an exception
  env5 <- emptyEnv ()
  (res5, wrts5) <- runHaxlWithWrites env5 $ mapWrites func $ try $ do
    _ <- doNonMemoWrites
    _ <- throw (NotFound "exception")
    return 0
  assertBool "Throw: Expected Computation Result" $ isLeft
    (res5 :: Either HaxlException Int)
  assertEqualIgnoreOrder
    "Datasource writes correctly transformed"
    [ SimpleWrite "INNER"
    , SimpleWrite "INNER NOT MEMO"
    ]
    wrts5

  -- inner computation calls a datasource
  env6 <- initEnv (stateSet DSState stateEmpty) ()
  (res6, wrts6) <- runHaxlWithWrites env6 $ mapWrites func $ do
    _ <- doNonMemoWrites
    dataFetch GetNumber

  assertEqual "Datasource: Expected Computation Result" 37 res6
  assertEqualIgnoreOrder
    "Datasource writes correctly transformed"
    [ SimpleWrite "INNER"
    , SimpleWrite "INNER NOT MEMO"
    ]
    wrts6

  -- inner computation calls a datasource, flipped calls
  env7 <- initEnv (stateSet DSState stateEmpty) ()
  (res7, wrts7) <- runHaxlWithWrites env7 $ mapWrites func $ do
    df <- dataFetch GetNumber
    _ <- doNonMemoWrites
    return df

  assertEqual "Flipped Datasource: Expected Computation Result" 37 res7
  assertEqualIgnoreOrder
    "Flipped: Datasource writes correctly transformed"
    [ SimpleWrite "INNER"
    , SimpleWrite "INNER NOT MEMO"
    ]
    wrts7

tests = TestList
  [ TestLabel "Write Soundness" writeSoundness,
    TestLabel "writeLogs_correctness" writeLogsCorrectnessTest,
    TestLabel "mapWrites" mapWritesTest
  ]