8000 Better handling for introduce node failures by thomasjm · Pull Request #110 · codedownio/sandwich · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Better handling for introduce node failures #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sandwich/sandwich.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ executable sandwich-test
Before
Describe
Introduce
IntroduceWith
TestUtil
Paths_sandwich
hs-source-dirs:
Expand Down Expand Up @@ -346,6 +347,7 @@ test-suite sandwich-test-suite
Before
Describe
Introduce
IntroduceWith
TestUtil
Paths_sandwich
hs-source-dirs:
Expand Do 10000 wn
20 changes: 12 additions & 8 deletions sandwich/src/Test/Sandwich.hs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ import System.Win32.Console (setConsoleOutputCP)
-- | Run the spec with the given 'Options'.
runSandwich :: Options -> CoreSpec -> IO ()
runSandwich options spec = do
(_exitReason, failures) <- runSandwich' Nothing options spec
when (0 < failures) $ exitFailure
(_exitReason, _itNodeFailures, totalFailures) <- runSandwich' Nothing options spec
when (0 < totalFailures) $ exitFailure

-- | Run the spec, configuring the options from the command line.
runSandwichWithCommandLineArgs :: Options -> TopSpecWithOptions -> IO ()
Expand Down Expand Up @@ -181,11 +181,13 @@ runSandwichWithCommandLineArgs' baseOptions userOptionsParser spec = do
baseArgs <- getArgs
withArgs (baseArgs L.\\ (fmap T.unpack individualTestFlagStrings)) $
tryAny x >>= \case
Left _ -> return (NormalExit, 1)
Right _ -> return (NormalExit, 0)
Left _ -> return (NormalExit, 1, 1)
Right _ -> return (NormalExit, 0, 0)

-- | Run the spec with optional custom 'CommandLineOptions'. When finished, return the exit reason and number of failures.
runSandwich' :: Maybe (CommandLineOptions ()) -> Options -> CoreSpec -> IO (ExitReason, Int)
-- | Run the spec with optional custom 'CommandLineOptions'. When finished, return the exit reason,
-- the number of "it" nodes which failed, and the total failed nodes (which includes "it" nodes, and
-- may also include other nodes like "introduce" nodes).
runSandwich' :: Maybe (CommandLineOptions ()) -> Options -> CoreSpec -> IO (ExitReason, Int, Int)
runSandwich' maybeCommandLineOptions options spec' = do
baseContext <- baseContextFromOptions options

Expand Down Expand Up @@ -249,9 +251,11 @@ runSandwich' maybeCommandLineOptions options spec' = do
loggingFn $ finalizeFormatter f rts baseContext

fixedTree <- atomically $ mapM fixRunTree rts
let failed = countWhere isFailedItBlock fixedTree

exitReason <- readIORef exitReasonRef
return (exitReason, failed)
let failedItBlocks = countWhere isFailedItBlock fixedTree
let failedBlocks = countWhere isFailedBlock fixedTree
return (exitReason, failedItBlocks, failedBlocks)


-- | Count the it nodes
Expand Down
5 changes: 5 additions & 0 deletions sandwich/src/Test/Sandwich/Formatters/Common/Count.hs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ isFailedItBlock (RunNodeIt {runNodeCommon=(RunNodeCommonWithStatus {runTreeStatu
isFailedItBlock (RunNodeIt {runNodeCommon=(RunNodeCommonWithStatus {runTreeStatus=(Done {statusResult=(Failure {})})})}) = True
isFailedItBlock _ = False

isFailedNonItBlock (RunNodeIt {}) = False
isFailedNonItBlock (runNodeCommon -> (RunNodeCommonWithStatus {runTreeStatus=(Done {statusResult=(Failure (Pending {}))})})) = False
isFailedNonItBlock (runNodeCommon -> (RunNodeCommonWithStatus {runTreeStatus=(Done {statusResult=(Failure {})})})) = True
isFailedNonItBlock _ = False

isFailedBlock (runNodeCommon -> (RunNodeCommonWithStatus {runTreeStatus=(Done {statusResult=(Failure (Pending {}))})})) = False
isFailedBlock (runNodeCommon -> (RunNodeCommonWithStatus {runTreeStatus=(Done {statusResult=(Failure {})})})) = True
isFailedBlock _ = False
Expand Down
2 changes: 2 additions & 0 deletions sandwich/src/Test/Sandwich/Formatters/TerminalUI/Draw.hs
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@ borderWithCounts app = hBorderWithLabel $ padLeftRight 1 $ hBox (L.intercalate [
<> (if totalFailedTests > 0 then [[withAttr failureAttr $ str $ show totalFailedTests, str " failed"]] else mempty)
<> (if totalPendingTests > 0 then [[withAttr pendingAttr $ str $ show totalPendingTests, str " pending"]] else mempty)
<> (if totalRunningTests > 0 then [[withAttr runningAttr $ str $ show totalRunningTests, str " running"]] else mempty)
<> (if totalFailedInteriorNodes > 0 then [[withAttr failureAttr $ str $ show totalFailedInteriorNodes, str (" failed non-test " <> (if totalFailedInteriorNodes == 1 then "node" else "nodes"))]] else mempty)
<> (if totalNotStartedTests > 0 then [[str $ show totalNotStartedTests, str " not started"]] else mempty)

totalNumTests = countWhere isItBlock (app ^. appRunTree)
totalSucceededTests = countWhere isSuccessItBlock (app ^. appRunTree)
totalPendingTests = countWhere isPendingItBlock (app ^. appRunTree)
totalFailedTests = countWhere isFailedItBlock (app ^. appRunTree)
totalFailedInteriorNodes = countWhere isFailedNonItBlock (app ^. appRunTree)
totalRunningTests = countWhere isRunningItBlock (app ^. appRunTree)
totalNotStartedTests = countWhere isNotStartedItBlock (app ^. appRunTree)

Expand Down
10 changes: 5 additions & 5 deletions sandwich/src/Test/Sandwich/Internal/Running.hs
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,17 @@ runSandwichTree options spec = do
return rts

-- | For 0 repeats, repeat until a failure
runWithRepeat :: Int -> Int -> IO (ExitReason, Int) -> IO ()
runWithRepeat :: Int -> Int -> IO (ExitReason, Int, Int) -> IO ()
runWithRepeat 0 totalTests action = do
(_, numFailures) <- action
if | numFailures == 0 -> runWithRepeat 0 totalTests action
(_, _itNodeFailures, totalFailures) <- action
if | totalFailures == 0 -> runWithRepeat 0 totalTests action
| otherwise -> exitFailure
-- | For 1 repeat, run once and return
runWithRepeat n totalTests action = do
(successes, total) <- (flip execStateT (0 :: Int, 0 :: Int)) $ flip fix (n - 1) $ \loop n' -> do
(exitReason, numFailures) <- liftIO action
(exitReason, _itNodeFailures, totalFailures) <- liftIO action

modify $ \(successes, total) -> (successes + (if numFailures == 0 then 1 else 0), total + 1)
modify $ \(successes, total) -> (successes + (if totalFailures == 0 then 1 else 0), total + 1)

if | exitReason == SignalExit -> return ()
| n' > 0 -> loop (n' - 1)
Expand Down
26 changes: 20 additions & 6 deletions sandwich/src/Test/Sandwich/Interpreters/StartTree.hs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,22 @@ startTree node@(RunNodeIntroduceWith {..}) ctx' = do
didRunWrappedAction <- liftIO $ newIORef (Left (), emptyExtraTimingInfo)
runInAsync node ctx $ do
let wrappedAction = do
let failureResult e = case fromException e of
Just fr@(Pending {}) -> Failure fr
_ -> Failure $ Reason Nothing [i|introduceWith '#{runTreeLabel}' handler threw exception|]
flip withException (\e -> recordExceptionInStatus runTreeStatus e >> markAllChildrenWithResult runNodeChildrenAugmented ctx (failureResult e)) $ do
didAllocateVar <- liftIO $ newIORef False
let handler e = do
recordExceptionInStatus runTreeStatus e

liftIO (readIORef didAllocateVar) >>= \case
False -> do
-- We didn't successfully allocate, so mark the children below this point as failed due to a
-- context issue.
markAllChildrenWithResult runNodeChildrenAugmented ctx $ case fromException e of
Just fr@(Pending {}) -> Failure fr
_ -> Failure $ GetContextException Nothing (SomeExceptionWithEq e)
True ->
-- Otherwise, let their existing statuses stand.
return ()

flip withException handler $ do
beginningCleanupVar <- liftIO $ newIORef Nothing

-- Record a start event in the test timing, if configured
Expand All @@ -146,6 +158,8 @@ startTree node@(RunNodeIntroduceWith {..}) ctx' = do
setupFinishTime <- liftIO getCurrentTime
addSetupFinishTimeToStatus runTreeStatus setupFinishTime

liftIO $ writeIORef didAllocateVar True

(results, _, _) <- timed runTreeRecordTime (getBaseContext ctx) (runTreeLabel <> " (body)") $
liftIO $ runNodesSequentially runNodeChildrenAugmented (LabelValue intro :> ctx)

Expand Down Expand Up @@ -181,7 +195,7 @@ startTree node@(RunNodeAround {..}) ctx' = do
let failureResult e = case fromException e of
Just fr@(Pending {}) -> Failure fr
_ -> Failure $ Reason Nothing [i|around '#{runTreeLabel}' handler threw exception|]
flip withException (\e -> recordExceptionInStatus runTreeStatus e >> markAllChildrenWithResult runNodeChildren ctx (failureResult e)) $ do
flip withException (\e -> recordExceptionInStatus runTreeStatus e) $ do
runNodeActionWith $ do
setupFinishTime <- liftIO getCurrentTime
addSetupFinishTimeToStatus runTreeStatus setupFinishTime
Expand All @@ -192,7 +206,7 @@ startTree node@(RunNodeAround {..}) ctx' = do
(liftIO $ readIORef didRunWrappedAction) >>= \case
(Left (), timingInfo) -> return (Failure $ Reason Nothing [i|around '#{runTreeLabel}' handler didn't call action|], timingInfo)
(Right _, timingInfo) -> return (Success, timingInfo)
runExampleM' wrappedAction ctx runTreeLogs (Just [i|Exception in introduceWith '#{runTreeLabel}' handler|]) >>= \case
runExampleM' wrappedAction ctx runTreeLogs (Just [i|Exception in around '#{runTreeLabel}' handler|]) >>= \case
Left err -> return (Failure err, emptyExtraTimingInfo)
Right x -> pure x
startTree node@(RunNodeDescribe {..}) ctx' = do
Expand Down
21 changes: 10 additions & 11 deletions sandwich/test/Around.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,32 @@

module Around where


import Control.Concurrent
import UnliftIO.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Writer
import Data.String.Interpolate
import GHC.Stack
import Test.Sandwich

import TestUtil
import UnliftIO.Exception


tests :: MonadIO m => WriterT [SomeException] m ()
tests = do
run aroundDoesNotFailureOnChildFailure
run aroundDoesNotFailOnChildFailure
run aroundReceivesSubtreeResult

main :: IO ()
main = mainWith tests

-- * Tests


aroundDoesNotFailureOnChildFailure :: (HasCallStack) => IO ()
aroundDoesNotFailureOnChildFailure = do
aroundDoesNotFailOnChildFailure :: (HasCallStack) => IO ()
aroundDoesNotFailOnChildFailure = do
results <- runAndGetResults $ around "around label" void $ do
it "does thing 1" $ 2 `shouldBe` 3
it "does thing 1" $ 2 `shouldBe` 2
it "does thing 1" $ 2 `shouldBe` (3 :: Int)
it "does thing 1" $ 2 `shouldBe` (2 :: Int)

case results of
[Success, Failure {}, Success] -> return ()
Expand All @@ -39,8 +38,8 @@ aroundReceivesSubtreeResult = do
mvar <- newEmptyMVar

_ <- runAndGetResults $ around "around label" (>>= (liftIO . putMVar mvar)) $ do
it "does thing 1" $ 2 `shouldBe` 3
it "does thing 1" $ 2 `shouldBe` 2
it "does thing 1" $ 2 `shouldBe` (3 :: Int)
it "does thing 1" $ 2 `shouldBe` (2 :: Int)

takeMVar mvar >>= \case
[Failure {}, Success] -> return ()
Expand Down
3 changes: 2 additions & 1 deletion sandwich/test/Before.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

module Before where

import UnliftIO.Exception
import Control.Monad.IO.Class
import Control.Monad.Trans.Writer
import qualified Data.List as L
import GHC.Stack
import Test.Sandwich
import TestUtil
import UnliftIO.Exception


tests :: MonadIO m => WriterT [SomeException] m ()
tests = do
run beforeExceptionSafety
run beforeExceptionSafetyNested

main :: IO ()
main = mainWith tests

-- * Tests
Expand Down
3 changes: 2 additions & 1 deletion sandwich/test/Describe.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

module Describe where

import UnliftIO.Exception
import Control.Monad.IO.Class
import Control.Monad.Trans.Writer
import Data.String.Interpolate
import GHC.Stack
import Test.Sandwich
import TestUtil
import UnliftIO.Exception


tests :: MonadIO m => WriterT [SomeException] m ()
tests = do
run describeFailsWhenChildFails

main :: IO ()
main = mainWith tests

-- * Tests
Expand Down
7 changes: 5 additions & 2 deletions sandwich/test/Introduce.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ module Introduce where
import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import UnliftIO.Exception
import Control.Monad.IO.Class
import Control.Monad.Trans.Writer
import Data.Foldable
import GHC.Stack
import Test.Sandwich
import Test.Sandwich.Internal
import UnliftIO.Exception

import TestUtil

Expand All @@ -23,6 +23,7 @@ tests = do
run introduceFailsOnCleanUpException
run introduceCleansUpOnCancelDuringTest

main :: IO ()
main = mainWith tests

-- * Tests
Expand Down Expand Up @@ -63,7 +64,9 @@ introduceCleansUpOnCancelDuringTest = do
liftIO $ putMVar mvar ()
liftIO $ threadDelay 999999999999999

let [topNode@(RunNodeIntroduce {runNodeChildrenAugmented=[RunNodeIt {}]})] = rts
topNode <- case rts of
[x@(RunNodeIntroduce {runNodeChildrenAugmented=[RunNodeIt {}]})] -> pure x
_ -> error "Unexpected rts"

-- Wait until we get into the actual test example, then cancel the top level async
takeMVar mvar
Expand Down
49 changes: 49 additions & 0 deletions sandwich/test/IntroduceWith.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{-# LANGUAGE DataKinds #-}

module IntroduceWith where


import Control.Monad.IO.Class
import Control.Monad.Trans.Writer
import GHC.Stack
import Test.Sandwich
import UnliftIO.Exception

import TestUtil

tests :: MonadIO m => WriterT [SomeException] m ()
tests = do
run introduceWithFailsOnAllocateException
run introduceWithFailsOnCleanUpException

main :: IO ()
main = mainWith tests

-- * Tests

introduceWithFailsOnAllocateException :: (HasCallStack) => IO ()
introduceWithFailsOnAllocateException = do
(results, msgs) <- runAndGetResultsAndLogs $ introduceWith "introduce with" fakeDatabaseLabel withAction $ do
it "does thing 1" $ return ()

msgs `mustBe` [[], []]
results `mustBe` [Failure (GotException Nothing (Just "Exception in introduceWith 'introduce with' handler") someUserErrorWrapped)
, Failure (GetContextException Nothing someUserErrorWrapped)]
where
withAction action = do
throwSomeUserError
_ <- action FakeDatabase
return ()

introduceWithFailsOnCleanUpException :: (HasCallStack) => IO ()
introduceWithFailsOnCleanUpException = do
(results, msgs) <- runAndGetResultsAndLogs $ introduceWith "introduce with" fakeDatabaseLabel withAction $ do
it "does thing 1" $ return ()

msgs `mustBe` [[], []]
results `mustBe` [Failure (GotException Nothing (Just "Exception in introduceWith 'introduce with' handler") someUserErrorWrapped)
, Success]
where
withAction action = do
_ <- action FakeDatabase
throwSomeUserError
3 changes: 3 additions & 0 deletions sandwich/test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import qualified Around
import qualified Before
import qualified Describe
import qualified Introduce
import qualified IntroduceWith
import TestUtil


main :: IO ()
main = mainWith $ do
Around.tests
Before.tests
Describe.tests
Introduce.tests
IntroduceWith.tests
Loading
Loading
0