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)"