packages feed

migrant-core-0.1.0.0: test/Database/Migrant/Run_Tests.hs

{-#LANGUAGE ScopedTypeVariables #-}
{-#LANGUAGE OverloadedStrings #-}

module Database.Migrant.Run_Tests
where

import Test.Tasty
import Test.Tasty.QuickCheck
import Test.Tasty.HUnit

import Data.Text (Text)
import qualified Data.Text as Text
import Data.List (isSuffixOf, isPrefixOf)
import Data.IORef
import Control.Exception
import System.IO.Unsafe (unsafePerformIO)

import Database.Migrant.Run
import Database.Migrant.Driver.Class
import Database.Migrant.MigrationName
import Database.Migrant.MigrationName_Orphans
import Database.Migrant.Run_Orphans

tests = testGroup "Database.Migrant.Run"
    [ testGroup "makePlan"
        makePlanTests
    , testGroup "executePlan"
        executePlanTests
    , testGroup "migrateTests"
        migrateTests
    ]

makePlanTests :: [TestTree]
makePlanTests =
  [ testProperty "same inputs, empty diff" propSameInputEmptyDiff
  , testProperty "appended becomes diff" propAppendIsDiff
  , testProperty "removed becomes diff" propRemoveIsDiff
  , testProperty "diff length never longer than inputs combined" propDiffNotLargerThanInputs
  , testProperty "removeBeforeAdd" propRemoveBeforeAdd
  ]

propSameInputEmptyDiff migs =
  makePlan migs migs == []

propAppendIsDiff base ext =
  makePlan (base ++ ext) base == [(MigrateUp, m) | m <- ext]

propRemoveIsDiff base ext =
  makePlan base (base ++ ext) == [(MigrateDown, m) | m <- ext]

propDiffNotLargerThanInputs target current =
  length (makePlan target current) <= length target + length current

propRemoveBeforeAdd ext1 ext2 =
  all not ((==) <$> ext1 <*> ext2) ==>
  makePlan ext1 ext2 ==
  [(MigrateDown, e) | e <- ext2] ++
  [(MigrateUp, e) | e <- ext1]

executePlanTests :: [TestTree]
executePlanTests =
  [ testProperty "updates only" propExecuteUpdatesOnly
  , testProperty "rollbacks only" propExecuteRollbacksOnly
  , testProperty "mixed updates/rollbacks" propExecuteMixed
  ]

propExecuteUpdatesOnly target =
  let expected = [("MARK MigrateUp " <> unpackMigrationName n) | n <- target]
      plan = [(MigrateUp, n) | n <- target]
      getMigration _ _ = return ()
      logged = runChat (executePlan plan getMigration getMigration) []
  in logged == expected

propExecuteRollbacksOnly target =
  let expected = [("MARK MigrateDown " <> unpackMigrationName n) | n <- target]
      plan = [(MigrateDown, n) | n <- target]
      getMigration _ _ = return ()
      logged = runChat (executePlan plan getMigration getMigration) []
  in logged == expected

propExecuteMixed plan =
  let expected = [("MARK " <> Text.pack (show d) <> " " <> unpackMigrationName n) | (d, n) <- plan]
      getMigration _ _ = return ()
      logged = runChat (executePlan plan getMigration getMigration) []
  in logged == expected

migrateTests :: [TestTree]
migrateTests =
  [ testCase "transactionally do nothing" testMigrateNothing
  , testCase "transactionally do something" testMigrateSomething
  , testCase "roll back on host failure" testMigrateError
  ]

testMigrateNothing :: Assertion
testMigrateNothing = do
  let target = []
      up name = logChatQuery ("MigrateUp " <> unpackMigrationName name)
      down name = logChatQuery ("MigrateDown " <> unpackMigrationName name)
  actual <- runChatIO (migrate target up down) [[]]
  let expected =
        [ "BEGIN TRANSACTION"
        , "INIT"
        , "COMMIT TRANSACTION"
        ]
  assertEqual "migration log" expected actual

testMigrateSomething :: Assertion
testMigrateSomething = do
  let target = ["foo", "bar"]
      situation = ["bar", "quux"]
      up name = logChatQuery ("MigrateUp " <> unpackMigrationName name)
      down name = logChatQuery ("MigrateDown " <> unpackMigrationName name)
  actual <- runChatIO (migrate target up down) (repeat situation)
  let expected =
        [ "BEGIN TRANSACTION"
        , "INIT"
        , "MigrateDown bar"
        , "MARK MigrateDown bar"
        , "MigrateDown quux"
        , "MARK MigrateDown quux"
        , "MigrateUp foo"
        , "MARK MigrateUp foo"
        , "MigrateUp bar"
        , "MARK MigrateUp bar"
        , "COMMIT TRANSACTION"
        ]
  assertEqual "migration log" expected actual

testMigrateError :: Assertion
testMigrateError = do
  let target = ["foo"]
      situation = ["bar"]
      up name driver = throwIO DummyError
      down name = logChatQuery ("MigrateDown " <> unpackMigrationName name)
  actual <- runChatIO (migrate target up down) (repeat situation)
  let expected =
        [ "BEGIN TRANSACTION"
        , "INIT"
        , "MigrateDown bar"
        , "MARK MigrateDown bar"
        , "ROLLBACK TRANSACTION"
        ]
  assertEqual "migration log" expected actual

-- | A mockup driver we can use to test the migration planner/executor
data ChatDriver =
  ChatDriver
    { queries :: IORef [Text]
    , responses :: IORef [[MigrationName]]
    }

logChatQuery :: Text -> ChatDriver -> IO ()
logChatQuery msg ChatDriver { queries = queries } = 
  modifyIORef queries (++ [msg])

runChatIO :: (ChatDriver -> IO ()) -> [[MigrationName]] -> IO [Text]
runChatIO action responses = do
  queriesRef <- newIORef []
  responsesRef <- newIORef responses
  action (ChatDriver queriesRef responsesRef)
  readIORef queriesRef

runChat :: (ChatDriver -> IO ()) -> [[MigrationName]] -> [Text]
runChat action responses = unsafePerformIO (runChatIO action responses)

instance Driver ChatDriver where
  withTransaction action driver = do
    logChatQuery "BEGIN TRANSACTION" driver
    catch
        (do
          result <- action driver
          logChatQuery "COMMIT TRANSACTION" driver
          return result
        )
        (\(e :: SomeException) -> do
          logChatQuery "ROLLBACK TRANSACTION" driver
          return undefined
        )

  initMigrations driver = do
    logChatQuery "INIT" driver

  markUp name driver = do
    logChatQuery ("MARK MigrateUp " <> unpackMigrationName name) driver

  markDown name driver = do
    logChatQuery ("MARK MigrateDown " <> unpackMigrationName name) driver

  getMigrations driver = do
    (r:rs) <- readIORef (responses driver)
    writeIORef (responses driver) rs
    return r

data DummyError = DummyError
  deriving (Show)

instance Exception DummyError where