Skip to content

Commit 9565d0b

Browse files
komikatfendor
andauthored
Using captureKicksDiagnostics to speed up multiple plugin tests (#4339)
* WIP: Speed up hls-hlint-plugin-tests Move test data to temporary directory. Avoid `waitForDiagnosticsWithSource` as it unconditionally waits for diagnostics. * use captureKickdiagnostics for cabal plugin * fix hlint-plugin resolve tests * haskell-stylish fix * fix unused imports * fix unused imports, unused defs * resolve conflicts with master with refactor kickSignal * remove redundant imports * remove more redundant imports * refactor kicks to use runWithsignal --------- Co-authored-by: Fendor <[email protected]>
1 parent 0bf3348 commit 9565d0b

File tree

9 files changed

+157
-86
lines changed

9 files changed

+157
-86
lines changed

ghcide/src/Development/IDE/Core/OfInterest.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import Development.IDE.Graph
2929
import Control.Concurrent.STM.Stats (atomically,
3030
modifyTVar')
3131
import Data.Aeson (toJSON)
32+
import qualified Data.Aeson as Aeson
3233
import qualified Data.ByteString as BS
3334
import Data.Maybe (catMaybes)
3435
import Development.IDE.Core.ProgressReporting

ghcide/src/Development/IDE/Core/Shake.hs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ module Development.IDE.Core.Shake(
7373
garbageCollectDirtyKeysOlderThan,
7474
Log(..),
7575
VFSModified(..), getClientConfigAction,
76-
ThreadQueue(..)
76+
ThreadQueue(..),
77+
runWithSignal
7778
) where
7879

7980
import Control.Concurrent.Async
@@ -123,6 +124,10 @@ import Development.IDE.Core.FileUtils (getModTime)
123124
import Development.IDE.Core.PositionMapping
124125
import Development.IDE.Core.ProgressReporting
125126
import Development.IDE.Core.RuleTypes
127+
import Development.IDE.Types.Options as Options
128+
import qualified Language.LSP.Protocol.Message as LSP
129+
import qualified Language.LSP.Server as LSP
130+
126131
import Development.IDE.Core.Tracing
127132
import Development.IDE.Core.WorkerThread
128133
import Development.IDE.GHC.Compat (NameCache,
@@ -147,11 +152,11 @@ import qualified Development.IDE.Types.Exports as ExportsMap
147152
import Development.IDE.Types.KnownTargets
148153
import Development.IDE.Types.Location
149154
import Development.IDE.Types.Monitoring (Monitoring (..))
150-
import Development.IDE.Types.Options
151155
import Development.IDE.Types.Shake
152156
import qualified Focus
153157
import GHC.Fingerprint
154158
import GHC.Stack (HasCallStack)
159+
import GHC.TypeLits (KnownSymbol)
155160
import HieDb.Types
156161
import Ide.Logger hiding (Priority)
157162
import qualified Ide.Logger as Logger
@@ -165,7 +170,6 @@ import qualified Language.LSP.Protocol.Lens as L
165170
import Language.LSP.Protocol.Message
166171
import Language.LSP.Protocol.Types
167172
import qualified Language.LSP.Protocol.Types as LSP
168-
import qualified Language.LSP.Server as LSP
169173
import Language.LSP.VFS hiding (start)
170174
import qualified "list-t" ListT
171175
import OpenTelemetry.Eventlog hiding (addEvent)
@@ -1350,29 +1354,28 @@ updateFileDiagnostics recorder fp ver k ShakeExtras{diagnostics, hiddenDiagnosti
13501354
let uri' = filePathToUri' fp
13511355
let delay = if null newDiags then 0.1 else 0
13521356
registerEvent debouncer delay uri' $ withTrace ("report diagnostics " <> fromString (fromNormalizedFilePath fp)) $ \tag -> do
1353-
join $ mask_ $ do
1354-
lastPublish <- atomicallyNamed "diagnostics - publish" $ STM.focus (Focus.lookupWithDefault [] <* Focus.insert newDiags) uri' publishedDiagnostics
1355-
let action = when (lastPublish /= newDiags) $ case lspEnv of
1357+
join $ mask_ $ do
1358+
lastPublish <- atomicallyNamed "diagnostics - publish" $ STM.focus (Focus.lookupWithDefault [] <* Focus.insert newDiags) uri' publishedDiagnostics
1359+
let action = when (lastPublish /= newDiags) $ case lspEnv of
13561360
Nothing -> -- Print an LSP event.
13571361
logWith recorder Info $ LogDiagsDiffButNoLspEnv (map (fp, ShowDiag,) newDiags)
13581362
Just env -> LSP.runLspT env $ do
13591363
liftIO $ tag "count" (show $ Prelude.length newDiags)
13601364
liftIO $ tag "key" (show k)
13611365
LSP.sendNotification SMethod_TextDocumentPublishDiagnostics $
13621366
LSP.PublishDiagnosticsParams (fromNormalizedUri uri') (fmap fromIntegral ver) newDiags
1363-
return action
1367+
return action
13641368
where
13651369
diagsFromRule :: Diagnostic -> Diagnostic
13661370
diagsFromRule c@Diagnostic{_range}
13671371
| coerce ideTesting = c & L.relatedInformation ?~
1368-
[
1369-
DiagnosticRelatedInformation
1372+
[ DiagnosticRelatedInformation
13701373
(Location
13711374
(filePathToUri $ fromNormalizedFilePath fp)
13721375
_range
13731376
)
13741377
(T.pack $ show k)
1375-
]
1378+
]
13761379
| otherwise = c
13771380

13781381

@@ -1444,3 +1447,19 @@ updatePositionMappingHelper ver changes mappingForUri = snd $
14441447
EM.mapAccumRWithKey (\acc _k (delta, _) -> let new = addOldDelta delta acc in (new, (delta, acc)))
14451448
zeroMapping
14461449
(EM.insert ver (mkDelta changes, zeroMapping) mappingForUri)
1450+
1451+
-- | sends a signal whenever shake session is run/restarted
1452+
-- being used in cabal and hlint plugin tests to know when its time
1453+
-- to look for file diagnostics
1454+
kickSignal :: KnownSymbol s => Bool -> Maybe (LSP.LanguageContextEnv c) -> [NormalizedFilePath] -> Proxy s -> Action ()
1455+
kickSignal testing lspEnv files msg = when testing $ liftIO $ mRunLspT lspEnv $
1456+
LSP.sendNotification (LSP.SMethod_CustomMethod msg) $
1457+
toJSON $ map fromNormalizedFilePath files
1458+
1459+
-- | Add kick start/done signal to rule
1460+
runWithSignal :: (KnownSymbol s0, KnownSymbol s1, IdeRule k v) => Proxy s0 -> Proxy s1 -> [NormalizedFilePath] -> k -> Action ()
1461+
runWithSignal msgStart msgEnd files rule = do
1462+
ShakeExtras{ideTesting = Options.IdeTesting testing, lspEnv} <- getShakeExtras
1463+
kickSignal testing lspEnv files msgStart
1464+
void $ uses rule files
1465+
kickSignal testing lspEnv files msgEnd

haskell-language-server.cabal

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,6 @@ library hls-hlint-plugin
716716
, hlint >= 3.5 && < 3.9
717717
, hls-plugin-api == 2.9.0.1
718718
, lens
719-
, lsp
720719
, mtl
721720
, refact
722721
, regex-tdfa
@@ -727,6 +726,8 @@ library hls-hlint-plugin
727726
, unordered-containers
728727
, ghc-lib-parser-ex
729728
, apply-refact
729+
--
730+
, lsp-types
730731

731732
if flag(ghc-lib)
732733
cpp-options: -DGHC_LIB

hls-test-utils/src/Test/Hls.hs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,17 @@ module Test.Hls
6161
WithPriority(..),
6262
Recorder,
6363
Priority(..),
64-
TestConfig(..),
64+
captureKickDiagnostics,
65+
kick,
66+
TestConfig(..)
6567
)
6668
where
6769

6870
import Control.Applicative.Combinators
6971
import Control.Concurrent.Async (async, cancel, wait)
7072
import Control.Concurrent.Extra
7173
import Control.Exception.Safe
74+
import Control.Lens ((^.))
7275
import Control.Lens.Extras (is)
7376
import Control.Monad (guard, unless, void)
7477
import Control.Monad.Extra (forM)
@@ -80,7 +83,7 @@ import qualified Data.Aeson as A
8083
import Data.ByteString.Lazy (ByteString)
8184
import Data.Default (Default, def)
8285
import qualified Data.Map as M
83-
import Data.Maybe (fromMaybe)
86+
import Data.Maybe (fromMaybe, mapMaybe)
8487
import Data.Proxy (Proxy (Proxy))
8588
import qualified Data.Text as T
8689
import qualified Data.Text.Lazy as TL
@@ -114,6 +117,7 @@ import Ide.PluginUtils (idePluginsToPluginDes
114117
pluginDescToIdePlugins)
115118
import Ide.Types
116119
import Language.LSP.Protocol.Capabilities
120+
import qualified Language.LSP.Protocol.Lens as L
117121
import Language.LSP.Protocol.Message
118122
import qualified Language.LSP.Protocol.Message as LSP
119123
import Language.LSP.Protocol.Types hiding (Null)
@@ -231,14 +235,14 @@ goldenWithTestConfig
231235
:: Pretty b
232236
=> TestConfig b
233237
-> TestName
234-
-> FilePath
238+
-> VirtualFileTree
235239
-> FilePath
236240
-> FilePath
237241
-> FilePath
238242
-> (TextDocumentIdentifier -> Session ())
239243
-> TestTree
240-
goldenWithTestConfig config title testDataDir path desc ext act =
241-
goldenGitDiff title (testDataDir </> path <.> desc <.> ext)
244+
goldenWithTestConfig config title tree path desc ext act =
245+
goldenGitDiff title (vftOriginalRoot tree </> path <.> desc <.> ext)
242246
$ runSessionWithTestConfig config $ const
243247
$ TL.encodeUtf8 . TL.fromStrict
244248
<$> do
@@ -869,6 +873,17 @@ setHlsConfig config = do
869873
-- requests!
870874
skipManyTill anyMessage (void configurationRequest)
871875

876+
captureKickDiagnostics :: Session () -> Session () -> Session [Diagnostic]
877+
captureKickDiagnostics start done = do
878+
_ <- skipManyTill anyMessage start
879+
messages <- manyTill anyMessage done
880+
pure $ concat $ mapMaybe diagnostics messages
881+
where
882+
diagnostics :: FromServerMessage' a -> Maybe [Diagnostic]
883+
diagnostics = \msg -> case msg of
884+
FromServerMess SMethod_TextDocumentPublishDiagnostics diags -> Just (diags ^. L.params . L.diagnostics)
885+
_ -> Nothing
886+
872887
waitForKickDone :: Session ()
873888
waitForKickDone = void $ skipManyTill anyMessage nonTrivialKickDone
874889

@@ -881,9 +896,11 @@ nonTrivialKickDone = kick (Proxy @"kick/done") >>= guard . not . null
881896
nonTrivialKickStart :: Session ()
882897
nonTrivialKickStart = kick (Proxy @"kick/start") >>= guard . not . null
883898

899+
884900
kick :: KnownSymbol k => Proxy k -> Session [FilePath]
885901
kick proxyMsg = do
886902
NotMess TNotificationMessage{_params} <- customNotification proxyMsg
887903
case fromJSON _params of
888904
Success x -> return x
889905
other -> error $ "Failed to parse kick/done details: " <> show other
906+

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ function invocation.
231231
kick :: Action ()
232232
kick = do
233233
files <- HashMap.keys <$> getCabalFilesOfInterestUntracked
234-
void $ uses Types.ParseCabalFile files
234+
Shake.runWithSignal (Proxy @"kick/start/cabal") (Proxy @"kick/done/cabal") files Types.ParseCabalFile
235235

236236
-- ----------------------------------------------------------------
237237
-- Code Actions

plugins/hls-cabal-plugin/test/Main.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ codeActionUnitTests =
8585
where
8686
maxCompletions = 100
8787

88+
8889
-- ------------------------ ------------------------------------------------
8990
-- Integration Tests
9091
-- ------------------------------------------------------------------------
@@ -96,23 +97,23 @@ pluginTests =
9697
[ testGroup
9798
"Diagnostics"
9899
[ runCabalTestCaseSession "Publishes Diagnostics on Error" "" $ do
99-
doc <- openDoc "invalid.cabal" "cabal"
100-
diags <- waitForDiagnosticsFromSource doc "cabal"
100+
_ <- openDoc "invalid.cabal" "cabal"
101+
diags <- cabalCaptureKick
101102
unknownLicenseDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'BSD3'"]
102103
liftIO $ do
103104
length diags @?= 1
104105
unknownLicenseDiag ^. L.range @?= Range (Position 3 24) (Position 4 0)
105106
unknownLicenseDiag ^. L.severity @?= Just DiagnosticSeverity_Error
106107
, runCabalTestCaseSession "Clears diagnostics" "" $ do
107108
doc <- openDoc "invalid.cabal" "cabal"
108-
diags <- waitForDiagnosticsFrom doc
109+
diags <- cabalCaptureKick
109110
unknownLicenseDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'BSD3'"]
110111
liftIO $ do
111112
length diags @?= 1
112113
unknownLicenseDiag ^. L.range @?= Range (Position 3 24) (Position 4 0)
113114
unknownLicenseDiag ^. L.severity @?= Just DiagnosticSeverity_Error
114115
_ <- applyEdit doc $ TextEdit (Range (Position 3 20) (Position 4 0)) "BSD-3-Clause\n"
115-
newDiags <- waitForDiagnosticsFrom doc
116+
newDiags <- cabalCaptureKick
116117
liftIO $ newDiags @?= []
117118
, runCabalTestCaseSession "No Diagnostics in .hs files from valid .cabal file" "simple-cabal" $ do
118119
hsDoc <- openDoc "A.hs" "haskell"

plugins/hls-cabal-plugin/test/Utils.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
{-# LANGUAGE DataKinds #-}
12
{-# LANGUAGE DisambiguateRecordFields #-}
23
{-# LANGUAGE OverloadedStrings #-}
34

45
module Utils where
56

7+
import Control.Monad (guard)
68
import Data.List (sort)
9+
import Data.Proxy (Proxy (Proxy))
710
import qualified Data.Text as T
811
import Ide.Plugin.Cabal (descriptor)
912
import qualified Ide.Plugin.Cabal
@@ -52,6 +55,18 @@ runCabalGoldenSession title subdir fp act = goldenWithCabalDoc def cabalPlugin t
5255
testDataDir :: FilePath
5356
testDataDir = "plugins" </> "hls-cabal-plugin" </> "test" </> "testdata"
5457

58+
-- | these functions are used to detect cabal kicks
59+
-- and look at diagnostics for cabal files
60+
-- kicks are run everytime there is a shake session run/restart
61+
cabalKickDone :: Session ()
62+
cabalKickDone = kick (Proxy @"kick/done/cabal") >>= guard . not . null
63+
64+
cabalKickStart :: Session ()
65+
cabalKickStart = kick (Proxy @"kick/start/cabal") >>= guard . not . null
66+
67+
cabalCaptureKick :: Session [Diagnostic]
68+
cabalCaptureKick = captureKickDiagnostics cabalKickStart cabalKickDone
69+
5570
-- | list comparison where the order in the list is irrelevant
5671
(@?==) :: (HasCallStack, Ord a, Show a) => [a] -> [a] -> Assertion
5772
(@?==) l1 l2 = sort l1 @?= sort l2

plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import Control.Concurrent.STM
2929
import Control.DeepSeq
3030
import Control.Exception
3131
import Control.Lens ((?~), (^.))
32-
import Control.Monad
3332
import Control.Monad.Error.Class (MonadError (throwError))
3433
import Control.Monad.IO.Class (MonadIO (liftIO))
3534
import Control.Monad.Trans.Class (MonadTrans (lift))
@@ -119,6 +118,7 @@ import System.Environment (setEnv,
119118
#endif
120119
import Development.IDE.Core.PluginUtils as PluginUtils
121120
import Text.Regex.TDFA.Text ()
121+
122122
-- ---------------------------------------------------------------------
123123

124124
data Log
@@ -134,7 +134,7 @@ instance Pretty Log where
134134
LogShake log -> pretty log
135135
LogApplying fp res -> "Applying hint(s) for" <+> viaShow fp <> ":" <+> viaShow res
136136
LogGeneratedIdeas fp ideas -> "Generated hlint ideas for for" <+> viaShow fp <> ":" <+> viaShow ideas
137-
LogUsingExtensions fp exts -> "Using extensions for " <+> viaShow fp <> ":" <+> pretty exts
137+
LogUsingExtensions fp exts -> "Using extensions for " <+> viaShow fp <> ":" <> line <> indent 4 (pretty exts)
138138
LogGetIdeas fp -> "Getting hlint ideas for " <+> viaShow fp
139139
LogResolve msg -> pretty msg
140140

@@ -183,12 +183,12 @@ instance NFData GetHlintDiagnostics
183183
type instance RuleResult GetHlintDiagnostics = ()
184184

185185
-- | Hlint rules to generate file diagnostics based on hlint hints
186-
-- | This rule is recomputed when:
187-
-- | - A file has been edited via
188-
-- | - `getIdeas` -> `getParsedModule` in any case
189-
-- | - `getIdeas` -> `getFileContents` if the hls ghc does not match the hlint default ghc
190-
-- | - The client settings have changed, to honour the `hlintOn` setting, via `getClientConfigAction`
191-
-- | - The hlint specific settings have changed, via `getHlintSettingsRule`
186+
-- This rule is recomputed when:
187+
-- - A file has been edited via
188+
-- - `getIdeas` -> `getParsedModule` in any case
189+
-- - `getIdeas` -> `getFileContents` if the hls ghc does not match the hlint default ghc
190+
-- - The client settings have changed, to honour the `hlintOn` setting, via `getClientConfigAction`
191+
-- - The hlint specific settings have changed, via `getHlintSettingsRule`
192192
rules :: Recorder (WithPriority Log) -> PluginId -> Rules ()
193193
rules recorder plugin = do
194194
define (cmapWithPrio LogShake recorder) $ \GetHlintDiagnostics file -> do
@@ -202,8 +202,8 @@ rules recorder plugin = do
202202
liftIO $ argsSettings flags
203203

204204
action $ do
205-
files <- getFilesOfInterestUntracked
206-
void $ uses GetHlintDiagnostics $ Map.keys files
205+
files <- Map.keys <$> getFilesOfInterestUntracked
206+
Shake.runWithSignal (Proxy @"kick/start/hlint") (Proxy @"kick/done/hlint") files GetHlintDiagnostics
207207

208208
where
209209

0 commit comments

Comments
 (0)