11
2- {-# LANGUAGE GADTs #-}
2+ {-# LANGUAGE DataKinds #-}
3+ {-# LANGUAGE GADTs #-}
34
45module CradleTests (tests ) where
56
6- import Config (checkDefs , mkL ,
7+ import Config (checkDefs , dummyPlugin ,
8+ lspTestCaps , mkIdeTestFs , mkL ,
79 runWithExtraFiles ,
810 testWithDummyPluginEmpty' )
911import Control.Applicative.Combinators
1012import Control.Lens ((^.) )
1113import Control.Monad.IO.Class (liftIO )
14+ import qualified Data.Aeson as A
15+ import Data.Proxy (Proxy (.. ))
1216import qualified Data.Text as T
1317import Development.IDE.GHC.Util
14- import Development.IDE.Plugin.Test (WaitForIdeRuleResult (.. ))
18+ import Development.IDE.Plugin.Test (TestRequest (.. ),
19+ WaitForIdeRuleResult (.. ))
1520import Development.IDE.Test (expectDiagnostics ,
1621 expectDiagnosticsWithTags ,
1722 expectNoMoreDiagnostics ,
1823 isReferenceReady ,
1924 waitForAction )
2025import Development.IDE.Types.Location
2126import GHC.TypeLits (symbolVal )
27+ import Ide.Types (Config (.. ),
28+ SessionLoadingPreferenceConfig (.. ))
2229import qualified Language.LSP.Protocol.Lens as L
2330import Language.LSP.Protocol.Message
2431import Language.LSP.Protocol.Types hiding
@@ -28,7 +35,9 @@ import Language.LSP.Protocol.Types hiding
2835 mkRange )
2936import Language.LSP.Test
3037import System.FilePath
31- import System.IO.Extra hiding (withTempDir )
38+ import Test.Hls (TestConfig (.. ), def ,
39+ runSessionWithTestConfig ,
40+ waitForBuildQueue )
3241import Test.Hls.FileSystem
3342import Test.Hls.Util (EnvSpec (.. ), OS (.. ),
3443 ignoreInEnv )
@@ -41,6 +50,7 @@ tests = testGroup "cradle"
4150 [testGroup " dependencies" [sessionDepsArePickedUp]
4251 ,testGroup " ignore-fatal" [ignoreFatalWarning]
4352 ,testGroup " loading" [loadCradleOnlyonce, retryFailedCradle]
53+ ,testGroup " regression.batch" batchLoadRegressionTests
4454 ,testGroup " multi" (multiTests " multi" )
4555 ,testGroup " multi-unit" (multiTests " multi-unit" )
4656 ,testGroup " sub-directory" [simpleSubDirectoryTest]
@@ -174,6 +184,218 @@ simpleMultiTest3 variant =
174184 checkDefs locs (pure [fooL])
175185 expectNoMoreDiagnostics 0.5
176186
187+ runRegressionMultiOpenAThenB :: FilePath -> Session ()
188+ runRegressionMultiOpenAThenB dir = do
189+ let aPath = dir </> " a/A.hs"
190+ bPath = dir </> " b/B.hs"
191+ adoc <- openDoc aPath " haskell"
192+ bdoc <- openDoc bPath " haskell"
193+ _ <- waitForBuildQueue
194+ [aRes, bRes] <- waitForTypeChecksBatched [adoc, bdoc]
195+ liftIO $ assertBool " A should typecheck" (ideResultSuccess aRes)
196+ liftIO $ assertBool " B should typecheck" (ideResultSuccess bRes)
197+ locs <- getDefinitions bdoc (Position 2 7 )
198+ let fooL = mkL (adoc ^. L. uri) 2 0 2 3
199+ checkDefs locs (pure [fooL])
200+ expectNoMoreDiagnostics 0.5
201+
202+ runRegressionMultiOpenBThenA :: FilePath -> Session ()
203+ runRegressionMultiOpenBThenA dir = do
204+ let aPath = dir </> " a/A.hs"
205+ bPath = dir </> " b/B.hs"
206+ bdoc <- openDoc bPath " haskell"
207+ adoc <- openDoc aPath " haskell"
208+ _ <- waitForBuildQueue
209+ [bRes, aRes] <- waitForTypeChecksBatched [bdoc, adoc]
210+ liftIO $ assertBool " B should typecheck" (ideResultSuccess bRes)
211+ liftIO $ assertBool " A should typecheck" (ideResultSuccess aRes)
212+ locs <- getDefinitions bdoc (Position 2 7 )
213+ let TextDocumentIdentifier auri = adoc
214+ let fooL = mkL auri 2 0 2 3
215+ checkDefs locs (pure [fooL])
216+ expectNoMoreDiagnostics 0.5
217+
218+ runRegressionMultiOpenBThenAThenC :: FilePath -> Session ()
219+ runRegressionMultiOpenBThenAThenC dir = do
220+ let aPath = dir </> " a/A.hs"
221+ bPath = dir </> " b/B.hs"
222+ cPath = dir </> " c/C.hs"
223+ bdoc <- openDoc bPath " haskell"
224+ adoc <- openDoc aPath " haskell"
225+ cdoc <- openDoc cPath " haskell"
226+ _ <- waitForBuildQueue
227+ [bRes, aRes, cRes] <- waitForTypeChecksBatched [bdoc, adoc, cdoc]
228+ liftIO $ assertBool " B should typecheck" (ideResultSuccess bRes)
229+ liftIO $ assertBool " A should typecheck" (ideResultSuccess aRes)
230+ liftIO $ assertBool " C should typecheck" (ideResultSuccess cRes)
231+ locs <- getDefinitions cdoc (Position 2 7 )
232+ let TextDocumentIdentifier auri = adoc
233+ let fooL = mkL auri 2 0 2 3
234+ checkDefs locs (pure [fooL])
235+ expectNoMoreDiagnostics 0.5
236+
237+ sendTestRequest :: TestRequest -> Session A. Value
238+ sendTestRequest req = do
239+ let method = SMethod_CustomMethod (Proxy @ " test" )
240+ reqId <- sendRequest method (A. toJSON req)
241+ TResponseMessage {_result} <- skipManyTill anyMessage $ responseForId method reqId
242+ case _result of
243+ Left err -> liftIO (assertFailure $ " test plugin request failed: " <> show err) >> pure A. Null
244+ Right val -> pure val
245+
246+ waitForTypeChecksBatched :: [TextDocumentIdentifier ] -> Session [WaitForIdeRuleResult ]
247+ waitForTypeChecksBatched docs = do
248+ let uris = map (\ TextDocumentIdentifier {_uri} -> _uri) docs
249+ val <- sendTestRequest (WaitForIdeRules " TypeCheck" uris)
250+ case A. fromJSON val of
251+ A. Success res -> pure res
252+ A. Error parseErr -> liftIO (assertFailure $ " batched typecheck parse failed: " <> parseErr) >> pure []
253+
254+ batchLoadRegressionTests :: [TestTree ]
255+ batchLoadRegressionTests =
256+ -- Note [Batch regression scheduling semantics]
257+ -- `didOpen` alone does not enqueue session-loader pending files.
258+ -- Pending entries come from GhcSession demand. For these tests, the `test`
259+ -- plugin uses `WaitForIdeRules` plus a pending-size barrier in session-loader
260+ -- to force all requested files into pending before load begins.
261+ [ testCase " m1-open-a-then-b-batch-pending-and-success" $
262+ runWithExtraFilesMultiComponent " multi" runRegressionMultiOpenAThenB
263+ , testCase " m2-open-b-then-a-batch-pending-and-success" $
264+ runWithExtraFilesMultiComponent " multi" runRegressionMultiOpenBThenA
265+ , testCase " m3-open-b-then-a-then-c-batch-pending-and-success" $
266+ runWithExtraFilesMultiComponent " multi" runRegressionMultiOpenBThenAThenC
267+ , testCase " f1-batch-pending-failure-isolates-broken-file" $
268+ runWithExtraFilesMultiComponent " multi" regressionBatchFailureIsolatesBrokenFile
269+ , testCase " f2-failed-file-keeps-failing-until-cradle-fix" $
270+ runWithExtraFilesMultiComponent " multi" regressionFailedFileKeepsFailingUntilFix
271+ , testCase " r1-failed-file-recovers-after-cradle-fix" $
272+ runWithExtraFilesMultiComponent " multi" regressionFailedFileRecoversAfterFix
273+ , testCase " s1-no-stale-outcomes-across-restart-paths" $
274+ runWithExtraFilesMultiComponent " multi" regressionNoStaleOutcomesOnRestart
275+ ]
276+
277+ runWithExtraFilesMultiComponent :: String -> (FilePath -> Session a ) -> IO a
278+ runWithExtraFilesMultiComponent dirName action = do
279+ let vfs = mkIdeTestFs [copyDir dirName]
280+ lspConfig :: Config
281+ lspConfig = def { sessionLoading = PreferMultiComponentLoading }
282+ conf :: TestConfig ()
283+ conf = def
284+ { testPluginDescriptor = dummyPlugin
285+ , testDirLocation = Right vfs
286+ , testConfigCaps = lspTestCaps
287+ , testShiftRoot = True
288+ , testDisableKick = True
289+ , testLspConfig = lspConfig
290+ }
291+ runSessionWithTestConfig conf action
292+
293+ brokenMultiHieYaml :: T. Text
294+ brokenMultiHieYaml = T. unlines
295+ [ " cradle:"
296+ , " cabal:"
297+ , " - path: \" ./a\" "
298+ , " component: \" lib:a\" "
299+ , " - path: \" ./b\" "
300+ , " component: \" lib:does-not-exist\" "
301+ , " - path: \" ./c\" "
302+ , " component: \" lib:c\" "
303+ ]
304+
305+ writeBrokenMultiHieYaml :: FilePath -> Session ()
306+ writeBrokenMultiHieYaml dir =
307+ liftIO $ atomicFileWriteStringUTF8 (dir </> " hie.yaml" ) (T. unpack brokenMultiHieYaml)
308+
309+ notifyHieYamlChanged :: FilePath -> Session ()
310+ notifyHieYamlChanged dir =
311+ sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams
312+ [FileEvent (filePathToUri $ dir </> " hie.yaml" ) FileChangeType_Changed ]
313+
314+ assertTypeCheckSuccess :: TextDocumentIdentifier -> String -> Session ()
315+ assertTypeCheckSuccess doc msg = do
316+ WaitForIdeRuleResult {.. } <- waitForAction " TypeCheck" doc
317+ liftIO $ assertBool msg ideResultSuccess
318+
319+ assertTypeCheckFailure :: TextDocumentIdentifier -> String -> Session ()
320+ assertTypeCheckFailure doc msg = do
321+ WaitForIdeRuleResult {.. } <- waitForAction " TypeCheck" doc
322+ liftIO $ assertBool msg (not ideResultSuccess)
323+
324+ regressionBatchFailureIsolatesBrokenFile :: FilePath -> Session ()
325+ regressionBatchFailureIsolatesBrokenFile dir = do
326+ writeBrokenMultiHieYaml dir
327+ let aPath = dir </> " a/A.hs"
328+ bPath = dir </> " b/B.hs"
329+ adoc <- openDoc aPath " haskell"
330+ bdoc <- openDoc bPath " haskell"
331+ _ <- waitForBuildQueue
332+ [aRes, bRes] <- waitForTypeChecksBatched [adoc, bdoc]
333+ liftIO $ assertBool " A should typecheck when B cradle mapping is broken" (ideResultSuccess aRes)
334+ liftIO $ assertBool " B should fail with a broken cradle mapping" (not $ ideResultSuccess bRes)
335+
336+ regressionFailedFileKeepsFailingUntilFix :: FilePath -> Session ()
337+ regressionFailedFileKeepsFailingUntilFix dir = do
338+ writeBrokenMultiHieYaml dir
339+ let aPath = dir </> " a/A.hs"
340+ bPath = dir </> " b/B.hs"
341+ cPath = dir </> " c/C.hs"
342+ bdoc <- openDoc bPath " haskell"
343+ assertTypeCheckFailure bdoc " B should fail with broken cradle mapping"
344+
345+ bSource <- liftIO $ readFileUtf8 bPath
346+ changeDoc bdoc
347+ [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ bSource <> " \n " ]
348+ assertTypeCheckFailure bdoc " B should keep failing until the cradle is fixed"
349+
350+ adoc <- openDoc aPath " haskell"
351+ cdoc <- openDoc cPath " haskell"
352+ assertTypeCheckSuccess adoc " A should still typecheck while B remains broken"
353+ assertTypeCheckSuccess cdoc " C should still typecheck while B remains broken"
354+
355+ regressionFailedFileRecoversAfterFix :: FilePath -> Session ()
356+ regressionFailedFileRecoversAfterFix dir = do
357+ let hiePath = dir </> " hie.yaml"
358+ bPath = dir </> " b/B.hs"
359+ validHie <- liftIO $ readFileUtf8 hiePath
360+ writeBrokenMultiHieYaml dir
361+
362+ bdoc <- openDoc bPath " haskell"
363+ assertTypeCheckFailure bdoc " B should fail before fixing the cradle"
364+
365+ liftIO $ atomicFileWriteStringUTF8 hiePath (T. unpack validHie)
366+ notifyHieYamlChanged dir
367+
368+ bSource <- liftIO $ readFileUtf8 bPath
369+ changeDoc bdoc
370+ [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ bSource <> " \n " ]
371+ assertTypeCheckSuccess bdoc " B should recover after restoring the cradle"
372+
373+ regressionNoStaleOutcomesOnRestart :: FilePath -> Session ()
374+ regressionNoStaleOutcomesOnRestart dir = do
375+ let hiePath = dir </> " hie.yaml"
376+ aPath = dir </> " a/A.hs"
377+ bPath = dir </> " b/B.hs"
378+ cPath = dir </> " c/C.hs"
379+ validHie <- liftIO $ readFileUtf8 hiePath
380+ writeBrokenMultiHieYaml dir
381+
382+ bdoc <- openDoc bPath " haskell"
383+ assertTypeCheckFailure bdoc " B should fail before cradle fix"
384+
385+ adoc <- openDoc aPath " haskell"
386+ assertTypeCheckSuccess adoc " A should remain healthy while B is broken"
387+
388+ liftIO $ atomicFileWriteStringUTF8 hiePath (T. unpack validHie)
389+ notifyHieYamlChanged dir
390+
391+ cdoc <- openDoc cPath " haskell"
392+ assertTypeCheckSuccess cdoc " C should typecheck after cradle restart"
393+
394+ bSource <- liftIO $ readFileUtf8 bPath
395+ changeDoc bdoc
396+ [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ bSource <> " \n " ]
397+ assertTypeCheckSuccess bdoc " B should not keep stale failure after cradle restart"
398+
177399-- Like simpleMultiTest but open the files in component 'a' in a separate session
178400simpleMultiDefTest :: FilePath -> TestTree
179401simpleMultiDefTest variant = ignoreForWindows $ testCase testName $
0 commit comments