packages feed

kesha-0.1.0.0: test/Main.hs

{-# LANGUAGE OverloadedStrings #-}

module Main
  ( main,
  )
where

{- HLINT ignore "Redundant do" -}
{- HLINT ignore "Use camelCase" -}
{- HLINT ignore "Use lambda-case" -}

import Control.Applicative (liftA2)
import Control.Monad (when)
import qualified Data.ByteString as BS
import Data.Foldable (traverse_)
import qualified Data.Map as Map
import Data.Semigroup ((<>))
import qualified Kesha
import qualified Kesha.NAR
import qualified System.Directory as Directory
import qualified System.Exit as Exit
import System.FilePath ((</>))
import qualified System.IO.Temp as Temp
import qualified System.Process as Process
import Test.Hspec
  ( Expectation,
    Spec,
    describe,
    expectationFailure,
    hspec,
    it,
    shouldBe,
  )
import Test.Hspec.QuickCheck
  ( modifyMaxSuccess,
  )
import Test.QuickCheck
  ( Arbitrary (arbitrary),
    choose,
    elements,
    oneof,
    property,
    resize,
    scale,
    sized,
    vectorOf,
  )
import Prelude

main :: IO ()
main = do
  exes <-
    liftA2
      (,)
      (Directory.findExecutable "nix-store")
      (Directory.findExecutable "nix-hash")
  case exes of
    (Nothing, Nothing) -> do
      putStrLn "Nix tooling not found on path - skipping tests"
    (Just _, Nothing) -> do
      putStrLn "`nix-store` not found on path - aborting"
      Exit.exitFailure
    (Nothing, Just _) -> do
      putStrLn "`nix-hash` not found on path - aborting"
      Exit.exitFailure
    (Just _, Just _) ->
      Temp.withSystemTempDirectory "kesha-test" (hspec . spec)

spec :: FilePath -> Spec
spec tempDir = do
  describe "NAR packing" $ do
    describe "matches the output of `nix-store --dump`" $ do
      modifyMaxSuccess (const 20) $
        it "matches for Regular files" $
          property $
            \regular ->
              inTempDirectory tempDir "Regular" $
                checkNAR =<< createFSO_Regular regular
      modifyMaxSuccess (const 20) $
        it "matches for SymLinks" $
          property $
            \symLink -> do
              inTempDirectory tempDir "SymLink" $
                checkNAR =<< createFSO_SymLink symLink
      modifyMaxSuccess (const 20) $
        it "matches for Directories" $
          property $
            \directory ->
              Temp.withTempDirectory tempDir "Directory" $ \path -> do
                createFSO_Directory path directory
                checkNAR path

  describe "Hashing" $ do
    describe "matches the output of `nix-hash --type md5" $ do
      hashTests 20 (Kesha.HashOptions Kesha.MD5 Kesha.Base16)

    describe "matches the output of `nix-hash --type md5 --base32" $ do
      hashTests 20 (Kesha.HashOptions Kesha.MD5 Kesha.Base32)

    describe "matches the output of `nix-hash --type sha1" $ do
      hashTests 20 (Kesha.HashOptions Kesha.SHA1 Kesha.Base16)

    describe "matches the output of `nix-hash --type sha1 --base32" $ do
      hashTests 20 (Kesha.HashOptions Kesha.SHA1 Kesha.Base32)

    describe "matches the output of `nix-hash --type sha256" $ do
      hashTests 20 (Kesha.HashOptions Kesha.SHA256 Kesha.Base16)

    describe "matches the output of `nix-hash --type sha256 --base32`" $ do
      hashTests 20 (Kesha.HashOptions Kesha.SHA256 Kesha.Base32)
  where
    hashTests n opts = do
      modifyMaxSuccess (const n) $
        it "matches for any FSO" $
          property $ \fso -> case fso of
            Regular regular ->
              inTempDirectory tempDir "Regular" $
                checkHash opts =<< createFSO_Regular regular
            SymLink symLink ->
              inTempDirectory tempDir "SymLink" $
                checkHash opts =<< createFSO_SymLink symLink
            Directory directory ->
              Temp.withTempDirectory tempDir "Directory" $ \path -> do
                createFSO_Directory path directory
                checkHash opts path

    checkNAR :: FilePath -> Expectation
    checkNAR path = do
      result <- liftA2 (,) (nixStoreDump path) (Kesha.NAR.localPack path)
      case result of
        (Right want, Right got) ->
          want `shouldBe` Kesha.NAR.dump got
        (Left exitCode, _) ->
          expectationFailure ("nix-store --dump failed: " <> show exitCode)
        (_, Left err) ->
          expectationFailure ("Kesha.NAR.localPack failed: " <> show err)

    checkHash :: Kesha.HashOptions -> FilePath -> Expectation
    checkHash opts path = do
      result <- liftA2 (,) (nixHash (optsToArgs opts) path) (Kesha.hashWith opts path)
      case result of
        (Right want, Right got) ->
          want `shouldBe` got
        (Left exitCode, _) ->
          expectationFailure ("nix-hash failed: " <> show exitCode)
        (_, Left err) ->
          expectationFailure ("Kesha.hash failed: " <> show err)

    optsToArgs :: Kesha.HashOptions -> [String]
    optsToArgs (Kesha.HashOptions algo repr) =
      ( case algo of
          Kesha.MD5 -> ["--type", "md5"]
          Kesha.SHA1 -> ["--type", "sha1"]
          Kesha.SHA256 -> ["--type", "sha256"]
      )
        <> ( case repr of
               Kesha.Base16 -> []
               Kesha.Base32 -> ["--base32"]
           )

data FSO
  = Regular FSO_Regular
  | SymLink FSO_SymLink
  | Directory FSO_Directory
  deriving (Show)

instance Arbitrary FSO where
  arbitrary =
    oneof
      [ Regular <$> arbitrary,
        SymLink <$> arbitrary,
        Directory <$> arbitrary
      ]

data FSO_Regular = FSO_Regular
  { _regularIsExecutable :: Bool,
    regularName :: PathSegment,
    _regularContents :: Contents
  }
  deriving (Show)

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

data FSO_SymLink = FSO_SymLink
  { _symLinkIsFile :: Bool,
    _symLinkTarget :: PathSegment,
    symLinkName :: PathSegment
  }
  deriving (Show)

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

newtype FSO_Directory = FSO_Directory {directoryMap :: Map.Map PathSegment FSO}
  deriving (Show)

instance Arbitrary FSO_Directory where
  arbitrary =
    scale (min 5) $
      sized $ \size -> do
        len <- choose (0, size)
        FSO_Directory . Map.fromList <$> vectorOf len (resize (pred size) arbitrary)

newtype PathSegment = PathSegment {unPathSegment :: String}
  deriving (Eq, Ord, Show)

instance Arbitrary PathSegment where
  arbitrary = do
    len <- choose (10, 20)
    PathSegment <$> vectorOf len (elements validChars)
    where
      validChars :: String
      validChars = ['A' .. 'Z'] <> ['a' .. 'z']

newtype Contents = Contents {unContents :: BS.ByteString}
  deriving (Show)

instance Arbitrary Contents where
  arbitrary = fmap (Contents . BS.pack) arbitrary

createFSO_Regular :: FSO_Regular -> IO FilePath
createFSO_Regular (FSO_Regular isExecutable (PathSegment path) contents) = do
  BS.writeFile path (unContents contents)
  when isExecutable $ do
    perm <- Directory.getPermissions path
    Directory.setPermissions path perm {Directory.executable = True}
  pure path

createFSO_SymLink :: FSO_SymLink -> IO FilePath
createFSO_SymLink (FSO_SymLink isFile (PathSegment target) (PathSegment name))
  | isFile = do
    BS.writeFile target mempty
    Directory.createFileLink target name
    pure target
  | otherwise = do
    Directory.createDirectory target
    Directory.createDirectoryLink target name
    pure target

createFSO_Directory :: FilePath -> FSO_Directory -> IO ()
createFSO_Directory root =
  traverse_ (uncurry writeNode) . flattenNodes root
  where
    flattenNodes ::
      FilePath -> FSO_Directory -> [(FilePath, Either FSO_SymLink FSO_Regular)]
    flattenNodes dir =
      Map.foldMapWithKey
        ( \piece fso ->
            case fso of
              Regular regular -> [(dir, Right regular {regularName = piece})]
              SymLink symLink -> [(dir, Left symLink {symLinkName = piece})]
              Directory directory -> flattenNodes (dir </> unPathSegment piece) directory
        )
        . directoryMap

    writeNode :: FilePath -> Either FSO_SymLink FSO_Regular -> IO FilePath
    writeNode path (Left symLink) = do
      Directory.createDirectoryIfMissing True path
      Directory.withCurrentDirectory path (createFSO_SymLink symLink)
    writeNode path (Right regular) = do
      Directory.createDirectoryIfMissing True path
      Directory.withCurrentDirectory path (createFSO_Regular regular)

nixStoreDump :: FilePath -> IO (Either Int BS.ByteString)
nixStoreDump path = do
  (_, Just hout, _, processHandle) <-
    Process.createProcess
      (Process.proc "nix-store" ["--dump", path])
        { Process.std_out = Process.CreatePipe
        }
  exit <- Process.waitForProcess processHandle
  case exit of
    Exit.ExitFailure code -> pure (Left code)
    Exit.ExitSuccess -> Right <$> BS.hGetContents hout

nixHash :: [String] -> FilePath -> IO (Either Int BS.ByteString)
nixHash args path = do
  (_, Just hout, _, processHandle) <-
    Process.createProcess
      (Process.proc "nix-hash" (args ++ [path]))
        { Process.std_out = Process.CreatePipe
        }
  exit <- Process.waitForProcess processHandle
  case exit of
    Exit.ExitFailure code -> pure (Left code)
    Exit.ExitSuccess ->
      -- `BS.init` is to drop the trailing newline
      Right . BS.init <$> BS.hGetContents hout

inTempDirectory :: FilePath -> String -> IO a -> IO a
inTempDirectory parent template m =
  Temp.withTempDirectory parent template $ \tempDir ->
    Directory.withCurrentDirectory tempDir m