-
-
Notifications
You must be signed in to change notification settings - Fork 389
/
Copy pathTest.hs
280 lines (243 loc) · 11 KB
/
Test.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
-- Copyright (c) 2019 The DAML Authors. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PolyKinds #-}
module Development.IDE.Test
( Cursor
, cursorPosition
, requireDiagnostic
, diagnostic
, expectDiagnostics
, expectDiagnosticsWithTags
, expectNoMoreDiagnostics
, expectMessages
, expectCurrentDiagnostics
, checkDiagnosticsForDoc
, canonicalizeUri
, standardizeQuotes
, flushMessages
, waitForAction
, getInterfaceFilesDir
, garbageCollectDirtyKeys
, getFilesOfInterest
, waitForTypecheck
, waitForBuildQueue
, getStoredKeys
, waitForCustomMessage
, waitForGC
, getBuildKeysBuilt
, getBuildKeysVisited
, getBuildKeysChanged
, getBuildEdgesCount
, getRebuildsCount
, configureCheckProject
, isReferenceReady
, referenceReady) where
import Control.Applicative.Combinators
import Control.Lens hiding (List)
import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson (toJSON)
import qualified Data.Aeson as A
import Data.Bifunctor (second)
import Data.Default
import qualified Data.Map.Strict as Map
import Data.Maybe (fromJust)
import Data.Text (Text)
import qualified Data.Text as T
import Development.IDE.Plugin.Test (TestRequest (..),
WaitForIdeRuleResult,
ideResultSuccess)
import Development.IDE.Test.Diagnostic
import Ide.Plugin.Config (CheckParents, checkProject)
import Language.LSP.Test hiding (message)
import qualified Language.LSP.Test as LspTest
import Language.LSP.Types hiding
(SemanticTokenAbsolute (length, line),
SemanticTokenRelative (length),
SemanticTokensEdit (_start))
import Language.LSP.Types.Lens as Lsp
import System.Directory (canonicalizePath)
import System.Time.Extra
import Test.Tasty.HUnit
import System.FilePath (equalFilePath)
requireDiagnosticM
:: (Foldable f, Show (f Diagnostic), HasCallStack)
=> f Diagnostic
-> (DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag)
-> Assertion
requireDiagnosticM actuals expected = case requireDiagnostic actuals expected of
Nothing -> pure ()
Just err -> assertFailure err
-- |wait for @timeout@ seconds and report an assertion failure
-- if any diagnostic messages arrive in that period
expectNoMoreDiagnostics :: HasCallStack => Seconds -> Session ()
expectNoMoreDiagnostics timeout =
expectMessages STextDocumentPublishDiagnostics timeout $ \diagsNot -> do
let fileUri = diagsNot ^. params . uri
actual = diagsNot ^. params . diagnostics
liftIO $
assertFailure $
"Got unexpected diagnostics for " <> show fileUri
<> " got "
<> show actual
expectMessages :: SMethod m -> Seconds -> (ServerMessage m -> Session ()) -> Session ()
expectMessages m timeout handle = do
-- Give any further diagnostic messages time to arrive.
liftIO $ sleep timeout
-- Send a dummy message to provoke a response from the server.
-- This guarantees that we have at least one message to
-- process, so message won't block or timeout.
let cm = SCustomMethod "test"
i <- sendRequest cm $ A.toJSON GetShakeSessionQueueCount
go cm i
where
go cm i = handleMessages
where
handleMessages = (LspTest.message m >>= handle) <|> (void $ responseForId cm i) <|> ignoreOthers
ignoreOthers = void anyMessage >> handleMessages
flushMessages :: Session ()
flushMessages = do
let cm = SCustomMethod "non-existent-method"
i <- sendRequest cm A.Null
void (responseForId cm i) <|> ignoreOthers cm i
where
ignoreOthers cm i = skipManyTill anyMessage (responseForId cm i) >> flushMessages
-- | It is not possible to use 'expectDiagnostics []' to assert the absence of diagnostics,
-- only that existing diagnostics have been cleared.
--
-- Rather than trying to assert the absence of diagnostics, introduce an
-- expected diagnostic (e.g. a redundant import) and assert the singleton diagnostic.
expectDiagnostics :: HasCallStack => [(FilePath, [(DiagnosticSeverity, Cursor, T.Text)])] -> Session ()
expectDiagnostics
= expectDiagnosticsWithTags
. map (second (map (\(ds, c, t) -> (ds, c, t, Nothing))))
unwrapDiagnostic :: NotificationMessage TextDocumentPublishDiagnostics -> (Uri, List Diagnostic)
unwrapDiagnostic diagsNot = (diagsNot^.params.uri, diagsNot^.params.diagnostics)
expectDiagnosticsWithTags :: HasCallStack => [(String, [(DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag)])] -> Session ()
expectDiagnosticsWithTags expected = do
let f = getDocUri >=> liftIO . canonicalizeUri >=> pure . toNormalizedUri
next = unwrapDiagnostic <$> skipManyTill anyMessage diagnostic
expected' <- Map.fromListWith (<>) <$> traverseOf (traverse . _1) f expected
expectDiagnosticsWithTags' next expected'
expectDiagnosticsWithTags' ::
(HasCallStack, MonadIO m) =>
m (Uri, List Diagnostic) ->
Map.Map NormalizedUri [(DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag)] ->
m ()
expectDiagnosticsWithTags' next m | null m = do
(_,actual) <- next
case actual of
List [] ->
return ()
_ ->
liftIO $ assertFailure $ "Got unexpected diagnostics:" <> show actual
expectDiagnosticsWithTags' next expected = go expected
where
go m
| Map.null m = pure ()
| otherwise = do
(fileUri, actual) <- next
canonUri <- liftIO $ toNormalizedUri <$> canonicalizeUri fileUri
case Map.lookup canonUri m of
Nothing -> do
liftIO $
assertFailure $
"Got diagnostics for " <> show fileUri
<> " but only expected diagnostics for "
<> show (Map.keys m)
<> " got "
<> show actual
Just expected -> do
liftIO $ mapM_ (requireDiagnosticM actual) expected
liftIO $
unless (length expected == length actual) $
assertFailure $
"Incorrect number of diagnostics for " <> show fileUri
<> ", expected "
<> show expected
<> " but got "
<> show actual
go $ Map.delete canonUri m
expectCurrentDiagnostics :: HasCallStack => TextDocumentIdentifier -> [(DiagnosticSeverity, Cursor, T.Text)] -> Session ()
expectCurrentDiagnostics doc expected = do
diags <- getCurrentDiagnostics doc
checkDiagnosticsForDoc doc expected diags
checkDiagnosticsForDoc :: HasCallStack => TextDocumentIdentifier -> [(DiagnosticSeverity, Cursor, T.Text)] -> [Diagnostic] -> Session ()
checkDiagnosticsForDoc TextDocumentIdentifier {_uri} expected obtained = do
let expected' = Map.fromList [(nuri, map (\(ds, c, t) -> (ds, c, t, Nothing)) expected)]
nuri = toNormalizedUri _uri
expectDiagnosticsWithTags' (return (_uri, List obtained)) expected'
canonicalizeUri :: Uri -> IO Uri
canonicalizeUri uri = filePathToUri <$> canonicalizePath (fromJust (uriToFilePath uri))
diagnostic :: Session (NotificationMessage TextDocumentPublishDiagnostics)
diagnostic = LspTest.message STextDocumentPublishDiagnostics
tryCallTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either ResponseError b)
tryCallTestPlugin cmd = do
let cm = SCustomMethod "test"
waitId <- sendRequest cm (A.toJSON cmd)
ResponseMessage{_result} <- skipManyTill anyMessage $ responseForId cm waitId
return $ case _result of
Left e -> Left e
Right json -> case A.fromJSON json of
A.Success a -> Right a
A.Error e -> error e
callTestPlugin :: (A.FromJSON b) => TestRequest -> Session b
callTestPlugin cmd = do
res <- tryCallTestPlugin cmd
case res of
Left (ResponseError t err _) -> error $ show t <> ": " <> T.unpack err
Right a -> pure a
waitForAction :: String -> TextDocumentIdentifier -> Session WaitForIdeRuleResult
waitForAction key TextDocumentIdentifier{_uri} =
callTestPlugin (WaitForIdeRule key _uri)
getBuildKeysBuilt :: Session (Either ResponseError [T.Text])
getBuildKeysBuilt = tryCallTestPlugin GetBuildKeysBuilt
getBuildKeysVisited :: Session (Either ResponseError [T.Text])
getBuildKeysVisited = tryCallTestPlugin GetBuildKeysVisited
getBuildKeysChanged :: Session (Either ResponseError [T.Text])
getBuildKeysChanged = tryCallTestPlugin GetBuildKeysChanged
getBuildEdgesCount :: Session (Either ResponseError Int)
getBuildEdgesCount = tryCallTestPlugin GetBuildEdgesCount
getRebuildsCount :: Session (Either ResponseError Int)
getRebuildsCount = tryCallTestPlugin GetRebuildsCount
getInterfaceFilesDir :: TextDocumentIdentifier -> Session FilePath
getInterfaceFilesDir TextDocumentIdentifier{_uri} = callTestPlugin (GetInterfaceFilesDir _uri)
garbageCollectDirtyKeys :: CheckParents -> Int -> Session [String]
garbageCollectDirtyKeys parents age = callTestPlugin (GarbageCollectDirtyKeys parents age)
getStoredKeys :: Session [Text]
getStoredKeys = callTestPlugin GetStoredKeys
waitForTypecheck :: TextDocumentIdentifier -> Session Bool
waitForTypecheck tid = ideResultSuccess <$> waitForAction "typecheck" tid
waitForBuildQueue :: Session ()
waitForBuildQueue = callTestPlugin WaitForShakeQueue
getFilesOfInterest :: Session [FilePath]
getFilesOfInterest = callTestPlugin GetFilesOfInterest
waitForCustomMessage :: T.Text -> (A.Value -> Maybe res) -> Session res
waitForCustomMessage msg pred =
skipManyTill anyMessage $ satisfyMaybe $ \case
FromServerMess (SCustomMethod lbl) (NotMess NotificationMessage{_params = value})
| lbl == msg -> pred value
_ -> Nothing
waitForGC :: Session [T.Text]
waitForGC = waitForCustomMessage "ghcide/GC" $ \v ->
case A.fromJSON v of
A.Success x -> Just x
_ -> Nothing
configureCheckProject :: Bool -> Session ()
configureCheckProject overrideCheckProject =
sendNotification SWorkspaceDidChangeConfiguration
(DidChangeConfigurationParams $ toJSON
def{checkProject = overrideCheckProject})
-- | Pattern match a message from ghcide indicating that a file has been indexed
isReferenceReady :: FilePath -> Session ()
isReferenceReady p = void $ referenceReady (equalFilePath p)
referenceReady :: (FilePath -> Bool) -> Session FilePath
referenceReady pred = satisfyMaybe $ \case
FromServerMess (SCustomMethod "ghcide/reference/ready") (NotMess NotificationMessage{_params})
| A.Success fp <- A.fromJSON _params
, pred fp
-> Just fp
_ -> Nothing