@@ -129,3 +129,97 @@ spec = do
129129 res `shouldBe` [[True ]]
130130 Just (Clean res) <- lookup (newKey theKey) <$> getDatabaseValues theDb
131131 resultDeps res `shouldBe` UnknownDeps
132+
133+ describe " Discard superseded computations" $ do
134+ it " leaves a key dirty when a restart bumps the step mid-computation" $ do
135+ started <- C. newEmptyMVar
136+ proceed <- C. newEmptyMVar
137+ done <- C. newEmptyMVar
138+ db@ (ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $
139+ addRule $ \ (Rule :: Rule () ) _old _mode -> do
140+ liftIO $ C. putMVar started ()
141+ liftIO $ C. takeMVar proceed
142+ return $ RunResult ChangedRecomputeDiff " " () (return () )
143+ -- Fork so a restart can bump the step while the rule is still computing.
144+ _ <- C. forkIO $ shakeRunDatabase db [apply1 (Rule @ () )] >>= C. putMVar done
145+ C. takeMVar started
146+ -- Bumps the step without dirtying anything, so only the guard can leave
147+ -- this key dirty.
148+ incDatabase theDb (Just [] )
149+ C. putMVar proceed ()
150+ _ <- C. takeMVar done
151+ Just status <- lookup (newKey (Rule @ () )) <$> getDatabaseValues theDb
152+ case status of
153+ Dirty {} -> pure ()
154+ Clean {} -> expectationFailure " superseded computation was committed clean"
155+ Running {} -> expectationFailure " superseded computation left running"
156+ it " commits clean when the step does not advance" $ do
157+ db@ (ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $
158+ addRule $ \ (Rule :: Rule () ) _old _mode ->
159+ return $ RunResult ChangedRecomputeDiff " " () (return () )
160+ _ <- shakeRunDatabase db [apply1 (Rule @ () )]
161+ Just status <- lookup (newKey (Rule @ () )) <$> getDatabaseValues theDb
162+ case status of
163+ Clean {} -> pure ()
164+ _ -> expectationFailure " expected a clean commit"
165+ it " leaves a newer build's Running intact instead of stomping it" $
166+ withSupersededRespawn $ \ theDb proceedA doneA proceedB doneB -> do
167+ -- A finishes while B is still Running{2}. Guard must keep B's Running.
168+ C. putMVar proceedA ()
169+ _ <- C. takeMVar doneA
170+ status <- lookup (newKey (Rule @ () )) <$> getDatabaseValues theDb
171+ -- Release B before asserting so its thread finishes.
172+ C. putMVar proceedB ()
173+ _ <- C. takeMVar doneB
174+ case status of
175+ Just Running {} -> pure ()
176+ Just Dirty {} -> expectationFailure " superseded build unnecessary marked dirty"
177+ Just Clean {} -> expectationFailure " newer build committed too early"
178+ Nothing -> expectationFailure " key missing from the database"
179+ it " leaves a newer build's committed Clean intact instead of dirtying it" $
180+ withSupersededRespawn $ \ theDb proceedA doneA proceedB doneB -> do
181+ -- B commits Clean{2} before A demotes. Guard must keep B's Clean.
182+ C. putMVar proceedB ()
183+ _ <- C. takeMVar doneB
184+ C. putMVar proceedA ()
185+ _ <- C. takeMVar doneA
186+ status <- lookup (newKey (Rule @ () )) <$> getDatabaseValues theDb
187+ case status of
188+ Just Clean {} -> pure ()
189+ Just Dirty {} -> expectationFailure " superseded build unnecessary marked dirty"
190+ Just Running {} -> expectationFailure " newer build didn't commit"
191+ Nothing -> expectationFailure " key missing from the database"
192+ where
193+ -- Two builds of the same key.
194+ --
195+ -- 1. A, the superseded build, runs at step 1.
196+ -- 2. B, the re-spawn, runs at step 2. B's shakeRunDatabase bumps the step
197+ -- and re-dirties A's in-flight key, so the rule runs again and leaves B
198+ -- Running{2}.
199+ --
200+ -- Both are started and blocked before the continuation runs. The
201+ -- continuation picks the release ordering that decides whether the guard
202+ -- meets B as Running or as Clean.
203+ withSupersededRespawn
204+ :: (Database -> MVar () -> MVar () -> MVar () -> MVar () -> IO () )
205+ -> IO ()
206+ withSupersededRespawn k = do
207+ calls <- newTVarIO (0 :: Int )
208+ startedA <- C. newEmptyMVar
209+ proceedA <- C. newEmptyMVar
210+ startedB <- C. newEmptyMVar
211+ proceedB <- C. newEmptyMVar
212+ doneA <- C. newEmptyMVar
213+ doneB <- C. newEmptyMVar
214+ db@ (ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $
215+ addRule $ \ (Rule :: Rule () ) _old _mode -> do
216+ n <- liftIO $ atomically $ modifyTVar' calls (+ 1 ) >> readTVar calls
217+ liftIO $ if n == 1
218+ then C. putMVar startedA () >> C. takeMVar proceedA
219+ else C. putMVar startedB () >> C. takeMVar proceedB
220+ return $ RunResult ChangedRecomputeDiff " " () (return () )
221+ _ <- C. forkIO $ shakeRunDatabase db [apply1 (Rule @ () )] >> C. putMVar doneA ()
222+ C. takeMVar startedA
223+ _ <- C. forkIO $ shakeRunDatabase db [apply1 (Rule @ () )] >> C. putMVar doneB ()
224+ C. takeMVar startedB
225+ k theDb proceedA doneA proceedB doneB
0 commit comments