66{-# LANGUAGE LambdaCase #-}
77{-# LANGUAGE RecordWildCards #-}
88
9- -- TODO: We should be using ToJSON/FromJSON for all messages which `dap` has.
10- -- It's just we'd have to derive those instances as orphans or patch upstream.
11- -- (Upstream is better. Otherwise very long compile times)
129module Test.DAP
1310 ( module Test.DAP
1411 , module Test.DAP.Init
@@ -31,6 +28,10 @@ import System.FilePath
3128import qualified Data.Text as T
3229import Data.Aeson.Types
3330import Test.Tasty.HUnit
31+ import DAP (ScopesArguments (.. ), StackTraceArguments (.. ), DisconnectArguments (.. ))
32+ import qualified DAP
33+ import Test.DAP.Orphans ()
34+ import DAP.Types
3435
3536--------------------------------------------------------------------------------
3637-- * Highest level DSL
@@ -49,52 +50,50 @@ defaultLaunch testDir =
4950 , " request" .= (" launch" :: String )
5051 ]
5152
52- defaultSetBreakpoints :: FilePath -> [( Int , Maybe String , Maybe String )] {- use `dap` types... -} -> ResponseCont Value a -> TestDAP a
53+ defaultSetBreakpoints :: FilePath -> [SourceBreakpoint ] -> ResponseCont Value a -> TestDAP a
5354defaultSetBreakpoints testDir bps = do
54- setBreakpointsRequest $
55- object
56- [ " source" .= object
57- [ " name" .= (" Main.hs" :: String )
58- , " path" .= T. pack (testDir </> " Main.hs" )
59- ]
60- , " breakpoints" .=
61- [ object
62- [ " line" .= line
63- , " logMessage" .= logMessage
64- , " condition" .= condition
65- ]
66- | (line,condition, logMessage) <- bps
67- ]
68- , " lines" .= [line | (line,_,_) <- bps ]
69- , " sourceModified" .= False
70- ]
55+ setBreakpointsRequest DAP. SetBreakpointsArguments
56+ { DAP. setBreakpointsArgumentsSource = DAP. defaultSource
57+ { DAP. sourceName = Just " Main.hs"
58+ , DAP. sourcePath = Just (T. pack (testDir </> " Main.hs" ))
59+ }
60+ , DAP. setBreakpointsArgumentsBreakpoints = Just bps
61+ , DAP. setBreakpointsArgumentsLines = Just [sourceBreakpointLine bp | bp <- bps]
62+ , DAP. setBreakpointsArgumentsSourceModified = Just False
63+ }
7164
7265defaultSetLineBreakpoints :: FilePath -> [Int ] -> ResponseCont Value a -> TestDAP a
73- defaultSetLineBreakpoints testDir bps = defaultSetBreakpoints testDir [(b, Nothing , Nothing ) | b <- bps]
66+ defaultSetLineBreakpoints testDir bps =
67+ defaultSetBreakpoints testDir
68+ [ defaultSourceBreakpoint { sourceBreakpointLine = line }
69+ | line <- bps
70+ ]
7471
7572next , stepIn :: TestDAP ()
76- next = void . sync $ nextRequest Null
77- stepIn = void . sync $ stepInRequest Null
73+ next = void . sync $ nextRequest @ Value @ Value Null
74+ stepIn = void . sync $ stepInRequest @ Value @ Value Null
7875
79- threads :: TestDAP [Int ]
76+ threads :: TestDAP [Thread ]
8077threads = do
81- v <- sync threadsRequest
82- return $ fromMaybe [] $ parseThreadIds v
83-
84- stackTrace :: Int -> TestDAP [Int {- stack frame id-} ]
85- stackTrace threadId = do
86- v <- sync $ stackTraceRequest $ object [ " threadId" .= threadId ]
87- return $ fromMaybe [] $ parseFramesIds v
88-
89- -- | Request scopes and parse (name, expensive) pairs.
90- --
91- -- todo: please, use FromJSON/ToJSON of `dap` library datatypes. this is madness!
92- scopes :: Int {- stack frame id -} -> TestDAP [(String , Bool )]
93- scopes stackFrameId = do
94- v <- sync $ scopesRequest $ object [ " frameId" .= stackFrameId ]
95- case parseScopes v of
96- Nothing -> fail $ " Could not parse scopes from: " ++ show v
97- Just scs -> pure scs
78+ Response {responseBody= Just ThreadsResponse {threads= ts}} <- sync threadsRequest
79+ return ts
80+
81+ stackTrace :: Int -> TestDAP [StackFrame ]
82+ stackTrace tid = do
83+ Response {responseBody= Just StackTraceResponse {stackFrames= fs}} <- sync $ stackTraceRequest $
84+ StackTraceArguments
85+ { DAP. stackTraceArgumentsThreadId = tid
86+ , DAP. stackTraceArgumentsStartFrame = Nothing
87+ , DAP. stackTraceArgumentsLevels = Nothing
88+ , DAP. stackTraceArgumentsFormat = Nothing
89+ }
90+ return fs
91+
92+ scopes :: Int {- stack frame id -} -> TestDAP [Scope ]
93+ scopes frameId = do
94+ Response {responseBody= Just ScopesResponse {scopes= scs}} <- sync $ scopesRequest $
95+ ScopesArguments { DAP. scopesArgumentsFrameId = frameId }
96+ return scs
9897
9998configurationDone :: ResponseCont Value a -> TestDAP a
10099configurationDone = configurationDoneRequest Nothing
@@ -104,7 +103,7 @@ configurationDone = configurationDoneRequest Nothing
104103--------------------------------------------------------------------------------
105104
106105-- | Register handler that will reply to runInTerminal reverse request
107- handleRunInTerminal :: ResponseCont (Maybe (H. HashMap T. Text T. Text ), [T. Text ]) (a , Int )
106+ handleRunInTerminal :: AsyncCont (Maybe (H. HashMap T. Text T. Text ), [T. Text ]) (a , Int )
108107 -- ^ Continuation receives async with args of runInTerm req.
109108 --
110109 -- - Waiting means block waiting for reverse request to be received
@@ -141,57 +140,55 @@ defaultHitBreakpoint testDir line = do
141140 ctx <- ask
142141 liftIO $ mapConcurrently_
143142 (`runTestDAP` ctx)
144- [ do _ <- waitFiltering Event " initialized"
143+ [ do waitFiltering_ EventTy " initialized"
145144 _ <- sync $ defaultSetLineBreakpoints testDir [line]
146145 _ <- sync $ configurationDone
147- _ <- assertStoppedLocation " breakpoint " line
146+ _ <- assertStoppedLocation DAP. StoppedEventReasonBreakpoint line
148147 return ()
149148 , void $ sync $ defaultLaunch testDir
150149 ]
151150
152151disconnect :: TestDAP ()
153152disconnect = do
154- -- wait for disconnect response
155- r <- sync $ disconnectRequest $ Just $ object
156- [ " restart" .= False
157- , " terminateDebuggee" .= True
158- , " suspendDebuggee" .= False
159- ]
160- liftIO $ assertBool " disconnect response should indicate success" $
161- fromMaybe False $ parseMaybe (withObject " disconnect response" $ \ o -> o .: " success" ) r
153+ Response {responseSuccess} <- sync $ disconnectRequest @ _ @ Value $ Just
154+ DisconnectArguments
155+ { DAP. disconnectArgumentsRestart = False
156+ , DAP. disconnectArgumentsTerminateDebuggee = True
157+ , DAP. disconnectArgumentsSuspendDebuggee = False
158+ }
159+ liftIO $ assertBool " disconnect response should indicate success" responseSuccess
162160 return ()
163161
164162--------------------------------------------------------------------------------
165163-- ** Convenience methods (based on vscode-debugadapter-node/testSupport)
166164--------------------------------------------------------------------------------
167165
168- launch :: Value {-^ Launch args -} -> ResponseCont Value a {- LaunchResponse, todo: use `dap` types w Aeson -} -> TestDAP a
166+ launch :: Value {-^ Launch args -} -> ResponseCont Value a -> TestDAP a
169167launch args = runContT $
170168 ContT initializeRequest
171169 >>= liftIO . wait
172170 >> ContT (launchRequest args)
173171
174172configurationSequence :: ResponseCont Value a -> TestDAP a
175173configurationSequence k = do
176- _ <- waitFiltering Event " initialized"
174+ waitFiltering_ EventTy " initialized"
177175 configurationDone k
178176
179177-- | Assert that a "stopped" event with the given reason is received
180- assertStoppedLocation :: String -> Int -> TestDAP ()
178+ assertStoppedLocation :: DAP. StoppedEventReason -> Int -> TestDAP ()
181179assertStoppedLocation reason expectedLine = do
182- -- TODO: Timeouts on waiting!!
183- v <- waitFiltering Event " stopped"
180+ Event {eventBody = Just StoppedEvent {stoppedEventReason}} <- waitFiltering EventTy " stopped"
184181 liftIO $
185- assertBool " " ( maybe False ( == reason) (parseStoppedEventReason v) )
182+ assertBool " Stopped reason matches expected reason" (stoppedEventReason == reason )
186183 -- TODO: Validate expected line and potentially path too
187184 -- (see assertStoppedLocation in debugClient.ts)
188185
189186-- | Assert that all *pending*(not yet taken from channel) accumulated output
190187-- events (until any other event is found) contain a certain string
191188assertOutput :: T. Text -> TestDAP ()
192189assertOutput expected = do
193- events <- waitAccumulating Event " output"
194- let outputs = mapMaybe parseOutput events
190+ events <- waitAccumulating EventTy " output"
191+ let outputs = map (outputEventOutput . fromMaybe ( error " assertOutput:fromMaybe " ) . eventBody) events
195192 liftIO $
196193 assertBool
197194 (" assertOutput: expecting " ++ show expected ++ " but got " ++ show outputs)
@@ -210,30 +207,37 @@ assertFullOutput expected = do
210207-- * Waiting for messages
211208--------------------------------------------------------------------------------
212209
213- data MsgType = Event | Response | ReverseRequest
210+ data MsgType = EventTy | ResponseTy | ReverseRequestTy
211+ deriving Show
214212
215213msgChan :: MsgType -> TestDAPClientContext -> TChan Value
216214msgChan ty TestDAPClientContext {.. } = case ty of
217- Event -> clientEvents
218- Response -> clientResponses
219- ReverseRequest -> clientReverseRequests
215+ EventTy -> clientEvents
216+ ResponseTy -> clientResponses
217+ ReverseRequestTy -> clientReverseRequests
220218
221219msgMatch :: MsgType -> String -> MessageMatch
222220msgMatch ty s = case ty of
223- Event -> eventMatch s
224- Response -> responseMatch s
225- ReverseRequest -> reverseRequestMatch s
221+ EventTy -> eventMatch s
222+ ResponseTy -> responseMatch s
223+ ReverseRequestTy -> reverseRequestMatch s
224+
225+ waitFiltering_ :: MsgType -> String -> TestDAP ()
226+ waitFiltering_ ty s = void $ waitFiltering @ Value ty s
226227
227228-- | Drop messages of the given type until a message with the given
228229-- eventType/command is found. The matching message is returned.
229- waitFiltering :: MsgType -> String -> TestDAP Value
230+ -- FIXME: Timeouts on waiting, to avoid hanging forever in the testsuite!!
231+ waitFiltering :: forall a . FromJSON a => MsgType -> String -> TestDAP a
230232waitFiltering ty s = do
231233 ch <- asks (msgChan ty)
232234 let mm = msgMatch ty s
233235 let loop = do
234236 v <- atomically $ readTChan ch -- block waiting for input
235237 if messageMatchMatches mm v
236- then return v
238+ then case fromJSON @ a v of
239+ Error e -> error $ " waitFiltering: Failed to parse message MATCHING " ++ s ++ " :" ++ show ty ++ " with error: " ++ e ++ " \n Full message was: " ++ show v
240+ Success x -> return x
237241 else loop
238242 liftIO loop
239243
@@ -242,15 +246,17 @@ waitFiltering ty s = do
242246--
243247-- The non-matching message is not consumed, nor returned, and will be kept in
244248-- the messages buffer.
245- waitAccumulating :: MsgType -> String -> TestDAP [Value ]
249+ waitAccumulating :: forall a . FromJSON a => MsgType -> String -> TestDAP [a ]
246250waitAccumulating ty s = do
247251 ch <- asks (msgChan ty)
248252 let mm = msgMatch ty s
249253 let loop acc = do
250254 r <- atomically $ do
251255 v <- readTChan ch
252256 if messageMatchMatches mm v
253- then pure (Just v)
257+ then case fromJSON @ a v of
258+ Error e -> error $ " waitAccumulating: Failed to parse MATCHING message body with error: " ++ e ++ " \n Full message was: " ++ show v
259+ Success x -> pure (Just x)
254260 else Nothing <$ unGetTChan ch v
255261 case r of
256262 Nothing -> return (reverse acc)
@@ -287,7 +293,7 @@ launchRequest, attachRequest, restartRequest, setBreakpointsRequest,
287293 gotoRequest, pauseRequest, stackTraceRequest, scopesRequest, variablesRequest,
288294 setVariableRequest, sourceRequest, modulesRequest, evaluateRequest,
289295 disassembleRequest, stepInTargetsRequest, gotoTargetsRequest, completionsRequest,
290- exceptionInfoRequest , readMemoryRequest , writeMemoryRequest :: Value -> ResponseCont Value a -> TestDAP a
296+ exceptionInfoRequest , readMemoryRequest , writeMemoryRequest :: ( ToJSON a , FromJSON b ) => a -> ResponseCont b r -> TestDAP r
291297
292298launchRequest = requestWithArgs " launch"
293299attachRequest = requestWithArgs " attach"
@@ -322,7 +328,7 @@ exceptionInfoRequest = requestWithArgs "exceptionInfo"
322328readMemoryRequest = requestWithArgs " readMemory"
323329writeMemoryRequest = requestWithArgs " writeMemory"
324330
325- terminateRequest , disconnectRequest :: Maybe Value -> ResponseCont Value a -> TestDAP a
331+ terminateRequest , disconnectRequest :: ( ToJSON a , FromJSON b ) => Maybe a -> ResponseCont b r -> TestDAP r
326332terminateRequest = customRequest " terminate"
327333-- example:
328334-- object
@@ -335,10 +341,10 @@ terminateRequest = customRequest "terminate"
335341-- waitEventFiltering $ eventMatch "terminated"
336342disconnectRequest = customRequest " disconnect"
337343
338- threadsRequest :: ResponseCont Value a -> TestDAP a
339- threadsRequest = customRequest " threads" Nothing
344+ threadsRequest :: FromJSON b => ResponseCont b r -> TestDAP r
345+ threadsRequest = customRequest " threads" ( Nothing :: Maybe () )
340346--------------------------------------------------------------------------------
341- requestWithArgs :: String -> Value -> ResponseCont Value a -> TestDAP a
347+ requestWithArgs :: ( ToJSON a , FromJSON b ) => String -> a -> ResponseCont b r -> TestDAP r
342348requestWithArgs command args = customRequest command (Just args)
343349
344350-- example: respondWithBody revReqNum [ "shellProcessId" .= ... ]
@@ -351,7 +357,7 @@ respondWithBody seqNum command body =
351357 , " body" .= body
352358 ]
353359--------------------------------------------------------------------------------
354- customRequest :: String -> Maybe Value -> ResponseCont Value a -> TestDAP a
360+ customRequest :: ( ToJSON a , FromJSON b ) => String -> Maybe a -> ResponseCont b r -> TestDAP r
355361customRequest command args = do
356362 send $
357363 [ " type" .= (" request" :: String )
0 commit comments