Skip to content

Commit c53691b

Browse files
committed
testsuite: Use dap types in DAP testsuite driver
Add orphan FromJSON/ToJSON instances in Test.DAP.Orphans for types used in the client direction then update the testsuite driver to prefer the Haskell types rather than `Value`. Fixes #270
1 parent 1d66952 commit c53691b

11 files changed

Lines changed: 275 additions & 170 deletions

File tree

haskell-debugger.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ test-suite haskell-debugger-test
214214
Test.DAP.Init,
215215
Test.DAP.Messages,
216216
Test.DAP.Messages.Parser,
217+
Test.DAP.Orphans,
217218
Test.Utils,
218219

219220
Test.Unit.DAP.RunInTerminal,

test/haskell/Main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import System.Environment
1919
import Control.Exception
2020

2121
import Test.Tasty
22+
#ifdef mingw32_HOST_OS
2223
import Test.Tasty.ExpectedFailure
24+
#endif
2325
import Test.Tasty.Golden as G
2426
import Test.Tasty.Golden.Advanced as G
2527

test/haskell/Test/DAP.hs

Lines changed: 84 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
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)
129
module Test.DAP
1310
( module Test.DAP
1411
, module Test.DAP.Init
@@ -31,6 +28,10 @@ import System.FilePath
3128
import qualified Data.Text as T
3229
import Data.Aeson.Types
3330
import 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
5354
defaultSetBreakpoints 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

7265
defaultSetLineBreakpoints :: 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

7572
next, 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]
8077
threads = 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

9998
configurationDone :: ResponseCont Value a -> TestDAP a
10099
configurationDone = 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

152151
disconnect :: TestDAP ()
153152
disconnect = 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
169167
launch args = runContT $
170168
ContT initializeRequest
171169
>>= liftIO . wait
172170
>> ContT (launchRequest args)
173171

174172
configurationSequence :: ResponseCont Value a -> TestDAP a
175173
configurationSequence 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 ()
181179
assertStoppedLocation 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
191188
assertOutput :: T.Text -> TestDAP ()
192189
assertOutput 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

215213
msgChan :: MsgType -> TestDAPClientContext -> TChan Value
216214
msgChan ty TestDAPClientContext{..} = case ty of
217-
Event -> clientEvents
218-
Response -> clientResponses
219-
ReverseRequest -> clientReverseRequests
215+
EventTy -> clientEvents
216+
ResponseTy -> clientResponses
217+
ReverseRequestTy -> clientReverseRequests
220218

221219
msgMatch :: MsgType -> String -> MessageMatch
222220
msgMatch 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
230232
waitFiltering 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 ++ "\nFull 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]
246250
waitAccumulating 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 ++ "\nFull 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

292298
launchRequest = requestWithArgs "launch"
293299
attachRequest = requestWithArgs "attach"
@@ -322,7 +328,7 @@ exceptionInfoRequest = requestWithArgs "exceptionInfo"
322328
readMemoryRequest = requestWithArgs "readMemory"
323329
writeMemoryRequest = 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
326332
terminateRequest = customRequest "terminate"
327333
-- example:
328334
-- object
@@ -335,10 +341,10 @@ terminateRequest = customRequest "terminate"
335341
-- waitEventFiltering $ eventMatch "terminated"
336342
disconnectRequest = 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
342348
requestWithArgs 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
355361
customRequest command args = do
356362
send $
357363
[ "type" .= ("request" :: String)

test/haskell/Test/DAP/Init.hs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ module Test.DAP.Init where
99
----------------------------------------------------------------------------
1010
import Data.Maybe
1111
import Data.List (isInfixOf)
12-
import Control.Concurrent
1312
import Control.Exception hiding (handle)
1413
import qualified Control.Exception as E
1514
import Network.Run.TCP
@@ -30,8 +29,9 @@ import Data.Aeson.Types
3029
import Test.Tasty.HUnit (assertFailure)
3130
import DAP.Server (readPayload)
3231
import qualified Control.Monad.Catch
33-
import Test.DAP.Messages.Parser
3432
import Test.Utils (withHermeticDir)
33+
import DAP.Types (OutputEvent (..))
34+
import Test.DAP.Messages.Parser
3535

3636
--------------------------------------------------------------------------------
3737
-- * Launch the DAP server process (what we're testing)
@@ -82,12 +82,12 @@ startTestDAPServer testDir flags = do
8282

8383
-- | Prefer this to startTestDAPServer
8484
withTestDAPServer :: FilePath -> [String] -> (FilePath -> TestDAPServer -> IO a) -> IO a
85-
withTestDAPServer dir flags check = do
85+
withTestDAPServer dir flags check' = do
8686
keep_tmp_dirs <- maybe False read <$> lookupEnv "KEEP_TEMP_DIRS"
8787
withHermeticDir keep_tmp_dirs dir $ \test_dir ->
8888
bracket (startTestDAPServer test_dir flags)
8989
testDAPServerCleanup
90-
(check test_dir)
90+
(check' test_dir)
9191

9292
getAvailablePort :: IO Int
9393
getAvailablePort =
@@ -150,12 +150,12 @@ handleServerTestDAP = do
150150
payload <- nextPayload
151151
liftIO $ case parseMaybe parseType payload of
152152
Just "event" -> do
153-
let mtxt = parseOutput payload
153+
let mtxt = fromJSON @(Event OutputEvent) payload
154154
atomically $ do
155155
writeTChan clientEvents payload
156156
case mtxt of
157-
Just txt -> modifyTVar' clientFullOutput (txt:)
158-
Nothing -> pure ()
157+
Success (Event _ (Just txt)) -> modifyTVar' clientFullOutput (outputEventOutput txt:)
158+
_ -> pure ()
159159
Just "response" ->
160160
-- Fail immediately if the server reports failure, even if the test
161161
-- is blocked waiting for some other specific message.

0 commit comments

Comments
 (0)