angel 0.6.1 → 0.6.2
raw patch · 11 files changed
+344/−217 lines, 11 filesdep +tastydep +tasty-hunitdep +tasty-quickcheckdep −hspecdep ~basebuild-type:Customsetup-changed
Dependencies added: tasty, tasty-hunit, tasty-quickcheck
Dependencies removed: hspec
Dependency ranges changed: base
Files
- README.md +12/−13
- Setup.hs +38/−1
- angel.cabal +7/−5
- changelog.md +3/−0
- test/Angel/ConfigSpec.hs +124/−115
- test/Angel/JobSpec.hs +47/−37
- test/Angel/LogSpec.hs +9/−4
- test/Angel/PidFileSpec.hs +20/−16
- test/Angel/UtilSpec.hs +28/−22
- test/Spec.hs +23/−1
- test/SpecHelper.hs +33/−3
README.md view
@@ -13,10 +13,10 @@ The author is a long-time user of `daemontools` due to its reliability and simplicity; however, `daemontools` is quirky and follows many-unusual conventions. +unusual conventions. -`angel` is an attempt to recreate `daemontools`'s capabilities (though -not the various bundled utility programs which are still quite useful) +`angel` is an attempt to recreate `daemontools`'s capabilities (though+not the various bundled utility programs which are still quite useful) in a more intuitive and modern unix style. @@ -24,7 +24,7 @@ ------------- `angel` is driven by a configuration file that contains a list of-program specifications to run. `angel` assumes every program listed in +program specifications to run. `angel` assumes every program listed in the specification file should be running at all times. `angel` starts each program, and optionally sets the program's stdout@@ -35,7 +35,7 @@ If the program dies for any reason, `angel` waits a specified number of seconds (default, 5), then restarts the program. -The `angel` process itself will respond to a HUP signal by +The `angel` process itself will respond to a HUP signal by re-processing its configuration file, and synchronizing the run states with the new configuration. Specifically: @@ -47,7 +47,7 @@ normal supervision, will be restarted with the updated spec * If a program has been removed from the configuration file, the corresponding child process will be sent a TERM signal;- when it dies, supervision of the process will end, and + when it dies, supervision of the process will end, and therefore, it will not be restarted Safety and Reliability@@ -77,7 +77,7 @@ Building -------- - 1. Install the haskell-platform (or somehow, ghc 7.0 + + 1. Install the haskell-platform (or somehow, ghc 7.6 + cabal-install) 2. Run `cabal install` in the project root (this directory) 3. Either add the ~/.cabal/bin file to your $PATH or copy@@ -85,8 +85,7 @@ Notes: - * I have not tried building `angel` against ghc 6.10 or earlier;- 6.12, 7.0, 7.2, 7.4, and 7.6 are known to work+ * Angel is recommended to be built on GHC 7.6 and newer. Configuration and Usage Example -------------------------------@@ -144,7 +143,7 @@ * `exec` is the exact command line to run (required) * `user` is the user the program will run as (optional, defaults to the user angel is launched as)- * `stdout` is a path to a file where the program's standard output + * `stdout` is a path to a file where the program's standard output should be appended (optional, defaults to /dev/null) * `stderr` is a path to a file where the program's standard error should be appended (optional, defaults to /dev/null)@@ -184,7 +183,7 @@ Assuming the above configuration was in a file called "example.conf", here's what a shell session might look like: - jamie@choo:~/random/angel$ angel example.conf + jamie@choo:~/random/angel$ angel example.conf [2010/08/24 15:21:22] {main} Angel started [2010/08/24 15:21:22] {main} Using config file: example.conf [2010/08/24 15:21:22] {process-monitor} Must kill=0, must start=2@@ -264,7 +263,7 @@ You can run the test suite with ```-make test+cabal test ``` FAQ@@ -274,7 +273,7 @@ Yes, angel `dup()`s file descriptors and makes effort to safely allow concurrent writes by child programs; you should DEFINITELY-make sure your child program is doing stdout/stderr writes in +make sure your child program is doing stdout/stderr writes in line-buffered mode so this doesn't result in a complete interleaved mess in the log file.
Setup.hs view
@@ -1,3 +1,40 @@ #!/usr/bin/env runhaskell+import Control.Monad import Distribution.Simple-main = defaultMain+import Distribution.Simple.Setup+import Distribution.Simple.Program.Run+import Distribution.Simple.UserHooks+import Distribution.PackageDescription+import Distribution.Verbosity+-- import System.Process+++main :: IO ()+main = defaultMainWithHooks simpleUserHooks+ {+ preTest = myTestHook+ }+++-------------------------------------------------------------------------------+myTestHook+ :: Args+ -> TestFlags+ -> IO HookedBuildInfo+myTestHook _ _ = do+ buildTestFixtures+ return emptyHookedBuildInfo+++-------------------------------------------------------------------------------+buildTestFixtures :: IO ()+buildTestFixtures = do+ runScript "ghc" ["--make", "test/test_jobs/StubbornJob.hs", "-o", "test/test_jobs/StubbornJob"]+ runScript "ghc" ["--make", "test/test_jobs/CompliantJob.hs", "-o", "test/test_jobs/CompliantJob"]+++-------------------------------------------------------------------------------+runScript :: String -> [String] -> IO ()+runScript cmd args = do+ putStrLn $ unwords [cmd, unwords args]+ runProgramInvocation normal $ simpleProgramInvocation cmd args
angel.cabal view
@@ -1,5 +1,5 @@ Name: angel-Version: 0.6.1+Version: 0.6.2 License: BSD3 License-file: LICENSE Author: Jamie Turner@@ -18,7 +18,7 @@ Stability: Stable Category: System-Build-type: Simple+Build-type: Custom Extra-source-files: README.md changelog.md@@ -36,7 +36,7 @@ Hs-Source-Dirs: src Main-is: Angel/Main.hs - Build-depends: base >= 4.0 && < 5+ Build-depends: base >= 4.5 && < 5 Build-depends: process >= 1.2.0.0 && < 2.0 Build-depends: mtl Build-depends: configurator >= 0.1@@ -74,8 +74,10 @@ Angel.PidFileSpec Angel.UtilSpec SpecHelper- Build-Depends: base- Build-Depends: hspec+ Build-Depends: base+ Build-Depends: tasty+ Build-Depends: tasty-hunit+ Build-Depends: tasty-quickcheck Build-depends: base >= 4.0 && < 5 Build-depends: process >= 1.0 && < 2.0 Build-depends: mtl
changelog.md view
@@ -1,3 +1,6 @@+0.6.2+* Switch to cabal-only build strategy+ 0.6.1 * Fix build for GHC 7.10 by adding FlexibleContexts
test/Angel/ConfigSpec.hs view
@@ -10,131 +10,140 @@ import SpecHelper -spec :: Spec-spec = do- describe "modifyProg" $ do- it "modifies exec" $- modifyProg prog "exec" (String "foo") `shouldBe`- prog { exec = Just "foo"}-- it "errors for non-string execs" $- evaluate (modifyProg prog "exec" (Bool True)) `shouldThrow`- anyErrorCall-- it "modifies delay for positive numbers" $- modifyProg prog "delay" (Number 1) `shouldBe`- prog { delay = Just 1}- it "modifies delay for 0" $- modifyProg prog "delay" (Number 0) `shouldBe`- prog { delay = Just 0}- it "errors on negative delays" $- evaluate (modifyProg prog "delay" (Number (-1))) `shouldThrow`- anyErrorCall+spec :: TestTree+spec = testGroup "Angel.Config"+ [+ testGroup "modifyProg"+ [+ testCase "modifies exec" $+ modifyProg prog "exec" (String "foo") @?=+ prog { exec = Just "foo"} - it "modifies stdout" $- modifyProg prog "stdout" (String "foo") `shouldBe`- prog { stdout = Just "foo"}- it "errors for non-string stdout" $- evaluate (modifyProg prog "stdout" (Bool True)) `shouldThrow`- anyErrorCall+ , testCase "errors for non-string execs" $+ evaluate (modifyProg prog "exec" (Bool True)) `shouldThrow`+ anyErrorCall - it "modifies stderr" $- modifyProg prog "stderr" (String "foo") `shouldBe`- prog { stderr = Just "foo"}- it "errors for non-string stderr" $- evaluate (modifyProg prog "stderr" (Bool True)) `shouldThrow`- anyErrorCall+ , testCase "modifies delay for positive numbers" $+ modifyProg prog "delay" (Number 1) @?=+ prog { delay = Just 1}+ , testCase "modifies delay for 0" $+ modifyProg prog "delay" (Number 0) @?=+ prog { delay = Just 0}+ , testCase "errors on negative delays" $+ evaluate (modifyProg prog "delay" (Number (-1))) `shouldThrow`+ anyErrorCall - it "modifies directory" $- modifyProg prog "directory" (String "foo") `shouldBe`- prog { workingDir = Just "foo"}- it "errors for non-string directory" $- evaluate (modifyProg prog "directory" (Bool True)) `shouldThrow`- anyErrorCall+ , testCase "modifies stdout" $+ modifyProg prog "stdout" (String "foo") @?=+ prog { stdout = Just "foo"}+ , testCase "errors for non-string stdout" $+ evaluate (modifyProg prog "stdout" (Bool True)) `shouldThrow`+ anyErrorCall - it "modifies pidfile" $- modifyProg prog "pidfile" (String "foo.pid") `shouldBe`- prog { pidFile = Just "foo.pid"}- it "errors for non-string path" $- evaluate (modifyProg prog "pidfile" (Bool True)) `shouldThrow`- anyErrorCall+ , testCase "modifies stderr" $+ modifyProg prog "stderr" (String "foo") @?=+ prog { stderr = Just "foo"}+ , testCase "errors for non-string stderr" $+ evaluate (modifyProg prog "stderr" (Bool True)) `shouldThrow`+ anyErrorCall - it "appends env to the empty list" $- modifyProg prog "env.foo" (String "bar") `shouldBe`- prog { env = [("foo", "bar")]}- it "errors for non-string value" $- evaluate (modifyProg prog "env.foo" (Bool True)) `shouldThrow`- anyErrorCall- it "prepends env to an existing list" $- modifyProg prog { env = [("previous", "value")]} "env.foo" (String "bar") `shouldBe`- prog { env = [("foo", "bar"), ("previous", "value")]}+ , testCase "modifies directory" $+ modifyProg prog "directory" (String "foo") @?=+ prog { workingDir = Just "foo"}+ , testCase "errors for non-string directory" $+ evaluate (modifyProg prog "directory" (Bool True)) `shouldThrow`+ anyErrorCall - it "interprets boolean False as Nothing" $- modifyProg prog "termgrace" (Bool False) `shouldBe`- prog { termGrace = Nothing }- it "interprets 0 as Nothing" $- modifyProg prog "termgrace" (Number 0) `shouldBe`- prog { termGrace = Nothing }- it "interprets > 0 as a set termGrace" $- modifyProg prog "termgrace" (Number 2) `shouldBe`- prog { termGrace = Just 2 }- it "interprets boolean True as an error" $- evaluate (modifyProg prog "termgrace" (Bool True)) `shouldThrow`- anyErrorCall- it "interprets negative numbers as an error" $- evaluate (modifyProg prog "termgrace" (Number (-1))) `shouldThrow`- anyErrorCall- it "interprets anything else as an error" $- evaluate (modifyProg prog "termgrace" (String "yeah")) `shouldThrow`- anyErrorCall+ , testCase "modifies pidfile" $+ modifyProg prog "pidfile" (String "foo.pid") @?=+ prog { pidFile = Just "foo.pid"}+ , testCase "errors for non-string path" $+ evaluate (modifyProg prog "pidfile" (Bool True)) `shouldThrow`+ anyErrorCall - it "does nothing for all other cases" $- modifyProg prog "bogus" (String "foo") `shouldBe`- prog+ , testCase "appends env to the empty list" $+ modifyProg prog "env.foo" (String "bar") @?=+ prog { env = [("foo", "bar")]}+ , testCase "errors for non-string value" $+ evaluate (modifyProg prog "env.foo" (Bool True)) `shouldThrow`+ anyErrorCall+ , testCase "prepends env to an existing list" $+ modifyProg prog { env = [("previous", "value")]} "env.foo" (String "bar") @?=+ prog { env = [("foo", "bar"), ("previous", "value")]} - describe "expandByCount" $ do- it "doesn't affect empty hashes" $- expandByCount HM.empty `shouldBe`- HM.empty- it "doesn't affect hashes without counts" $- expandByCount (HM.fromList [baseProgPair]) `shouldBe`- HM.fromList [baseProgPair]- it "errors on mistyped count field" $- evaluate (expandByCount (HM.fromList [baseProgPair- , ("prog.count", String "wat")])) `shouldThrow`+ , testCase "interprets boolean False as Nothing" $+ modifyProg prog "termgrace" (Bool False) @?=+ prog { termGrace = Nothing }+ , testCase "interprets 0 as Nothing" $+ modifyProg prog "termgrace" (Number 0) @?=+ prog { termGrace = Nothing }+ , testCase "interprets > 0 as a set termGrace" $+ modifyProg prog "termgrace" (Number 2) @?=+ prog { termGrace = Just 2 }+ , testCase "interprets boolean True as an error" $+ evaluate (modifyProg prog "termgrace" (Bool True)) `shouldThrow` anyErrorCall- it "errors on negative count field" $- evaluate (expandByCount (HM.fromList [ baseProgPair- , ("prog.count", Number (-1))])) `shouldThrow`+ , testCase "interprets negative numbers as an error" $+ evaluate (modifyProg prog "termgrace" (Number (-1))) `shouldThrow` anyErrorCall- it "generates no configs with a count of 0" $- expandByCount (HM.fromList [ baseProgPair- , ("prog.count", Number 0)]) `shouldBe`- HM.empty- it "expands with a count of 1" $- expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 1)]) `shouldBe`- HM.fromList [ ("prog-1.exec", String "foo")- , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")]- it "expands with a count of > 1" $- expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 2)]) `shouldBe`- HM.fromList [ ("prog-1.exec", String "foo")- , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")- , ("prog-2.exec", String "foo")- , ("prog-2.env.ANGEL_PROCESS_NUMBER", String "2")]- it "preserves explicit env variables" $- expandByCount (HM.fromList [baseProgPair, ("prog.env.FOO", String "bar")]) `shouldBe`- HM.fromList [ ("prog.exec", String "foo")- , ("prog.env.FOO", String "bar")]- it "expands pidfiles with a count of 1" $- expandByCount (HM.fromList [ baseProgPair- , ("prog.count", Number 1)- , ("prog.pidfile", String "foo.pid")]) `shouldBe`- HM.fromList [ ("prog-1.exec", String "foo")- , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")- , ("prog-1.pidfile", String "foo-1.pid")] --TODO: try without expanding if count == 1- describe "processConfig internal API" $- it "can parse the example config" $- shouldReturnRight $ processConfig "example.conf"+ , testCase "interprets anything else as an error" $+ evaluate (modifyProg prog "termgrace" (String "yeah")) `shouldThrow`+ anyErrorCall++ , testCase "does nothing for all other cases" $+ modifyProg prog "bogus" (String "foo") @?=+ prog+ ]++ , testGroup "expandByCount"+ [+ testCase "doesn't affect empty hashes" $+ expandByCount HM.empty @?=+ HM.empty+ , testCase "doesn't affect hashes without counts" $+ expandByCount (HM.fromList [baseProgPair]) @?=+ HM.fromList [baseProgPair]+ , testCase "errors on mistyped count field" $+ evaluate (expandByCount (HM.fromList [baseProgPair+ , ("prog.count", String "wat")])) `shouldThrow`+ anyErrorCall+ , testCase "errors on negative count field" $+ evaluate (expandByCount (HM.fromList [ baseProgPair+ , ("prog.count", Number (-1))])) `shouldThrow`+ anyErrorCall+ , testCase "generates no configs with a count of 0" $+ expandByCount (HM.fromList [ baseProgPair+ , ("prog.count", Number 0)]) @?=+ HM.empty+ , testCase "expands with a count of 1" $+ expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 1)]) @?=+ HM.fromList [ ("prog-1.exec", String "foo")+ , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")]+ , testCase "expands with a count of > 1" $+ expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 2)]) @?=+ HM.fromList [ ("prog-1.exec", String "foo")+ , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")+ , ("prog-2.exec", String "foo")+ , ("prog-2.env.ANGEL_PROCESS_NUMBER", String "2")]+ , testCase "preserves explicit env variables" $+ expandByCount (HM.fromList [baseProgPair, ("prog.env.FOO", String "bar")]) @?=+ HM.fromList [ ("prog.exec", String "foo")+ , ("prog.env.FOO", String "bar")]+ , testCase "expands pidfiles with a count of 1" $+ expandByCount (HM.fromList [ baseProgPair+ , ("prog.count", Number 1)+ , ("prog.pidfile", String "foo.pid")]) @?=+ HM.fromList [ ("prog-1.exec", String "foo")+ , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")+ , ("prog-1.pidfile", String "foo-1.pid")] --TODO: try without expanding if count == 1+ ]++ , testGroup "processConfig internal API"+ [+ testCase "can parse the example config" $+ shouldReturnRight $ processConfig "example.conf"+ ]+ ] where prog = defaultProgram baseProgPair = ("prog.exec", String "foo") shouldReturnRight a = flip shouldSatisfy isRight =<< a
test/Angel/JobSpec.hs view
@@ -18,47 +18,57 @@ import SpecHelper -spec :: Spec-spec =- describe "killProcess" $ do- describe "using SoftKill" $ do- it "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do- ph <- liftIO launchCompliantJob- killProcess $ SoftKill "thing" ph Nothing- liftIO $- patientlyGetProcessExitCode ph `shouldReturn` (Just $ Exited ExitSuccess)-- it "does not forcefully kill stubborn processes" $ runAngelM dummyOptions $ do- ph <- liftIO launchStubbornJob- killProcess $ SoftKill "thing" ph Nothing- -- stubborn job gets marked as [defunct] here. no idea why. it should be able to survive a SIGTERM- liftIO $ do- patientlyGetProcessExitCode ph `shouldReturn` Nothing- hardKillProcessHandle ph -- cleanup+spec :: TestTree+spec = testGroup "Angel.Jon"+ [+ testGroup "killProcess"+ [+ testGroup "using SoftKill"+ [+ testCase "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do+ ph <- liftIO launchCompliantJob+ killProcess $ SoftKill "thing" ph Nothing+ liftIO $+ patientlyGetProcessExitCode ph `shouldReturn` (Just $ Exited ExitSuccess) - describe "using HardKill" $ do- it "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do- ph <- liftIO launchCompliantJob- killProcess $ HardKill "thing" ph Nothing 1- -- Can't geth the exiit status because the life check in Job "uses up" the waitpid- liftIO $- patientlyGetProcessExitCode ph `shouldReturn` Nothing- it "forcefully kills stubborn processes" $ runAngelM dummyOptions $ do- ph <- liftIO launchStubbornJob- killProcess $ HardKill "thing" ph Nothing 1- liftIO $+ , testCase "does not forcefully kill stubborn processes" $ runAngelM dummyOptions $ do+ ph <- liftIO launchStubbornJob+ killProcess $ SoftKill "thing" ph Nothing+ -- stubborn job gets marked as [defunct] here. no idea why. it should be able to survive a SIGTERM+ liftIO $ do+ patientlyGetProcessExitCode ph `shouldReturn` Nothing+ hardKillProcessHandle ph -- cleanup+ ],+ testGroup "using HardKill"+ [+ testCase "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do+ ph <- liftIO launchCompliantJob+ killProcess $ HardKill "thing" ph Nothing 1+ -- Can't geth the exiit status because the life check in Job "uses up" the waitpid+ liftIO $+ patientlyGetProcessExitCode ph `shouldReturn` Nothing+ , testCase "forcefully kills stubborn processes" $ runAngelM dummyOptions $ do+ ph <- liftIO launchStubbornJob+ killProcess $ HardKill "thing" ph Nothing 1+ liftIO $ #if MIN_VERSION_unix(2,7,0)- patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL False)+ patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL False) #else- patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL)+ patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL) #endif- describe "with a logger" $- it "cleanly kills well-behaved loggers" $ runAngelM dummyOptions $ do- ph <- liftIO launchCompliantJob- lph <- liftIO launchCompliantJob- killProcess $ SoftKill "thing" ph (Just lph)- liftIO $- patientlyGetProcessExitCode lph `shouldReturn` (Just $ Exited ExitSuccess)+ ],+ testGroup "with a logger"+ [+ testCase "cleanly kills well-behaved loggers" $ runAngelM dummyOptions $ do+ ph <- liftIO launchCompliantJob+ lph <- liftIO launchCompliantJob+ killProcess $ SoftKill "thing" ph (Just lph)+ liftIO $+ patientlyGetProcessExitCode lph `shouldReturn` (Just $ Exited ExitSuccess)+ ]+ ]+ ]+ launchCompliantJob :: IO ProcessHandle launchCompliantJob = launchJob "CompliantJob"
test/Angel/LogSpec.hs view
@@ -12,10 +12,15 @@ import SpecHelper -spec :: Spec-spec = do- describe "cleanCalendar" $ do- it "formats the time correctly" $ cleanCalendar dateTime `shouldBe` "2012/09/12 03:14:59"+spec :: TestTree+spec = testGroup "Angel.Log"+ [+ testGroup "cleanCalendar"+ [+ testCase "formats the time correctly" $+ cleanCalendar dateTime @?= "2012/09/12 03:14:59"+ ]+ ] --where time = CalendarTime 2012 September 12 3 14 59 0 Tuesday 263 "Pacific" -25200 True where dateTime = ZonedTime localTime zone localTime = LocalTime day tod
test/Angel/PidFileSpec.hs view
@@ -14,22 +14,26 @@ import SpecHelper -spec :: Spec-spec =- describe "startWithPidFile" $ do- it "creates the pidfile and cleans up" $ do- startWithPidFile procSpec fileName jogOn $ \_pHandle -> do- fileShouldExist fileName- pid <- readFile fileName- null pid `shouldBe` False- all isNumber pid `shouldBe` True- fileShouldNotExist fileName- it "calls the error callback when pidfile can't be created and re-raises" $ do- called <- newIORef False- let onPidError = const $ writeIORef called True- (res :: Either SomeException ()) <- try $ startWithPidFile procSpec badPidFile jogOn onPidError- readIORef called `shouldReturn` True- isLeft res `shouldBe` True+spec :: TestTree+spec = testGroup "Angel.PidFile"+ [+ testGroup "startWithPidFile"+ [+ testCase "creates the pidfile and cleans up" $ do+ startWithPidFile procSpec fileName jogOn $ \_pHandle -> do+ fileShouldExist fileName+ pid <- readFile fileName+ null pid @?= False+ all isNumber pid @?= True+ fileShouldNotExist fileName+ , testCase "calls the error callback when pidfile can't be created and re-raises" $ do+ called <- newIORef False+ let onPidError = const $ writeIORef called True+ (res :: Either SomeException ()) <- try $ startWithPidFile procSpec badPidFile jogOn onPidError+ readIORef called `shouldReturn` True+ isLeft res @?= True+ ]+ ] where fileName = "temp.pid" badPidFile = "/bogus/path/to/pidfile"
test/Angel/UtilSpec.hs view
@@ -8,27 +8,33 @@ import SpecHelper -spec :: Spec-spec = do- describe "expandPath" $ do- it "generates the correct path for just a tilde" $ do- UserEntry { homeDirectory = home } <- getUserEntry- path <- expandPath "~/foo"- path `shouldBe` home ++ "/foo"- it "generates the correct path for tilde with a specific user" $ do- UserEntry { homeDirectory = home,- userName = user } <- getUserEntry- path <- expandPath $ "~" ++ user ++ "/foo"- path `shouldBe` home ++ "/foo"- it "leaves paths without tildes alone" $ do- path <- expandPath "/foo"- path `shouldBe` "/foo"- describe "split" $ do- prop "produces no null values" $ \(a :: Char) (xs :: [Char]) ->- none null $ split a xs- prop "produces no instances of the split element" $ \(a :: Char) (xs :: [Char]) ->- none (elem a) $ split a xs- it "splits" $- split ' ' " foo bar baz " `shouldBe` ["foo", "bar", "baz"]+spec :: TestTree+spec = testGroup "Angel.Util"+ [+ testGroup "expandPath"+ [+ testCase "generates the correct path for just a tilde" $ do+ UserEntry { homeDirectory = home } <- getUserEntry+ path <- expandPath "~/foo"+ path @?= home ++ "/foo"+ , testCase "generates the correct path for tilde with a specific user" $ do+ UserEntry { homeDirectory = home,+ userName = user } <- getUserEntry+ path <- expandPath $ "~" ++ user ++ "/foo"+ path @?= home ++ "/foo"+ , testCase "leaves paths without tildes alone" $ do+ path <- expandPath "/foo"+ path @?= "/foo"+ ]+ , testGroup "split"+ [+ testProperty "produces no null values" $ \(a :: Char) (xs :: [Char]) ->+ none null $ split a xs+ , testProperty "produces no instances of the split element" $ \(a :: Char) (xs :: [Char]) ->+ none (elem a) $ split a xs+ , testCase "splits" $+ split ' ' " foo bar baz " @?= ["foo", "bar", "baz"]+ ]+ ] where getUserEntry = getUserEntryForID =<< getEffectiveUserID none p = not . any p
test/Spec.hs view
@@ -1,1 +1,23 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}+module Main (main) where++-------------------------------------------------------------------------------+import qualified Angel.ConfigSpec+import qualified Angel.JobSpec+import qualified Angel.LogSpec+import qualified Angel.PidFileSpec+import qualified Angel.UtilSpec+-------------------------------------------------------------------------------+import SpecHelper+-------------------------------------------------------------------------------++++main :: IO ()+main = defaultMain $ testGroup "angel"+ [+ Angel.ConfigSpec.spec+ , Angel.JobSpec.spec+ , Angel.LogSpec.spec+ , Angel.PidFileSpec.spec+ , Angel.UtilSpec.spec+ ]
test/SpecHelper.hs view
@@ -1,4 +1,34 @@-module SpecHelper (module X) where+module SpecHelper+ ( module X+ , module SpecHelper+ ) where -import Test.Hspec as X-import Test.Hspec.QuickCheck as X+import Control.Exception+import Test.Tasty as X+import Test.Tasty.HUnit as X+import Test.Tasty.QuickCheck as X++++-------------------------------------------------------------------------------+shouldReturn :: (Show a, Eq a) => IO a -> a -> Assertion+shouldReturn f v = do+ v' <- f+ v' @?= v+++-------------------------------------------------------------------------------+shouldThrow :: Exception e => IO a -> (e -> Bool) -> Assertion+shouldThrow f p = do+ res <- try f+ either (`shouldSatisfy` p) (const $ assertFailure "Did not throw an exception") res+++-------------------------------------------------------------------------------+anyErrorCall :: ErrorCall -> Bool+anyErrorCall = const True+++-------------------------------------------------------------------------------+shouldSatisfy :: (Show a) => a -> (a -> Bool) -> Assertion+shouldSatisfy a p = assertBool ("predicate failed on " ++ show a ) (p a)