hapistrano/spec/System/HapistranoSpec.hs
Franz Guzmán c377835763
Maintenance mode commands (#169)
* Create maintenance command

* Read filepath and filename

zsh:1: command not found: q

* Addd config and test

* Addd config and test

* Add tests and imports

* Add test for writing maintenance file

Co-authored-by: Cristhian Motoche <CristhianMotoche@users.noreply.github.com>

* Expand writeMaintenancFile function

Co-authored-by: Cristhian Motoche <CristhianMotoche@users.noreply.github.com>

* Add functionality for command enable

Co-authored-by: Cristhian Motoche <CristhianMotoche@users.noreply.github.com>

* Add delete function

* Add filename and directory from configPath

zsh:1: command not found: wq

* Remove unused file

* Change variable name

* Remove Utils from cabal file

* Remove environment file

* Change pattern

* Add suggested formatting and comments

* Add more suggestions and option to run stack

* Update README with new variables

* Update README with changes

Co-authored-by: Cristhian Motoche <CristhianMotoche@users.noreply.github.com>
2022-04-19 14:03:17 -05:00

452 lines
20 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{-# 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 (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, 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.Directory
import System.Hapistrano.Maintenance
import Path
import Control.Monad.IO.Class
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"
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.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