hapistrano-0.4.6.0: spec/System/HapistranoSpec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module System.HapistranoSpec
( spec
) where
import Control.Monad
import Control.Monad.Reader
import Data.List (isPrefixOf)
import Data.Maybe (mapMaybe)
import Numeric.Natural
import Path
import Path.IO
import System.Directory
( doesFileExist, getCurrentDirectory, listDirectory )
import qualified System.Hapistrano as Hap
import qualified System.Hapistrano.Commands as Hap
import qualified System.Hapistrano.Core as Hap
import System.Hapistrano.Types
import System.IO
import System.IO.Silently (capture_)
import System.Info (os)
import Test.Hspec hiding (shouldBe, shouldContain, shouldReturn)
import qualified Test.Hspec as Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck hiding (Success)
import System.Hapistrano (releasePath)
import System.Hapistrano.Config (deployStateFilename)
import System.Hapistrano.Maintenance
testBranchName :: String
testBranchName = "another_branch"
workingDir :: Path Rel Dir
workingDir = $(mkRelDir "working_dir")
releaseDir :: Path Rel Dir
releaseDir = $(mkRelDir "releases")
spec :: Spec
spec = do
describe "execWithInheritStdout" $
context "given a command that prints to stdout" $
it "redirects commands' output to stdout first" $
let (Just commandTest) =
Hap.mkGenericCommand
"echo \"hapistrano\"; sleep 2; echo \"onartsipah\""
commandExecution = Hap.execWithInheritStdout commandTest Nothing
expectedOutput = "hapistrano\nonartsipah"
in do actualOutput <- capture_ (runHap commandExecution)
expectedOutput `Hspec.shouldSatisfy` (`isPrefixOf` actualOutput)
describe "readScript" $
it "performs all the necessary normalizations correctly" $ do
spath <- do
currentDirectory <- getCurrentDirectory >>= parseAbsDir
scriptFile <- parseRelFile "script/clean-build.sh"
return (currentDirectory </> scriptFile)
(fmap Hap.unGenericCommand <$> Hap.readScript spath) `Hspec.shouldReturn`
[ "export PATH=~/.cabal/bin:/usr/local/bin:$PATH"
, "cabal sandbox delete"
, "cabal sandbox init"
, "cabal clean"
, "cabal update"
, "cabal install --only-dependencies -j"
, "cabal build -j"
]
describe "fromMaybeReleaseFormat" $ do
context "when the command line value is present" $ do
context "and the config file value is present" $
prop "returns the command line value" $
forAll ((,) <$> arbitraryReleaseFormat <*> arbitraryReleaseFormat) $ \(rf1, rf2) ->
fromMaybeReleaseFormat (Just rf1) (Just rf2) `Hspec.shouldBe` rf1
context "and the config file value is not present" $
prop "returns the command line value" $
forAll arbitraryReleaseFormat $ \rf ->
fromMaybeReleaseFormat (Just rf) Nothing `Hspec.shouldBe` rf
context "when the command line value is not present" $ do
context "and the config file value is present" $
prop "returns the config file value" $
forAll arbitraryReleaseFormat $ \rf ->
fromMaybeReleaseFormat Nothing (Just rf) `Hspec.shouldBe` rf
context "and the config file value is not present" $
it "returns the default value" $
fromMaybeReleaseFormat Nothing Nothing `Hspec.shouldBe` ReleaseShort
describe "fromMaybeKeepReleases" $ do
context "when the command line value is present" $ do
context "and the config file value is present" $
prop "returns the command line value" $
forAll ((,) <$> arbitraryKeepReleases <*> arbitraryKeepReleases) $ \(kr1, kr2) ->
fromMaybeKeepReleases (Just kr1) (Just kr2) `Hspec.shouldBe` kr1
context "and the second value is not present" $
prop "returns the command line value" $
forAll arbitraryKeepReleases $ \kr ->
fromMaybeKeepReleases (Just kr) Nothing `Hspec.shouldBe` kr
context "when the command line value is not present" $ do
context "and the config file value is present" $
prop "returns the config file value" $
forAll arbitraryKeepReleases $ \kr ->
fromMaybeKeepReleases Nothing (Just kr) `Hspec.shouldBe` kr
context "and the config file value is not present" $
it "returns the default value" $
fromMaybeKeepReleases Nothing Nothing `Hspec.shouldBe` 5
around withSandbox $ do
describe "writeMaintenanceFile" $
context "when the file doesn't exist" $
it "creates the maintenance file in the given path" $ \(deployPath, _) -> do
result <- runHap $ do
writeMaintenanceFile deployPath $(mkRelDir "maintenance") $(mkRelFile "maintenance.html")
liftIO $ System.Directory.doesFileExist ((fromAbsDir deployPath) <> "/maintenance/maintenance.html")
result `shouldBe` True
describe "deleteMaintenanceFile" $
context "when the file exists" $
it "removes the maintenance file from the given path" $ \(deployPath, _) -> do
result <- runHap $ do
writeMaintenanceFile deployPath $(mkRelDir "maintenance") $(mkRelFile "maintenance.html")
deleteMaintenanceFile deployPath $(mkRelDir "maintenance") $(mkRelFile "maintenance.html")
liftIO $ System.Directory.doesFileExist ((fromAbsDir deployPath) <> "/maintenance/maintenance.html")
result `shouldBe` False
describe "releasePath" $ do
context "when the configWorkingDir is Nothing" $
it "should return the release path" $ \(deployPath, repoPath) -> do
(rpath, release) <- runHap $ do
release <- Hap.pushRelease $ mkTask deployPath repoPath
(,) <$> Hap.releasePath deployPath release Nothing
<*> pure release
rel <- parseRelDir $ renderRelease release
rpath `shouldBe` deployPath </> releaseDir </> rel
context "when the configWorkingDir is Just" $
it "should return the release path with WorkingDir" $ \(deployPath, repoPath) -> do
(rpath, release) <- runHap $ do
release <- Hap.pushRelease $ mkTask deployPath repoPath
(,) <$> Hap.releasePath deployPath release (Just workingDir)
<*> pure release
rel <- parseRelDir $ renderRelease release
rpath `shouldBe` deployPath </> releaseDir </> rel </> workingDir
describe "pushRelease" $ do
it "sets up repo all right in Zsh" $ \(deployPath, repoPath) ->
runHapWithShell Zsh $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
-- let's check that the dir exists and contains the right files
(liftIO . readFile . fromAbsFile) (rpath </> $(mkRelFile "foo.txt")) `shouldReturn`
"Foo!\n"
it "sets up repo all right" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
-- let's check that the dir exists and contains the right files
(liftIO . readFile . fromAbsFile) (rpath </> $(mkRelFile "foo.txt")) `shouldReturn`
"Foo!\n"
it "deploys properly a branch other than master" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTaskWithCustomRevision deployPath repoPath testBranchName
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
-- let's check that the dir exists and contains the right files
(liftIO . readFile . fromAbsFile) (rpath </> $(mkRelFile "bar.txt")) `shouldReturn`
"Bar!\n"
-- This fails if the opened branch is not testBranchName
justExec
rpath
("test `git rev-parse --abbrev-ref HEAD` = " ++ testBranchName)
-- This fails if there are unstaged changes
justExec rpath "git diff --exit-code"
it "updates the origin url when it's changed" $ \(deployPath, repoPath) ->
runHap $ do
let tempDirPrefix = "hap-test-repotwo"
withSystemTempDir tempDirPrefix $ \repoPathTwo -> do
let task1 = mkTask deployPath repoPath
task2 = mkTask deployPath repoPathTwo
repoConfigFile = deployPath </> $(mkRelDir "repo") </> $(mkRelFile "config")
liftIO $ populateTestRepo repoPathTwo
void $ Hap.pushRelease task1
void $ Hap.pushRelease task2
repoFile <- (liftIO . readFile . fromAbsFile) repoConfigFile
repoFile `shouldContain` tempDirPrefix
describe "createHapistranoDeployState" $ do
it ("creates the " <> deployStateFilename <> " file correctly") $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
parseStatePath <- parseRelFile deployStateFilename
actualReleasePath <- releasePath deployPath release Nothing
let stateFilePath = actualReleasePath </> parseStatePath
Hap.createHapistranoDeployState deployPath release Success
Path.IO.doesFileExist stateFilePath `shouldReturn`
True
it "when created in a successful deploy, the contents are \"Success\"" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
Hap.createHapistranoDeployState deployPath release Success
Hap.deployState deployPath Nothing release `shouldReturn`
Success
describe "activateRelease" $
it "creates the ‘current’ symlink correctly" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
Hap.activateRelease currentSystem deployPath release
rpath <- Hap.releasePath deployPath release Nothing
let rc :: Hap.Readlink Dir
rc =
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc (Just release) `shouldReturn` rpath
Path.IO.doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
describe "playScriptLocally (successful run)" $
it "check that local scripts are run and deployment is successful" $ \(deployPath, repoPath) ->
runHap $ do
let localCommands = mapMaybe Hap.mkGenericCommand ["pwd", "ls"]
task = mkTask deployPath repoPath
Hap.playScriptLocally localCommands
release <- Hap.pushRelease task
parseStatePath <- parseRelFile deployStateFilename
actualReleasePath <- releasePath deployPath release Nothing
let stateFilePath = actualReleasePath </> parseStatePath
Hap.createHapistranoDeployState deployPath release Success
Path.IO.doesFileExist stateFilePath `shouldReturn`
True
describe "playScriptLocally (error exit)" $
it "check that deployment isn't done" $ \(deployPath, repoPath) ->
(runHap $ do
let localCommands =
mapMaybe Hap.mkGenericCommand ["pwd", "ls", "false"]
task = mkTask deployPath repoPath
Hap.playScriptLocally localCommands
release <- Hap.pushRelease task
Hap.createHapistranoDeployState deployPath release Success) `shouldThrow`
anyException
describe "rollback" $ do
it "resets the ‘current’ symlink correctly" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
rs <- replicateM 5 (Hap.pushRelease task)
Hap.rollback currentSystem deployPath 2
rpath <- Hap.releasePath deployPath (rs !! 2) Nothing
let rc :: Hap.Readlink Dir
rc =
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc Nothing `shouldReturn` rpath
Path.IO.doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
describe "dropOldReleases" $ do
it "works" $ \(deployPath, repoPath) ->
runHap $ do
rs <-
replicateM 7 $ do
r <- Hap.pushRelease (mkTask deployPath repoPath)
Hap.createHapistranoDeployState deployPath r Success
return r
Hap.dropOldReleases deployPath 5 False
-- two oldest releases should not survive:
forM_ (take 2 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` False
-- 5 most recent releases should stay alive:
forM_ (drop 2 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` True
context "when the --keep-one-failed flag is active" $
it "should delete failed releases other than the most recent" $ \(deployPath, repoPath) ->
let successfulRelease = mkReleaseWithState deployPath repoPath Success
failedRelease = mkReleaseWithState deployPath repoPath Fail in
runHap $ do
rs <- sequence [successfulRelease, successfulRelease, failedRelease, failedRelease, failedRelease]
Hap.dropOldReleases deployPath 5 True
-- The two successful releases should survive
forM_ (take 2 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` True
-- The latest failed release should survive:
forM_ (drop 4 rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` True
-- The two older failed releases should not survive:
forM_ (take 2 . drop 2 $ rs) $ \r ->
(Hap.releasePath deployPath r Nothing >>= doesDirExist) `shouldReturn` False
describe "linkToShared" $ do
context "when the deploy_path/shared directory doesn't exist" $
it "should create the link anyway" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
sharedDir = Hap.sharedPath deployPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
Hap.exec (Hap.Rm sharedDir) (Just release)
Hap.linkToShared currentSystem rpath deployPath "thing" (Just release) `shouldReturn`
()
context "when the file/directory to link exists in the repository" $
it "should throw an error" $ \(deployPath, repoPath) ->
runHap
(do let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
Hap.linkToShared currentSystem rpath deployPath "foo.txt" $ Just release) `shouldThrow`
anyException
context "when it attempts to link a file" $ do
context "when the file is not at the root of the shared directory" $
it "should throw an error" $ \(deployPath, repoPath) ->
runHap
(do let task = mkTask deployPath repoPath
sharedDir = Hap.sharedPath deployPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/bar.txt" $ Just release) `shouldThrow`
anyException
context "when the file is at the root of the shared directory" $
it "should link the file successfully" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
sharedDir = Hap.sharedPath deployPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
justExec sharedDir "echo 'Bar!' > bar.txt"
Hap.linkToShared currentSystem rpath deployPath "bar.txt" (Just release)
(liftIO . readFile . fromAbsFile)
(rpath </> $(mkRelFile "bar.txt")) `shouldReturn`
"Bar!\n"
context "when it attemps to link a directory" $ do
context "when the directory ends in '/'" $
it "should throw an error" $ \(deployPath, repoPath) ->
runHap
(do let task = mkTask deployPath repoPath
sharedDir = Hap.sharedPath deployPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
justExec sharedDir "echo 'Baz!' > foo/baz.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/" $ Just release) `shouldThrow`
anyException
it "should link the file successfully" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
sharedDir = Hap.sharedPath deployPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release Nothing
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
justExec sharedDir "echo 'Baz!' > foo/baz.txt"
Hap.linkToShared currentSystem rpath deployPath "foo" (Just release)
files <-
(liftIO . listDirectory . fromAbsDir)
(rpath </> $(mkRelDir "foo"))
liftIO $ files `shouldMatchList` ["baz.txt", "bar.txt"]
----------------------------------------------------------------------------
-- Helpers
infix 1 `shouldBe`, `shouldReturn`
-- | Lifted 'Hspec.shouldBe'.
shouldBe :: (MonadIO m, Show a, Eq a) => a -> a -> m ()
shouldBe x y = liftIO (x `Hspec.shouldBe` y)
-- | Lifted 'Hspec.shouldContain'.
shouldContain :: (MonadIO m, Show a, Eq a) => [a] -> [a] -> m ()
shouldContain x y = liftIO (x `Hspec.shouldContain` y)
-- | Lifted 'Hspec.shouldReturn'.
shouldReturn :: (MonadIO m, Show a, Eq a) => m a -> a -> m ()
shouldReturn m y = m >>= (`shouldBe` y)
-- | The sandbox prepares the environment for an independent round of
-- testing. It provides two paths: deploy path and path where git repo is
-- located.
withSandbox :: ActionWith (Path Abs Dir, Path Abs Dir) -> IO ()
withSandbox action =
withSystemTempDir "hap-test" $ \dir -> do
let dpath = dir </> $(mkRelDir "deploy")
rpath = dir </> $(mkRelDir "repo")
ensureDir dpath
ensureDir rpath
populateTestRepo rpath
action (dpath, rpath)
-- | Given path where to put the repo, generate it for testing.
populateTestRepo :: Path Abs Dir -> IO ()
populateTestRepo path =
runHap $ do
justExec path "git init"
justExec path "git config --local --replace-all push.default simple"
justExec path "git config --local --replace-all user.email hap@hap"
justExec path "git config --local --replace-all user.name Hap"
justExec path "echo 'Foo!' > foo.txt"
justExec path "git add -A"
justExec path "git commit -m 'Initial commit'"
-- Add dummy content to a branch that is not master
justExec path ("git checkout -b " ++ testBranchName)
justExec path "echo 'Bar!' > bar.txt"
justExec path "git add bar.txt"
justExec path "git commit -m 'Added more bars to another branch'"
justExec path "git checkout master"
-- | Execute arbitrary commands in the specified directory.
justExec :: Path Abs Dir -> String -> Hapistrano ()
justExec path cmd' =
case Hap.mkGenericCommand cmd' of
Nothing -> Hap.failWith 1 (Just $ "Failed to parse the command: " ++ cmd') Nothing
Just cmd -> Hap.exec (Hap.Cd path cmd) Nothing
-- | Run 'Hapistrano' monad locally.
runHap :: Hapistrano a -> IO a
runHap = runHapWithShell Bash
-- | Run 'Hapistrano' monad setting a particular shell.
runHapWithShell :: Shell -> Hapistrano a -> IO a
runHapWithShell shell m = do
let printFnc dest str =
case dest of
StdoutDest -> putStr str
StderrDest -> hPutStr stderr str
r <- Hap.runHapistrano Nothing shell printFnc m
case r of
Left n -> do
expectationFailure ("Failed with status code: " ++ show n)
return undefined
-- ↑ because expectationFailure from Hspec has wrong type :-(
Right x -> return x
-- | Make a 'Task' given deploy path and path to the repo.
mkTask :: Path Abs Dir -> Path Abs Dir -> Task
mkTask deployPath repoPath =
mkTaskWithCustomRevision deployPath repoPath "master"
mkTaskWithCustomRevision :: Path Abs Dir -> Path Abs Dir -> String -> Task
mkTaskWithCustomRevision deployPath repoPath revision =
Task
{ taskDeployPath = deployPath
, taskSource =
GitRepository
{ gitRepositoryURL = fromAbsDir repoPath
, gitRepositoryRevision = revision
}
, taskReleaseFormat = ReleaseLong
}
-- | Creates a release tagged with 'Success' or 'Fail'
mkReleaseWithState :: Path Abs Dir -> Path Abs Dir -> DeployState -> Hapistrano Release
mkReleaseWithState deployPath repoPath state = do
r <- Hap.pushRelease (mkTask deployPath repoPath)
Hap.createHapistranoDeployState deployPath r state
return r
currentSystem :: TargetSystem
currentSystem =
if os == "linux"
then GNULinux
else BSD
arbitraryReleaseFormat :: Gen ReleaseFormat
arbitraryReleaseFormat = elements [ReleaseShort, ReleaseLong]
arbitraryKeepReleases :: Gen Natural
arbitraryKeepReleases = fromInteger . getPositive <$> arbitrary