packages feed

graphula-core-2.0.0.1: test/README.lhs

# Graphula Core

Graphula is a simple interface for generating persistent data and linking its dependencies. We use this interface to generate fixtures for automated testing. The interface is extensible and supports pluggable front-ends.


<!--
```haskell
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-missing-deriving-strategies #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Main (module Main) where

import Control.Monad (replicateM_)
import Control.Monad.IO.Class
import Control.Monad.IO.Unlift
import Control.Monad.Logger (NoLoggingT)
import Control.Monad.Trans.Reader (ReaderT)
import Control.Monad.Trans.Resource (ResourceT)
import Database.Persist.Sqlite
import Database.Persist.TH
import Data.Typeable
import GHC.Generics (Generic)
import Graphula
import Graphula.UUIDKey
import Test.Hspec
import Test.QuickCheck

instance (ToBackendKey SqlBackend a) => Arbitrary (Key a) where
  arbitrary = toSqlKey <$> arbitrary
```
-->

## Arbitrary Data

Graphula utilizes `QuickCheck` to generate random data. We need to declare `Arbitrary` instances for our types.

```haskell
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
A
  a String
  b Int
  deriving Show Eq Generic

B
  a AId
  b String
  deriving Show Eq Generic

C
  a AId
  b BId
  c String
  deriving Show Eq Generic

D
  Id UUIDKey
  a Int
  b String
  deriving Show Eq Generic

E
  Id DId sqltype=uuid
  a String
  deriving Show Eq Generic

F
  a Bool
  UniqueFA a
  deriving Show Eq Generic
|]

instance Arbitrary A where
  arbitrary = A <$> arbitrary <*> arbitrary

instance Arbitrary B where
  arbitrary = B <$> arbitrary <*> arbitrary

instance Arbitrary C where
  arbitrary = C <$> arbitrary <*> arbitrary <*> arbitrary

instance Arbitrary D where
  arbitrary = D <$> arbitrary <*> arbitrary

instance Arbitrary E where
  arbitrary = E <$> arbitrary

instance Arbitrary F where
  arbitrary = F <$> arbitrary
```

## Dependencies

We declare dependencies via the `HasDependencies` typeclass and its associated type `Dependencies`.

By default a type does not have any dependencies. We only need to declare an empty instance.

```haskell
instance HasDependencies A
instance HasDependencies F
```

For single dependencies we use the `Only` type.

```haskell
instance HasDependencies B where
  type Dependencies B = Only AId
```

Groups of dependencies use tuples. Declare these dependencies in the order they appear in the type. `HasDependencies` leverages generic programming to inject dependencies for you.

```haskell
instance HasDependencies C where
  type Dependencies C = (AId, BId)
```

## Non Sequential Keys

Graphula supports non-sequential keys with the `KeySource` associated type. To generate a key using
its `Arbitrary` instance, use `'SourceArbitrary`. Non-serial keys will need to also derive
an overlapping `Arbitrary` instance.

```haskell
instance HasDependencies D where
  type KeySource D = 'SourceArbitrary

deriving newtype instance {-# OVERLAPPING #-} Arbitrary (Key D)
```

You can also elect to always specify an external key using `'SourceExternal`. This means that
this type cannot be constructed with `node`; use `nodeKeyed` instead.

```haskell
instance HasDependencies E where
  type KeySource E = 'SourceExternal
```

By default, `HasDependencies` instances use `type KeySource _ = 'SourceDefault`, which means
that graphula will expect the database to provide a key.

## Serialization

Graphula allows logging of graphs via `runGraphulaLogged`. Graphula dumps graphs
to a temp file on test failure.

```haskell
loggingSpec :: IO ()
loggingSpec = do
  let
    logFile :: FilePath
    logFile = "test.graphula"

    -- We'd typically use `runGraphulaLogged` which utilizes a temp file.
    failingGraph :: IO ()
    failingGraph = runGraphulaT Nothing runDB . runGraphulaLoggedWithFileT logFile $ do
      Entity _ a <- node @A () $ edit $ \n ->
        n {aA = "success"}
      liftIO $ aA a `shouldBe` "failed"

  failingGraph
    `shouldThrow` anyException

  n <- lines <$> readFile "test.graphula"
  n `shouldSatisfy` (not . null)
```

## Running It

```haskell
simpleSpec :: IO ()
simpleSpec =
  runGraphulaT Nothing runDB $ do
    -- Type application is not necessary, but recommended for clarity.
    Entity aId _ <- node @A () mempty
    Entity bId b <- node @B (only aId) mempty
    Entity _ c <- node @C (aId, bId) $ edit $ \n -> n { cC = "edited" }
    Entity dId _ <- node @D () mempty
    Entity eId _ <- nodeKeyed @E (EKey dId) () mempty

    -- Do something with your data
    liftIO $ do
      cC c `shouldBe` "edited"
      cA c `shouldBe` bA b
      unEKey eId `shouldBe` dId
```

`runGraphulaT` carries frontend instructions. If we'd like to override them we need to declare our own frontend.

For example, a front-end that always fails to insert.

```haskell
newtype GraphulaFailT m a = GraphulaFailT { runGraphulaFailT :: m a }
  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadGraphulaBackend)

instance MonadGraphulaFrontend (GraphulaFailT m) where
  insert _ _ = pure Nothing
  remove = const (pure ())

insertionFailureSpec :: IO ()
insertionFailureSpec = do
  let
    failingGraph :: IO ()
    failingGraph =  runGraphulaT Nothing runDB . runGraphulaFailT $ do
      Entity _ _ <- node @A () mempty
      pure ()
  failingGraph
    `shouldThrow` (== (GenerationFailureMaxAttemptsToInsert (typeRep $ Proxy @A)))
```

Note that graphula can fail naturally if we define a graph that violates unique constraints
in the database:

```haskell
constraintFailureSpec :: IO ()
constraintFailureSpec = do
  let
    failingGraph :: IO ()
    failingGraph =  runGraphulaT Nothing runDB $
      replicateM_ 3 $ node @F () mempty
  failingGraph
    `shouldThrow` (== (GenerationFailureMaxAttemptsToInsert (typeRep $ Proxy @F)))
```

or if we define a graph with an unsatisfiable predicates:

```haskell
ensureFailureSpec :: IO ()
ensureFailureSpec = do
  let
    failingGraph :: IO ()
    failingGraph =  runGraphulaT Nothing runDB $ do
      Entity _ _ <- node @A () $ ensure $ \a -> a /= a
      pure ()
  failingGraph
    `shouldThrow` (== (GenerationFailureMaxAttemptsToConstrain (typeRep $ Proxy @A)))
```

<!--
```haskell
main :: IO ()
main = hspec $
  describe "graphula-core" . parallel $ do
    it "generates and links arbitrary graphs of data" simpleSpec
    it "allows logging graphs" loggingSpec
    it "attempts to retry node generation on insertion failure" insertionFailureSpec
    it "attempts to retry node generation on a database constraint violation" constraintFailureSpec
    it "attempts to retry node generation on unsatisfiable predicates" ensureFailureSpec

runDB :: MonadUnliftIO m => ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a
runDB f = runSqlite "test.db" $ do
  runMigration migrateAll
  f
```
-->