{-# LANGUAGE RecordWildCards, TupleSections #-} -- | Define a continuous integration system. module Development.Bake.Server.Brain( Memory(..), expire, Question(..), Answer(..), Ping(..), ClientInfo(..), prod ) where import Development.Bake.Core.Run import Development.Bake.Core.Type import Development.Bake.Core.Message import General.Extra import General.BigString import Data.Tuple.Extra import Data.Maybe import Data.Monoid import Control.Monad.Extra import Data.List.Extra import qualified Data.Map as Map import qualified Data.Set as Set import Development.Bake.Server.Store import Control.Exception.Extra import General.HTML import Prelude import Development.Bake.Server.Memory import Development.Bake.Server.Property -- any question that has been asked of a client who hasn't pinged since the time is thrown away expire :: UTCTime -> Memory -> Memory expire cutoff s | null died = s | otherwise = s{running = filter (flip notElem died . qClient . snd) $ running s ,clients = Map.map (\ci@ClientInfo{..} -> ci{ciAlive = ciAlive && pClient ciPing `notElem` died}) $ clients s} where died = [pClient ciPing | ClientInfo{..} <- Map.elems $ clients s, ciPingTime < cutoff, ciAlive] prod :: Memory -> Message -> IO (Memory, Maybe (Either String Question)) prod mem msg = safely $ do res <- update mem msg case res of Left err -> return (mem, Just $ Left err) Right mem -> do mem <- reacts mem case msg of Pinged p | null $ fatal mem, Just q <- output (ovenTestInfo $ oven mem) mem p -> case () of -- we still test things on the skip list when testing on a state (to get some feedback) _ | Just t <- qTest q, snd (qCandidate q) /= [], Just reason <- Map.lookup t (storeSkip $ store mem) -> do prod mem $ Finished q $ Answer (bigStringFromString $ "Skipped due to being on the skip list\n" ++ reason) Nothing [] True _ -> do now <- getCurrentTime return (mem{running = (now,q) : running mem}, Just $ Right q) _ -> return (mem, Nothing) where safely x = do res <- try_ x case res of Left e -> return (mem{fatal = show e : fatal mem}, Nothing) Right v -> return v reacts :: Memory -> IO Memory reacts = f 10 where f 0 mem = return mem{fatal = "React got into a loop" : fatal mem} f i mem | null $ fatal mem, Just mem <- react mem = f (i-1) =<< mem | otherwise = return mem failingTestOutput :: Store -> Point -> Maybe Test -> Maybe String failingTestOutput store (state, patch) test = listToMaybe $ catMaybes [ storeRunFile store runid | (runid, _, _, Answer{aSuccess=False}) <- storeRunList store Nothing (Just test) (Just state) patch Nothing] react :: Memory -> Maybe (IO Memory) react mem@Memory{..} | xs <- rejectable mem , xs@(_:_) <- filter (\(p,t) -> t `Map.notMember` maybe Map.empty snd (paReject $ storePatch store p)) xs = Just $ do let fresh = filter (isNothing . paReject . storePatch store . fst) xs let point p = (fst active, takeWhile (/= p) (snd active) ++ [p]) bad <- if fresh == [] then return id else do -- only notify on the first rejectable test for each patch Shower{..} <- shower mem False notify mem "Rejected" [ (paAuthor,) $ do showPatch p <> str_ " submitted by " <> str_ paAuthor <> str_ " at " <> showTime paQueued str_ " rejected due to " <> showTestAt (point p) t whenJust (failingTestOutput store (point p) t) $ \s -> br_ <> br_ <> pre_ (summary s) | (p,t) <- nubOrdOn fst xs, let PatchInfo{..} = storePatch store p] store <- storeUpdate store [IUReject p t (point p) | (p,t) <- xs] return $ bad mem{store = store} | plausible mem , xs@(_:_) <- filter (isNothing . paPlausible . storePatch store) $ snd active = Just $ do Shower{..} <- shower mem False -- don't notify people twice in quick succession bad <- if mergeable mem then return id else notify mem "Plausible" [ (paAuthor, showPatch p <> str_ " submitted by " <> str_ paAuthor <> str_ " at " <> showTime paQueued <> str_ " is now plausible") | p <- xs, let PatchInfo{..} = storePatch store p] store <- storeUpdate store $ map IUPlausible xs return $ bad mem{store = store} | mergeable mem , not $ null $ snd active = Just $ do (s, answer) <- if not simulated then uncurry runUpdate active else do s <- ovenUpdate oven (fst active) (snd active) return (Just s, Answer mempty (Just 0) mempty True) case s of Nothing -> do return mem{fatal = ("Failed to update\n" ++ bigStringToString (aStdout answer)) : fatal} Just s -> do Shower{..} <- shower mem False bad <- notify mem "Merged" [ (paAuthor, showPatch p <> str_ " submitted by " <> str_ paAuthor <> str_ " at " <> showTime paQueued <> str_ " is now merged") | p <- snd active, let PatchInfo{..} = storePatch store p] store <- storeUpdate store $ IUState s answer (Just active) : map IUMerge (snd active) return $ bad mem{active = (s, []), store = store} | restrictActive mem , (reject@(_:_), keep) <- partition (isJust . paReject . storePatch store) $ snd active = Just $ do return mem{active = (fst active, keep)} | not paused , extendActive mem , add@(_:_) <- Set.toList $ storeAlive store `Set.difference` Set.fromList (snd active) = Just $ do add <- return $ sortOn (paQueued . storePatch store) add store <- storeUpdate store $ map IUStart add return mem {active = (fst active, snd active ++ add) ,store = store} | otherwise = Nothing update :: Memory -> Message -> IO (Either String Memory) update mem _ | fatal mem /= [] = return $ Right mem update mem@Memory{..} (AddPatch author p) = if storeIsPatch store p then return $ Left "patch has already been submitted" else do let queued = storeAlive store `Set.difference` Set.fromList (snd active) supersede = filter (\old -> ovenSupersede oven old p) $ Set.toList queued store <- storeUpdate store $ IUQueue p author : map IUSupersede supersede return $ Right mem{store = store} update mem@Memory{..} (DelPatch p) = if not $ p `Set.member` storeAlive store then return $ Left "patch is already dead or not known" else do store <- storeUpdate store [IUDelete p] return $ Right mem{store = store, active = second (delete p) active} update mem@Memory{..} (SetState author s) = if fst active == s then return $ Left "state is already at that value" else do store <- storeUpdate store [IUState s (Answer (bigStringFromString $ "From SetState by " ++ author) Nothing [] True) Nothing] return $ Right mem{store = store, active = (s, snd active)} update mem@Memory{..} Requeue = do let add = Set.toList $ storeAlive store `Set.difference` Set.fromList (snd active) add <- return $ sortOn (paQueued . storePatch store) add store <- storeUpdate store $ map IUStart add return $ Right mem {active = (fst active, snd active ++ add) ,store = store} update mem@Memory{..} Pause | paused = return $ Left "already paused" | otherwise = return $ Right mem{paused = True} update mem@Memory{..} Unpause | not paused = return $ Left "already unpaused" | otherwise = return $ Right mem{paused = False} update mem@Memory{..} (Pinged ping) = do now <- getCurrentTime return $ Right mem{clients = Map.alter (Just . ClientInfo now ping True . maybe Map.empty ciTests) (pClient ping) clients} update mem@Memory{..} (AddSkip author test) | test `Map.member` storeSkip store = return $ Left "already skipped" | otherwise = do store <- storeUpdate store [SUAdd test author] return $ Right mem{store = store} update mem@Memory{..} (DelSkip test) | test `Map.notMember` storeSkip store = return $ Left "already not skipped" | otherwise = do store <- storeUpdate store [SUDel test] return $ Right mem{store = store} update mem@Memory{..} (Finished q@Question{..} a@Answer{..}) = do bad <- case () of _ | snd qCandidate == [] -- on a state , not aSuccess , let skip = Set.mapMonotonic Just $ Map.keysSet $ storeSkip store , qTest `Set.notMember` skip -- not on the skip list , let failed = poFail $ storePoint store qCandidate , failed `Set.isSubsetOf` skip -- no notifications already -> do Shower{..} <- shower mem False notifyAdmins mem "State failure" $ do str_ "State " <> showState (fst qCandidate) str_ " failed due to " <> showTestAt qCandidate qTest <> br_ <> br_ pre_ (bigStringWithString aStdout summary) _ -> return id case () of _ | qTest == Nothing , Left bad <- validTests (ovenTestInfo oven) aTests -> fail bad _ -> return () now <- getCurrentTime let (eq,neq) = partition ((==) q . snd) running let time = head $ map fst eq ++ [now] store <- storeUpdate store [PURun time q a] let add ci = ci{ciTests = Map.insertWith (&&) (qCandidate, qTest) aSuccess $ ciTests ci} return $ Right $ bad mem {store = store ,clients = Map.adjust add qClient clients ,running = neq} -- | Given a state, figure out what you should do next. output :: (Test -> TestInfo Test) -> Memory -> Ping -> Maybe Question output info mem Ping{..} | pNowThreads == 0 = Nothing {- 1) try anyone who failed in active by bisecting 2) anyone not done in active or a superset 3) anyone not done in active -} output info mem@Memory{..} Ping{..} | False, pNowThreads == pMaxThreads, isNothing res = error $ show (enoughThreads $ listToMaybe $ filter suitable $ nubOrd $ concatMap dependencies $ bad ++ good, filter suitable good, concatMap dependencies $ bad ++ good, bad, good) | otherwise = res where res = fmap question $ enoughThreads $ listToMaybe $ filter suitable $ nubOrd $ concatMap dependencies $ bad ++ good self = storePoint store active failedSelf = Set.toList $ poFail self failedPrefix = Map.fromListWith mappend $ [ (t, case poTest po t of Just True -> ([i],[]); Just False -> ([],[i]); Nothing -> ([],[])) | (i, ps) <- zip [1..] $ tail $ inits $ snd active, let po = storePoint store (fst active, ps) , t <- failedSelf] bad = -- trace ("bisecting: " ++ show failedSelf) $ [(i, t) | (t,(pass,fail)) <- Map.toList failedPrefix -- assume 0 passed, so add to pass and delete from fail, -- ensures we never try and "blame" 0 (which we can't reject) , i <- bisect (0:pass) $ filter (/= 0) $ length (snd active):fail] setAddNothing = Set.insert Nothing . Set.mapMonotonic Just tests = setAddNothing $ fromMaybe Set.empty $ poTodo self doneSelf = poPass self `Set.union` poFail self passSuper = setAddNothing $ storeSupersetPass store active good = let (pri2,pri1) = partition (`Set.member` passSuper) $ sortOn (maybe 0 $ negate . testPriority . info) $ Set.toList $ tests `Set.difference` doneSelf in map (length $ snd active,) $ pri1 ++ pri2 dependencies :: (Int, Maybe Test) -> [(Int, Maybe Test)] dependencies (i, t) = map (i,) $ flip transitiveClosure [t] $ \t -> case t of Nothing -> [] Just t -> Nothing : map Just (testDepend $ info t) histDone = ciTests $ clients Map.! pClient histStarted = Map.keysSet histDone `Set.union` Set.fromList [(qCandidate, qTest) | (_,Question{..}) <- running, qClient == pClient] threadsForTest = fromMaybe pMaxThreads . testThreads . info -- if there are not enough threads, don't do anything else, just wait for threads to become available enoughThreads :: Maybe (Int, Maybe Test) -> Maybe (Int, Maybe Test) enoughThreads (Just (i, t)) | pNowThreads >= maybe 1 threadsForTest t = Just (i, t) enoughThreads _ = Nothing unprefix i = second (take i) active suitable :: (Int, Maybe Test) -> Bool suitable (i, Nothing) | (unprefix i,Nothing) `Set.notMember` histStarted -- I have not done it = True suitable (i,Just t) | (unprefix i,Just t) `Set.notMember` histStarted -- I have not done it , Map.lookup (unprefix i,Nothing) histDone == Just True -- I have prepared , Just ts <- poTodo $ storePoint store (unprefix i) , t `Set.member` ts -- this test is relevant to this patch , all (`elem` pProvide) $ testRequire $ info t -- I can do this test , all (\t -> Map.lookup (unprefix i, Just t) histDone == Just True) $ testDepend $ info t -- I have done all the dependencies = True suitable _ = False question (i, t) = Question (second (take i) active) t (maybe 1 threadsForTest t) pClient -- | Given the passes, and the fails, suggest what you would like to try next bisect :: [Int] -> [Int] -> [Int] bisect pass fail | Just fail <- if null fail then Nothing else Just $ minimum fail , pass <- filter (< fail) pass , Just pass <- if null pass then Nothing else Just $ maximum pass = if fail - pass == 4 then [pass+2, pass+1, pass+3] else if fail - pass <= 3 then [pass+1 .. fail-1] else [(pass + fail) `div` 2] bisect _ _ = []