packages feed

darcs-2.18.5: src/Darcs/UI/Commands/Test/Impl.hs

{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Darcs.UI.Commands.Test.Impl
    ( TestRunner(..), runStrategy
    , TestResult(..), TestResultValid(..), TestFailure(..)
    , TestingDone
    , PatchSeq(..), patchTreeToFL
    , StrategyResult, StrategyResultRaw(..)
    , explanatoryTextFor
    , runTestingEnv
    , exitCodeToTestResult
    , mkTestCmd
    , runTestable
    ) where

import Darcs.Prelude hiding ( init, Monad(..) )
import Darcs.Util.IndexedMonad

import qualified Control.Monad as Base ( Monad(..) )

import Data.Constraint ( Dict(..) )
import Data.String ( fromString )

import GHC.Exts ( Constraint )
import GHC.Show ( showSpace )

import System.Exit ( ExitCode(..) )
import System.IO ( hFlush, stdout )

import qualified Darcs.UI.Options.All as O
import Darcs.Repository ( setScriptsExecutablePatches )
import Darcs.Patch.Witnesses.Ordered
    ( RL(..)
    , FL(..)
    , (:>)(..)
    , splitAtRL
    , reverseRL
    , lengthRL
    , mapRL_RL
    , lengthFL
    , reverseFL
    , (+>+)
    )
import Darcs.Patch.Witnesses.Sealed ( Sealed2(..) )
import Darcs.Patch.Witnesses.Show
  ( Show1(..), Show2(..)
  , showsPrec2
  )
import Darcs.Patch.ApplyMonad ( ApplyMonad )
import Darcs.Patch.Apply ( Apply(..) )
import Darcs.Patch.Commute ( Commute, commute )
import Darcs.Patch.CommuteFn ( commuterIdFL )
import Darcs.Patch.Inspect ( PatchInspect(..) )
import Darcs.Patch ( description )
import Darcs.Patch.Show ( ShowPatch )
import Darcs.Util.Printer ( putDocLn )
import Darcs.Repository.ApplyPatches ( DefaultIO, runDefault )

-- |This type is used to track the state of the testing tree.
-- For example, 'Testing IO wX wY Int' requires that the testing
-- tree start in state 'wX', and leaves it in state 'wY'.
newtype Testing m (wX :: *) (wY :: *) a = Testing { unTesting :: m a }

-- |Once we've finished tracking down a test failure, we no longer care
-- about tracking the actual state of the testing tree. This witness
-- constant is never used in any patch, so once we use it for the state
-- of the testing tree, in practice we can no longer do anything more with
-- that tree.
--
-- We could also use some kind of existential or different monad type
-- to represent this, but it would make composing code with 'do' harder.
data TestingDone

type TestingIO = Testing IO

instance Base.Monad m => Monad (Testing m) where
  return v = Testing (Base.return v)
  Testing m >>= f = Testing (m Base.>>= unTesting . f)
  Testing m1 >> Testing m2 = Testing (m1 Base.>> m2)

instance LiftIx Testing where
  liftIx = Testing

data TestingParams =
  TestingParams
  { tpSetScriptsExecutable :: O.SetScriptsExecutable
  , tpTestCmd :: TestCmd
  }

-- |The 'Testing' monad, augmented with configuration parameters
newtype TestingEnv m wX wY a =
  TestingEnv { unTestingEnv :: ReaderT TestingParams (Testing m) wX wY a }

type TestingEnvIO = TestingEnv IO

deriving instance Base.Monad m => Monad (TestingEnv m)
deriving instance Base.Monad m => MonadReader TestingParams (TestingEnv m)

instance LiftIx TestingEnv where
  liftIx m = TestingEnv (ReaderT (\_ -> liftIx m))

runTestingEnv :: TestingParams -> TestingEnv m wA TestingDone a -> m a
runTestingEnv args = unTesting . ($ args) . runReaderT . unTestingEnv

liftTesting :: Testing m wX wY a -> TestingEnv m wX wY a
liftTesting m = TestingEnv $ ReaderT $ \_ -> m

-- |An indexed monad that can be used to run tests. 'TestingEnvIO' is
-- the only real implementation, the unit tests for testing are based on
-- mock implementations.
class Monad m => TestRunner m where
  type ApplyPatchReqs m (p :: * -> * -> *) :: Constraint
  type DisplayPatchReqs m (p :: * -> * -> *) :: Constraint

  -- |Output a message
  writeMsg :: String -> m wX wX ()

  -- |Output a message containing the name of a patch
  mentionPatch :: DisplayPatchReqs m p => p wA wB -> m wX wX ()
  
  -- |Apply a patch to the testing tree.
  applyPatch :: ApplyPatchReqs m p => p wX wY -> m wX wY ()

  -- |Unapply a patch from the testing tree
  unapplyPatch :: ApplyPatchReqs m p => p wX wY -> m wY wX ()

  -- |Get the current status (pass/skip/fail) of the testing tree,
  -- e.g. by running the test command.
  getCurrentTestResult :: m wX wX (TestResult wX)

  -- |Flag that all testing has completed.
  finishedTesting :: a -> m wX TestingDone a

type TestRunnerPatchReqs m p =
  ( -- Having to enumerate these different cases for ApplyPatchReqs is
    -- a bit ugly, but necessary because it is a type function and we
    -- don't know that ApplyPatchReqs m p => ApplyPatchReqs m (FL p), etc.
    -- In theory QuantifiedConstraints could be used to simplify this but
    -- the fact that ApplyPatchReqs is a type function makes this a bit tricky.
    ApplyPatchReqs m p, ApplyPatchReqs m (RL p), ApplyPatchReqs m (FL p)
  , ApplyPatchReqs m (PatchSeq p), ApplyPatchReqs m (RL (PatchSeq p))
  , DisplayPatchReqs m p)

type TestablePatch m p = (TestRunner m, TestRunnerPatchReqs m p, Commute p)

instance TestRunner TestingEnvIO where
  type ApplyPatchReqs TestingEnvIO p = (Apply p, ApplyMonad (ApplyState p) DefaultIO, PatchInspect p)
  type DisplayPatchReqs TestingEnvIO p = ShowPatch p

  writeMsg str = liftIx (putStrLn str Base.>> hFlush stdout)
  mentionPatch p = liftIx (putDocLn (description p) Base.>> hFlush stdout)

  applyPatch p = do
    liftTesting $ Testing $ runDefault (apply p)
    opts <- asks tpSetScriptsExecutable
    when (opts == O.YesSetScriptsExecutable) $
      liftIx $ setScriptsExecutablePatches p

  unapplyPatch p = do
    liftTesting $ Testing $ runDefault (unapply p)
    opts <- asks tpSetScriptsExecutable
    when (opts == O.YesSetScriptsExecutable) $
      liftIx $ setScriptsExecutablePatches p

  getCurrentTestResult = do
    testCmd <- asks tpTestCmd
    liftTesting $ runTestCmd testCmd

  finishedTesting r = TestingEnv $ ReaderT $ \_ -> Testing (Base.return r)

-- |The result of running a test on state 'wX' of the repository.
data TestResult wX
  = Testable (TestResultValid wX) -- ^We got a usable test result.
  | Untestable
    -- ^The test result could not be identified as either pass or fail,
    -- for example it might have been a build failure. External test
    -- scripts report this by reporting exit code 125.

-- |A usable test result, i.e. not an untestable state.
data TestResultValid wX
  = Success -- ^The test passed.
  | Failure (TestFailure wX) -- ^The test failed with the given exit code.

data TestFailure wX = TestFailure Int

exitCodeToTestResult :: ExitCode -> TestResult wX
exitCodeToTestResult ExitSuccess = Testable Success
exitCodeToTestResult (ExitFailure 125) = Untestable
exitCodeToTestResult (ExitFailure n) = Testable (Failure (TestFailure n))

-- |A 'TestCmd' runs the test on a given repository state.
data TestCmd = TestCmd (forall (wX :: *) . TestingIO wX wX (TestResult wX))

runTestCmd :: TestCmd -> TestingIO wX wX (TestResult wX)
runTestCmd (TestCmd cmd) = cmd

mkTestCmd :: (forall (wX :: *) . IO (TestResult wX)) -> TestCmd
mkTestCmd cmd = TestCmd (Testing cmd)

-- |'PatchSeq' is a sequence of patches, implemented as a binary tree,
-- balanced in an arbitrary way depending on how it happened to be constructed.
-- In the 'darcs test' implementation it is used to
-- wrap up a single patch or group of patches that might be the cause of a failure.
data PatchSeq p wX wY where
  Single :: p wX wY -> PatchSeq p wX wY
  Joined :: PatchSeq p wX wY -> PatchSeq p wY wZ -> PatchSeq p wX wZ

instance Show2 p => Show (PatchSeq p wX wY) where
  showsPrec prec (Single p) =
    showParen (prec >= 11) (showString "Darcs.UI.Commands.Test.Single " . showsPrec2 11 p)
  showsPrec prec (Joined p1 p2) =
    showParen (prec >= 11)
              (showString "Darcs.UI.Commands.Test.Joined "
                 . showsPrec2 11 p1 . showSpace . showsPrec2 11 p2)


instance Show2 p => Show1 (PatchSeq p wX) where
  showDict1 = Dict

instance Show2 p => Show2 (PatchSeq p) where
  showDict2 = Dict

instance Apply p => Apply (PatchSeq p) where
  type ApplyState (PatchSeq p) = ApplyState p
  apply (Single p) = apply p
  apply (Joined p1 p2) = apply p1 Base.>> apply p2
  unapply (Single p) = unapply p
  unapply (Joined p1 p2) = unapply p2 Base.>> unapply p1

instance PatchInspect p => PatchInspect (PatchSeq p) where
  listTouchedFiles (Single p) = listTouchedFiles p
  listTouchedFiles (Joined p1 p2) = listTouchedFiles p1 ++ listTouchedFiles p2
  hunkMatches f (Single p) = hunkMatches f p
  hunkMatches f (Joined p1 p2) = hunkMatches f p1 || hunkMatches f p2

patchTreeToFL :: PatchSeq p wX wY -> FL p wX wY
patchTreeToFL t = go t NilFL
  where
    go :: PatchSeq p wA wB -> FL p wB wC -> FL p wA wC
    go (Single p) rest = p :>: rest
    go (Joined p1 p2) rest = go p1 (go p2 rest)

flToPatchTree :: p wX wY -> FL p wY wZ -> PatchSeq p wX wZ
flToPatchTree p NilFL = Single p
flToPatchTree p (q :>: qs) = Joined (Single p) (flToPatchTree q qs)

rlToPatchTree :: RL p wX wY -> p wY wZ -> PatchSeq p wX wZ
rlToPatchTree NilRL p = Single p
rlToPatchTree (qs :<: q) p = Joined (rlToPatchTree qs q) (Single p)

-- |The result of running a test strategy.
data StrategyResultRaw patches =
    NoPasses -- ^The chosen strategy didn't find any passing states in the repository.
  | NoFailureOnHead -- ^The test didn't fail on head so there's no failure to track down.
  | Blame patches -- ^The failure was tracked down to the given patches.
  -- these two are just for oneTest
  | RunSuccess -- ^The single test run passed.
  | RunFailed Int -- ^The single test run failed with the given exit code.
  deriving (Eq, Show, Functor)

type StrategyResult p wSuccess wFailure =
  StrategyResultRaw (PatchSeq p wSuccess wFailure)

type StrategyResultSealed p =
  StrategyResultRaw (Sealed2 (PatchSeq p))

-- |'WithResult' is a continuation passed to a test strategy indicating
-- what should be done with the final result of the strategy. This for
-- example allows a post-processing "minimise blame" pass to be run.
-- The witnesses make it hard to wrap this up in a standard abstraction.
data WithResult (m :: * -> * -> * -> *) p a =
  WithResult
  { runWithResult
      :: forall wSuccess wFailure
       . StrategyResult p wSuccess wFailure
      -> m wSuccess TestingDone a
  }

-- |After a strategy has finished, untestable states might mean that it
-- was only able to assign blame to a group of patches rather than a
-- single patch. This function tries to reorder the group of patches
-- (using commutation). The hope is that a reordered sequence will reveal
-- a testable state, allowing us to cut down the group.
--
-- The type is logically
-- something like 'StrategyResult -> m StrategyResult', but is expressed
-- as a transformation of a 'WithResult' to manage the witnesses. These
-- are complicated because we want to re-use the testing tree left by the
-- strategy.
minimiseBlame :: forall m p a . TestablePatch m p => WithResult m p a -> WithResult m p a
minimiseBlame (WithResult finalRunner) =
  WithResult $ \result ->
    case result of
      Blame p -> doMinimiseFwd NilRL (patchTreeToFL p)
      _ -> finalRunner result
  where
    -- This minimisation code is a bit ad-hoc and almost certainly
    -- doesn't find every possible minimisation (which might require
    -- an exponential search). It also doesn't cache anything and
    -- therefore may do some repeated shuffling.
    
    -- The witnesses do guarantee that it is
    -- correct and the implementation is structured to guarantee
    -- termination.

    -- The overall algorithm is to work through the sequence from left
    -- to right, treating each patch in turn as a 'focus'. We then try
    -- to commute the focus with the patches to the left of it, and test
    -- each new intermediate state this produces.
    --
    -- If we do find a testable intermediate state, we can chop the sequence
    -- at that state.

    -- In 'doMinimiseFwd kept rest', 'kept' are the patches that we
    -- have looked at already, and 'rest' are the ones still to be
    -- processed.
    doMinimiseFwd
      :: RL p wSuccess wFocus
      -> FL p wFocus wFailure
      -> m wFocus TestingDone a

    doMinimiseFwd kept (focus :>: rest) = do
      -- Call 'doMinimiseRev' to work on the first of the so-far-unprocessed
      -- patches. In the end 'doMinimiseRev' will call back to 'doMinimiseFwd',
      -- and either 'focus' will have been moved into 'kept' or dropped entirely
      -- because the sequence has been cut down.
      --
      -- Whilst 'kept' marks the patches that have already been visited,
      -- 'doMinimiseRev' will still try to commute them with the 'focus' patch.
      doMinimiseRev kept (focus :>: NilFL :> NilFL :> rest)

    doMinimiseFwd (kept :<: final) NilFL = do
      -- This unapply is only needed because WithResult
      -- is based around leaving the test tree in the 'wSuccess'
      -- state in case something else needs it.
      -- In practice no more tests will be run after we finish minimising blame,
      -- so it's wasted work.
      -- It could probably be removed by making the type of WithResult
      -- more sophisticated somehow, but it's not clear the complexity
      -- is worth it.
      unapplyPatch (kept :<: final)
      finalRunner (Blame (rlToPatchTree kept final))

    doMinimiseFwd NilRL NilFL = error "internal error: trying to minimise an empty sequence"

    -- In 'doMinimiseRev tocommute (focus :> ps :> qs)':
    --   - 'qs' are the patches that are yet to be processed. They will just be sent
    --     back to 'doMinimiseFwd' unless we end up dropping them entirely.
    --   - 'ps' are patches we have managed to commute with 'focus' but still produced
    --     untestable states.
    --   - 'focus' are the patches we are trying to move around to see if it helps
    --     find a testable state. It starts out as a singleton but gains more patches
    --     as commutes fail.
    --   - 'tocommute' are the patches we still need to commute with the 'focus'.
    doMinimiseRev
      :: RL p wSuccess wFocus
      -> (FL p :> FL p :> FL p) wFocus wFailure
      -> m wFocus TestingDone a

    doMinimiseRev NilRL (focus :> ps :> qs) = do
      -- We've run out of things to commute, so pass everything that we
      -- looked at back to 'doMinimiseFwd' as the 'kept' parameter.
      let kept = reverseFL (focus +>+ ps)
      applyPatch kept
      doMinimiseFwd kept qs

    doMinimiseRev (tocommute :<: p) (focus :> ps :> qs) = do
      unapplyPatch p
      case commuterIdFL commute (p :> focus) of
        Nothing ->
          -- if we can't commute just attach it to the focus
          doMinimiseRev tocommute (p :>: focus :> ps :> qs)
        Just (focus' :> p') -> do
          applyPatch focus'
          testResult <- getCurrentTestResult
          case testResult of
            Untestable -> do
              -- The newly commuted state is also untestable, leave the patch we
              -- just commuted in 'ps' and keep working on the focus.
              unapplyPatch focus'
              doMinimiseRev tocommute (focus' :> p' :>: ps :> qs)
            -- Since we got a result, we can chop the sequence here, we just need
            -- to decide which part to keep.
            -- The full sequence after the commute is kept ; focus' | p' ; ps ; qs
            Testable Success -> doMinimiseRev NilRL (NilFL :> p' :>: ps :> qs)
            Testable (Failure _) -> do
              unapplyPatch focus'
              doMinimiseRev tocommute (focus' :> NilFL :> NilFL)

-- |StrategyDone captures the final result of running a "test strategy" like
-- bisect, backoff, linear or once. It has a slightly complicated type because of the
-- witnesses and because we may want to run a continuation afterwards to minimise
-- the result. Essentially it is just a 'StrategyResult'.
type StrategyDone m p wY = forall a . WithResult m p a ->  m wY TestingDone a

-- |Report that the strategy has finished with the given result.
strategyDone :: StrategyResult p wSuccess wFailure -> StrategyDone m p wSuccess
strategyDone result withResult = runWithResult withResult result

-- |The implementation type for a given "test strategy" like bisect, backoff, linear or once.
-- It is given a sequence of patches we might want to search inside to identify the cause of
-- a test failure, and also passed the initial testing result for the end of that sequence.
type Strategy
   = forall m p wOlder wNewer
   . TestablePatch m p
  => TestResult wNewer
  -> RL p wOlder wNewer
  -> StrategyDone m p wNewer

-- runStrategy orchestrates the whole process of isolating patches
-- triggering the failure.
runStrategy
  :: TestablePatch m p
  => O.TestStrategy
  -> O.ShrinkFailure
  -> RL p wOlder wNewer
  -> m wNewer TestingDone (StrategyResultSealed p)
runStrategy strategy shrinkFailure patches = do
  -- The starting point is a full patch sequence 'RL p wStart wEnd' with the
  -- testing tree in state 'wEnd'. We get the initial testing result for that
  -- state as 'Strategy' requires it.
  testResult <- getCurrentTestResult
  -- We narrow down the failure via a strategy (linear/bisect/backoff). If we
  -- find patches to blame, this has type 'Testing p wSuccess wFailure', leaving the testing
  -- tree in state 'wSuccess'.
  -- If the strategy is "one test" then the result is just success/failure.
  chooseStrategy strategy testResult patches $
    -- What to do with the result of the strategy is passed as a continuation to the strategy.
    -- First we try to minimise any patches to blame, resulting in 'Testing p wSuccess2 wFailure2'.
    -- The testing tree is left in state 'wSuccess2' although we don't actually care about
    -- it any more.
    (if shrinkFailure == O.ShrinkFailure then minimiseBlame else id) $
    -- Finally the result is wrapped up in a Sealed2 and returned.
    WithResult (finishedTesting . fmap Sealed2)

runTestable
  :: ( Commute p
     , TestRunner (TestingEnv m)
     , TestRunnerPatchReqs (TestingEnv m) p
     )
  => O.SetScriptsExecutable
  -> TestCmd
  -> O.TestStrategy
  -> O.ShrinkFailure
  -> RL p wStart wA
  -> m (StrategyResultSealed p)
runTestable sse tcmd strategy shrinkFailure ps =
  runTestingEnv (TestingParams sse tcmd) $ runStrategy strategy shrinkFailure ps

chooseStrategy :: O.TestStrategy -> Strategy
chooseStrategy O.Bisect = trackBisect
chooseStrategy O.Linear = trackLinear
chooseStrategy O.Backoff = trackBackoff
chooseStrategy O.Once = oneTest

explanatoryTextFor :: O.TestStrategy -> String
explanatoryTextFor strategy =
  case strategy of
    O.Bisect -> assumedMonotony
    O.Backoff -> assumedMonotony
    O.Linear -> wasLinear
    O.Once -> wasLinear -- this case won't actually be reached
  where
    -- We did a bisection type search so a given patch that causes
    -- the failure is only the most recent if there is actually only
    -- one transition from "passed" to "failed" in the repository history.
    assumedMonotony = " (assuming monotony in the given range)"
    -- We did a linear search so the patch we found is definitely the
    -- most recent to have triggered a failure.
    wasLinear = ""

-- | test only the last recorded state
oneTest :: Strategy
oneTest (Testable Success) _ = strategyDone RunSuccess
oneTest Untestable  _ = strategyDone $ RunFailed 125
oneTest (Testable (Failure (TestFailure n)))  _ = strategyDone $ RunFailed n

-- | linear search (with --linear)
trackLinear :: Strategy
trackLinear (Testable (Failure _)) ps = trackNextLinear NilFL ps
trackLinear _ _ = strategyDone NoFailureOnHead

-- |The guts of tracking down a test failure by linear search
-- Precondition: 'wZ' is a failing state and any states
-- in the (possibly empty) range of states '[wY, wZ)' are untestable.
trackNextLinear
  :: TestablePatch m p
  => FL p wY wZ -- ^a buffer of patches that start with an untestable state
  -> RL p wX wY -- ^patches we haven't visited yet
  -> StrategyDone m p wY
trackNextLinear _ NilRL withResult = strategyDone NoPasses withResult
trackNextLinear untestables (ps:<:p) withResult = do
  unapplyPatch p
  writeMsg "Trying without the patch:"
  mentionPatch p
  testResult <- getCurrentTestResult
  case testResult of
    -- If the test passes we're done.
    Testable Success -> strategyDone (Blame (flToPatchTree p untestables)) withResult
    -- If the test fails then we can drop the 'untestables' buffer and keep going.
    Testable (Failure _) -> trackNextLinear NilFL ps withResult
    -- If the state is untestable then we add to the 'untestables' buffer and keep going.
    Untestable -> trackNextLinear (p :>: untestables) ps withResult

-- |A 'TestingState' is used to keep track of the set of patches
-- a search strategy is currently working on, split at a given point
-- with an explicit witness for that intermediate point (the 'focus'),
-- so we can connect it to the state of the testing tree.
data TestingState p wOlder wFocus wNewer where
  TestingState
    :: RL (PatchSeq p) wOlder wFocus
    -> FL (PatchSeq p) wFocus wNewer
    -> TestingState p wOlder wFocus wNewer

lengthTS :: TestingState p wX wZ wY -> Int
lengthTS (TestingState ps1 ps2) = lengthRL ps1 + lengthFL ps2

lengthsTS :: TestingState p wX wZ wY -> (Int, Int)
lengthsTS (TestingState ps1 ps2) = (lengthFL ps2, lengthRL ps1)

-- |Exponential backoff search (with --backoff): first search backwards looking for
-- a successful state, then bisect between that successful state and the current (failed)
-- state.
trackBackoff :: Strategy
trackBackoff (Testable (Failure tf)) ps =
  -- 4 is an arbitrary choice for how far to start jumping backwards
  trackNextBackoff tf 4 (mapRL_RL Single ps)
trackBackoff _ _ = strategyDone NoFailureOnHead

-- |Precondition: the test fails at 'wNewer'.
trackNextBackoff
  :: TestablePatch m p
  => TestFailure wNewer -- ^Failure witness
  -> Int -- ^Number of patches to skip.
  -> RL (PatchSeq p) wOlder wNewer -- ^Patches not yet skipped.
  -> StrategyDone m p wNewer

-- Normal base case: we've run out of patches.
trackNextBackoff _ _ NilRL withResult = strategyDone NoPasses withResult

-- Edge case: if there's just one patch left then either the test
-- passes before this patch and we can blame it, or we've run out of
-- places to look for success.
trackNextBackoff _ _ (NilRL :<: p) withResult = do
  unapplyPatch p
  testResult <- getCurrentTestResult
  case testResult of
    Testable Success -> strategyDone (Blame p) withResult
    _ -> strategyDone NoPasses withResult

-- There's more than one patch to go.
trackNextBackoff tf n ahead withResult = do
  case splitAtRL n ahead of
    ahead' :> skipped' -> do
      writeMsg $ "Skipping " ++ show n ++ " patches..."++show (lengthRL skipped', lengthRL ahead')
      unapplyPatch skipped'
      -- After backing off by n more patches, look for a testable state, working through the skipped
      -- patches if necessary because the current state isn't testable.
      findTestableTowardsNewer (Failure tf) (TestingState ahead' (reverseRL skipped')) $
        \testResult (TestingState ahead'' skipped'') ->
        case testResult of
          -- Another failure, keep going. Note that it's possible that
          -- findTestableTowardsNewer will have to go all the way to the end of
          -- skipped', leaving us in the same testing position as before, but
          -- the backoff count is doubled so we'll still make progress.
          Failure tf2 -> trackNextBackoff tf2 (2*n) ahead'' withResult
          -- Found a success state, so now we can start the bisect.
          Success -> initialBisect (TestingState NilRL skipped'') withResult

-- |Given a patch sequence which has a valid test result at the end ('wNewer'),
-- try to find another point with a valid test result, starting from 'wFocus' and
-- jumping towards 'wNewer' if necessary.
findTestableTowardsNewer
  :: TestablePatch m p
  => TestResultValid wNewer
  -> TestingState p wOlder wFocus wNewer
  -> (forall wFocus2
       . TestResultValid wFocus2
      -> TestingState p wOlder wFocus2 wNewer
      -> m wFocus2 wResult a
     )
  -> m wFocus wResult a

findTestableTowardsNewer newerResult ts@(TestingState _ NilFL) cont = cont newerResult ts
findTestableTowardsNewer newerResult ts@(TestingState older (p :>: ps)) cont = do
  focusResult <- getCurrentTestResult
  case focusResult of
    Testable res -> cont res ts
    Untestable -> do
      writeMsg $ "Found untestable state " ++ show (lengthsTS ts)
      applyPatch p
      let
        -- The 'wB' state is untestable, so try to attach the patches on either side of
        -- it together into the same 'PatchSeq' so we don't try it again.
        joinT :: RL (PatchSeq p) wA wB -> PatchSeq p wB wC -> RL (PatchSeq p) wA wC
        -- If we don't have any patches on the left, we can't do anything.
        joinT NilRL x = NilRL :<: x
        -- Otherwise peel off the first patch on the left and attach it to the patch on the right.
        joinT (ys :<: y) x = ys :<: Joined y x
      moveHalfNewer (TestingState (joinT older p) ps) $ \tsNew ->
        findTestableTowardsNewer newerResult tsNew cont


-- |Binary search (with --bisect): bisect from the start of the repository.
-- This strategy is a bit dubious as the test probably doesn't actually pass
-- at the start of the repository so the hope is that at some point during the
-- bisect we will come across a passing state. The two different entry points into
-- 'initialBisect' (trackBisect and trackBackoff) also complicate the set of cases
-- we have to consider.
trackBisect :: Strategy
trackBisect (Testable (Failure _)) ps = initialBisect (TestingState (mapRL_RL Single ps) NilFL)
trackBisect _ _ = strategyDone NoFailureOnHead

-- |Progress of Bisect: current step, currently predicted total steps.
-- The total steps prediction will increase if we run into untestable states.
type BisectProgress = (Int, Int)

-- |Launch a bisect. Precondition: the test fails at 'wNewer'.
-- If called via backoff, then the test also passes at 'wOlder',
-- but there is no guarantee if bisect is called directly. 
initialBisect
  :: TestablePatch m p
  => TestingState p wOlder wFocus wNewer
  -> StrategyDone m p wFocus
initialBisect ps = trackNextBisect currProg ps
  where
    flooredLength = lengthTS ps `min` 1
    maxProg  = 1 + round ((logBase 2 $ fromIntegral flooredLength) :: Double)
    currProg = (1, maxProg) :: BisectProgress

-- |Given a testing state, work out what to do next.
-- Precondition: the test fails at 'wNewer'.
trackNextBisect
  :: forall m p wOlder wNewer wFocus
   . TestablePatch m p
  => BisectProgress
  -> TestingState p wOlder wFocus wNewer
  -> StrategyDone m p wFocus

trackNextBisect _ (TestingState NilRL NilFL) withResult = strategyDone NoPasses withResult

-- With these two cases we're down to a single patch, so either it's to blame
-- or there are no passing states found (subject to the limitations of the bisect strategy -
-- not every state was visited).
trackNextBisect _ (TestingState NilRL (p :>: NilFL)) withResult = checkAndReturnFinalBisectResult p withResult
trackNextBisect _ (TestingState (NilRL :<: p) NilFL) withResult = do
  unapplyPatch p
  checkAndReturnFinalBisectResult p withResult

-- More than one patch left. Find the middle of the TestingState and work from that.
trackNextBisect (dnow, dtotal) ps withResult = do
  writeMsg $ "Trying " ++ show dnow ++ "/" ++ show dtotal ++ " sequences..." ++ show (lengthsTS ps)
  moveToMiddle ps (\ts -> runNextBisect (dnow, dtotal) ts withResult)

-- |Once we only have one patch left in bisect, we need to check that the test passes before the patch.
-- This is not guaranteed when bisect is called directly from the command-line. If we changed the UI to
-- ensure that bisect was only launched with both a passing and a failing state, we could strengthen
-- the precondition of 'initialBisect' and things it calls, and this function would be unnecessary.
-- Precondition: the test fails at 'wNewer'.
checkAndReturnFinalBisectResult
  :: TestablePatch m p
  => PatchSeq p wOlder wNewer
  -> StrategyDone m p wOlder
checkAndReturnFinalBisectResult p withResult = do
  testResult <- getCurrentTestResult
  case testResult of
    Testable Success -> strategyDone (Blame p) withResult
    _ -> strategyDone NoPasses withResult

-- |The guts of bisection. Normally it will be passed an evenly split
-- 'TestingState older newer' with the focus in the middle, but if we find an
-- untestable state then we will start jumping around to find something testable.
-- Preconditions: 'older' is non-empty; the test fails at wNewer.
runNextBisect
  :: forall m p wOlder wNewer wFocus
   . TestablePatch m p
  => BisectProgress
  -> TestingState p wOlder wFocus wNewer
  -> StrategyDone m p wFocus
runNextBisect (dnow, dtotal) (TestingState older newer) withResult = do
  testResult <- getCurrentTestResult
  case testResult of

    -- The standard case for bisect: we have a result for the focus and we use it to pick
    -- either the left or right half.
    Testable result -> do
      let doNext newState = trackNextBisect (dnow+1, dtotal) newState withResult
      case result of
        Success   -> doNext (TestingState NilRL newer) -- continue left  (to the present)
        Failure _ -> doNext (TestingState older NilFL) -- continue right (to the past)

    -- If we couldn't test the bisect state then we need to move around to try to find
    -- a testable state.
    Untestable -> do
      writeMsg $ "Found untestable state " ++ show (lengthsTS (TestingState older newer))
      case (older, newer) of
        (NilRL, _) -> error "internal error: older bisect state reached 0 patches (runNextBisect)"
        -- Although 'newer' can become empty, the precondition that the test fails at wNewer means
        -- we shouldn't get here.
        -- TODO the user might supply an unreliable test script, maybe we should deal with the NilFL
        -- case before running the test.
        (_, NilFL) -> error "internal error: newer bisect state reached 0 patches (runNextBisect)"
        (older' :<: p1, p2 :>: newer') -> do
          applyPatch p2
          moveHalfNewer (TestingState (older' :<: Joined p1 p2) newer') $
            \ts -> runNextBisect (dnow+1, dtotal+1) ts withResult

-- |Given a 'TestingState older newer', move the focus to the middle of 'newer',
-- updating the testing tree to match, and call the given continuation.
moveHalfNewer
  :: forall m p wOlder wNewer wFocus wResult a
   . TestablePatch m p
  => TestingState p wOlder wFocus wNewer
  -> (forall wFocus2 . TestingState p wOlder wFocus2 wNewer -> m wFocus2 wResult a)
  -> m wFocus wResult a

moveHalfNewer (TestingState older newer) f = doMove older (lengthFL newer `div` 2, newer)
  where
    doMove
      :: forall wFocus2
       . RL (PatchSeq p) wOlder wFocus2
      -> (Int, FL (PatchSeq p) wFocus2 wNewer)
      -> m wFocus2 wResult a

    doMove ps1 (0, ps2) = f (TestingState ps1 ps2)
    doMove _ (_, NilFL) = error "impossible: exhausted newer patches (moveHalfNewer)"
    doMove ps1 (n, p :>: ps2) = do
      applyPatch p
      doMove (ps1 :<: p) (n-1, ps2)

-- |Given a 'TestingState older newer', move the focus to the middle of
-- 'older +>+ newer', updating the testing tree to match, and call the given
-- continuation.
moveToMiddle
  :: forall m p wOlder wNewer wFocus wResult a
   . TestablePatch m p
  => TestingState p wOlder wFocus wNewer
  -> (forall wFocus2 . TestingState p wOlder wFocus2 wNewer -> m wFocus2 wResult a)
  -> m wFocus wResult a

moveToMiddle (TestingState older newer) f = doMove (lengthRL older, older) (lengthFL newer, newer)
  where
    doMove
      :: forall wFocus2
       . (Int, RL (PatchSeq p) wOlder wFocus2)
      -> (Int, FL (PatchSeq p) wFocus2 wNewer)
      -> m wFocus2 wResult a

    doMove (len1, ps1) (len2, ps2) | abs (len1 - len2) <= 1 = f (TestingState ps1 ps2)

    doMove (len1, ps1 :<: p1) (len2, ps2) | len1 > len2 = do
      unapplyPatch p1
      doMove (len1-1, ps1) (len2+1, p1 :>: ps2)

    doMove (len1, ps1) (len2, p2 :>: ps2) = do -- len2 > len1
      applyPatch p2
      doMove (len1+1, ps1 :<: p2) (len2-1, ps2)

    -- these cases should only be reachable if the lengths get out of sync
    doMove (_, NilRL) _ = error "internal error: right bisect state reached 0 patches (moveToMiddle)"
    doMove _ (_, NilFL) = error "internal error: left bisect state reached 0 patches (moveToMiddle)"