packages feed

lsm-tree-1.0.0.0: test/Test/Database/LSMTree/Internal/Snapshot/Codec/Golden.hs

{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE UndecidableInstances #-}

module Test.Database.LSMTree.Internal.Snapshot.Codec.Golden (
    tests
  , EnumGolden (..)
  , Annotation
  ) where

import           Codec.CBOR.Write (toLazyByteString)
import           Control.Monad (when)
import qualified Data.ByteString.Lazy as BSL (writeFile)
import qualified Data.Set as Set
import           Data.Typeable
import qualified Data.Vector as V
import           Database.LSMTree.Internal.Config (BloomFilterAlloc (..),
                     DiskCachePolicy (..), FencePointerIndexType (..),
                     MergeBatchSize (..), MergePolicy (..), MergeSchedule (..),
                     SizeRatio (..), TableConfig (..), WriteBufferAlloc (..))
import           Database.LSMTree.Internal.MergeSchedule
                     (MergePolicyForLevel (..), NominalCredits (..),
                     NominalDebt (..))
import           Database.LSMTree.Internal.MergingRun as MR
import           Database.LSMTree.Internal.RunBuilder (IndexType (..),
                     RunBloomFilterAlloc (..), RunDataCaching (..))
import           Database.LSMTree.Internal.RunNumber (RunNumber (..))
import           Database.LSMTree.Internal.Snapshot
import           Database.LSMTree.Internal.Snapshot.Codec
import           Paths_lsm_tree
import qualified System.Directory as Dir
import           System.FilePath
import qualified System.FS.API as FS
import           System.FS.API.Types (MountPoint (..))
import           System.FS.IO (ioHasFS)
import           System.IO.Unsafe
import           Test.QuickCheck (Property, counterexample, ioProperty, once,
                     (.&&.))
import qualified Test.Tasty as Tasty
import           Test.Tasty (TestTree, testGroup)
import qualified Test.Tasty.Golden as Au
import           Test.Tasty.QuickCheck (testProperty)

tests :: TestTree
tests =
    handleOutputFiles $
    testGroup "Test.Database.LSMTree.Internal.Snapshot.Codec.Golden" $
         concat (forallSnapshotTypes snapshotCodecGoldenTest)
      ++ [testProperty "prop_noUnexpectedOrMissingGoldenFiles" prop_noUnexpectedOrMissingGoldenFiles]

{-------------------------------------------------------------------------------
  Configuration
-------------------------------------------------------------------------------}

-- | The location of the golden file data directory relative to the project root.
goldenDataFilePath :: FilePath
goldenDataFilePath = unsafePerformIO getDataDir </> "snapshot-codec"

goldenDataMountPoint :: MountPoint
goldenDataMountPoint = MountPoint goldenDataFilePath

-- | Delete output files on test-case success.
-- Change the option here if this is undesirable.
handleOutputFiles :: TestTree -> TestTree
handleOutputFiles = Tasty.localOption Au.OnPass

{-------------------------------------------------------------------------------
  Golden tests
-------------------------------------------------------------------------------}

-- | Compare the serialization of snapshot metadata with a known reference file.
snapshotCodecGoldenTest ::
     forall a. (Typeable a, EnumGolden a, Encode a)
  => Proxy a
  -> [TestTree]
snapshotCodecGoldenTest proxy = [
      go annotation datum
    | (annotation, datum) <- enumGoldenAnnotated' proxy
    ]
  where
    go ann datum =
      let v = currentSnapshotVersion
          outputFilePath = goldenDataFilePath </> filePathOutput proxy ann v
          goldenFilePath = goldenDataFilePath </> filePathGolden proxy ann v

          -- IO actions
          removeIfExists :: FilePath -> IO ()
          removeIfExists fp =
              Dir.doesFileExist fp >>= (`when` (Dir.removeFile fp))
          outputAction :: IO ()
          outputAction = do
            -- Ensure that if the output file already exists, we remove it and
            -- re-write out the serialized data. This ensures that there are no
            -- false-positives, false-negatives, or irrelevant I/O exceptions.
            removeIfExists outputFilePath
            BSL.writeFile outputFilePath . toLazyByteString $ encode datum

      in  Au.goldenVsFile (nameGolden proxy ann v) goldenFilePath outputFilePath outputAction

-- | Check that are no missing or unexpected files in the output directory
prop_noUnexpectedOrMissingGoldenFiles :: Property
prop_noUnexpectedOrMissingGoldenFiles = once $ ioProperty $ do
    let expectedFiles = Set.fromList $ concat $ forallSnapshotTypes $ \p -> concat [
            filePathsGolden p v
          | v <- supportedVersions p
          ]

    let hfs = ioHasFS goldenDataMountPoint
    actualDirectoryEntries <- FS.listDirectory hfs (FS.mkFsPath [])

    let missingFiles = expectedFiles Set.\\ actualDirectoryEntries
        propMissing =
            counterexample ("Missing expected files: " ++ show missingFiles)
          $ counterexample ("Run the golden tests to regenerate the missing files")
          $ (Set.null missingFiles)

    let unexpectedFiles = actualDirectoryEntries Set.\\ expectedFiles
        propUnexpected =
            counterexample ("Found unexpected files: " ++ show unexpectedFiles)
          $ counterexample ("Delete the unexpected files manually from " ++ goldenDataFilePath)
            (Set.null unexpectedFiles)

    pure $ propMissing .&&. propUnexpected

{-------------------------------------------------------------------------------
  Mapping
-------------------------------------------------------------------------------}

type Constraints a = (Typeable a, Encode a, EnumGolden a)

-- | Do something for all snapshot types and collect the results
forallSnapshotTypes ::
     (forall a. Constraints a => Proxy a -> b)
  -> [b]
forallSnapshotTypes f = [
      -- SnapshotMetaData
      f (Proxy @SnapshotMetaData)
    , f (Proxy @SnapshotLabel)
    , f (Proxy @SnapshotRun)
      -- TableConfig
    , f (Proxy @TableConfig)
    , f (Proxy @MergePolicy)
    , f (Proxy @SizeRatio)
    , f (Proxy @WriteBufferAlloc)
    , f (Proxy @BloomFilterAlloc)
    , f (Proxy @FencePointerIndexType)
    , f (Proxy @DiskCachePolicy)
    , f (Proxy @MergeSchedule)
    , f (Proxy @MergeBatchSize)
      -- SnapLevels
    , f (Proxy @(SnapLevels SnapshotRun))
    , f (Proxy @(SnapLevel SnapshotRun))
    , f (Proxy @(V.Vector SnapshotRun))
    , f (Proxy @RunNumber)
    , f (Proxy @(SnapIncomingRun SnapshotRun))
    , f (Proxy @MergePolicyForLevel)
    , f (Proxy @RunDataCaching)
    , f (Proxy @RunBloomFilterAlloc)
    , f (Proxy @IndexType)
    , f (Proxy @RunParams)
    , f (Proxy @(SnapMergingRun LevelMergeType SnapshotRun))
    , f (Proxy @MergeDebt)
    , f (Proxy @MergeCredits)
    , f (Proxy @NominalDebt)
    , f (Proxy @NominalCredits)
    , f (Proxy @LevelMergeType)
    , f (Proxy @TreeMergeType)
    , f (Proxy @(SnapMergingTree SnapshotRun))
    , f (Proxy @(SnapMergingTreeState SnapshotRun))
    , f (Proxy @(SnapMergingRun TreeMergeType SnapshotRun))
    , f (Proxy @(SnapPendingMerge SnapshotRun))
    , f (Proxy @(SnapPreExistingRun SnapshotRun))
    ]

{-------------------------------------------------------------------------------
  Enumeration class
-------------------------------------------------------------------------------}

-- | Enumerate values of type @a@ for golden tests
--
-- To prevent combinatorial explosion, the enumeration should generally be
-- /shallow/: the different constructors for type @a@ should be enumerated
-- without recursively enumerating the constructors' fields. For example,
-- enumerating @Maybe Int@ should give us something like:
--
-- > enumGolden @(Maybe Int) = [ Just 17, Nothing ]
--
-- This is generally a suitable approach, since the snapshot encodings do not
-- encode types differently depending on values in the constructor fields.
--
-- Example (recursive) instances that prevent combinatorial explosion:
--
-- @
--  instance EnumGolden a => EnumGolden (Maybe a) where
--    enumGolden = [ Just (singGolden @a), Nothing ]
--  instance EnumGolden Int where
--    enumGolden = [17, -72] -- singGolden = 17
-- @
--
-- If there are encoders that do require more elaborate (recursive)
-- enumerations, then it is okay to deviate from shallow enumerations, but take
-- care not to explode the combinatorics ;)
class EnumGolden a where
  {-# MINIMAL enumGolden | enumGoldenAnnotated | singGolden #-}

  -- | Enumerated values. The enumeration should be /shallow/.
  --
  -- The default implementation is to return a singleton list containing
  -- 'singGolden'.
  enumGolden :: [a]
  enumGolden = [ singGolden ]

  -- | Enumerated values with an annotation for naming purposes. The enumeration
  -- should be /shallow/, and the annotations should be unique.
  --
  -- The default implementation is to annotate 'enumGolden' with capital letters
  -- starting with @\'A\'@.
  enumGoldenAnnotated :: [(Annotation, a)]
  enumGoldenAnnotated = zip [[c] | c <- ['A' .. 'Z']] enumGolden

  -- | A singleton enumerated value. This is mainly useful for superclass
  -- instances.
  --
  -- The default implementation is to take the 'head' of 'enumGoldenAnnotated'.
  singGolden :: a
  singGolden = snd $ head enumGoldenAnnotated

  supportedVersions :: Proxy a -> [SnapshotVersion]
  supportedVersions _ = allCompatibleSnapshotVersions

type Annotation = String

enumGoldenAnnotated' :: EnumGolden a => Proxy a -> [(Annotation, a)]
enumGoldenAnnotated' _ = enumGoldenAnnotated

{-------------------------------------------------------------------------------
  Enumeration class: names and file paths
-------------------------------------------------------------------------------}

nameGolden :: Typeable a => Proxy a -> Annotation -> SnapshotVersion -> String
nameGolden p ann v = show v ++ "." ++ map spaceToUnderscore (show $ typeRep p) ++ "." ++ ann

spaceToUnderscore :: Char -> Char
spaceToUnderscore ' ' = '_'
spaceToUnderscore c   = c

filePathsGolden ::
     (EnumGolden a, Typeable a)
  => Proxy a
  -> SnapshotVersion
  -> [String]
filePathsGolden p v = [
      filePathGolden p annotation v
    | (annotation, _) <- enumGoldenAnnotated' p
    ]

filePathOutput :: Typeable a => Proxy a -> String -> SnapshotVersion -> String
filePathOutput p ann v = nameGolden p ann v ++ ".snapshot"

filePathGolden :: Typeable a => Proxy a -> String -> SnapshotVersion -> String
filePathGolden p ann v = nameGolden p ann v ++ ".snapshot.golden"

{-------------------------------------------------------------------------------
  Enumeration class: instances
-------------------------------------------------------------------------------}

instance EnumGolden SnapshotMetaData where
  singGolden = SnapshotMetaData singGolden singGolden singGolden singGolden singGolden
    where
      _coveredAllCases = \case
        SnapshotMetaData{} -> ()

instance EnumGolden SnapshotLabel where
  enumGolden = [
        SnapshotLabel "UserProvidedLabel"
      , SnapshotLabel ""
      ]
    where
      _coveredAllCases = \case
        SnapshotLabel{} -> ()

instance EnumGolden TableConfig where
  singGolden = TableConfig singGolden singGolden singGolden singGolden
                           singGolden singGolden singGolden singGolden
    where
      _coveredAllCases = \case
        TableConfig{} -> ()

instance EnumGolden MergePolicy where
  singGolden = LazyLevelling
    where
      _coveredAllCases = \case
        LazyLevelling{} -> ()


instance EnumGolden SizeRatio where
  singGolden = Four
    where
      _coveredAllCases = \case
        Four{} -> ()

instance EnumGolden WriteBufferAlloc where
  singGolden = AllocNumEntries magicNumber2
    where
      _coveredAllCases = \case
        AllocNumEntries{} -> ()

instance EnumGolden BloomFilterAlloc where
  enumGolden = [ AllocFixed magicNumber3, AllocRequestFPR pi ]
    where
      _coveredAllCases = \case
        AllocFixed{} -> ()
        AllocRequestFPR{} -> ()

instance EnumGolden FencePointerIndexType where
  enumGolden = [ CompactIndex, OrdinaryIndex ]
    where
      _coveredAllCases = \case
        CompactIndex{} -> ()
        OrdinaryIndex{} -> ()

instance EnumGolden DiskCachePolicy where
  enumGolden = [ DiskCacheAll, DiskCacheLevelOneTo magicNumber3, DiskCacheNone ]
    where
      _coveredAllCases = \case
        DiskCacheAll{} -> ()
        DiskCacheLevelOneTo{} -> ()
        DiskCacheNone{} -> ()

instance EnumGolden MergeSchedule where
  enumGolden = [ OneShot, Incremental ]
    where
      _coveredAllCases = \case
        OneShot{} -> ()
        Incremental{} -> ()

instance EnumGolden MergeBatchSize where
  enumGolden = map MergeBatchSize [ 1, 1000 ]
  supportedVersions _ = [V1]

instance EnumGolden (SnapLevels SnapshotRun) where
  singGolden = SnapLevels singGolden
    where
      _coveredAllCases = \case
        SnapLevels{} -> ()

instance EnumGolden (SnapLevel SnapshotRun) where
  singGolden = SnapLevel singGolden singGolden
    where
      _coveredAllCases = \case
        SnapLevel{} -> ()

instance EnumGolden (SnapIncomingRun SnapshotRun) where
  enumGolden = [
        SnapIncomingMergingRun singGolden singGolden singGolden singGolden
      , SnapIncomingSingleRun singGolden
      ]
    where
      _coveredAllCases = \case
        SnapIncomingMergingRun{} -> ()
        SnapIncomingSingleRun{} -> ()

instance EnumGolden MergePolicyForLevel where
  enumGolden = [ LevelTiering, LevelLevelling ]
    where
      _coveredAllCases = \case
        LevelTiering -> ()
        LevelLevelling -> ()

instance EnumGolden LevelMergeType where
  enumGolden = [ MergeMidLevel, MergeLastLevel ]
    where
      _coveredAllCases = \case
        MergeMidLevel{} -> ()
        MergeLastLevel{} -> ()

instance EnumGolden (SnapMergingTree SnapshotRun) where
  singGolden = SnapMergingTree singGolden
    where
      _coveredAllCases = \case
        SnapMergingTree{} -> ()

instance EnumGolden (SnapMergingTreeState SnapshotRun) where
  enumGolden = [
        SnapCompletedTreeMerge singGolden
      , SnapPendingTreeMerge singGolden
      , SnapOngoingTreeMerge singGolden
      ]
    where
      _coveredAllCases = \case
        SnapCompletedTreeMerge{} -> ()
        SnapPendingTreeMerge{} -> ()
        SnapOngoingTreeMerge{} -> ()

instance EnumGolden (SnapPendingMerge SnapshotRun) where
  enumGolden = [
        SnapPendingLevelMerge singGolden singGolden
      , SnapPendingUnionMerge singGolden
      ]
    where
      _coveredAllCases = \case
        SnapPendingLevelMerge{} -> ()
        SnapPendingUnionMerge{} -> ()

instance EnumGolden (SnapPreExistingRun SnapshotRun) where
  enumGolden = [
        SnapPreExistingRun singGolden
      , SnapPreExistingMergingRun singGolden
      ]
    where
      _coveredAllCases = \case
        SnapPreExistingRun{} -> ()
        SnapPreExistingMergingRun{} -> ()

instance EnumGolden TreeMergeType where
  enumGolden = [ MergeLevel, MergeUnion ]
    where
      _coveredAllCases = \case
        MergeLevel{} -> ()
        MergeUnion{} -> ()

instance EnumGolden a => EnumGolden (Maybe a) where
  enumGolden = [ Just singGolden, Nothing ]
    where
      _coveredAllCases = \case
        Just{} -> ()
        Nothing{} -> ()

instance EnumGolden a => EnumGolden (V.Vector a) where
  enumGolden = [
      V.fromList [ singGolden, singGolden ]
    , mempty
    , V.fromList [ singGolden ]
    ]

instance EnumGolden a => EnumGolden [a] where
  enumGolden = [
      [singGolden, singGolden]
    , []
    , [singGolden]
    ]

instance EnumGolden RunParams where
  singGolden = RunParams singGolden singGolden singGolden
    where
      _coveredAllCases = \case
        RunParams{} -> ()

instance EnumGolden t => EnumGolden (SnapMergingRun t SnapshotRun) where
  enumGolden = [
        SnapCompletedMerge singGolden singGolden
      , SnapOngoingMerge singGolden singGolden singGolden singGolden
      ]
    where
      _coveredAllCases = \case
        SnapCompletedMerge{} -> ()
        SnapOngoingMerge{} -> ()

instance EnumGolden RunBloomFilterAlloc where
  enumGolden = [
        RunAllocFixed magicNumber3
      , RunAllocRequestFPR pi
      ]
    where
      _coveredAllCases = \case
        RunAllocFixed{} -> ()
        RunAllocRequestFPR{} -> ()

instance EnumGolden RunNumber where
  singGolden = RunNumber magicNumber3
    where
      _coveredAllCases = \case
        RunNumber{} -> ()

instance EnumGolden IndexType where
  enumGolden = [
        Compact
      , Ordinary
      ]
    where
      _coveredAllCases = \case
        Compact{} -> ()
        Ordinary{} -> ()

instance EnumGolden RunDataCaching where
  enumGolden = [
        CacheRunData
      , NoCacheRunData
      ]
    where
      _coveredAllCases = \case
        CacheRunData{} -> ()
        NoCacheRunData{} -> ()

instance EnumGolden SnapshotRun where
  singGolden = SnapshotRun singGolden singGolden singGolden
    where
      _coveredAllCases = \case
        SnapshotRun{} -> ()

instance EnumGolden MergeDebt where
  singGolden = MergeDebt magicNumber2
    where
      _coveredAllCases = \case
        MergeDebt{} -> ()

instance EnumGolden MergeCredits where
  singGolden = MergeCredits magicNumber2
    where
      _coveredAllCases = \case
        MergeCredits{} -> ()

instance EnumGolden NominalDebt where
  singGolden = NominalDebt magicNumber2
    where
      _coveredAllCases = \case
        NominalDebt{} -> ()

instance EnumGolden NominalCredits where
  singGolden = NominalCredits magicNumber1
    where
      _coveredAllCases = \case
        NominalCredits{} -> ()

  -- Randomly chosen numbers
magicNumber1, magicNumber2, magicNumber3 :: Enum e => e
magicNumber1 = toEnum 42
magicNumber2 = toEnum 88
magicNumber3 = toEnum 1024