diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,1 @@
+See https://github.com/feuerbach/tasty/blob/master/CHANGES.md
diff --git a/Test/Tasty.hs b/Test/Tasty.hs
--- a/Test/Tasty.hs
+++ b/Test/Tasty.hs
@@ -15,9 +15,16 @@
   -- | Normally options are specified on the command line. But you can
   -- also have different options for different subtrees in the same tree,
   -- using the functions below.
+  --
+  -- Note that /ingredient options/ (number of threads, hide successes
+  -- etc.) set in this way will not have any effect. This is for modifying
+  -- per-test options, such as timeout, number of generated tests etc.
   , adjustOption
   , localOption
   , askOption
+  -- ** Standard options
+  , Timeout(..)
+  , mkTimeout
   -- * Resources
   -- | Sometimes several tests need to access the same resource — say,
   -- a file or a socket. We want to create or grab the resource before
@@ -29,7 +36,8 @@
 import Test.Tasty.Core
 import Test.Tasty.Runners
 import Test.Tasty.Options
-import Test.Tasty.Ingredients.IncludingOptions
+import Test.Tasty.Options.Core
+import Test.Tasty.Ingredients.Basic
 
 -- | List of the default ingredients. This is what 'defaultMain' uses.
 --
diff --git a/Test/Tasty/CmdLine.hs b/Test/Tasty/CmdLine.hs
--- a/Test/Tasty/CmdLine.hs
+++ b/Test/Tasty/CmdLine.hs
@@ -10,31 +10,21 @@
 import Options.Applicative
 import Data.Monoid
 import Data.Proxy
+import Data.Foldable
 import System.Exit
 
-
 import Test.Tasty.Core
-import Test.Tasty.CoreOptions
 import Test.Tasty.Ingredients
 import Test.Tasty.Options
+import Test.Tasty.Options.Env
+import Test.Tasty.Runners.Reducers
 
 -- | Generate a command line parser from a list of option descriptions
 optionParser :: [OptionDescription] -> Parser OptionSet
-optionParser = foldr addOption (pure mempty) where
-  addOption :: OptionDescription -> Parser OptionSet -> Parser OptionSet
-  addOption (Option (Proxy :: Proxy v)) p =
-    setOption <$> (optionCLParser :: Parser v) <*> p
-
--- suiteOptions doesn't really belong here (since it's not CmdLine
--- specific), but I didn't want to create a new module just for it.
-
--- | All the options relevant for this test suite. This includes the
--- options for the test tree and ingredients, and the core options.
-suiteOptions :: [Ingredient] -> TestTree -> [OptionDescription]
-suiteOptions ins tree =
-  coreOptions ++
-  ingredientsOptions ins ++
-  treeOptions tree
+optionParser = getApp . foldMap toSet where
+  toSet :: OptionDescription -> Ap Parser OptionSet
+  toSet (Option (Proxy :: Proxy v)) = Ap $
+    (singleOption <$> (optionCLParser :: Parser v)) <|> pure mempty
 
 -- | The command line parser for the test suite
 suiteOptionParser :: [Ingredient] -> TestTree -> Parser OptionSet
@@ -44,11 +34,15 @@
 -- ingredient list
 defaultMainWithIngredients :: [Ingredient] -> TestTree -> IO ()
 defaultMainWithIngredients ins testTree = do
-  opts <- execParser $
+  cmdlineOpts <- execParser $
     info (helper <*> suiteOptionParser ins testTree)
     ( fullDesc <>
       header "Mmm... tasty test suite"
     )
+
+  envOpts <- suiteEnvOptions ins testTree
+
+  let opts = envOpts <> cmdlineOpts
 
   case tryIngredients ins opts testTree of
     Nothing ->
diff --git a/Test/Tasty/Core.hs b/Test/Tasty/Core.hs
--- a/Test/Tasty/Core.hs
+++ b/Test/Tasty/Core.hs
@@ -1,9 +1,9 @@
 -- | Core types and definitions
 {-# LANGUAGE GeneralizedNewtypeDeriving, FlexibleContexts,
-             ExistentialQuantification, RankNTypes, DeriveDataTypeable #-}
+             ExistentialQuantification, RankNTypes, DeriveDataTypeable,
+             DeriveGeneric #-}
 module Test.Tasty.Core where
 
-import Control.Applicative
 import Control.Exception
 import Test.Tasty.Options
 import Test.Tasty.Patterns
@@ -12,14 +12,35 @@
 import Data.Typeable
 import qualified Data.Map as Map
 import Data.Tagged
+import GHC.Generics
 import Text.Printf
 
+-- | If a test failed, 'FailureReason' describes why
+data FailureReason
+  = TestFailed
+    -- ^ test provider indicated failure
+  | TestThrewException SomeException
+    -- ^ test resulted in an exception. Note that some test providers may
+    -- catch exceptions in order to provide more meaningful errors. In that
+    -- case, the 'FailureReason' will be 'TestFailed', not
+    -- 'TestThrewException'.
+  | TestTimedOut Integer
+    -- ^ test didn't complete in allotted time
+  deriving Show
+
+-- | Outcome of a test run
+--
+-- Note: this is isomorphic to @'Maybe' 'FailureReason'@. You can use the
+-- @generic-maybe@ package to exploit that.
+data Outcome
+  = Success -- ^ test succeeded
+  | Failure FailureReason -- ^ test failed because of the 'FailureReason'
+  deriving (Show, Generic)
+
 -- | A test result
 data Result = Result
-  { resultSuccessful :: Bool
-    -- ^
-    -- 'resultSuccessful' should be 'True' for a passed test and 'False' for
-    -- a failed one.
+  { resultOutcome :: Outcome
+    -- ^ Did the test fail? If so, why?
   , resultDescription :: String
     -- ^
     -- 'resultDescription' may contain some details about the test. For
@@ -31,6 +52,20 @@
     -- information about the failure.
   }
 
+-- | 'True' for a passed test, 'False' for a failed one.
+resultSuccessful :: Result -> Bool
+resultSuccessful r =
+  case resultOutcome r of
+    Success -> True
+    Failure {} -> False
+
+-- | Shortcut for creating a 'Result' that indicates exception
+exceptionResult :: SomeException -> Result
+exceptionResult e = Result
+  { resultOutcome = Failure $ TestThrewException e
+  , resultDescription = "Exception: " ++ show e
+  }
+
 -- | Test progress information.
 --
 -- This may be used by a runner to provide some feedback to the user while
@@ -66,17 +101,21 @@
 -- and how to release it (the second field).
 data ResourceSpec a = ResourceSpec (IO a) (a -> IO ())
 
+-- | A resources-related exception
 data ResourceError
   = NotRunningTests
-  | UnexpectedState String
+  | UnexpectedState String String
+  | UseOutsideOfTest
   deriving Typeable
 
 instance Show ResourceError where
   show NotRunningTests =
     "Unhandled resource. Probably a bug in the runner you're using."
-  show (UnexpectedState state) =
-    printf "Unexpected state of the resource (%s). Report as a tasty bug."
-      state
+  show (UnexpectedState where_ what) =
+    printf "Unexpected state of the resource (%s) in %s. Report as a tasty bug."
+      what where_
+  show UseOutsideOfTest =
+    "It looks like you're attempting to use a resource outside of its test. Don't do that!"
 
 instance Exception ResourceError
 
@@ -180,12 +219,6 @@
         PlusTestOptions f tree -> go pat path (f opts) tree
         WithResource res tree -> fResource res $ \res -> go pat path opts (tree res)
         AskOptions f -> go pat path opts (f opts)
-
--- | Useful wrapper for use with foldTestTree
-newtype AppMonoid f = AppMonoid { getApp :: f () }
-instance Applicative f => Monoid (AppMonoid f) where
-  mempty = AppMonoid $ pure ()
-  AppMonoid f1 `mappend` AppMonoid f2 = AppMonoid $ f1 *> f2
 
 -- | Get the list of options that are relevant for a given test tree
 treeOptions :: TestTree -> [OptionDescription]
diff --git a/Test/Tasty/CoreOptions.hs b/Test/Tasty/CoreOptions.hs
deleted file mode 100644
--- a/Test/Tasty/CoreOptions.hs
+++ /dev/null
@@ -1,35 +0,0 @@
--- | Core options, i.e. the options used by tasty itself
-{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-}
-module Test.Tasty.CoreOptions
-  ( NumThreads(..)
-  , coreOptions
-  )
-  where
-
-import Data.Typeable
-import Data.Proxy
-
-import Test.Tasty.Options
-import Test.Tasty.Patterns
-
--- | Number of parallel threads to use for running tests.
---
--- Note that this is /not/ included in 'coreOptions'.
--- Instead, it's automatically included in the options for any
--- 'TestReporter' ingredient by 'ingredientOptions', because the way test
--- reporters are handled already involves parallelism. Other ingredients
--- may also choose to include this option.
-newtype NumThreads = NumThreads { getNumThreads :: Int }
-  deriving (Eq, Ord, Num, Typeable)
-instance IsOption NumThreads where
-  defaultValue = 1
-  parseValue = fmap NumThreads . safeRead
-  optionName = return "num-threads"
-  optionHelp = return "Number of threads to use for tests execution"
-
--- | The list of all core options, i.e. the options not specific to any
--- provider or ingredient, but to tasty itself. Currently only contains 'TestPattern'.
-coreOptions :: [OptionDescription]
-coreOptions =
-  [ Option (Proxy :: Proxy TestPattern)
-  ]
diff --git a/Test/Tasty/Ingredients.hs b/Test/Tasty/Ingredients.hs
--- a/Test/Tasty/Ingredients.hs
+++ b/Test/Tasty/Ingredients.hs
@@ -1,8 +1,13 @@
+-- | This module contains the core definitions related to ingredients.
+--
+-- Ingredients themselves are provided by other modules (usually under
+-- the @Test.Tasty.Ingredients.*@ hierarchy).
 module Test.Tasty.Ingredients
   ( Ingredient(..)
   , tryIngredients
   , ingredientOptions
   , ingredientsOptions
+  , suiteOptions
   ) where
 
 import Control.Monad
@@ -12,7 +17,7 @@
 import Test.Tasty.Core
 import Test.Tasty.Run
 import Test.Tasty.Options
-import Test.Tasty.CoreOptions
+import Test.Tasty.Options.Core
 
 -- | 'Ingredient's make your test suite tasty.
 --
@@ -74,7 +79,7 @@
 tryIngredient :: Ingredient -> OptionSet -> TestTree -> Maybe (IO Bool)
 tryIngredient (TestReporter _ report) opts testTree = do -- Maybe monad
   reportFn <- report opts testTree
-  return $ reportFn =<< launchTestTree opts testTree
+  return $ launchTestTree opts testTree $ \smap -> reportFn smap
 tryIngredient (TestManager _ manage) opts testTree =
   manage opts testTree
 
@@ -99,3 +104,11 @@
 -- | Like 'ingredientOption', but folds over multiple ingredients.
 ingredientsOptions :: [Ingredient] -> [OptionDescription]
 ingredientsOptions = F.foldMap ingredientOptions
+
+-- | All the options relevant for this test suite. This includes the
+-- options for the test tree and ingredients, and the core options.
+suiteOptions :: [Ingredient] -> TestTree -> [OptionDescription]
+suiteOptions ins tree =
+  coreOptions ++
+  ingredientsOptions ins ++
+  treeOptions tree
diff --git a/Test/Tasty/Ingredients/Basic.hs b/Test/Tasty/Ingredients/Basic.hs
new file mode 100644
--- /dev/null
+++ b/Test/Tasty/Ingredients/Basic.hs
@@ -0,0 +1,23 @@
+-- | This module exports the basic ingredients defined in the 'tasty'
+-- packages.
+--
+-- Note that if @defaultIngredients@ from "Test.Tasty" suits your needs,
+-- use that instead of importing this module.
+module Test.Tasty.Ingredients.Basic
+  (
+    -- ** Console test reporter
+    consoleTestReporter
+  , Quiet(..)
+  , HideSuccesses(..)
+    -- ** Listing tests
+  , listingTests
+  , ListTests(..)
+  , testsNames
+    -- ** Adding options
+  , includingOptions
+  )
+  where
+
+import Test.Tasty.Ingredients.ConsoleReporter
+import Test.Tasty.Ingredients.ListTests
+import Test.Tasty.Ingredients.IncludingOptions
diff --git a/Test/Tasty/Ingredients/ConsoleReporter.hs b/Test/Tasty/Ingredients/ConsoleReporter.hs
--- a/Test/Tasty/Ingredients/ConsoleReporter.hs
+++ b/Test/Tasty/Ingredients/ConsoleReporter.hs
@@ -1,9 +1,15 @@
-{-# LANGUAGE TupleSections, CPP, ImplicitParams #-}
+-- vim:fdm=marker:foldtext=foldtext()
+{-# LANGUAGE BangPatterns, CPP, ImplicitParams, MultiParamTypeClasses, DeriveDataTypeable #-}
 -- | Console reporter ingredient
-module Test.Tasty.Ingredients.ConsoleReporter (consoleTestReporter) where
+module Test.Tasty.Ingredients.ConsoleReporter
+  ( consoleTestReporter
+  , Quiet(..)
+  , HideSuccesses(..)
+  ) where
 
 import Prelude hiding (fail)
 import Control.Monad.State hiding (fail)
+import Control.Monad.Reader hiding (fail)
 import Control.Concurrent.STM
 import Control.Exception
 import Control.DeepSeq
@@ -12,27 +18,335 @@
 import Test.Tasty.Run
 import Test.Tasty.Ingredients
 import Test.Tasty.Options
+import Test.Tasty.Runners.Reducers
 import Text.Printf
 import qualified Data.IntMap as IntMap
 import Data.Maybe
 import Data.Monoid
+import Data.Proxy
+import Data.Typeable
+import Data.Foldable (foldMap)
 import System.IO
 
 #ifdef COLORS
 import System.Console.ANSI
 #endif
 
-data RunnerState = RunnerState
-  { ix :: !Int
-  , nestedLevel :: !Int
-  , failures :: !Int
+--------------------------------------------------
+-- TestOutput base definitions
+--------------------------------------------------
+-- {{{
+-- | 'TestOutput' is an intermediary between output formatting and output
+-- printing. It lets us have several different printing modes (normal; print
+-- failures only; quiet).
+data TestOutput
+  = PrintTest
+      {- print test name   -} (IO ())
+      {- print test result -} (Result -> IO ())
+  | PrintHeading (IO ()) TestOutput
+  | Skip
+  | Seq TestOutput TestOutput
+
+-- The monoid laws should hold observationally w.r.t. the semantics defined
+-- in this module
+instance Monoid TestOutput where
+  mempty = Skip
+  mappend = Seq
+
+type Level = Int
+
+produceOutput :: (?colors :: Bool) => OptionSet -> TestTree -> TestOutput
+produceOutput opts tree =
+  let
+    -- Do not retain the reference to the tree more than necessary
+    !alignment = computeAlignment opts tree
+
+    runSingleTest
+      :: (IsTest t, ?colors :: Bool)
+      => OptionSet -> TestName -> t -> Ap (Reader Level) TestOutput
+    runSingleTest _opts name _test = Ap $ do
+      level <- ask
+
+      let
+        printTestName =
+          printf "%s%s: %s" (indent level) name
+            (replicate (alignment - indentSize * level - length name) ' ')
+
+        printTestResult result = do
+          rDesc <- formatMessage $ resultDescription result
+
+          if resultSuccessful result
+            then ok "OK\n"
+            else fail "FAIL\n"
+
+          when (not $ null rDesc) $
+            (if resultSuccessful result then infoOk else infoFail) $
+              printf "%s%s\n" (indent $ level + 1) (formatDesc (level+1) rDesc)
+
+      return $ PrintTest printTestName printTestResult
+
+    runGroup :: TestName -> Ap (Reader Level) TestOutput -> Ap (Reader Level) TestOutput
+    runGroup name grp = Ap $ do
+      level <- ask
+      let
+        printHeading = printf "%s%s\n" (indent level) name
+        printBody = runReader (getApp grp) (level + 1)
+      return $ PrintHeading printHeading printBody
+
+  in
+    flip runReader 0 $ getApp $
+      foldTestTree
+        trivialFold
+          { foldSingle = runSingleTest
+          , foldGroup = runGroup
+          }
+          opts tree
+
+foldTestOutput
+  :: (?colors :: Bool, Monoid b)
+  => (IO () -> IO Result -> (Result -> IO ()) -> b)
+  -> (IO () -> b -> b)
+  -> TestOutput -> StatusMap -> b
+foldTestOutput foldTest foldHeading outputTree smap =
+  flip evalState 0 $ getApp $ go outputTree where
+  go (PrintTest printName printResult) = Ap $ do
+    ix <- get
+    put $! ix + 1
+    let
+      statusVar =
+        fromMaybe (error "internal error: index out of bounds") $
+        IntMap.lookup ix smap
+      readStatusVar = getResultFromTVar statusVar
+    return $ foldTest printName readStatusVar printResult
+  go (PrintHeading printName printBody) = Ap $
+    foldHeading printName <$> getApp (go printBody)
+  go (Seq a b) = mappend (go a) (go b)
+  go Skip = mempty
+
+-- }}}
+
+--------------------------------------------------
+-- TestOutput modes
+--------------------------------------------------
+-- {{{
+consoleOutput :: (?colors :: Bool) => TestOutput -> StatusMap -> IO ()
+consoleOutput output smap =
+  getTraversal . fst $ foldTestOutput foldTest foldHeading output smap
+  where
+    foldTest printName getResult printResult =
+      ( Traversal $ do
+          printName
+          r <- getResult
+          printResult r
+      , Any True)
+    foldHeading printHeading (printBody, Any nonempty) =
+      ( Traversal $ do
+          when nonempty $ do printHeading; getTraversal printBody
+      , Any nonempty
+      )
+
+consoleOutputHidingSuccesses :: (?colors :: Bool) => TestOutput -> StatusMap -> IO ()
+consoleOutputHidingSuccesses output smap =
+  void . getApp $ foldTestOutput foldTest foldHeading output smap
+  where
+    foldTest printName getResult printResult =
+      Ap $ do
+          printName
+          r <- getResult
+          if resultSuccessful r
+            then do clearThisLine; return $ Any False
+            else do printResult r; return $ Any True
+
+    foldHeading printHeading printBody =
+      Ap $ do
+        printHeading
+        Any failed <- getApp printBody
+        unless failed clearAboveLine
+        return $ Any failed
+
+    clearAboveLine = do cursorUpLine 1; clearThisLine
+    clearThisLine = do clearLine; setCursorColumn 0
+
+streamOutputHidingSuccesses :: (?colors :: Bool) => TestOutput -> StatusMap -> IO ()
+streamOutputHidingSuccesses output smap =
+  void . flip evalStateT [] . getApp $
+    foldTestOutput foldTest foldHeading output smap
+  where
+    foldTest printName getResult printResult =
+      Ap $ do
+          r <- liftIO $ getResult
+          if resultSuccessful r
+            then return $ Any False
+            else do
+              stack <- get
+              put []
+
+              liftIO $ do
+                sequence_ $ reverse stack
+                printName
+                printResult r
+
+              return $ Any True
+
+    foldHeading printHeading printBody =
+      Ap $ do
+        modify (printHeading :)
+        Any failed <- getApp printBody
+        unless failed $
+          modify $ \stack ->
+            case stack of
+              _:rest -> rest
+              [] -> [] -- shouldn't happen anyway
+        return $ Any failed
+
+-- }}}
+
+--------------------------------------------------
+-- Statistics
+--------------------------------------------------
+-- {{{
+
+data Statistics = Statistics
+  { statTotal :: !Int
+  , statFailures :: !Int
   }
 
-initialState :: RunnerState
-initialState = RunnerState 0 0 0
+instance Monoid Statistics where
+  Statistics t1 f1 `mappend` Statistics t2 f2 = Statistics (t1 + t2) (f1 + f2)
+  mempty = Statistics 0 0
 
-type M = StateT RunnerState IO
+computeStatistics :: StatusMap -> IO Statistics
+computeStatistics = getApp . foldMap (\var -> Ap $
+  (\r -> Statistics 1 (if resultSuccessful r then 0 else 1))
+    <$> getResultFromTVar var)
 
+printStatistics :: (?colors :: Bool) => Statistics -> IO ()
+printStatistics st = do
+  printf "\n"
+
+  case statFailures st of
+    0 -> do
+      ok $ printf "All %d tests passed\n" (statTotal st)
+
+    fs -> do
+      fail $ printf "%d out of %d tests failed\n" fs (statTotal st)
+
+data FailureStatus
+  = Unknown
+  | Failed
+  | OK
+
+instance Monoid FailureStatus where
+  mappend Failed _ = Failed
+  mappend _ Failed = Failed
+
+  mappend OK OK = OK
+
+  mappend _ _ = Unknown
+
+  mempty = OK
+
+failureStatus :: StatusMap -> IO FailureStatus
+failureStatus smap = atomically $ do
+  fst <- getApp $ flip foldMap smap $ \svar -> Ap $ do
+    status <- readTVar svar
+    return $ case status of
+        Done r ->
+          if resultSuccessful r then OK else Failed
+        _ -> Unknown
+  case fst of
+    Unknown -> retry
+    _ -> return fst
+
+-- }}}
+
+--------------------------------------------------
+-- Console test reporter
+--------------------------------------------------
+-- {{{
+
+-- | A simple console UI
+consoleTestReporter :: Ingredient
+consoleTestReporter =
+  TestReporter
+    [ Option (Proxy :: Proxy Quiet)
+    , Option (Proxy :: Proxy HideSuccesses)
+    ] $
+  \opts tree -> Just $ \smap ->
+
+  do
+  isTerm <- hIsTerminalDevice stdout
+  hSetBuffering stdout NoBuffering
+
+  let
+    ?colors = isTerm
+  let
+    Quiet quiet = lookupOption opts
+    HideSuccesses hideSuccesses = lookupOption opts
+
+    output = produceOutput opts tree
+
+  case () of { _
+    | quiet -> return ()
+    | hideSuccesses && isTerm ->
+        consoleOutputHidingSuccesses output smap
+    | hideSuccesses && not isTerm ->
+        streamOutputHidingSuccesses output smap
+    | otherwise -> consoleOutput output smap
+  }
+
+  if quiet
+    then do
+      fst <- failureStatus smap
+      return $ case fst of
+        OK -> True
+        _ -> False
+    else do
+      stats <- computeStatistics smap
+      printStatistics stats
+      return $ statFailures stats == 0
+
+-- | Do not print test results (see README for details)
+newtype Quiet = Quiet Bool
+  deriving (Eq, Ord, Typeable)
+instance IsOption Quiet where
+  defaultValue = Quiet False
+  parseValue = fmap Quiet . safeRead
+  optionName = return "quiet"
+  optionHelp = return "Do not produce any output; indicate success only by the exit code"
+  optionCLParser = flagCLParser (Just 'q') (Quiet True)
+
+-- | Report only failed tests
+newtype HideSuccesses = HideSuccesses Bool
+  deriving (Eq, Ord, Typeable)
+instance IsOption HideSuccesses where
+  defaultValue = HideSuccesses False
+  parseValue = fmap HideSuccesses . safeRead
+  optionName = return "hide-successes"
+  optionHelp = return "Do not print tests that passed successfully"
+  optionCLParser = flagCLParser Nothing (HideSuccesses True)
+
+-- }}}
+
+--------------------------------------------------
+-- Various utilities
+--------------------------------------------------
+-- {{{
+getResultFromTVar :: TVar Status -> IO Result
+getResultFromTVar var =
+  atomically $ do
+    status <- readTVar var
+    case status of
+      Done r -> return r
+      _ -> retry
+
+-- }}}
+
+--------------------------------------------------
+-- Formatting
+--------------------------------------------------
+-- {{{
+
 indentSize :: Int
 indentSize = 2
 
@@ -89,96 +403,6 @@
         MinusInfinity -> 0
         Maximum x -> x
 
--- | A simple console UI
-consoleTestReporter :: Ingredient
--- We fold the test tree using (AppMonoid m, Any) monoid.
---
--- The 'Any' part is needed to know whether a group is empty, in which case
--- we shouldn't display it.
-consoleTestReporter = TestReporter [] $ \opts tree -> Just $ \smap -> do
-  isTerm <- hIsTerminalDevice stdout
-
-  let
-    ?colors = isTerm
-
-  let
-    alignment = computeAlignment opts tree
-
-    runSingleTest
-      :: (IsTest t, ?colors :: Bool)
-      => IntMap.IntMap (TVar Status)
-      -> OptionSet -> TestName -> t -> (AppMonoid M, Any)
-    runSingleTest smap _opts name _test = (, Any True) $ AppMonoid $ do
-      st@RunnerState { ix = ix, nestedLevel = level } <- get
-      let
-        statusVar =
-          fromMaybe (error "internal error: index out of bounds") $
-          IntMap.lookup ix smap
-
-      -- Print the test name before waiting for the test. This is useful
-      -- for long-running tests.
-      liftIO $ printf "%s%s: %s" (indent level) name
-        (replicate (alignment - indentSize * level - length name) ' ')
-
-      (rOk, rDesc) <-
-        liftIO $ atomically $ do
-          status <- readTVar statusVar
-          case status of
-            Done r -> return $ (resultSuccessful r, resultDescription r)
-            Exception e -> return (False, "Exception: " ++ show e)
-            _ -> retry
-
-      rDesc <- liftIO $ formatMessage rDesc
-
-      liftIO $
-        if rOk
-          then ok "OK\n"
-          else fail "FAIL\n"
-
-      when (not $ null rDesc) $
-        liftIO $ (if rOk then infoOk else infoFail) $
-          printf "%s%s\n" (indent $ level + 1) (formatDesc (level+1) rDesc)
-      let
-        ix' = ix+1
-        updateFailures = if rOk then id else (+1)
-      put $! st { ix = ix', failures = updateFailures (failures st) }
-
-    runGroup :: TestName -> (AppMonoid M, Any) -> (AppMonoid M, Any)
-    runGroup _ (_, Any False) = mempty
-    runGroup name (AppMonoid act, nonEmpty) = (, nonEmpty) $ AppMonoid $ do
-      st@RunnerState { nestedLevel = level } <- get
-      liftIO $ printf "%s%s\n" (indent level) name
-      put $! st { nestedLevel = level + 1 }
-      act
-      modify $ \st -> st { nestedLevel = level }
-
-  hSetBuffering stdout NoBuffering
-
-  -- Do not retain the reference to the tree more than necessary
-  _ <- evaluate alignment
-
-  st <-
-    flip execStateT initialState $ getApp $ fst $
-      foldTestTree
-        trivialFold
-          { foldSingle = runSingleTest smap
-          , foldGroup = runGroup
-          }
-        opts
-        tree
-
-  printf "\n"
-
-  case failures st of
-    0 -> do
-      ok $ printf "All %d tests passed\n" (ix st)
-      return True
-
-    fs -> do
-      fail $ printf "%d out of %d tests failed\n" fs (ix st)
-      return False
-
-
 -- | Printing exceptions or other messages is tricky — in the process we
 -- can get new exceptions!
 --
@@ -226,3 +450,5 @@
 infoOk   = putStr
 infoFail = putStr
 #endif
+
+-- }}}
diff --git a/Test/Tasty/Ingredients/IncludingOptions.hs b/Test/Tasty/Ingredients/IncludingOptions.hs
--- a/Test/Tasty/Ingredients/IncludingOptions.hs
+++ b/Test/Tasty/Ingredients/IncludingOptions.hs
@@ -1,3 +1,4 @@
+-- | Ingredient for registering user-defined options
 module Test.Tasty.Ingredients.IncludingOptions where
 
 import Test.Tasty.Ingredients
diff --git a/Test/Tasty/Ingredients/ListTests.hs b/Test/Tasty/Ingredients/ListTests.hs
--- a/Test/Tasty/Ingredients/ListTests.hs
+++ b/Test/Tasty/Ingredients/ListTests.hs
@@ -1,3 +1,4 @@
+-- | Ingredient for listing test names
 {-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-}
 module Test.Tasty.Ingredients.ListTests
   ( ListTests(..)
@@ -5,10 +6,8 @@
   , listingTests
   ) where
 
-import Options.Applicative
 import Data.Typeable
 import Data.Proxy
-import Data.Tagged
 
 import Test.Tasty.Core
 import Test.Tasty.Options
@@ -23,13 +22,7 @@
   parseValue = fmap ListTests . safeRead
   optionName = return "list-tests"
   optionHelp = return "Do not run the tests; just print their names"
-  optionCLParser =
-    fmap ListTests $
-    switch
-      (  short 'l'
-      <> long (untag (optionName :: Tagged ListTests String))
-      <> help (untag (optionHelp :: Tagged ListTests String))
-      )
+  optionCLParser = flagCLParser (Just 'l') (ListTests True)
 
 -- | Obtain the list of all tests in the suite
 testsNames :: OptionSet -> TestTree -> [TestName]
diff --git a/Test/Tasty/Options.hs b/Test/Tasty/Options.hs
--- a/Test/Tasty/Options.hs
+++ b/Test/Tasty/Options.hs
@@ -13,8 +13,10 @@
   , setOption
   , changeOption
   , lookupOption
+  , singleOption
   , OptionDescription(..)
     -- * Utilities
+  , flagCLParser
   , safeRead
   ) where
 
@@ -24,6 +26,7 @@
 import Data.Tagged
 import Data.Proxy
 import Data.Monoid
+import Data.Foldable
 
 import Options.Applicative
 import Options.Applicative.Types
@@ -48,12 +51,22 @@
   --
   -- Even if you override this, you still should implement all the methods
   -- above, to allow alternative interfaces.
+  --
+  -- Do not supply a default value here for this parser!
+  -- This is because if no value was provided on the command line we may
+  -- lookup the option e.g. in the environment. But if the parser always
+  -- succeeds, we have no way to tell whether the user really provided the
+  -- option on the command line.
+
+  -- (If we don't specify a default, the option becomes mandatory.
+  -- So, when we build the complete parser for OptionSet, we turn a
+  -- failing parser into an always-succeeding one that may return an empty
+  -- OptionSet.)
   optionCLParser :: Parser v
   optionCLParser =
     nullOption
       (  reader parse
       <> long name
-      <> value defaultValue
       <> help helpString
       )
     where
@@ -95,10 +108,26 @@
 changeOption :: forall v . IsOption v => (v -> v) -> OptionSet -> OptionSet
 changeOption f s = setOption (f $ lookupOption s) s
 
+-- | Create a singleton 'OptionSet'
+singleOption :: IsOption v => v -> OptionSet
+singleOption v = setOption v mempty
+
 -- | The purpose of this data type is to capture the dictionary
 -- corresponding to a particular option.
 data OptionDescription where
   Option :: IsOption v => Proxy v -> OptionDescription
+
+-- | Command-line parser to use with flags
+flagCLParser
+  :: forall v . IsOption v
+  => Maybe Char -- ^ optional short flag
+  -> v          -- ^ non-default value (when the flag is supplied)
+  -> Parser v
+flagCLParser mbShort v = flag' v
+  (  foldMap short mbShort
+  <> long (untag (optionName :: Tagged v String))
+  <> help (untag (optionHelp :: Tagged v String))
+  )
 
 -- | Safe read function. Defined here for convenience to use for
 -- 'parseValue'.
diff --git a/Test/Tasty/Options/Core.hs b/Test/Tasty/Options/Core.hs
new file mode 100644
--- /dev/null
+++ b/Test/Tasty/Options/Core.hs
@@ -0,0 +1,112 @@
+-- | Core options, i.e. the options used by tasty itself
+{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-}
+{-# OPTIONS_GHC -fno-warn-type-defaults #-} -- for (^)
+module Test.Tasty.Options.Core
+  ( NumThreads(..)
+  , Timeout(..)
+  , mkTimeout
+  , coreOptions
+  )
+  where
+
+import Data.Typeable
+import Data.Proxy
+import Data.Tagged
+import Data.Fixed
+import Options.Applicative
+import Options.Applicative.Types (ReadM(..))
+
+import Test.Tasty.Options
+import Test.Tasty.Patterns
+
+-- | Number of parallel threads to use for running tests.
+--
+-- Note that this is /not/ included in 'coreOptions'.
+-- Instead, it's automatically included in the options for any
+-- 'TestReporter' ingredient by 'ingredientOptions', because the way test
+-- reporters are handled already involves parallelism. Other ingredients
+-- may also choose to include this option.
+newtype NumThreads = NumThreads { getNumThreads :: Int }
+  deriving (Eq, Ord, Num, Typeable)
+instance IsOption NumThreads where
+  defaultValue = 1
+  parseValue = fmap NumThreads . safeRead
+  optionName = return "num-threads"
+  optionHelp = return "Number of threads to use for tests execution"
+  optionCLParser =
+    nullOption
+      (  reader parse
+      <> short 'j'
+      <> long name
+      <> help (untag (optionHelp :: Tagged NumThreads String))
+      )
+    where
+      name = untag (optionName :: Tagged NumThreads String)
+      parse =
+        ReadM .
+        maybe (Left (ErrorMsg $ "Could not parse " ++ name)) Right .
+        parseValue
+
+-- | Timeout to be applied to individual tests
+data Timeout
+  = Timeout Integer String
+    -- ^ 'String' is the original representation of the timeout (such as
+    -- @\"0.5m\"@), so that we can print it back. 'Integer' is the number of
+    -- microseconds.
+  | NoTimeout
+  deriving (Show, Typeable)
+
+instance IsOption Timeout where
+  defaultValue = NoTimeout
+  parseValue str =
+    Timeout
+      <$> parseTimeout str
+      <*> pure str
+  optionName = return "timeout"
+  optionHelp = return "Timeout for individual tests (suffixes: ms,s,m,h; default: s)"
+  optionCLParser =
+    nullOption
+      (  reader parse
+      <> short 't'
+      <> long name
+      <> help (untag (optionHelp :: Tagged Timeout String))
+      )
+    where
+      name = untag (optionName :: Tagged Timeout String)
+      parse =
+        ReadM .
+        maybe (Left (ErrorMsg $ "Could not parse " ++ name)) Right .
+        parseValue
+
+parseTimeout :: String -> Maybe Integer
+parseTimeout str =
+  -- it sucks that there's no more direct way to convert to a number of
+  -- microseconds
+  (round :: Micro -> Integer) . (* 10^6) <$>
+  case reads str of
+    [(n, suffix)] ->
+      case suffix of
+        "ms" -> Just (n / 10^3)
+        "" -> Just n
+        "s" -> Just n
+        "m" -> Just (n * 60)
+        "h" -> Just (n * 60^2)
+        _ -> Nothing
+    _ -> Nothing
+
+-- | A shortcut for creating 'Timeout' values
+mkTimeout
+  :: Integer -- ^ microseconds
+  -> Timeout
+mkTimeout n =
+  Timeout n $
+    showFixed True (fromInteger n / (10^6) :: Micro) ++ "s"
+
+-- | The list of all core options, i.e. the options not specific to any
+-- provider or ingredient, but to tasty itself. Currently contains
+-- 'TestPattern' and 'Timeout'.
+coreOptions :: [OptionDescription]
+coreOptions =
+  [ Option (Proxy :: Proxy TestPattern)
+  , Option (Proxy :: Proxy Timeout)
+  ]
diff --git a/Test/Tasty/Options/Env.hs b/Test/Tasty/Options/Env.hs
new file mode 100644
--- /dev/null
+++ b/Test/Tasty/Options/Env.hs
@@ -0,0 +1,62 @@
+-- | Get options from the environment
+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables #-}
+module Test.Tasty.Options.Env (getEnvOptions, suiteEnvOptions) where
+
+import Test.Tasty.Options
+import Test.Tasty.Core
+import Test.Tasty.Ingredients
+import Test.Tasty.Runners.Reducers
+
+import System.Environment
+import Data.Foldable
+import Data.Tagged
+import Data.Proxy
+import Data.Char
+import Data.Typeable
+import Control.Exception
+import Control.Applicative
+import Text.Printf
+
+data EnvOptionException
+  = BadOption
+      String -- option name
+      String -- variable name
+      String -- value
+  deriving (Typeable)
+
+instance Show EnvOptionException where
+  show (BadOption optName varName value) =
+    printf
+      "Bad environment variable %s='%s' (parsed as option %s)"
+        varName value optName
+
+instance Exception EnvOptionException
+
+-- | Search the environment for given options
+getEnvOptions :: [OptionDescription] -> IO OptionSet
+getEnvOptions = getApp . foldMap lookupOpt
+  where
+    lookupOpt (Option (px :: Proxy v)) = do
+      let
+        name = proxy optionName px
+        envName = ("TASTY_" ++) . flip map name $ \c ->
+          if c == '-'
+            then '_'
+            else toUpper c
+      mbValueStr <- Ap $ myLookupEnv envName
+      flip foldMap mbValueStr $ \valueStr ->
+        let
+          mbValue :: Maybe v
+          mbValue = parseValue valueStr
+
+          err = throwIO $ BadOption name envName valueStr
+
+        in Ap $ maybe err (return . singleOption) mbValue
+
+-- | Search the environment for all options relevant for this suite
+suiteEnvOptions :: [Ingredient] -> TestTree -> IO OptionSet
+suiteEnvOptions ins tree = getEnvOptions $ suiteOptions ins tree
+
+-- note: switch to lookupEnv once we no longer support 7.4
+myLookupEnv :: String -> IO (Maybe String)
+myLookupEnv name = either (const Nothing) Just <$> (try (getEnv name) :: IO (Either IOException String))
diff --git a/Test/Tasty/Parallel.hs b/Test/Tasty/Parallel.hs
--- a/Test/Tasty/Parallel.hs
+++ b/Test/Tasty/Parallel.hs
@@ -1,4 +1,5 @@
 -- | A helper module which takes care of parallelism
+{-# LANGUAGE DeriveDataTypeable #-}
 module Test.Tasty.Parallel (runInParallel) where
 
 import Control.Monad
@@ -6,13 +7,35 @@
 import Control.Concurrent.STM
 import Control.Exception
 import Foreign.StablePtr
+import Data.Typeable
 
+data Interrupt = Interrupt
+  deriving Typeable
+instance Show Interrupt where
+  show Interrupt = "interrupted"
+instance Exception Interrupt
+
+data ParThreadKilled = ParThreadKilled SomeException
+  deriving Typeable
+instance Show ParThreadKilled where
+  show (ParThreadKilled exn) =
+    "tasty: one of the test running threads was killed by: " ++
+    show exn
+instance Exception ParThreadKilled
+
+shutdown :: ThreadId -> IO ()
+shutdown = flip throwTo Interrupt
+
 -- | Take a list of actions and execute them in parallel, no more than @n@
--- at the same time
+-- at the same time.
+--
+-- The action itself is asynchronous, ie. it returns immediately and does
+-- the work in new threads. It returns an action which aborts tests and
+-- cleans up.
 runInParallel
   :: Int -- ^ maximum number of parallel threads
   -> [IO ()] -- ^ list of actions to execute
-  -> IO ()
+  -> IO (IO ())
 -- This implementation tries its best to ensure that exceptions are
 -- properly propagated to the caller and threads are not left running.
 --
@@ -41,21 +64,33 @@
 
   let
     -- Kill all threads.
-    killAll :: IO ()
-    killAll = do
+    shutdownAll :: IO ()
+    shutdownAll = do
       pids <- atomically $ do
         writeTVar aliveVar False
         readTVar pidsVar
 
       -- be sure not to kill myself!
       me <- myThreadId
-      mapM_ killThread $ filter (/= me) pids
+      mapM_ shutdown $ filter (/= me) pids
 
     cleanup :: Either SomeException () -> IO ()
-    cleanup = either (\e -> killAll >> throwTo callingThread e) (const $ return ())
+    cleanup Right {} = return ()
+    cleanup (Left exn)
+      | Just Interrupt <- fromException exn
+        -- I'm being shut down either by a fellow thread (which caught an
+        -- exception), or by the main thread which decided to stop running
+        -- tests. In any case, just end silently.
+        = return ()
+      | otherwise = do
+        -- Wow, I caught an exception (most probably an async one,
+        -- although it doesn't really matter). Shut down all other
+        -- threads, and re-throw my exception to the calling thread.
+        shutdownAll
+        throwTo callingThread $ ParThreadKilled exn
 
-    forkCarefully :: IO () -> IO ()
-    forkCarefully action = void . flip myForkFinally cleanup $ do
+    forkCarefully :: IO () -> IO ThreadId
+    forkCarefully action = flip myForkFinally cleanup $ do
       -- We cannot check liveness and update the pidsVar in one
       -- transaction before forking, because we don't know the new pid yet.
       --
@@ -95,6 +130,7 @@
   -- fork here as well, so that we can move to the UI without waiting
   -- untill all tests have finished
   forkCarefully $ foldr go (return ()) actions
+  return shutdownAll
 
 -- Copied from base to stay compatible with GHC 7.4.
 myForkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId
diff --git a/Test/Tasty/Patterns.hs b/Test/Tasty/Patterns.hs
--- a/Test/Tasty/Patterns.hs
+++ b/Test/Tasty/Patterns.hs
@@ -44,7 +44,10 @@
 
 import Data.List
 import Data.Typeable
+import Data.Tagged
 
+import Options.Applicative
+import Options.Applicative.Types (ReadM(..))
 
 data Token = SlashToken
            | WildcardToken
@@ -81,10 +84,17 @@
     readsPrec _ string = [(parseTestPattern string, "")]
 
 instance IsOption TestPattern where
-    defaultValue = noPattern
-    parseValue = Just . parseTestPattern
-    optionName = return "pattern"
-    optionHelp = return "Select only tests that match pattern"
+  defaultValue = noPattern
+  parseValue = Just . parseTestPattern
+  optionName = return "pattern"
+  optionHelp = return "Select only tests that match pattern"
+  optionCLParser =
+    nullOption
+      (  reader (ReadM . Right . parseTestPattern)
+      <> short 'p'
+      <> long (untag (optionName :: Tagged TestPattern String))
+      <> help (untag (optionHelp :: Tagged TestPattern String))
+      )
 
 -- | Parse a pattern
 parseTestPattern :: String -> TestPattern
diff --git a/Test/Tasty/Providers.hs b/Test/Tasty/Providers.hs
--- a/Test/Tasty/Providers.hs
+++ b/Test/Tasty/Providers.hs
@@ -1,7 +1,9 @@
 -- | API for test providers
 module Test.Tasty.Providers
   ( IsTest(..)
-  , Result(..)
+  , testPassed
+  , testFailed
+  , Result
   , Progress(..)
   , TestName
   , TestTree
@@ -14,3 +16,21 @@
 -- | Convert a test to a leaf of the 'TestTree'
 singleTest :: IsTest t => TestName -> t -> TestTree
 singleTest = SingleTest
+
+-- | 'Result' of a passed test
+testPassed
+  :: String -- ^ description (may be empty)
+  -> Result
+testPassed desc = Result
+  { resultOutcome = Success
+  , resultDescription = desc
+  }
+
+-- | 'Result' of a failed test
+testFailed
+  :: String -- ^ description
+  -> Result
+testFailed desc = Result
+  { resultOutcome = Failure TestFailed
+  , resultDescription = desc
+  }
diff --git a/Test/Tasty/Run.hs b/Test/Tasty/Run.hs
--- a/Test/Tasty/Run.hs
+++ b/Test/Tasty/Run.hs
@@ -1,5 +1,5 @@
 -- | Running tests
-{-# LANGUAGE ScopedTypeVariables, ExistentialQuantification #-}
+{-# LANGUAGE ScopedTypeVariables, ExistentialQuantification, RankNTypes #-}
 module Test.Tasty.Run
   ( Status(..)
   , StatusMap
@@ -9,20 +9,22 @@
 import qualified Data.IntMap as IntMap
 import qualified Data.Sequence as Seq
 import qualified Data.Foldable as F
+import Data.Maybe
 import Control.Monad.State
 import Control.Monad.Writer
 import Control.Monad.Reader
-import Control.Monad.Trans.Either
-import Control.Concurrent
 import Control.Concurrent.STM
-import Control.Exception
+import Control.Concurrent.Timeout
+import Control.Concurrent.Async
+import Control.Exception as E
 import Control.Applicative
 import Control.Arrow
 
 import Test.Tasty.Core
 import Test.Tasty.Parallel
 import Test.Tasty.Options
-import Test.Tasty.CoreOptions
+import Test.Tasty.Options.Core
+import Test.Tasty.Runners.Reducers
 
 -- | Current status of a test
 data Status
@@ -30,8 +32,6 @@
     -- ^ test has not started running yet
   | Executing Progress
     -- ^ test is being run
-  | Exception SomeException
-    -- ^ test threw an exception and was aborted
   | Done Result
     -- ^ test finished with a given result
 
@@ -43,143 +43,229 @@
 
 data Resource r
   = NotCreated
+  | BeingCreated
   | FailedToCreate SomeException
   | Created r
+  | Destroyed
 
+instance Show (Resource r) where
+  show r = case r of
+    NotCreated -> "NotCreated"
+    BeingCreated -> "BeingCreated"
+    FailedToCreate exn -> "FailedToCreate " ++ show exn
+    Created {} -> "Created"
+    Destroyed -> "Destroyed"
+
+data ResourceVar = forall r . ResourceVar (TVar (Resource r))
+
 data Initializer
   = forall res . Initializer
       (IO res)
-      (MVar (Resource res))
+      (TVar (Resource res))
 data Finalizer
   = forall res . Finalizer
       (res -> IO ())
-      (MVar (Resource res))
-      (MVar Int)
+      (TVar (Resource res))
+      (TVar Int)
 
--- | Start executing a test
---
--- Note: we take the finalizer as an argument because it's important that
--- it's run *before* we write the status var and signal to other threads
--- that we're finished
+-- | Execute a test taking care of resources
 executeTest
   :: ((Progress -> IO ()) -> IO Result)
     -- ^ the action to execute the test, which takes a progress callback as
     -- a parameter
   -> TVar Status -- ^ variable to write status to
+  -> Timeout -- ^ optional timeout to apply
   -> Seq.Seq Initializer -- ^ initializers (to be executed in this order)
   -> Seq.Seq Finalizer -- ^ finalizers (to be executed in this order)
   -> IO ()
-executeTest action statusVar inits fins =
-  handle (atomically . writeTVar statusVar . Exception) $ do
-  -- We don't try to protect against async exceptions here.
-  -- This is because we use interruptible modifyMVar and wouldn't be able
-  -- to give any guarantees anyway.
-  -- So all we do is guard actual acquire/test/release actions using 'try'.
-  -- The only thing we guarantee upon catching an async exception is that
-  -- we'll write it to the status var, so that the UI won't be waiting
-  -- infinitely.
-  resultOrExcn <- runEitherT $ do
-    F.forM_ inits $ \(Initializer doInit initVar) -> EitherT $
-      modifyMVar initVar $ \resStatus  ->
-        case resStatus of
-          NotCreated -> do
-            mbRes <- try doInit
-            case mbRes of
-              Right res -> return (Created res, Right ())
-              Left ex -> return (FailedToCreate ex, Left ex)
-          Created {} -> return (resStatus, Right ())
-          FailedToCreate ex -> return (resStatus, Left ex)
+executeTest action statusVar timeoutOpt inits fins = mask $ \restore -> do
+  resultOrExn <- try $ restore $ do
+    -- N.B. this can (re-)throw an exception. It's okay. By design, the
+    -- actual test will not be run, then. We still run all the
+    -- finalizers.
+    --
+    -- There's no point to transform these exceptions to something like
+    -- EitherT, because an async exception (cancellation) can strike
+    -- anyway.
+    initResources
 
-    -- if all initializers ran successfully, actually run the test
-    EitherT . try $
-      -- pass our callback (which updates the status variable) to the test
-      -- action
-      action yieldProgress
+    -- If all initializers ran successfully, actually run the test.
+    -- We run it in a separate thread, so that the test's exception
+    -- handler doesn't interfere with our timeout.
+    withAsync (action yieldProgress) $ \asy ->
+      applyTimeout timeoutOpt $ wait asy
 
   -- no matter what, try to run each finalizer
-  -- remember the first exception that occurred
-  mbExcn <- liftM getFirst . execWriterT . getApp $
-    flip F.foldMap fins $ \(Finalizer doRelease initVar finishVar) ->
-      AppMonoid $ do
-        mbExcn <-
-          liftIO $ modifyMVar finishVar $ \nUsers -> do
-            let nUsers' = nUsers - 1
-            mbExcn <-
-              if nUsers' == 0
-              then do
-                resStatus <- readMVar initVar
+  mbExn <- destroyResources restore
+
+  atomically . writeTVar statusVar $ Done $
+    case resultOrExn <* maybe (Right ()) Left mbExn of
+      Left ex -> exceptionResult ex
+      Right r -> r
+
+  where
+    initResources :: IO ()
+    initResources =
+      F.forM_ inits $ \(Initializer doInit initVar) -> do
+        join $ atomically $ do
+          resStatus <- readTVar initVar
+          case resStatus of
+            NotCreated -> do
+              -- signal to others that we're taking care of the resource
+              -- initialization
+              writeTVar initVar BeingCreated
+              return $
+                (do
+                  res <- doInit
+                  atomically $ writeTVar initVar $ Created res
+                 ) `E.catch` \exn -> do
+                  atomically $ writeTVar initVar $ FailedToCreate exn
+                  throwIO exn
+            BeingCreated -> retry
+            Created {} -> return $ return ()
+            FailedToCreate exn -> return $ throwIO exn
+            _ -> return $ throwIO $
+              unexpectedState "initResources" resStatus
+
+    applyTimeout :: Timeout -> IO Result -> IO Result
+    applyTimeout NoTimeout a = a
+    applyTimeout (Timeout t tstr) a = do
+      let
+        timeoutResult =
+          Result
+            { resultOutcome = Failure $ TestTimedOut t
+            , resultDescription =
+                "Timed out after " ++ tstr
+            }
+      fromMaybe timeoutResult <$> timeout t a
+
+    -- destroyResources should not be interrupted by an exception
+    -- Here's how we ensure this:
+    --
+    -- * the finalizer is wrapped in 'try'
+    -- * async exceptions are masked by the caller
+    -- * we don't use any interruptible operations here (outside of 'try')
+    destroyResources :: (forall a . IO a -> IO a) -> IO (Maybe SomeException)
+    destroyResources restore = do
+      -- remember the first exception that occurred
+      liftM getFirst . execWriterT . getTraversal $
+        flip F.foldMap fins $ \(Finalizer doRelease initVar finishVar) ->
+          Traversal $ do
+            iAmLast <- liftIO $ atomically $ do
+              nUsers <- readTVar finishVar
+              let nUsers' = nUsers - 1
+              writeTVar finishVar nUsers'
+              return $ nUsers' == 0
+
+            mbExcn <- liftIO $
+              if iAmLast
+              then join $ atomically $ do
+                resStatus <- readTVar initVar
                 case resStatus of
-                  Created res ->
-                    either
-                      (\ex -> Just ex)
-                      (\_ -> Nothing)
-                    <$> try (doRelease res)
-                  _ -> return Nothing
+                  Created res -> do
+                    -- Don't worry about double destroy — only one thread
+                    -- receives iAmLast
+                    return $
+                      (either Just (const Nothing)
+                        <$> try (restore $ doRelease res))
+                        <* atomically (writeTVar initVar Destroyed)
+                  FailedToCreate {} -> return $ return Nothing
+                  _ -> return $ return $ Just $
+                    unexpectedState "destroyResources" resStatus
               else return Nothing
-            return (nUsers', mbExcn) -- end of modifyMVar
 
-        tell $ First mbExcn
-
-  atomically . writeTVar statusVar $
-    case resultOrExcn <* maybe (return ()) Left mbExcn of
-      Left ex -> Exception ex
-      Right r -> Done r
+            tell $ First mbExcn
 
-  where
-    -- the callback
-    yieldProgress progress =
-      atomically $ writeTVar statusVar $ Executing progress
+    -- The callback
+    -- Since this is not used yet anyway, disable for now.
+    -- I'm not sure whether we should get rid of this altogether. For most
+    -- providers this is either difficult to implement or doesn't make
+    -- sense at all.
+    -- See also https://github.com/feuerbach/tasty/issues/33
+    yieldProgress _ = return ()
 
 type InitFinPair = (Seq.Seq Initializer, Seq.Seq Finalizer)
 
--- | Prepare the test tree to be run
-createTestActions :: OptionSet -> TestTree -> IO [(IO (), TVar Status)]
-createTestActions opts tree =
-  liftM (map (first ($ (Seq.empty, Seq.empty)))) $
-  execWriterT $ getApp $
-  (foldTestTree
-    trivialFold
-      { foldSingle = runSingleTest
-      , foldResource = addInitAndRelease
-      }
-    opts
-    tree
-    :: AppMonoid (WriterT [(InitFinPair -> IO (), TVar Status)] IO))
+-- | Turn a test tree into a list of actions to run tests coupled with
+-- variables to watch them
+createTestActions :: OptionSet -> TestTree -> IO ([(IO (), TVar Status)], [ResourceVar])
+createTestActions opts tree = do
+  let
+    traversal ::
+      Traversal (WriterT ([(InitFinPair -> IO (), TVar Status)], [ResourceVar]) IO)
+    traversal =
+      foldTestTree
+        trivialFold
+          { foldSingle = runSingleTest
+          , foldResource = addInitAndRelease
+          }
+        opts tree
+  (tests, rvars) <- unwrap traversal
+  let tests' = map (first ($ (Seq.empty, Seq.empty))) tests
+  return (tests', rvars)
+
   where
-    runSingleTest opts _ test = AppMonoid $ do
+    runSingleTest opts _ test = Traversal $ do
       statusVar <- liftIO $ atomically $ newTVar NotStarted
       let
         act (inits, fins) =
-          executeTest (run opts test) statusVar inits fins
-      tell [(act, statusVar)]
-    addInitAndRelease (ResourceSpec doInit doRelease) a =
-      AppMonoid . WriterT . fmap ((,) ()) $ do
-        initVar <- newMVar NotCreated
-        tests <- execWriterT $ getApp $ a (getResource initVar)
-        let ntests = length tests
-        finishVar <- newMVar ntests
-        let
-          ini = Initializer doInit initVar
-          fin = Finalizer doRelease initVar finishVar
-        return $ map (first $ local $ (Seq.|> ini) *** (fin Seq.<|)) tests
+          executeTest (run opts test) statusVar (lookupOption opts) inits fins
+      tell ([(act, statusVar)], mempty)
+    addInitAndRelease (ResourceSpec doInit doRelease) a = wrap $ do
+      initVar <- atomically $ newTVar NotCreated
+      (tests, rvars) <- unwrap $ a (getResource initVar)
+      let ntests = length tests
+      finishVar <- atomically $ newTVar ntests
+      let
+        ini = Initializer doInit initVar
+        fin = Finalizer doRelease initVar finishVar
+        tests' = map (first $ local $ (Seq.|> ini) *** (fin Seq.<|)) tests
+      return (tests', ResourceVar initVar : rvars)
+    wrap = Traversal . WriterT . fmap ((,) ())
+    unwrap = execWriterT . getTraversal
 
 -- | Used to create the IO action which is passed in a WithResource node
-getResource :: MVar (Resource r) -> IO r
+getResource :: TVar (Resource r) -> IO r
 getResource var =
-  readMVar var >>= \rState ->
+  atomically $ do
+    rState <- readTVar var
     case rState of
       Created r -> return r
-      NotCreated -> throwIO $ UnexpectedState "not created"
-      FailedToCreate {} -> throwIO $ UnexpectedState "failed to create"
+      Destroyed -> throwSTM UseOutsideOfTest
+      _ -> throwSTM $ unexpectedState "getResource" rState
 
 -- | Start running all the tests in a test tree in parallel. The number of
 -- threads is determined by the 'NumThreads' option.
 --
 -- Return a map from the test number (starting from 0) to its status
 -- variable.
-launchTestTree :: OptionSet -> TestTree -> IO StatusMap
-launchTestTree opts tree = do
-  testActions <- createTestActions opts tree
+launchTestTree
+  :: OptionSet
+  -> TestTree
+  -> (StatusMap -> IO a)
+  -> IO a
+launchTestTree opts tree k = do
+  (testActions, rvars) <- createTestActions opts tree
   let NumThreads numTheads = lookupOption opts
-  runInParallel numTheads (fst <$> testActions)
-  return $ IntMap.fromList $ zip [0..] (snd <$> testActions)
+  abortTests <- runInParallel numTheads (fst <$> testActions)
+  (do let smap = IntMap.fromList $ zip [0..] (snd <$> testActions)
+      k smap)
+   `finally` do
+      abortTests
+      waitForResources rvars
+  where
+    alive :: Resource r -> Bool
+    alive r = case r of
+      NotCreated -> False
+      BeingCreated -> True
+      FailedToCreate {} -> False
+      Created {} -> True
+      Destroyed -> False
+
+    waitForResources rvars = atomically $
+      forM_ rvars $ \(ResourceVar rvar) -> do
+        res <- readTVar rvar
+        check $ not $ alive res
+
+unexpectedState :: String -> Resource r -> SomeException
+unexpectedState where_ r = toException $ UnexpectedState where_ (show r)
diff --git a/Test/Tasty/Runners.hs b/Test/Tasty/Runners.hs
--- a/Test/Tasty/Runners.hs
+++ b/Test/Tasty/Runners.hs
@@ -6,14 +6,18 @@
   , foldTestTree
   , TreeFold(..)
   , trivialFold
-  , AppMonoid(..)
   , ResourceSpec(..)
+  , module Test.Tasty.Runners.Reducers
     -- * Ingredients
   , Ingredient(..)
   , tryIngredients
   , ingredientOptions
   , ingredientsOptions
     -- * Standard console ingredients
+    -- | NOTE: the exports in this section are deprecated and will be
+    -- removed in the future. Please import "Test.Tasty.Ingredients.Basic"
+    -- if you need them.
+
     -- ** Console test reporter
   , consoleTestReporter
     -- ** Tests list
@@ -27,6 +31,9 @@
     -- * Running tests
   , Status(..)
   , Result(..)
+  , Outcome(..)
+  , FailureReason(..)
+  , resultSuccessful
   , Progress(..)
   , StatusMap
   , launchTestTree
@@ -42,8 +49,8 @@
 import Test.Tasty.Core
 import Test.Tasty.Run
 import Test.Tasty.Ingredients
-import Test.Tasty.CoreOptions
+import Test.Tasty.Options.Core
 import Test.Tasty.Patterns
 import Test.Tasty.CmdLine
-import Test.Tasty.Ingredients.ConsoleReporter
-import Test.Tasty.Ingredients.ListTests
+import Test.Tasty.Ingredients.Basic
+import Test.Tasty.Runners.Reducers
diff --git a/Test/Tasty/Runners/Reducers.hs b/Test/Tasty/Runners/Reducers.hs
new file mode 100644
--- /dev/null
+++ b/Test/Tasty/Runners/Reducers.hs
@@ -0,0 +1,58 @@
+-- | Monoidal wrappers for applicative functors. Useful to define tree
+-- folds.
+
+-- These are the same as in the 'reducers' package. We do not use
+-- 'reducers' to avoid its dependencies.
+
+{- License for the 'reducers' package
+Copyright 2008-2011 Edward Kmett
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the author nor the names of his contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+-}
+
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Test.Tasty.Runners.Reducers where
+
+import Data.Monoid
+import Control.Applicative
+
+-- | Monoid generated by '*>'
+newtype Traversal f = Traversal { getTraversal :: f () }
+instance Applicative f => Monoid (Traversal f) where
+  mempty = Traversal $ pure ()
+  Traversal f1 `mappend` Traversal f2 = Traversal $ f1 *> f2
+
+-- | Monoid generated by @'liftA2' ('<>')@
+newtype Ap f a = Ap { getApp :: f a }
+  deriving (Functor, Applicative, Monad)
+instance (Applicative f, Monoid a) => Monoid (Ap f a) where
+  mempty = pure mempty
+  mappend = liftA2 mappend
diff --git a/tasty.cabal b/tasty.cabal
--- a/tasty.cabal
+++ b/tasty.cabal
@@ -2,22 +2,24 @@
 --  see http://haskell.org/cabal/users-guide/
 
 name:                tasty
-version:             0.7
+version:             0.8
 synopsis:            Modern and extensible testing framework
 description:         See <http://documentup.com/feuerbach/tasty>
 license:             MIT
 license-file:        LICENSE
 author:              Roman Cheplyaka
 maintainer:          roma@ro-che.info
+homepage:            http://documentup.com/feuerbach/tasty
 -- copyright:           
 category:            Testing
 build-type:          Simple
--- extra-source-files:  
+extra-source-files:  CHANGELOG
 cabal-version:       >=1.10
 
 Source-repository head
   type:     git
   location: git://github.com/feuerbach/tasty.git
+  subdir:   core
 
 flag colors
   description: Enable colorful output
@@ -29,13 +31,16 @@
     Test.Tasty.Options,
     Test.Tasty.Providers,
     Test.Tasty.Runners
+    Test.Tasty.Ingredients,
+    Test.Tasty.Ingredients.Basic
   other-modules:
     Test.Tasty.Parallel,
     Test.Tasty.Core,
-    Test.Tasty.CoreOptions,
+    Test.Tasty.Options.Core,
+    Test.Tasty.Options.Env,
     Test.Tasty.Patterns,
     Test.Tasty.Run,
-    Test.Tasty.Ingredients,
+    Test.Tasty.Runners.Reducers,
     Test.Tasty.CmdLine,
     Test.Tasty.Ingredients.ConsoleReporter
     Test.Tasty.Ingredients.ListTests
@@ -46,10 +51,15 @@
     containers,
     mtl,
     tagged >= 0.5,
-    regex-tdfa >= 1.1.8,
+    regex-tdfa >= 1.2,
     optparse-applicative >= 0.6,
     deepseq >= 1.3,
-    either >= 4.0
+    unbounded-delays >= 0.1,
+    async >= 2.0
+
+  if impl(ghc < 7.6)
+    -- for GHC.Generics
+    build-depends: ghc-prim
 
   if flag(colors)
     build-depends: ansi-terminal >= 0.6.1
