packages feed

pg-migrate-1.0.0.0: test/unit/Test/Plan.hs

module Test.Plan (tests) where

import Data.List.NonEmpty qualified as NonEmpty
import Data.Set qualified as Set
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text.Encoding
import Database.PostgreSQL.Migrate
import Database.PostgreSQL.Migrate.Internal
import PgMigrate.Prelude
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
import Test.Tasty.QuickCheck qualified as QuickCheck

tests :: TestTree
tests =
  testGroup
    "plan"
    [ testCase "explicit plans preserve caller order" testExplicitOrder,
      QuickCheck.testProperty
        "explicit plans preserve every permutation of unrelated components"
        propertyExplicitOrder,
      testCase "duplicate component names have a distinct error" testDuplicateComponent,
      testCase "duplicate local migration names have a distinct error" testDuplicateMigration,
      testCase "identical local names in different components are valid" testLocalNameScope,
      testCase "missing dependencies have a distinct error" testMissingDependency,
      testCase "dependencies after consumers have a distinct error" testInvalidOrder,
      testCase "dependency cycles have a distinct error" testCycle,
      testCase "stable resolution changes only constrained order" testStableResolution,
      testCase "stable resolution is deterministic" testStableResolutionDeterministic,
      testCase "plan descriptions include positions and migration metadata" testDescription
    ]

testExplicitOrder :: IO ()
testExplicitOrder = do
  let queue = makeComponent "queue" Set.empty ["0001"]
      eventStore = makeComponent "event-store" Set.empty ["0001"]
  plan <- requireRight (migrationPlan (queue :| [eventStore]))
  componentNames (planDescription plan)
    @?= requireNames ["queue", "event-store"]

propertyExplicitOrder :: QuickCheck.Property
propertyExplicitOrder =
  QuickCheck.forAll (QuickCheck.shuffle ["alpha", "bravo", "charlie"]) $ \rawNames ->
    let textNames = Text.pack <$> rawNames
        components = fmap (\name -> makeComponent name Set.empty ["0001"]) textNames
     in case NonEmpty.nonEmpty components of
          Nothing -> QuickCheck.property False
          Just nonEmptyComponents ->
            case migrationPlan nonEmptyComponents of
              Left err -> QuickCheck.counterexample (show err) False
              Right plan ->
                componentNames (planDescription plan)
                  QuickCheck.=== requireNames textNames

testDuplicateComponent :: IO ()
testDuplicateComponent = do
  duplicateName <- requireRight (componentName "duplicate")
  assertPlanError
    (DuplicateComponentName duplicateName)
    ( migrationPlan
        ( makeComponent "duplicate" Set.empty ["0001"]
            :| [makeComponent "duplicate" Set.empty ["0002"]]
        )
    )

testDuplicateMigration :: IO ()
testDuplicateMigration = do
  owner <- requireRight (componentName "owner")
  duplicateName <- requireRight (migrationName "0001")
  assertPlanError
    (DuplicateMigrationName owner duplicateName)
    (migrationPlan (makeComponent "owner" Set.empty ["0001", "0001"] :| []))

testLocalNameScope :: IO ()
testLocalNameScope =
  assertRight
    ( migrationPlan
        ( makeComponent "alpha" Set.empty ["0001"]
            :| [makeComponent "bravo" Set.empty ["0001"]]
        )
    )

testMissingDependency :: IO ()
testMissingDependency = do
  consumer <- requireRight (componentName "consumer")
  missing <- requireRight (componentName "missing")
  assertPlanError
    (MissingComponentDependency consumer missing)
    ( migrationPlan
        (makeComponent "consumer" (Set.singleton "missing") ["0001"] :| [])
    )

testInvalidOrder :: IO ()
testInvalidOrder = do
  consumer <- requireRight (componentName "consumer")
  dependency <- requireRight (componentName "dependency")
  assertPlanError
    (DependencyPlacedAfterConsumer consumer dependency)
    ( migrationPlan
        ( makeComponent "consumer" (Set.singleton "dependency") ["0001"]
            :| [makeComponent "dependency" Set.empty ["0001"]]
        )
    )

testCycle :: IO ()
testCycle =
  case migrationPlan
    ( makeComponent "alpha" (Set.singleton "bravo") ["0001"]
        :| [makeComponent "bravo" (Set.singleton "alpha") ["0001"]]
    ) of
    Left (ComponentDependencyCycle _) -> pure ()
    other -> assertFailure ("expected ComponentDependencyCycle, received " <> show other)

testStableResolution :: IO ()
testStableResolution = do
  let queue = makeComponent "queue" Set.empty ["0001"]
      eventStore = makeComponent "event-store" Set.empty ["0001"]
      eventSourcing = makeComponent "event-sourcing" (Set.singleton "event-store") ["0001"]
  plan <- requireRight (resolveMigrationPlan (eventSourcing :| [queue, eventStore]))
  componentNames (planDescription plan)
    @?= requireNames ["queue", "event-store", "event-sourcing"]

testStableResolutionDeterministic :: IO ()
testStableResolutionDeterministic = do
  let queue = makeComponent "queue" Set.empty ["0001"]
      eventStore = makeComponent "event-store" Set.empty ["0001"]
      eventSourcing = makeComponent "event-sourcing" (Set.singleton "event-store") ["0001"]
      input = eventSourcing :| [queue, eventStore]
  firstPlan <- requireRight (resolveMigrationPlan input)
  secondPlan <- requireRight (resolveMigrationPlan input)
  planDescription firstPlan @?= planDescription secondPlan

testDescription :: IO ()
testDescription = do
  let firstMigration = makeMigration "0001"
      secondMigration =
        requireDefinition
          (sessionMigration "0002" (migrationFingerprint "session-v1") (pure ()))
      component = requireDefinition (migrationComponent "owner" Set.empty (firstMigration :| [secondMigration]))
  plan <- requireRight (migrationPlan (component :| []))
  case planDescription plan of
    PlanDescription
      ( ComponentDescription
          { position = 1,
            migrations =
              MigrationDescription
                { position = 1,
                  kind = HaskellKind,
                  transactionMode = Transactional
                }
                :| [ MigrationDescription
                       { position = 2,
                         kind = HaskellKind,
                         transactionMode = NonTransactional
                       }
                     ]
          }
          :| []
        ) -> pure ()
    description -> assertFailure ("unexpected plan description: " <> show description)

componentNames :: PlanDescription -> [ComponentName]
componentNames (PlanDescription descriptions) =
  fmap (\ComponentDescription {name} -> name) (toList descriptions)

requireNames :: [Text] -> [ComponentName]
requireNames = fmap (requireDefinition . componentName)

makeComponent :: Text -> Set Text -> [Text] -> MigrationComponent
makeComponent name dependencies migrationNames =
  case NonEmpty.nonEmpty (makeMigration <$> migrationNames) of
    Nothing -> error "makeComponent requires at least one migration"
    Just migrations -> requireDefinition (migrationComponent name dependencies migrations)

makeMigration :: Text -> Migration
makeMigration name =
  requireDefinition
    (transactionMigration name (migrationFingerprint (Text.Encoding.encodeUtf8 name)) (pure ()))

requireDefinition :: Either DefinitionError value -> value
requireDefinition = \case
  Left err -> error (show err)
  Right value -> value

requireRight :: (Show error) => Either error value -> IO value
requireRight = \case
  Left err -> assertFailure (show err) >> error "assertFailure returned"
  Right value -> pure value

assertRight :: (Show error) => Either error value -> IO ()
assertRight result = do
  _ <- requireRight result
  pure ()

assertPlanError :: PlanError -> Either PlanError MigrationPlan -> IO ()
assertPlanError expected = \case
  Left actual -> actual @?= expected
  Right plan -> assertFailure ("expected Left " <> show expected <> ", received Right " <> show plan)