Quickcheck property tests (#128)

* Add property check quote command

* Add property test trim function

* Fix biased test cases for trim function

* Stack lock

* Separated property tests from other tests

* Fixes on imports

* PR quickcheck

* Remove wrong comment

* Add property test for generic and ungeneric command

* Refactoring isCmdString function

* Fix conflicts

* Add quickchek part 1
This commit is contained in:
William R. Arellano 2019-11-11 13:01:17 -05:00 committed by Juan Paucar
parent 9a7fa97794
commit f8a869ce3d
8 changed files with 757 additions and 668 deletions

View File

@ -40,7 +40,7 @@ script:
- cabal build --enable-tests
- cabal test --enable-tests
- cabal sdist
- cabal haddock | grep "100%" | wc -l | grep "4"
- cabal haddock | grep "100%" | wc -l | grep -G "[45]" # Fixes issue with different haddock coverage with different ghc versions https://github.com/haskell/haddock/issues/123
# There is a single Docker image, there are no variants for different versions of GHC
- if [[ "$GHCVER" == "8.0.2" ]]; then docker build . -t hapistrano; docker run --rm hapistrano --version; fi

View File

@ -50,6 +50,7 @@ library
, System.Hapistrano.Commands
, System.Hapistrano.Core
, System.Hapistrano.Types
, System.Hapistrano.Commands.Internal
build-depends: aeson >= 0.11 && < 1.5
, ansi-terminal >= 0.9 && < 0.11
, base >= 4.8 && < 5.0
@ -99,6 +100,7 @@ test-suite test
hs-source-dirs: spec
main-is: Spec.hs
other-modules: System.HapistranoSpec
, System.HapistranoPropsSpec
build-depends: base >= 4.8 && < 5.0
, directory >= 1.2.5 && < 1.4
, filepath >= 1.2 && < 1.5

View File

@ -0,0 +1,85 @@
module System.HapistranoPropsSpec
( spec
) where
import Data.Char (isSpace)
import System.Hapistrano.Commands.Internal (mkGenericCommand,
quoteCmd, trim,
unGenericCommand)
import Test.Hspec hiding (shouldBe,
shouldReturn)
import Test.QuickCheck
spec :: Spec
spec =
describe "QuickCheck" $
context "Properties" $ do
it "property of quote command" $ property propQuote'
it "property of trimming a command" $
property $ forAll trimGenerator propTrim'
it "property of mkGenericCommand and unGenericCommand" $
property $ forAll genericCmdGenerator propGenericCmd'
-- Is quoted determine
isQuoted :: String -> Bool
isQuoted str = head str == '"' && last str == '"'
-- | Quote function property
propQuote :: String -> Bool
propQuote str =
if any isSpace str
then isQuoted $ quoteCmd str
else quoteCmd str == str
propQuote' :: String -> Property
propQuote' str =
classify (any isSpace str) "has at least a space" $ propQuote str
-- | Is trimmed
isTrimmed' :: String -> Bool
isTrimmed' [] = True
isTrimmed' [x] = not $ isSpace x
isTrimmed' str =
let a = not . isSpace $ head str
b = not . isSpace $ last str
in a && b
-- | Prop trimm
propTrim :: String -> Bool
propTrim = isTrimmed' . trim
propTrim' :: String -> Property
propTrim' str =
classify (not $ isTrimmed' str) "non trimmed strings" $ propTrim str
-- | Check that the string is perfect command String
isCmdString :: String -> Bool
isCmdString str = all ($str) [not . null, notElem '#', notElem '\n', isTrimmed']
-- | Prop Generic Command
-- If the string does not contain # or \n, is trimmed and non null, the command should be created
propGenericCmd :: String -> Bool
propGenericCmd str =
if isCmdString str
then maybe False ((== str) . unGenericCommand) (mkGenericCommand str)
else maybe True ((/= str) . unGenericCommand) (mkGenericCommand str) -- Either the command cannot be created or the command str is different to the original
propGenericCmd' :: String -> Property
propGenericCmd' str =
classify (isCmdString str) "perfect command string" propGenericCmd
-- | Trim String Generator
trimGenerator :: Gen String
trimGenerator =
let strGen = listOf arbitraryUnicodeChar
in frequency
[ (1, suchThat strGen isTrimmed')
, (1, suchThat strGen (not . isTrimmed'))
]
-- | Generic Command generator
genericCmdGenerator :: Gen String
genericCmdGenerator =
let strGen = listOf $ elements $ ['A'..'Z'] ++ ['a'..'z'] ++ [' ', '#', '*', '/', '.']
in frequency
[(1, suchThat strGen isCmdString), (1, suchThat strGen (elem '#'))]

View File

@ -1,28 +1,29 @@
{-# LANGUAGE TemplateHaskell #-}
module System.HapistranoSpec
( spec )
where
( 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 (listDirectory)
import qualified System.Hapistrano as Hap
import Control.Monad
import Control.Monad.Reader
import Data.Char (isSpace)
import Data.List (isPrefixOf)
import Data.Maybe (mapMaybe)
import Numeric.Natural
import Path
import Path.IO
import System.Directory (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.Silently (capture_)
import System.Info (os)
import System.IO
import Test.Hspec hiding (shouldBe, shouldReturn)
import qualified Test.Hspec as Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
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
testBranchName :: String
testBranchName = "another_branch"
@ -31,296 +32,296 @@ 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
expectedOutput = "hapistrano\nonartsipah"
in do
actualOutput <- capture_ (runHap commandExecution)
it "redirects commands' output to stdout first" $
let (Just commandTest) =
Hap.mkGenericCommand
"echo \"hapistrano\"; sleep 2; echo \"onartsipah\""
commandExecution = Hap.execWithInheritStdout commandTest
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 <- makeAbsolute $(mkRelFile "script/clean-build.sh")
(fmap Hap.unGenericCommand <$> Hap.readScript spath)
`Hspec.shouldReturn`
(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" ]
, "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
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 ->
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 ->
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
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
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 ->
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 ->
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
fromMaybeKeepReleases Nothing Nothing `Hspec.shouldBe` 5
around withSandbox $ do
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
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
-- 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
(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
-- 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
(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
-- let's check that the dir exists and contains the right files
(liftIO . readFile . fromAbsFile) (rpath </> $(mkRelFile "bar.txt"))
`shouldReturn` "Bar!\n"
(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)
justExec
rpath
("test `git rev-parse --abbrev-ref HEAD` = " ++ testBranchName)
-- This fails if there are unstaged changes
justExec rpath "git diff --exit-code"
justExec rpath "git diff --exit-code"
describe "registerReleaseAsComplete" $
it "creates the token all right" $ \(deployPath, repoPath) -> runHap $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
Hap.registerReleaseAsComplete deployPath release
(Hap.ctokenPath deployPath release >>= doesFileExist) `shouldReturn` True
it "creates the token all right" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
Hap.registerReleaseAsComplete deployPath release
(Hap.ctokenPath deployPath release >>= doesFileExist) `shouldReturn`
True
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
let rc :: Hap.Readlink Dir
rc = Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
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
let rc :: Hap.Readlink Dir
rc =
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath
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
Hap.registerReleaseAsComplete deployPath release
(Hap.ctokenPath deployPath release >>= doesFileExist) `shouldReturn` True
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
Hap.registerReleaseAsComplete deployPath release
(Hap.ctokenPath deployPath release >>= doesFileExist) `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.registerReleaseAsComplete deployPath release) `shouldThrow` anyException
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.registerReleaseAsComplete deployPath release) `shouldThrow`
anyException
describe "rollback" $ do
context "without completion tokens" $
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)
let rc :: Hap.Readlink Dir
rc = Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
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)
let rc :: Hap.Readlink Dir
rc =
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
context "with completion tokens" $
it "resets the current symlink correctly" $ \(deployPath, repoPath) -> runHap $ do
let task = mkTask deployPath repoPath
rs <- replicateM 5 (Hap.pushRelease task)
forM_ (take 3 rs) (Hap.registerReleaseAsComplete deployPath)
Hap.rollback currentSystem deployPath 2
rpath <- Hap.releasePath deployPath (rs !! 0)
let rc :: Hap.Readlink Dir
rc = Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
it "resets the current symlink correctly" $ \(deployPath, repoPath) ->
runHap $ do
let task = mkTask deployPath repoPath
rs <- replicateM 5 (Hap.pushRelease task)
forM_ (take 3 rs) (Hap.registerReleaseAsComplete deployPath)
Hap.rollback currentSystem deployPath 2
rpath <- Hap.releasePath deployPath (rs !! 0)
let rc :: Hap.Readlink Dir
rc =
Hap.Readlink currentSystem (Hap.currentSymlinkPath deployPath)
Hap.exec rc `shouldReturn` rpath
doesFileExist (Hap.tempSymlinkPath deployPath) `shouldReturn` False
describe "dropOldReleases" $
it "works" $ \(deployPath, repoPath) -> runHap $ do
rs <- replicateM 7 $ do
r <- Hap.pushRelease (mkTask deployPath repoPath)
Hap.registerReleaseAsComplete deployPath r
return r
Hap.dropOldReleases deployPath 5
it "works" $ \(deployPath, repoPath) ->
runHap $ do
rs <-
replicateM 7 $ do
r <- Hap.pushRelease (mkTask deployPath repoPath)
Hap.registerReleaseAsComplete deployPath r
return r
Hap.dropOldReleases deployPath 5
-- two oldest releases should not survive:
forM_ (take 2 rs) $ \r ->
(Hap.releasePath deployPath r >>= doesDirExist)
`shouldReturn` False
forM_ (take 2 rs) $ \r ->
(Hap.releasePath deployPath r >>= doesDirExist) `shouldReturn` False
-- 5 most recent releases should stay alive:
forM_ (drop 2 rs) $ \r ->
(Hap.releasePath deployPath r >>= doesDirExist)
`shouldReturn` True
forM_ (drop 2 rs) $ \r ->
(Hap.releasePath deployPath r >>= doesDirExist) `shouldReturn` True
-- two oldest completion tokens should not survive:
forM_ (take 2 rs) $ \r ->
(Hap.ctokenPath deployPath r >>= doesFileExist)
`shouldReturn` False
forM_ (take 2 rs) $ \r ->
(Hap.ctokenPath deployPath r >>= doesFileExist) `shouldReturn` False
-- 5 most recent completion tokens should stay alive:
forM_ (drop 2 rs) $ \r ->
(Hap.ctokenPath deployPath r >>= doesFileExist)
`shouldReturn` True
forM_ (drop 2 rs) $ \r ->
(Hap.ctokenPath deployPath r >>= doesFileExist) `shouldReturn` True
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
Hap.exec $ Hap.Rm sharedDir
Hap.linkToShared currentSystem rpath deployPath "thing"
`shouldReturn` ()
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
Hap.exec $ Hap.Rm sharedDir
Hap.linkToShared currentSystem rpath deployPath "thing" `shouldReturn`
()
context "when the file/directory to link exists in the respository" $
it "should throw an error" $ \(deployPath, repoPath) -> runHap (do
let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release
Hap.linkToShared currentSystem rpath deployPath "foo.txt")
`shouldThrow` anyException
it "should throw an error" $ \(deployPath, repoPath) ->
runHap
(do let task = mkTask deployPath repoPath
release <- Hap.pushRelease task
rpath <- Hap.releasePath deployPath release
Hap.linkToShared currentSystem rpath deployPath "foo.txt") `shouldThrow`
anyException
context "when it attemps 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
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/bar.txt")
`shouldThrow` anyException
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
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/bar.txt") `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
justExec sharedDir "echo 'Bar!' > bar.txt"
Hap.linkToShared currentSystem rpath deployPath "bar.txt"
(liftIO . readFile . fromAbsFile) (rpath </> $(mkRelFile "bar.txt"))
`shouldReturn` "Bar!\n"
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
justExec sharedDir "echo 'Bar!' > bar.txt"
Hap.linkToShared currentSystem rpath deployPath "bar.txt"
(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
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
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
justExec sharedDir "echo 'Baz!' > foo/baz.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/") `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
rpath <- Hap.releasePath deployPath release
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
justExec sharedDir "echo 'Baz!' > foo/baz.txt"
Hap.linkToShared currentSystem rpath deployPath "foo/")
`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
justExec sharedDir "mkdir foo/"
justExec sharedDir "echo 'Bar!' > foo/bar.txt"
justExec sharedDir "echo 'Baz!' > foo/baz.txt"
Hap.linkToShared currentSystem rpath deployPath "foo"
files <- (liftIO . listDirectory . fromAbsDir) (rpath </> $(mkRelDir "foo"))
liftIO $ files `shouldMatchList` ["baz.txt","bar.txt"]
Hap.linkToShared currentSystem rpath deployPath "foo"
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)
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'"
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"
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
@ -328,12 +329,10 @@ justExec path cmd' =
Just cmd -> Hap.exec (Hap.Cd path cmd)
-- | 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 =
@ -349,24 +348,27 @@ runHapWithShell shell m = do
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"
mkTask deployPath repoPath =
mkTaskWithCustomRevision deployPath repoPath "master"
mkTaskWithCustomRevision :: Path Abs Dir -> Path Abs Dir -> String -> Task
mkTaskWithCustomRevision deployPath repoPath revision = Task
{ taskDeployPath = deployPath
, taskRepository = fromAbsDir repoPath
, taskRevision = revision
, taskReleaseFormat = ReleaseLong }
mkTaskWithCustomRevision deployPath repoPath revision =
Task
{ taskDeployPath = deployPath
, taskRepository = fromAbsDir repoPath
, taskRevision = revision
, taskReleaseFormat = ReleaseLong
}
currentSystem :: TargetSystem
currentSystem = if os == "linux"
then GNULinux
else BSD
currentSystem =
if os == "linux"
then GNULinux
else BSD
arbitraryReleaseFormat :: Gen ReleaseFormat
arbitraryReleaseFormat = elements [ReleaseShort, ReleaseLong]
arbitraryKeepReleases :: Gen Natural
arbitraryKeepReleases = fromInteger . getPositive <$> arbitrary
arbitraryKeepReleases = fromInteger . getPositive <$> arbitrary

View File

@ -9,308 +9,31 @@
--
-- Collection of type safe shell commands that can be fed into
-- 'System.Hapistrano.Core.runCommand'.
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeFamilies #-}
module System.Hapistrano.Commands
( Command (..)
, Whoami (..)
, Cd (..)
, MkDir (..)
, Rm (..)
, Mv (..)
, Ln (..)
, Ls (..)
, Readlink (..)
, Find (..)
, Touch (..)
, GitCheckout (..)
, GitClone (..)
, GitFetch (..)
, GitReset (..)
( Command(..)
, Whoami(..)
, Cd(..)
, MkDir(..)
, Rm(..)
, Mv(..)
, Ln(..)
, Ls(..)
, Readlink(..)
, Find(..)
, Touch(..)
, GitCheckout(..)
, GitClone(..)
, GitFetch(..)
, GitReset(..)
, GenericCommand
, mkGenericCommand
, unGenericCommand
, readScript )
where
, readScript
) where
import Control.Monad.IO.Class
import Data.Char (isSpace)
import Data.List (dropWhileEnd)
import Data.Maybe (catMaybes, fromJust, mapMaybe)
import Data.Proxy
import Numeric.Natural
import Path
import System.Hapistrano.Types (TargetSystem (..))
----------------------------------------------------------------------------
-- Commands
-- | Class for data types that represent shell commands in typed way.
class Command a where
-- | Type of result.
type Result a :: *
-- | How to render the command before feeding it into shell (possibly via
-- SSH).
renderCommand :: a -> String
-- | How to parse the result from stdout.
parseResult :: Proxy a -> String -> Result a
-- | Unix @whoami@.
data Whoami = Whoami
deriving (Show, Eq, Ord)
instance Command Whoami where
type Result Whoami = String
renderCommand Whoami = "whoami"
parseResult Proxy = trim
-- | Specify directory in which to perform another command.
data Cd cmd = Cd (Path Abs Dir) cmd
instance Command cmd => Command (Cd cmd) where
type Result (Cd cmd) = Result cmd
renderCommand (Cd path cmd) = "(cd " ++ quoteCmd (fromAbsDir path) ++
" && " ++ renderCommand cmd ++ ")"
parseResult Proxy = parseResult (Proxy :: Proxy cmd)
-- | Create a directory. Does not fail if the directory already exists.
data MkDir = MkDir (Path Abs Dir)
instance Command MkDir where
type Result MkDir = ()
renderCommand (MkDir path) = formatCmd "mkdir"
[ Just "-pv"
, Just (fromAbsDir path) ]
parseResult Proxy _ = ()
-- | Delete file or directory.
data Rm where
Rm :: Path Abs t -> Rm
instance Command Rm where
type Result Rm = ()
renderCommand (Rm path) = formatCmd "rm"
[ Just "-rf"
, Just (toFilePath path) ]
parseResult Proxy _ = ()
-- | Move or rename files or directories.
data Mv t = Mv TargetSystem (Path Abs t) (Path Abs t)
instance Command (Mv File) where
type Result (Mv File) = ()
renderCommand (Mv ts old new) = formatCmd "mv"
[ Just flags
, Just (fromAbsFile old)
, Just (fromAbsFile new) ]
where flags = if isLinux ts then "-fvT" else "-fv"
parseResult Proxy _ = ()
instance Command (Mv Dir) where
type Result (Mv Dir) = ()
renderCommand (Mv _ old new) = formatCmd "mv"
[ Just "-fv"
, Just (fromAbsDir old)
, Just (fromAbsDir new) ]
parseResult Proxy _ = ()
-- | Create symlinks.
data Ln where
Ln :: TargetSystem -> Path Abs t -> Path Abs File -> Ln
instance Command Ln where
type Result Ln = ()
renderCommand (Ln ts target linkName) = formatCmd "ln"
[ Just flags
, Just (toFilePath target)
, Just (fromAbsFile linkName) ]
where flags = if isLinux ts then "-svT" else "-sv"
parseResult Proxy _ = ()
-- | Read link.
data Readlink t = Readlink TargetSystem (Path Abs File)
instance Command (Readlink File) where
type Result (Readlink File) = Path Abs File
renderCommand (Readlink ts path) = formatCmd "readlink"
[ flags
, Just (toFilePath path) ]
where flags = if isLinux ts then Just "-f" else Nothing
parseResult Proxy = fromJust . parseAbsFile . trim
instance Command (Readlink Dir) where
type Result (Readlink Dir) = Path Abs Dir
renderCommand (Readlink ts path) = formatCmd "readlink"
[ flags
, Just (toFilePath path) ]
where flags = if isLinux ts then Just "-f" else Nothing
parseResult Proxy = fromJust . parseAbsDir . trim
-- | @ls@, so far used only to check existence of directories, so it's not
-- very functional right now.
data Ls = Ls (Path Abs Dir)
instance Command Ls where
type Result Ls = ()
renderCommand (Ls path) = formatCmd "ls"
[ Just (fromAbsDir path) ]
parseResult Proxy _ = ()
-- | Find (a very limited version).
data Find t = Find Natural (Path Abs Dir)
instance Command (Find Dir) where
type Result (Find Dir) = [Path Abs Dir]
renderCommand (Find maxDepth dir) = formatCmd "find"
[ Just (fromAbsDir dir)
, Just "-maxdepth"
, Just (show maxDepth)
, Just "-type"
, Just "d" ]
parseResult Proxy = mapMaybe parseAbsDir . fmap trim . lines
instance Command (Find File) where
type Result (Find File) = [Path Abs File]
renderCommand (Find maxDepth dir) = formatCmd "find"
[ Just (fromAbsDir dir)
, Just "-maxdepth"
, Just (show maxDepth)
, Just "-type"
, Just "f" ]
parseResult Proxy = mapMaybe parseAbsFile . fmap trim . lines
-- | @touch@.
data Touch = Touch (Path Abs File)
instance Command Touch where
type Result Touch = ()
renderCommand (Touch path) = formatCmd "touch"
[ Just (fromAbsFile path) ]
parseResult Proxy _ = ()
-- | Git checkout.
data GitCheckout = GitCheckout String
instance Command GitCheckout where
type Result GitCheckout = ()
renderCommand (GitCheckout revision) = formatCmd "git"
[ Just "checkout"
, Just revision ]
parseResult Proxy _ = ()
-- | Git clone.
data GitClone = GitClone Bool (Either String (Path Abs Dir)) (Path Abs Dir)
instance Command GitClone where
type Result GitClone = ()
renderCommand (GitClone bare src dest) = formatCmd "git"
[ Just "clone"
, if bare then Just "--bare" else Nothing
, Just (case src of
Left repoUrl -> repoUrl
Right srcPath -> fromAbsDir srcPath)
, Just (fromAbsDir dest) ]
parseResult Proxy _ = ()
-- | Git fetch (simplified).
data GitFetch = GitFetch String
instance Command GitFetch where
type Result GitFetch = ()
renderCommand (GitFetch remote) = formatCmd "git"
[ Just "fetch"
, Just remote
, Just "+refs/heads/\\*:refs/heads/\\*" ]
parseResult Proxy _ = ()
-- | Git reset.
data GitReset = GitReset String
instance Command GitReset where
type Result GitReset = ()
renderCommand (GitReset revision) = formatCmd "git"
[ Just "reset"
, Just revision ]
parseResult Proxy _ = ()
-- | Weakly-typed generic command, avoid using it directly.
data GenericCommand = GenericCommand String
deriving (Show, Eq, Ord)
instance Command GenericCommand where
type Result GenericCommand = ()
renderCommand (GenericCommand cmd) = cmd
parseResult Proxy _ = ()
-- | Smart constructor that allows to create 'GenericCommand's. Just a
-- little bit more safety.
mkGenericCommand :: String -> Maybe GenericCommand
mkGenericCommand str =
if '\n' `elem` str' || null str'
then Nothing
else Just (GenericCommand str')
where
str' = trim (takeWhile (/= '#') str)
-- | Get the raw command back from 'GenericCommand'.
unGenericCommand :: GenericCommand -> String
unGenericCommand (GenericCommand x) = x
-- | Read commands from a file.
readScript :: MonadIO m => Path Abs File -> m [GenericCommand]
readScript path = liftIO $ mapMaybe mkGenericCommand . lines
<$> readFile (fromAbsFile path)
----------------------------------------------------------------------------
-- Helpers
-- | Format a command.
formatCmd :: String -> [Maybe String] -> String
formatCmd cmd args = unwords (quoteCmd <$> (cmd : catMaybes args))
-- | Simple-minded quoter.
quoteCmd :: String -> String
quoteCmd str =
if any isSpace str
then "\"" ++ str ++ "\""
else str
-- | Trim whitespace from beginning and end.
trim :: String -> String
trim = dropWhileEnd isSpace . dropWhile isSpace
isLinux :: TargetSystem -> Bool
isLinux = (== GNULinux)
import System.Hapistrano.Commands.Internal

View File

@ -0,0 +1,294 @@
-- |
-- Module : System.Hapistrano.Commands
-- Copyright : © 2015-Present Stack Builders
-- License : MIT
--
-- Maintainer : Juan Paucar <jpaucar@stackbuilders.com>
-- Stability : experimental
-- Portability : portable
--
-- Collection of type safe shell commands that can be fed into
-- 'System.Hapistrano.Core.runCommand'.
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
module System.Hapistrano.Commands.Internal where
import Control.Monad.IO.Class
import Data.Char (isSpace)
import Data.List (dropWhileEnd)
import Data.Maybe (catMaybes, fromJust, mapMaybe)
import Data.Proxy
import Numeric.Natural
import Path
import System.Hapistrano.Types (TargetSystem (..))
----------------------------------------------------------------------------
-- Commands
-- | Class for data types that represent shell commands in typed way.
class Command a where
-- | Type of result.
type Result a :: *
-- | How to render the command before feeding it into shell (possibly via
-- SSH).
renderCommand :: a -> String
-- | How to parse the result from stdout.
parseResult :: Proxy a -> String -> Result a
-- | Unix @whoami@.
data Whoami =
Whoami
deriving (Show, Eq, Ord)
instance Command Whoami where
type Result Whoami = String
renderCommand Whoami = "whoami"
parseResult Proxy = trim
-- | Specify directory in which to perform another command.
data Cd cmd =
Cd (Path Abs Dir) cmd
instance Command cmd => Command (Cd cmd) where
type Result (Cd cmd) = Result cmd
renderCommand (Cd path cmd) =
"(cd " ++ quoteCmd (fromAbsDir path) ++ " && " ++ renderCommand cmd ++ ")"
parseResult Proxy = parseResult (Proxy :: Proxy cmd)
-- | Create a directory. Does not fail if the directory already exists.
data MkDir =
MkDir (Path Abs Dir)
instance Command MkDir where
type Result MkDir = ()
renderCommand (MkDir path) =
formatCmd "mkdir" [Just "-pv", Just (fromAbsDir path)]
parseResult Proxy _ = ()
-- | Delete file or directory.
data Rm where
Rm :: Path Abs t -> Rm
instance Command Rm where
type Result Rm = ()
renderCommand (Rm path) = formatCmd "rm" [Just "-rf", Just (toFilePath path)]
parseResult Proxy _ = ()
-- | Move or rename files or directories.
data Mv t =
Mv TargetSystem (Path Abs t) (Path Abs t)
instance Command (Mv File) where
type Result (Mv File) = ()
renderCommand (Mv ts old new) =
formatCmd "mv" [Just flags, Just (fromAbsFile old), Just (fromAbsFile new)]
where
flags =
if isLinux ts
then "-fvT"
else "-fv"
parseResult Proxy _ = ()
instance Command (Mv Dir) where
type Result (Mv Dir) = ()
renderCommand (Mv _ old new) =
formatCmd "mv" [Just "-fv", Just (fromAbsDir old), Just (fromAbsDir new)]
parseResult Proxy _ = ()
-- | Create symlinks.
data Ln where
Ln :: TargetSystem -> Path Abs t -> Path Abs File -> Ln
instance Command Ln where
type Result Ln = ()
renderCommand (Ln ts target linkName) =
formatCmd
"ln"
[Just flags, Just (toFilePath target), Just (fromAbsFile linkName)]
where
flags =
if isLinux ts
then "-svT"
else "-sv"
parseResult Proxy _ = ()
-- | Read link.
data Readlink t =
Readlink TargetSystem (Path Abs File)
instance Command (Readlink File) where
type Result (Readlink File) = Path Abs File
renderCommand (Readlink ts path) =
formatCmd "readlink" [flags, Just (toFilePath path)]
where
flags =
if isLinux ts
then Just "-f"
else Nothing
parseResult Proxy = fromJust . parseAbsFile . trim
instance Command (Readlink Dir) where
type Result (Readlink Dir) = Path Abs Dir
renderCommand (Readlink ts path) =
formatCmd "readlink" [flags, Just (toFilePath path)]
where
flags =
if isLinux ts
then Just "-f"
else Nothing
parseResult Proxy = fromJust . parseAbsDir . trim
-- | @ls@, so far used only to check existence of directories, so it's not
-- very functional right now.
data Ls =
Ls (Path Abs Dir)
instance Command Ls where
type Result Ls = ()
renderCommand (Ls path) = formatCmd "ls" [Just (fromAbsDir path)]
parseResult Proxy _ = ()
-- | Find (a very limited version).
data Find t =
Find Natural (Path Abs Dir)
instance Command (Find Dir) where
type Result (Find Dir) = [Path Abs Dir]
renderCommand (Find maxDepth dir) =
formatCmd
"find"
[ Just (fromAbsDir dir)
, Just "-maxdepth"
, Just (show maxDepth)
, Just "-type"
, Just "d"
]
parseResult Proxy = mapMaybe (parseAbsDir . trim) . lines
instance Command (Find File) where
type Result (Find File) = [Path Abs File]
renderCommand (Find maxDepth dir) =
formatCmd
"find"
[ Just (fromAbsDir dir)
, Just "-maxdepth"
, Just (show maxDepth)
, Just "-type"
, Just "f"
]
parseResult Proxy = mapMaybe (parseAbsFile . trim) . lines
-- | @touch@.
data Touch =
Touch (Path Abs File)
instance Command Touch where
type Result Touch = ()
renderCommand (Touch path) = formatCmd "touch" [Just (fromAbsFile path)]
parseResult Proxy _ = ()
-- | Git checkout.
data GitCheckout =
GitCheckout String
instance Command GitCheckout where
type Result GitCheckout = ()
renderCommand (GitCheckout revision) =
formatCmd "git" [Just "checkout", Just revision]
parseResult Proxy _ = ()
-- | Git clone.
data GitClone =
GitClone Bool (Either String (Path Abs Dir)) (Path Abs Dir)
instance Command GitClone where
type Result GitClone = ()
renderCommand (GitClone bare src dest) =
formatCmd
"git"
[ Just "clone"
, if bare
then Just "--bare"
else Nothing
, Just
(case src of
Left repoUrl -> repoUrl
Right srcPath -> fromAbsDir srcPath)
, Just (fromAbsDir dest)
]
parseResult Proxy _ = ()
-- | Git fetch (simplified).
data GitFetch =
GitFetch String
instance Command GitFetch where
type Result GitFetch = ()
renderCommand (GitFetch remote) =
formatCmd
"git"
[Just "fetch", Just remote, Just "+refs/heads/\\*:refs/heads/\\*"]
parseResult Proxy _ = ()
-- | Git reset.
data GitReset =
GitReset String
instance Command GitReset where
type Result GitReset = ()
renderCommand (GitReset revision) =
formatCmd "git" [Just "reset", Just revision]
parseResult Proxy _ = ()
-- | Weakly-typed generic command, avoid using it directly.
data GenericCommand =
GenericCommand String
deriving (Show, Eq, Ord)
instance Command GenericCommand where
type Result GenericCommand = ()
renderCommand (GenericCommand cmd) = cmd
parseResult Proxy _ = ()
-- | Smart constructor that allows to create 'GenericCommand's. Just a
-- little bit more safety.
mkGenericCommand :: String -> Maybe GenericCommand
mkGenericCommand str =
if '\n' `elem` str' || null str'
then Nothing
else Just (GenericCommand str')
where
str' = trim (takeWhile (/= '#') str)
-- | Get the raw command back from 'GenericCommand'.
unGenericCommand :: GenericCommand -> String
unGenericCommand (GenericCommand x) = x
-- | Read commands from a file.
readScript :: MonadIO m => Path Abs File -> m [GenericCommand]
readScript path =
liftIO $ mapMaybe mkGenericCommand . lines <$> readFile (fromAbsFile path)
----------------------------------------------------------------------------
-- Helpers
-- | Format a command.
formatCmd :: String -> [Maybe String] -> String
formatCmd cmd args = unwords (quoteCmd <$> (cmd : catMaybes args))
-- | Simple-minded quoter.
quoteCmd :: String -> String
quoteCmd str =
if any isSpace str
then "\"" ++ str ++ "\""
else str
-- | Trim whitespace from beginning and end.
trim :: String -> String
trim = dropWhileEnd isSpace . dropWhile isSpace
-- | Determines whether or not the target system is a Linux machine.
isLinux :: TargetSystem -> Bool
isLinux = (== GNULinux)

View File

@ -9,7 +9,6 @@
--
-- Core Hapistrano functions that provide basis on which all the
-- functionality is built.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
@ -39,28 +38,30 @@ import System.Process.Typed (ProcessConfig)
import qualified System.Process.Typed as SPT
-- | Run the 'Hapistrano' monad. The monad hosts 'exec' actions.
runHapistrano :: MonadIO m
=> Maybe SshOptions -- ^ SSH options to use or 'Nothing' if we run locally
-> Shell -- ^ Shell to run commands
runHapistrano ::
MonadIO m
=> Maybe SshOptions -- ^ SSH options to use or 'Nothing' if we run locally
-> Shell -- ^ Shell to run commands
-> (OutputDest -> String -> IO ()) -- ^ How to print messages
-> Hapistrano a -- ^ The computation to run
-> m (Either Int a) -- ^ Status code in 'Left' on failure, result in
-> Hapistrano a -- ^ The computation to run
-> m (Either Int a) -- ^ Status code in 'Left' on failure, result in
-- 'Right' on success
runHapistrano sshOptions shell' printFnc m = liftIO $ do
let config = Config
{ configSshOptions = sshOptions
, configShellOptions = shell'
, configPrint = printFnc }
r <- runReaderT (runExceptT m) config
case r of
Left (Failure n msg) -> do
forM_ msg (printFnc StderrDest)
return (Left n)
Right x -> return (Right x)
runHapistrano sshOptions shell' printFnc m =
liftIO $ do
let config =
Config
{ configSshOptions = sshOptions
, configShellOptions = shell'
, configPrint = printFnc
}
r <- runReaderT (runExceptT m) config
case r of
Left (Failure n msg) -> do
forM_ msg (printFnc StderrDest)
return (Left n)
Right x -> return (Right x)
-- | Fail returning the following status code and message.
failWith :: Int -> Maybe String -> Hapistrano a
failWith n msg = throwError (Failure n msg)
@ -71,15 +72,17 @@ failWith n msg = throwError (Failure n msg)
-- __NOTE:__ the commands executed with 'exec' will create their own pipe and
-- will stream output there and once the command finishes its execution it will
-- parse the result.
exec :: forall a. Command a => a -> Hapistrano (Result a)
exec ::
forall a. Command a
=> a
-> Hapistrano (Result a)
exec typedCmd = do
let cmd = renderCommand typedCmd
(prog, args) <- getProgAndArgs cmd
parseResult (Proxy :: Proxy a) <$> exec' cmd (readProcessWithExitCode prog args "")
parseResult (Proxy :: Proxy a) <$>
exec' cmd (readProcessWithExitCode prog args "")
-- | Same as 'exec' but it streams to stdout only for _GenericCommand_s
execWithInheritStdout :: Command a => a -> Hapistrano ()
execWithInheritStdout typedCmd = do
let cmd = renderCommand typedCmd
@ -89,27 +92,22 @@ execWithInheritStdout typedCmd = do
-- | Prepares a process, reads @stdout@ and @stderr@ and returns exit code
-- NOTE: @strdout@ and @stderr@ are empty string because we're writing
-- the output to the parent.
readProcessWithExitCode'
:: ProcessConfig stdin stdoutIgnored stderrIgnored
readProcessWithExitCode' ::
ProcessConfig stdin stdoutIgnored stderrIgnored
-> IO (ExitCode, String, String)
readProcessWithExitCode' pc =
SPT.withProcessTerm pc' $ \p -> atomically $
(,,) <$> SPT.waitExitCodeSTM p
<*> return ""
<*> return ""
where
pc' = SPT.setStdout SPT.inherit
$ SPT.setStderr SPT.inherit pc
SPT.withProcessTerm pc' $ \p ->
atomically $ (,,) <$> SPT.waitExitCodeSTM p <*> return "" <*> return ""
where
pc' = SPT.setStdout SPT.inherit $ SPT.setStderr SPT.inherit pc
-- | Get program and args to run a command locally or remotelly.
getProgAndArgs :: String -> Hapistrano (String, [String])
getProgAndArgs cmd = do
Config {..} <- ask
return $
case configSshOptions of
Nothing ->
(renderShell configShellOptions, ["-c", cmd])
Nothing -> (renderShell configShellOptions, ["-c", cmd])
Just SshOptions {..} ->
("ssh", sshArgs ++ [sshHost, "-p", show sshPort, cmd])
where
@ -117,29 +115,22 @@ getProgAndArgs cmd = do
renderShell Zsh = "zsh"
renderShell Bash = "bash"
-- | Copy a file from local path to target server.
scpFile
:: Path Abs File -- ^ Location of the file to copy
-> Path Abs File -- ^ Where to put the file on target machine
-- | Copy a file from local path to target server.
scpFile ::
Path Abs File -- ^ Location of the file to copy
-> Path Abs File -- ^ Where to put the file on target machine
-> Hapistrano ()
scpFile src dest =
scp' (fromAbsFile src) (fromAbsFile dest) ["-q"]
scpFile src dest = scp' (fromAbsFile src) (fromAbsFile dest) ["-q"]
-- | Copy a local directory recursively to target server.
scpDir
:: Path Abs Dir -- ^ Location of the directory to copy
-> Path Abs Dir -- ^ Where to put the dir on target machine
scpDir ::
Path Abs Dir -- ^ Location of the directory to copy
-> Path Abs Dir -- ^ Where to put the dir on target machine
-> Hapistrano ()
scpDir src dest =
scp' (fromAbsDir src) (fromAbsDir dest) ["-qr"]
scpDir src dest = scp' (fromAbsDir src) (fromAbsDir dest) ["-qr"]
scp'
:: FilePath
-> FilePath
-> [String]
-> Hapistrano ()
scp' :: FilePath -> FilePath -> [String] -> Hapistrano ()
scp' src dest extraArgs = do
Config {..} <- ask
let prog = "scp"
@ -152,21 +143,19 @@ scp' src dest extraArgs = do
Nothing -> ""
Just x -> x ++ ":"
args = extraArgs ++ portArg ++ [src, hostPrefix ++ dest]
void (exec' (prog ++ " " ++ unwords args) (readProcessWithExitCode prog args ""))
void
(exec' (prog ++ " " ++ unwords args) (readProcessWithExitCode prog args ""))
----------------------------------------------------------------------------
-- Helpers
-- | A helper for 'exec' and similar functions.
exec'
:: String -- ^ How to show the command in print-outs
exec' ::
String -- ^ How to show the command in print-outs
-> IO (ExitCode, String, String) -- ^ Handler to get (ExitCode, Output, Error) it can change accordingly to @stdout@ and @stderr@ of child process
-> Hapistrano String -- ^ Raw stdout output of that program
exec' cmd readProcessOutput = do
Config {..} <- ask
time <- liftIO getZonedTime
let timeStampFormat = "%T, %F (%Z)"
printableTime = formatTime defaultTimeLocale timeStampFormat time
hostLabel =
@ -178,18 +167,13 @@ exec' cmd readProcessOutput = do
cmdInfo = colorizeString Green (cmd ++ "\n")
liftIO $ configPrint StdoutDest (hostInfo ++ timestampInfo ++ cmdInfo)
(exitCode', stdout', stderr') <- liftIO readProcessOutput
unless (null stdout') . liftIO $
configPrint StdoutDest stdout'
unless (null stderr') . liftIO $
configPrint StderrDest stderr'
unless (null stdout') . liftIO $ configPrint StdoutDest stdout'
unless (null stderr') . liftIO $ configPrint StderrDest stderr'
case exitCode' of
ExitSuccess ->
return stdout'
ExitFailure n ->
failWith n Nothing
ExitSuccess -> return stdout'
ExitFailure n -> failWith n Nothing
-- | Put something “inside” a line, sort-of beautifully.
putLine :: String -> String
putLine str = "*** " ++ str ++ padding ++ "\n"
where

View File

@ -8,18 +8,17 @@
-- Portability : portable
--
-- Type definitions for the Hapistrano tool.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
module System.Hapistrano.Types
( Hapistrano
, Failure (..)
, Config (..)
, Task (..)
, Failure(..)
, Config(..)
, Task(..)
, ReleaseFormat(..)
, SshOptions (..)
, OutputDest (..)
, SshOptions(..)
, OutputDest(..)
, Release
, TargetSystem(..)
, Shell(..)
@ -28,142 +27,142 @@ module System.Hapistrano.Types
, renderRelease
, parseRelease
, fromMaybeReleaseFormat
, fromMaybeKeepReleases )
where
, fromMaybeKeepReleases
) where
import Control.Applicative
import Control.Monad.Except
import Control.Monad.Reader
import Data.Aeson
import Data.Maybe
import Data.Time
import Numeric.Natural
import Path
import Control.Applicative
import Control.Monad.Except
import Control.Monad.Reader
import Data.Aeson
import Data.Maybe
import Data.Time
import Numeric.Natural
import Path
-- | Hapistrano monad.
type Hapistrano a = ExceptT Failure (ReaderT Config IO) a
-- | Failure with status code and a message.
data Failure = Failure Int (Maybe String)
data Failure =
Failure Int (Maybe String)
-- | Hapistrano configuration options.
data Config = Config
{ configSshOptions :: !(Maybe SshOptions)
data Config =
Config
{ configSshOptions :: !(Maybe SshOptions)
-- ^ 'Nothing' if we are running locally, or SSH options to use.
, configShellOptions :: !Shell
, configShellOptions :: !Shell
-- ^ One of the supported 'Shell's
, configPrint :: !(OutputDest -> String -> IO ())
, configPrint :: !(OutputDest -> String -> IO ())
-- ^ How to print messages
}
}
-- | The records describes deployment task.
data Task = Task
{ taskDeployPath :: Path Abs Dir
data Task =
Task
{ taskDeployPath :: Path Abs Dir
-- ^ The root of the deploy target on the remote host
, taskRepository :: String
, taskRepository :: String
-- ^ The URL of remote Git repo to deploy
, taskRevision :: String
, taskRevision :: String
-- ^ A SHA1 or branch to release
, taskReleaseFormat :: ReleaseFormat
, taskReleaseFormat :: ReleaseFormat
-- ^ The 'ReleaseFormat' to use
} deriving (Show, Eq, Ord)
}
deriving (Show, Eq, Ord)
-- | Release format mode.
data ReleaseFormat
= ReleaseShort -- ^ Standard release path following Capistrano's format
| ReleaseLong -- ^ Long release path including picoseconds
| ReleaseLong -- ^ Long release path including picoseconds
deriving (Show, Read, Eq, Ord, Enum, Bounded)
instance FromJSON ReleaseFormat where
parseJSON = withText "release format" $ \case
parseJSON =
withText "release format" $ \case
"short" -> return ReleaseShort
"long" -> return ReleaseLong
_ -> fail "expected 'short' or 'long'"
"long" -> return ReleaseLong
_ -> fail "expected 'short' or 'long'"
-- | Current shells supported.
data Shell =
Bash
data Shell
= Bash
| Zsh
deriving (Show, Eq, Ord)
instance FromJSON Shell where
parseJSON = withText "shell" $ \case
parseJSON =
withText "shell" $ \case
"bash" -> return Bash
"zsh" -> return Zsh
_ -> fail "supported shells: 'bash' or 'zsh'"
"zsh" -> return Zsh
_ -> fail "supported shells: 'bash' or 'zsh'"
-- | SSH options.
data SshOptions = SshOptions
{ sshHost :: String -- ^ Host to use
, sshPort :: Word -- ^ Port to use
, sshArgs :: [String] -- ^ Arguments for ssh
} deriving (Show, Read, Eq, Ord)
data SshOptions =
SshOptions
{ sshHost :: String -- ^ Host to use
, sshPort :: Word -- ^ Port to use
, sshArgs :: [String] -- ^ Arguments for ssh
}
deriving (Show, Read, Eq, Ord)
-- | Output destination.
data OutputDest
= StdoutDest
| StderrDest
deriving (Eq, Show, Read, Ord, Bounded, Enum)
-- | Release indentifier.
data Release = Release ReleaseFormat UTCTime
data Release =
Release ReleaseFormat UTCTime
deriving (Eq, Show, Ord)
-- | Target's system where application will be deployed
data TargetSystem
= GNULinux
| BSD
deriving (Eq, Show, Read, Ord, Bounded, Enum)
-- | Create a 'Release' indentifier.
mkRelease :: ReleaseFormat -> UTCTime -> Release
mkRelease = Release
-- | Extract deployment time from 'Release'.
releaseTime :: Release -> UTCTime
releaseTime (Release _ time) = time
-- | Render 'Release' indentifier as a 'String'.
renderRelease :: Release -> String
renderRelease (Release rfmt time) = formatTime defaultTimeLocale fmt time
where
fmt = case rfmt of
ReleaseShort -> releaseFormatShort
ReleaseLong -> releaseFormatLong
fmt =
case rfmt of
ReleaseShort -> releaseFormatShort
ReleaseLong -> releaseFormatLong
-- | Parse 'Release' identifier from a 'String'.
parseRelease :: String -> Maybe Release
parseRelease s = (Release ReleaseLong <$> p releaseFormatLong s)
<|> (Release ReleaseShort <$> p releaseFormatShort s)
parseRelease s =
(Release ReleaseLong <$> p releaseFormatLong s) <|>
(Release ReleaseShort <$> p releaseFormatShort s)
where
p = parseTimeM False defaultTimeLocale
releaseFormatShort, releaseFormatLong :: String
releaseFormatShort = "%Y%m%d%H%M%S"
releaseFormatLong = "%Y%m%d%H%M%S%q"
releaseFormatLong = "%Y%m%d%H%M%S%q"
-- | Get release format based on the CLI and file configuration values.
fromMaybeReleaseFormat :: Maybe ReleaseFormat -> Maybe ReleaseFormat -> ReleaseFormat
fromMaybeReleaseFormat cliRF configRF = fromMaybe ReleaseShort (cliRF <|> configRF)
fromMaybeReleaseFormat ::
Maybe ReleaseFormat -> Maybe ReleaseFormat -> ReleaseFormat
fromMaybeReleaseFormat cliRF configRF =
fromMaybe ReleaseShort (cliRF <|> configRF)
-- | Get keep releases based on the CLI and file configuration values.
fromMaybeKeepReleases :: Maybe Natural -> Maybe Natural -> Natural
fromMaybeKeepReleases cliKR configKR = fromMaybe defaultKeepReleases (cliKR <|> configKR)
fromMaybeKeepReleases cliKR configKR =
fromMaybe defaultKeepReleases (cliKR <|> configKR)
defaultKeepReleases :: Natural
defaultKeepReleases = 5