chuchu 0.3 → 0.4.1
raw patch · 12 files changed
+321/−183 lines, 12 filesdep +lifted-basedep +monad-controldep ~unixPVP ok
version bump matches the API change (PVP)
Dependencies added: lifted-base, monad-control
Dependency ranges changed: unix
API changes (from Hackage documentation)
- Test.Chuchu: chuchuMain :: (MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> IO ()
+ Test.Chuchu: chuchuMain :: (MonadBaseControl IO m, MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> IO ()
Files
- chuchu.cabal +48/−12
- src/Test/Chuchu.hs +90/−139
- src/Test/Chuchu/Email.hs +3/−5
- src/Test/Chuchu/OutputPrinter.hs +74/−0
- src/Test/Chuchu/Parsec.hs +5/−5
- src/Test/Chuchu/Parser.hs +8/−9
- src/Test/Chuchu/Types.hs +10/−13
- tests/data/multiple_features1.feature +7/−0
- tests/data/multiple_features2.feature +7/−0
- tests/data/should_fail.feature +7/−0
- tests/multiple_features.hs +26/−0
- tests/should_fail.hs +36/−0
chuchu.cabal view
@@ -1,5 +1,5 @@ name: chuchu-version: 0.3+version: 0.4.1 cabal-version: >= 1.8 build-type: Simple license: OtherLicense@@ -22,7 +22,10 @@ tests/data/calculator.feature, tests/data/prefix.feature, tests/data/multiple_scenarios.feature,- tests/data/background_and_multiple_scenarios.feature+ tests/data/background_and_multiple_scenarios.feature,+ tests/data/multiple_features1.feature,+ tests/data/multiple_features2.feature,+ tests/data/should_fail.feature source-repository head type: git@@ -36,16 +39,25 @@ Test.Chuchu.Parser other-modules: Test.Chuchu.Parsec,- Test.Chuchu.Email+ Test.Chuchu.Email,+ Test.Chuchu.OutputPrinter build-depends:- base >= 4 && < 5,- text >= 0.11 && < 0.12,- transformers >= 0.3 && < 0.4,- parsec >= 3.1 && < 3.2,- cmdargs >= 0.9 && < 0.10,- ansi-wl-pprint == 0.6.*,- abacate >= 0.0 && < 0.1- extensions: DeriveDataTypeable, GADTs, GeneralizedNewtypeDeriving, OverloadedStrings+ base >= 4 && < 5,+ text >= 0.11 && < 0.12,+ transformers >= 0.3 && < 0.4,+ parsec >= 3.1 && < 3.2,+ cmdargs >= 0.9 && < 0.10,+ ansi-wl-pprint == 0.6.*,+ abacate >= 0.0 && < 0.1,+ monad-control >= 0.3 && < 0.4,+ lifted-base >= 0.2 && < 0.3+ extensions:+ DeriveDataTypeable+ FlexibleContexts+ GADTs+ GeneralizedNewtypeDeriving+ OverloadedStrings+ ScopedTypeVariables ghc-options: -Wall Test-Suite environment@@ -54,7 +66,7 @@ hs-source-dirs: tests build-depends: base >= 4 && < 5,- unix >= 2.5 && < 2.6,+ unix >= 2.5, text, chuchu, HUnit >= 1.2 && < 1.3@@ -100,6 +112,30 @@ Test-Suite background_and_multiple_scenarios type: exitcode-stdio-1.0 main-is: background_and_multiple_scenarios.hs+ hs-source-dirs: tests+ build-depends:+ base >= 4 && < 5,+ transformers >= 0.3 && < 0.4,+ chuchu,+ HUnit >= 1.2 && < 1.3+ extensions: OverloadedStrings+ ghc-options: -Wall++Test-Suite multiple_features+ type: exitcode-stdio-1.0+ main-is: multiple_features.hs+ hs-source-dirs: tests+ build-depends:+ base >= 4 && < 5,+ transformers >= 0.3 && < 0.4,+ chuchu,+ HUnit >= 1.2 && < 1.3+ extensions: OverloadedStrings+ ghc-options: -Wall++Test-Suite should_fail+ type: exitcode-stdio-1.0+ main-is: should_fail.hs hs-source-dirs: tests build-depends: base >= 4 && < 5,
src/Test/Chuchu.hs view
@@ -77,56 +77,49 @@ (chuchuMain, module Test.Chuchu.Types, module Test.Chuchu.Parser) where --- base-import Control.Applicative-import Control.Monad-import System.Environment-import System.Exit-import System.IO-import qualified Data.IORef as I---- text-import qualified Data.Text as T---- transformers-import Control.Monad.IO.Class-import Control.Monad.Trans.Class-import Control.Monad.Trans.Reader---- parsec-import Text.Parsec-import Text.Parsec.Text---- cmdargs-import System.Console.CmdArgs---- ansi-wl-pprint-import qualified Text.PrettyPrint.ANSI.Leijen as D---- abacate+import Control.Applicative ((<$>), Applicative((<*>)))+import Control.Monad (unless)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Control (MonadBaseControl)+import Control.Monad.Trans.Reader (ReaderT(..), ask)+import Data.Either (partitionEithers) import Language.Abacate hiding (StepKeyword (..))+import System.Console.CmdArgs+import System.Environment (getProgName)+import System.Exit (exitFailure, exitWith, ExitCode(ExitFailure))+import qualified Control.Exception.Lifted as E+import qualified Data.IORef as I+import qualified Text.Parsec as P --- chuchu import Test.Chuchu.Types import Test.Chuchu.Parser+import Test.Chuchu.OutputPrinter ---------------------------------------------------------------------- --- | The main function for the test file. It expects the @.feature@ file as the--- first parameter on the command line. If you want to use it inside a library,--- consider using 'withArgs'.-chuchuMain :: (MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> IO ()-chuchuMain cc runMIO- = do- path <- getPath- parsed <- parseFile path- case parsed of- Right abacate -> do- ret <- processAbacate cc runMIO abacate- unless ret exitFailure- Left e -> error $ "Could not parse " ++ path ++ ": " ++ show e+-- | The main function for the test file. It expects one or more+-- @.feature@ file as parameters on the command line. If you want to+-- use it inside a library, consider using 'withArgs'.+chuchuMain :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+ Chuchu m -> (m () -> IO ()) -> IO ()+chuchuMain stepDefinitions runMIO = do+ listOfPaths <- getPaths+ parsedFiles <- mapM parseFile listOfPaths+ let result = partitionEithers parsedFiles+ case result of+ -- no error in parsing, execute all files+ ([], filesToExecute) -> do+ rets <- concat <$> mapM (processAbacate stepDefinitions runMIO) filesToExecute+ let n = length $ filter not rets+ unless (n == 0) $ exitWith (ExitFailure (min 255 n)) -- the size of a Unix error code is 1 byte+ -- there were errors, print them and execute nothing+ (filesWithError, _) -> do+ warn "Could not parse the following files: "+ mapM_ (warn . flip (++) "\n" . show) filesWithError+ exitFailure ----------------------------------------------------------------------@@ -153,23 +146,19 @@ -- | Monad used when executing a feature's scenario. 'ReaderT' -- is used to carry along the step parser.-type Execution m a = ReaderT (Parser (m ())) m a----- | Print a 'D.Doc' describing what we're currently processing.-putDoc :: (MonadIO m, Applicative m) => D.Doc -> m ()-putDoc = liftIO . D.putDoc . (D.<> D.linebreak)+type Execution m a = ReaderT (ParseStep m) m a --- | Same as 'D.text' but using 'T.Text'.-t2d :: T.Text -> D.Doc-t2d = D.text . T.unpack+-- | A function that parses a step and, if successful, returns+-- the corresponding action to be executed.+type ParseStep m = Step -> Either P.ParseError (m ()) -- | Run the 'Execution' monad. runExecution :: (MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> Execution m () -> IO ()-runExecution cc runMIO act = runMIO $ runReaderT act $ runChuchu cc+runExecution stepDefinitions runMIO act = runMIO $ runReaderT act parseStep+ where parseStep = P.parse (runChuchu stepDefinitions) "Step definitions" . stBody ----------------------------------------------------------------------@@ -178,137 +167,99 @@ -- | Process a whole Abacate file, that is, a whole feature. -- Runs each background+scenario combination on a different -- instance of the 'Execution' monad.-processAbacate :: (MonadIO m, Applicative m) =>+processAbacate :: (MonadBaseControl IO m, MonadIO m, Applicative m) => Chuchu m -> (m () -> IO ()) -> Abacate- -> IO Bool-processAbacate cc runMIO feature = do+ -> IO [Bool]+processAbacate stepDefinitions runMIO feature = do -- Print feature description. putDoc $ describeAbacate feature -- Execute features. let plans = createExecutionPlans feature- retVar <- liftIO $ I.newIORef True- let checkRet ret = unless ret $ liftIO $ I.writeIORef retVar False- mapM_ (runExecution cc runMIO . (>>= checkRet) . processExecutionPlan) plans- liftIO $ I.readIORef retVar+ retVar <- liftIO $ I.newIORef []+ let addRet ret = liftIO $ I.modifyIORef retVar (ret:)+ mapM_ (runExecution stepDefinitions runMIO . (>>= addRet) . processExecutionPlan) plans+ reverse <$> liftIO (I.readIORef retVar) -- | Process a single execution plan, a combination of -- background+scenario, inside the 'Execution' monad.-processExecutionPlan :: (MonadIO m, Applicative m) => ExecutionPlan -> Execution m Bool+processExecutionPlan :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+ ExecutionPlan -> Execution m Bool processExecutionPlan (ExecutionPlan mbackground scenario) = do- putDoc D.empty -- empty line+ liftIO $ putStrLn "" -- empty line (TODO: move into OutputPrinter somehow) (&&) <$> maybe (return True) (processBasicScenario BackgroundKind) mbackground <*> processFeatureElement scenario --- | Creates a pretty description of the feature.-describeAbacate :: Abacate -> D.Doc-describeAbacate feature =- D.vsep $- describeTags (fTags feature) ++ [D.white $ t2d $ fHeader feature]----- | Creates a vertical list of tags.-describeTags :: Tags -> [D.Doc]-describeTags = map (D.dullcyan . ("@" D.<>) . t2d)-- ---------------------------------------------------------------------- -processFeatureElement :: (MonadIO m, Applicative m) => FeatureElement -> Execution m Bool+processFeatureElement :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+ FeatureElement -> Execution m Bool processFeatureElement (FESO _)- = liftIO (hPutStrLn stderr "Scenario Outlines are not supported yet.")- >> return False+ = warn "Scenario Outlines are not supported yet." >> return False processFeatureElement (FES sc) = processBasicScenario (ScenarioKind $ scTags sc) $ scBasicScenario sc -data BasicScenarioKind = BackgroundKind | ScenarioKind Tags---processBasicScenario :: (MonadIO m, Applicative m) => BasicScenarioKind -> BasicScenario -> Execution m Bool+processBasicScenario :: (MonadBaseControl IO m, MonadIO m, Applicative m) =>+ BasicScenarioKind -> BasicScenario -> Execution m Bool processBasicScenario kind scenario = do putDoc $ describeBasicScenario kind scenario processSteps (bsSteps scenario) --- | Creates a pretty description of the basic scenario's header.-describeBasicScenario :: BasicScenarioKind -> BasicScenario -> D.Doc-describeBasicScenario kind scenario =- D.indent 2 $- prettyTags kind $- D.bold ((describeBasicScenarioKind kind) D.<+> t2d (bsName scenario))- where describeBasicScenarioKind BackgroundKind = "Background:"- describeBasicScenarioKind (ScenarioKind _) = "Scenario:"-- prettyTags BackgroundKind = id- prettyTags (ScenarioKind tags) = D.vsep . (describeTags tags ++) . (:[])-- ---------------------------------------------------------------------- -processSteps :: (MonadIO m, Applicative m) => Steps -> Execution m Bool-processSteps steps- = do- codes <- mapM processStep steps- return $ and codes---processStep :: (MonadIO m, Applicative m) => Step -> Execution m Bool-processStep step- = do- cc <- ask- case parse cc "processStep" $ stBody step of- Left e- -> do- putDoc $ describeStep UnknownStep step- liftIO- $ hPutStrLn stderr- $ "The step "- ++ show (stBody step)- ++ " doesn't match any step definitions I know."- ++ show e- return False- Right m -> do- -- TODO: Catch failures and treat them nicely.- putDoc $ describeStep SuccessfulStep step- lift m- return True+processSteps :: (MonadBaseControl IO m, MonadIO m, Applicative m) => Steps -> Execution m Bool+processSteps steps = mapShortCircuitM processStep steps -data StepResult = SuccessfulStep | UnknownStep+mapShortCircuitM :: (Monad m) => (a -> m Bool) -> [a] -> m Bool+mapShortCircuitM _ [] = return True+mapShortCircuitM f (x:xs) = do+ ret <- f x+ if ret then mapShortCircuitM f xs+ else return False --- | Pretty-prints a step that has already finished executing.-describeStep :: StepResult -> Step -> D.Doc-describeStep result step =- D.indent 4 $- color result (D.text (show $ stStepKeyword step) D.<+> t2d (stBody step))- where- color SuccessfulStep = D.green- color UnknownStep = D.yellow+-- | Executes the parser of each step and prints the result on the+-- screen.+processStep :: (MonadBaseControl IO m, MonadIO m, Applicative m) => Step -> Execution m Bool+processStep step = do+ parseStep <- ask+ case parseStep step of+ Left e -> do+ putDoc $ describeStep UnknownStep step+ liftIO $ warn $ concat [ "The step "+ , show (stBody step)+ , " doesn't match any step definitions I know."+ , show e ]+ return False+ Right m -> do+ r <- E.catches (lift m >> return SuccessfulStep)+ [ E.Handler $ \(e :: E.AsyncException) -> E.throw (e :: E.AsyncException)+ , E.Handler $ \(_ :: E.SomeException) -> return FailedStep ]+ putDoc (describeStep r step)+ return (r == SuccessfulStep) ---------------------------------------------------------------------- data Options- = Options {file_ :: FilePath}+ = Options {file_ :: [FilePath]} deriving (Eq, Show, Typeable, Data) -getPath :: IO FilePath-getPath- = do- progName <- getProgName- file_- <$> cmdArgs- (Options (def &= typ "PATH" &= argPos 0)- &= program progName- &= details- ["Run test scenarios specified on the abacate file at PATH."])+-- Gets the feature files as arguments from the command-line.+getPaths :: IO [FilePath]+getPaths = do+ progName <- getProgName+ file_ <$> cmdArgs (Options (def &= typ "PATH" &= args)+ &= program progName+ &= details ["Run one or more abacate files."])
src/Test/Chuchu/Email.hs view
@@ -13,12 +13,10 @@ -- <http://porg.es/blog/email-address-validation-simpler-faster-more-correct>. module Test.Chuchu.Email (addrSpecSimple) where --- base-import Control.Applicative hiding ((<|>), many, optional)+import Control.Applicative ((<$>), (<|>))+import Text.Parsec (oneOf, many1, string, alphaNum)+import Text.Parsec.Text (Parser) --- parsec-import Text.Parsec-import Text.Parsec.Text -- | Parses a simplified e-mail address and return everything that was parsed as -- a simple 'String'.
+ src/Test/Chuchu/OutputPrinter.hs view
@@ -0,0 +1,74 @@+module Test.Chuchu.OutputPrinter+ ( putDoc+ , warn+ , describeAbacate+ , describeBasicScenario+ , BasicScenarioKind(..)+ , describeStep+ , StepResult(..)+ ) where++import Control.Monad.IO.Class (MonadIO(liftIO))+import Language.Abacate hiding (StepKeyword (..))+import System.IO (hPutStrLn, stderr)+import qualified Data.Text as T+import qualified Text.PrettyPrint.ANSI.Leijen as D+++-- | Print a 'D.Doc' describing what we're currently processing.+putDoc :: MonadIO m => D.Doc -> m ()+putDoc = liftIO . D.putDoc . (D.<> D.linebreak)+++-- | Print a warning message.+warn :: MonadIO m => String -> m ()+warn = liftIO . hPutStrLn stderr+++----------------------------------------------------------------------+++-- | Same as 'D.text' but using 'T.Text'.+t2d :: T.Text -> D.Doc+t2d = D.text . T.unpack+++-- | Creates a pretty description of the feature.+describeAbacate :: Abacate -> D.Doc+describeAbacate feature =+ D.vsep $+ describeTags (fTags feature) ++ [D.white $ t2d $ fHeader feature]+++-- | Creates a vertical list of tags.+describeTags :: Tags -> [D.Doc]+describeTags = map (D.dullcyan . ("@" D.<>) . t2d)+++-- | Creates a pretty description of the basic scenario's header.+describeBasicScenario :: BasicScenarioKind -> BasicScenario -> D.Doc+describeBasicScenario kind scenario =+ D.indent 2 $+ prettyTags kind $+ D.bold ((describeBasicScenarioKind kind) D.<+> t2d (bsName scenario))+ where describeBasicScenarioKind BackgroundKind = "Background:"+ describeBasicScenarioKind (ScenarioKind _) = "Scenario:"++ prettyTags BackgroundKind = id+ prettyTags (ScenarioKind tags) = D.vsep . (describeTags tags ++) . (:[])+++data BasicScenarioKind = BackgroundKind | ScenarioKind Tags+++-- | Pretty-prints a step that has already finished executing.+describeStep :: StepResult -> Step -> D.Doc+describeStep result step =+ D.indent 4 $+ color result (D.text (show $ stStepKeyword step) D.<+> t2d (stBody step))+ where+ color SuccessfulStep = D.green+ color FailedStep = D.red+ color UnknownStep = D.yellow++data StepResult = SuccessfulStep | FailedStep | UnknownStep deriving (Eq)
src/Test/Chuchu/Parsec.hs view
@@ -37,12 +37,12 @@ module Test.Chuchu.Parsec (stringLiteral, natFloat, int) where --- base-import Data.Char---- parsec+import Control.Applicative ((<|>), many)+import Data.Char (digitToInt) import Text.Parsec-import Text.Parsec.Text+ ( (<?>), between, choice, digit, char, hexDigit, many1+ , octDigit, oneOf, option, satisfy, space, string, try, upper )+import Text.Parsec.Text (Parser) {-# ANN module ("HLint: ignore" :: String) #-} -- | 'lexeme' removed.
src/Test/Chuchu/Parser.hs view
@@ -19,20 +19,19 @@ , try ) where -import Control.Applicative hiding ((<|>))-import Text.Parsec hiding (try)+import Control.Applicative ((<$>)) import qualified Data.Text as T-import qualified Text.Parsec as Parsec+import qualified Text.Parsec as P import Test.Chuchu.Types (ChuchuParser(..)) import Test.Chuchu.Email-import qualified Test.Chuchu.Parsec as P+import qualified Test.Chuchu.Parsec as P' -- | Parses a floating point number, with the same syntax as accepted by -- Haskell. number :: ChuchuParser Double-number = ChuchuParser $ nofToDouble <$> P.natFloat+number = ChuchuParser $ nofToDouble <$> P'.natFloat nofToDouble :: Either Integer Double -> Double@@ -42,17 +41,17 @@ -- | Parses an integer. int :: ChuchuParser Integer-int = ChuchuParser P.int+int = ChuchuParser P'.int -- | Parses a quoted string, with the same syntax as accepted by Haskell. text :: ChuchuParser T.Text-text = T.pack <$> ChuchuParser P.stringLiteral+text = T.pack <$> ChuchuParser P'.stringLiteral -- | Parses anything until the string passed as parameter, and also the string. wildcard :: T.Text -> ChuchuParser T.Text-wildcard = fmap T.pack . ChuchuParser . manyTill anyChar . Parsec.try . string . T.unpack+wildcard = fmap T.pack . ChuchuParser . P.manyTill P.anyChar . P.try . P.string . T.unpack -- | Parses a simplified e-mail address and return everything that was parsed as@@ -65,4 +64,4 @@ -- | Same as Parsec's 'Parsec.try' but for 'ChuchuParser'. try :: ChuchuParser a -> ChuchuParser a-try (ChuchuParser x) = ChuchuParser (Parsec.try x)+try (ChuchuParser x) = ChuchuParser (P.try x)
src/Test/Chuchu/Types.hs view
@@ -11,24 +11,21 @@ (ChuchuParser (..), Chuchu, ChuchuM (Given, When, Then, And, But), runChuchu) where --- base-import Control.Applicative hiding ((<|>))+import Control.Applicative (Applicative((<*)), Alternative((<|>)), (<$>)) import Control.Monad (MonadPlus)-import Data.String---- parsec-import Text.Parsec-import Text.Parsec.Text+import Data.String (IsString(..))+import qualified Text.Parsec as P+import qualified Text.Parsec.Text as P (Parser) -- | @newtype@ for Parsec's 'Parser' used on this library. The -- main reason for not using 'Parser' directly is to be able to -- define the 'IsString' instance.-newtype ChuchuParser a = ChuchuParser (Parser a)+newtype ChuchuParser a = ChuchuParser (P.Parser a) deriving (Functor, Applicative, Alternative, Monad, MonadPlus) instance (a ~ ()) => IsString (ChuchuParser a) where- fromString s = ChuchuParser (try (string s) >> return ())+ fromString s = ChuchuParser (P.try (P.string s) >> return ()) -- | The most common use case where the return value of the Monad is ignored.@@ -54,8 +51,8 @@ -- | Converts the Monad into a single 'Parser' that executes the specified -- action if the parser is executed correctly. It includes an 'eof' on the -- parser of each step to avoid it from accepting prefixes of the desired rule.-runChuchu :: ChuchuM m a -> Parser (m ())-runChuchu Nil = unexpected "Unknown step"+runChuchu :: ChuchuM m a -> P.Parser (m ())+runChuchu Nil = P.unexpected "Unknown step" runChuchu (Cons cc1 cc2) = runChuchu cc1 <|> runChuchu cc2 runChuchu (Given p f) = apply p f runChuchu (When p f) = apply p f@@ -63,5 +60,5 @@ runChuchu (And p f) = apply p f runChuchu (But p f) = apply p f -apply :: ChuchuParser a -> (a -> m ()) -> Parser (m ())-apply (ChuchuParser p) f = try $ f <$> p <* eof+apply :: ChuchuParser a -> (a -> m ()) -> P.Parser (m ())+apply (ChuchuParser p) f = P.try $ f <$> p <* P.eof
+ tests/data/multiple_features1.feature view
@@ -0,0 +1,7 @@+Feature: Multiple features 1++ This feature is intended to be used with another one to test if+ Chuchu executes more than one feature file.++ Scenario: Anybody can use the bathroom+ Given that "Bob" has entered the bathroom and locked the door
+ tests/data/multiple_features2.feature view
@@ -0,0 +1,7 @@+Feature: Multiple features 2++ This feature is intended to be used with another one to test if+ Chuchu executes more than one feature file.++ Scenario: Anybody can use the bathroom+ Given that "Alice" has entered the bathroom and locked the door
+ tests/data/should_fail.feature view
@@ -0,0 +1,7 @@+Feature: Chuchu should know how to catch failure++ Scenario: Unknown step definition must stop execution+ Given non-existent step++ Scenario: Step that fails must stop execution+ Given failing step
+ tests/multiple_features.hs view
@@ -0,0 +1,26 @@+import Control.Applicative+import Control.Monad.Trans.State+import System.Environment+import Test.Chuchu+++type Bathroom = StateT Door IO+++data Door = Open | Locked+++defs :: Chuchu Bathroom+defs = do++ Given ("that " *> text <* " has entered the bathroom and locked the door") $ \_ -> do+ door <- get+ case door of+ Open -> put Locked+ Locked -> fail "Door was already locked."+++main :: IO ()+main = withArgs [ "tests/data/multiple_features1.feature"+ , "tests/data/multiple_features2.feature" ] $+ chuchuMain defs (`evalStateT` Open)
+ tests/should_fail.hs view
@@ -0,0 +1,36 @@+import Control.Exception (handle)+import System.Environment (withArgs)+import System.Exit (exitWith, ExitCode(ExitSuccess, ExitFailure))+import Test.Chuchu+++defs :: Chuchu IO+defs = do+ Given "failing step" $ \() -> do+ fail "Error"+++-- | This functions verifies if the behaviour of Chuchu was+-- correct. The correct behaviour is to have 'n' number of failed+-- scenarios (this number comes from the exit code). If no scenarios+-- fail or the number of failed scenarios is different than 'n' then+-- Chuchu failed.+scenariosShouldFail :: Int -> IO () -> IO ()+scenariosShouldFail n = handle exit+ where exit ExitSuccess = failWith 255+ exit (ExitFailure n') | n /= n' = failWith n'+ | otherwise = putStrLn "Okay, everything failed!"+ failWith n' = do+ putStrLn $ concat+ [ "\n"+ , show n+ , " `scenariosShouldFail` _: failure, exiting with code "+ , show n'+ , "." ]+ exitWith (ExitFailure n')+++main :: IO ()+main = withArgs [ "tests/data/should_fail.feature" ] $+ 2 `scenariosShouldFail` chuchuMain defs id+