diff --git a/haskell-debugger.cabal b/haskell-debugger.cabal index d0340ca2..85ab28a5 100644 --- a/haskell-debugger.cabal +++ b/haskell-debugger.cabal @@ -214,6 +214,7 @@ test-suite haskell-debugger-test Test.DAP.Init, Test.DAP.Messages, Test.DAP.Messages.Parser, + Test.DAP.Orphans, Test.Utils, Test.Unit.DAP.RunInTerminal, diff --git a/test/haskell/Main.hs b/test/haskell/Main.hs index e3fe4f06..6cd22a47 100644 --- a/test/haskell/Main.hs +++ b/test/haskell/Main.hs @@ -19,7 +19,9 @@ import System.Environment import Control.Exception import Test.Tasty +#ifdef mingw32_HOST_OS import Test.Tasty.ExpectedFailure +#endif import Test.Tasty.Golden as G import Test.Tasty.Golden.Advanced as G diff --git a/test/haskell/Test/DAP.hs b/test/haskell/Test/DAP.hs index de52ea73..05a09f7f 100644 --- a/test/haskell/Test/DAP.hs +++ b/test/haskell/Test/DAP.hs @@ -6,9 +6,6 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-} --- TODO: We should be using ToJSON/FromJSON for all messages which `dap` has. --- It's just we'd have to derive those instances as orphans or patch upstream. --- (Upstream is better. Otherwise very long compile times) module Test.DAP ( module Test.DAP , module Test.DAP.Init @@ -31,6 +28,10 @@ import System.FilePath import qualified Data.Text as T import Data.Aeson.Types import Test.Tasty.HUnit +import DAP (ScopesArguments(..), StackTraceArguments(..), DisconnectArguments(..)) +import qualified DAP +import Test.DAP.Orphans () +import DAP.Types -------------------------------------------------------------------------------- -- * Highest level DSL @@ -49,52 +50,50 @@ defaultLaunch testDir = , "request" .= ("launch" :: String) ] -defaultSetBreakpoints :: FilePath -> [(Int, Maybe String, Maybe String)] {- use `dap` types... -} -> ResponseCont Value a -> TestDAP a +defaultSetBreakpoints :: FilePath -> [SourceBreakpoint] -> ResponseCont Value a -> TestDAP a defaultSetBreakpoints testDir bps = do - setBreakpointsRequest $ - object - [ "source" .= object - [ "name" .= ("Main.hs" :: String) - , "path" .= T.pack (testDir "Main.hs") - ] - , "breakpoints" .= - [ object - [ "line" .= line - , "logMessage" .= logMessage - , "condition" .= condition - ] - | (line,condition, logMessage) <- bps - ] - , "lines" .= [line | (line,_,_) <- bps ] - , "sourceModified" .= False - ] + setBreakpointsRequest DAP.SetBreakpointsArguments + { DAP.setBreakpointsArgumentsSource = DAP.defaultSource + { DAP.sourceName = Just "Main.hs" + , DAP.sourcePath = Just (T.pack (testDir "Main.hs")) + } + , DAP.setBreakpointsArgumentsBreakpoints = Just bps + , DAP.setBreakpointsArgumentsLines = Just [sourceBreakpointLine bp | bp <- bps] + , DAP.setBreakpointsArgumentsSourceModified = Just False + } defaultSetLineBreakpoints :: FilePath -> [Int] -> ResponseCont Value a -> TestDAP a -defaultSetLineBreakpoints testDir bps = defaultSetBreakpoints testDir [(b, Nothing, Nothing) | b <- bps] +defaultSetLineBreakpoints testDir bps = + defaultSetBreakpoints testDir + [ defaultSourceBreakpoint { sourceBreakpointLine = line } + | line <- bps + ] next, stepIn :: TestDAP () -next = void . sync $ nextRequest Null -stepIn = void . sync $ stepInRequest Null +next = void . sync $ nextRequest @Value @Value Null +stepIn = void . sync $ stepInRequest @Value @Value Null -threads :: TestDAP [Int] +threads :: TestDAP [Thread] threads = do - v <- sync threadsRequest - return $ fromMaybe [] $ parseThreadIds v - -stackTrace :: Int -> TestDAP [Int {-stack frame id-}] -stackTrace threadId = do - v <- sync $ stackTraceRequest $ object [ "threadId" .= threadId ] - return $ fromMaybe [] $ parseFramesIds v - --- | Request scopes and parse (name, expensive) pairs. --- --- todo: please, use FromJSON/ToJSON of `dap` library datatypes. this is madness! -scopes :: Int {- stack frame id -} -> TestDAP [(String, Bool)] -scopes stackFrameId = do - v <- sync $ scopesRequest $ object [ "frameId" .= stackFrameId ] - case parseScopes v of - Nothing -> fail $ "Could not parse scopes from: " ++ show v - Just scs -> pure scs + Response{responseBody=Just ThreadsResponse{threads=ts}} <- sync threadsRequest + return ts + +stackTrace :: Int -> TestDAP [StackFrame] +stackTrace tid = do + Response{responseBody=Just StackTraceResponse{stackFrames=fs}} <- sync $ stackTraceRequest $ + StackTraceArguments + { DAP.stackTraceArgumentsThreadId = tid + , DAP.stackTraceArgumentsStartFrame = Nothing + , DAP.stackTraceArgumentsLevels = Nothing + , DAP.stackTraceArgumentsFormat = Nothing + } + return fs + +scopes :: Int {- stack frame id -} -> TestDAP [Scope] +scopes frameId = do + Response{responseBody=Just ScopesResponse{scopes=scs}} <- sync $ scopesRequest $ + ScopesArguments { DAP.scopesArgumentsFrameId = frameId } + return scs configurationDone :: ResponseCont Value a -> TestDAP a configurationDone = configurationDoneRequest Nothing @@ -104,7 +103,7 @@ configurationDone = configurationDoneRequest Nothing -------------------------------------------------------------------------------- -- | Register handler that will reply to runInTerminal reverse request -handleRunInTerminal :: ResponseCont (Maybe (H.HashMap T.Text T.Text), [T.Text]) (a, Int) +handleRunInTerminal :: AsyncCont (Maybe (H.HashMap T.Text T.Text), [T.Text]) (a, Int) -- ^ Continuation receives async with args of runInTerm req. -- -- - Waiting means block waiting for reverse request to be received @@ -141,31 +140,30 @@ defaultHitBreakpoint testDir line = do ctx <- ask liftIO $ mapConcurrently_ (`runTestDAP` ctx) - [ do _ <- waitFiltering Event "initialized" + [ do waitFiltering_ EventTy "initialized" _ <- sync $ defaultSetLineBreakpoints testDir [line] _ <- sync $ configurationDone - _ <- assertStoppedLocation "breakpoint" line + _ <- assertStoppedLocation DAP.StoppedEventReasonBreakpoint line return () , void $ sync $ defaultLaunch testDir ] disconnect :: TestDAP () disconnect = do - -- wait for disconnect response - r <- sync $ disconnectRequest $ Just $ object - [ "restart" .= False - , "terminateDebuggee" .= True - , "suspendDebuggee" .= False - ] - liftIO $ assertBool "disconnect response should indicate success" $ - fromMaybe False $ parseMaybe (withObject "disconnect response" $ \o -> o .: "success") r + Response{responseSuccess} <- sync $ disconnectRequest @_ @Value $ Just + DisconnectArguments + { DAP.disconnectArgumentsRestart = False + , DAP.disconnectArgumentsTerminateDebuggee = True + , DAP.disconnectArgumentsSuspendDebuggee = False + } + liftIO $ assertBool "disconnect response should indicate success" responseSuccess return () -------------------------------------------------------------------------------- -- ** Convenience methods (based on vscode-debugadapter-node/testSupport) -------------------------------------------------------------------------------- -launch :: Value {-^ Launch args -} -> ResponseCont Value a {- LaunchResponse, todo: use `dap` types w Aeson -} -> TestDAP a +launch :: Value {-^ Launch args -} -> ResponseCont Value a -> TestDAP a launch args = runContT $ ContT initializeRequest >>= liftIO . wait @@ -173,16 +171,15 @@ launch args = runContT $ configurationSequence :: ResponseCont Value a -> TestDAP a configurationSequence k = do - _ <- waitFiltering Event "initialized" + waitFiltering_ EventTy "initialized" configurationDone k -- | Assert that a "stopped" event with the given reason is received -assertStoppedLocation :: String -> Int -> TestDAP () +assertStoppedLocation :: DAP.StoppedEventReason -> Int -> TestDAP () assertStoppedLocation reason expectedLine = do - -- TODO: Timeouts on waiting!! - v <- waitFiltering Event "stopped" + Event{eventBody = Just StoppedEvent{stoppedEventReason}} <- waitFiltering EventTy "stopped" liftIO $ - assertBool "" (maybe False (==reason) (parseStoppedEventReason v)) + assertBool "Stopped reason matches expected reason" (stoppedEventReason == reason) -- TODO: Validate expected line and potentially path too -- (see assertStoppedLocation in debugClient.ts) @@ -190,8 +187,8 @@ assertStoppedLocation reason expectedLine = do -- events (until any other event is found) contain a certain string assertOutput :: T.Text -> TestDAP () assertOutput expected = do - events <- waitAccumulating Event "output" - let outputs = mapMaybe parseOutput events + events <- waitAccumulating EventTy "output" + let outputs = map (outputEventOutput . fromMaybe (error "assertOutput:fromMaybe") . eventBody) events liftIO $ assertBool ("assertOutput: expecting " ++ show expected ++ " but got " ++ show outputs) @@ -210,30 +207,37 @@ assertFullOutput expected = do -- * Waiting for messages -------------------------------------------------------------------------------- -data MsgType = Event | Response | ReverseRequest +data MsgType = EventTy | ResponseTy | ReverseRequestTy + deriving Show msgChan :: MsgType -> TestDAPClientContext -> TChan Value msgChan ty TestDAPClientContext{..} = case ty of - Event -> clientEvents - Response -> clientResponses - ReverseRequest -> clientReverseRequests + EventTy -> clientEvents + ResponseTy -> clientResponses + ReverseRequestTy -> clientReverseRequests msgMatch :: MsgType -> String -> MessageMatch msgMatch ty s = case ty of - Event -> eventMatch s - Response -> responseMatch s - ReverseRequest -> reverseRequestMatch s + EventTy -> eventMatch s + ResponseTy -> responseMatch s + ReverseRequestTy -> reverseRequestMatch s + +waitFiltering_ :: MsgType -> String -> TestDAP () +waitFiltering_ ty s = void $ waitFiltering @Value ty s -- | Drop messages of the given type until a message with the given -- eventType/command is found. The matching message is returned. -waitFiltering :: MsgType -> String -> TestDAP Value +-- FIXME: Timeouts on waiting, to avoid hanging forever in the testsuite!! +waitFiltering :: forall a. FromJSON a => MsgType -> String -> TestDAP a waitFiltering ty s = do ch <- asks (msgChan ty) let mm = msgMatch ty s let loop = do v <- atomically $ readTChan ch -- block waiting for input if messageMatchMatches mm v - then return v + then case fromJSON @a v of + Error e -> error $ "waitFiltering: Failed to parse message MATCHING " ++ s ++ ":" ++ show ty ++ " with error: " ++ e ++ "\nFull message was: " ++ show v + Success x -> return x else loop liftIO loop @@ -242,7 +246,7 @@ waitFiltering ty s = do -- -- The non-matching message is not consumed, nor returned, and will be kept in -- the messages buffer. -waitAccumulating :: MsgType -> String -> TestDAP [Value] +waitAccumulating :: forall a. FromJSON a => MsgType -> String -> TestDAP [a] waitAccumulating ty s = do ch <- asks (msgChan ty) let mm = msgMatch ty s @@ -250,7 +254,9 @@ waitAccumulating ty s = do r <- atomically $ do v <- readTChan ch if messageMatchMatches mm v - then pure (Just v) + then case fromJSON @a v of + Error e -> error $ "waitAccumulating: Failed to parse MATCHING message body with error: " ++ e ++ "\nFull message was: " ++ show v + Success x -> pure (Just x) else Nothing <$ unGetTChan ch v case r of Nothing -> return (reverse acc) @@ -287,7 +293,7 @@ launchRequest, attachRequest, restartRequest, setBreakpointsRequest, gotoRequest, pauseRequest, stackTraceRequest, scopesRequest, variablesRequest, setVariableRequest, sourceRequest, modulesRequest, evaluateRequest, disassembleRequest, stepInTargetsRequest, gotoTargetsRequest, completionsRequest, - exceptionInfoRequest, readMemoryRequest, writeMemoryRequest :: Value -> ResponseCont Value a -> TestDAP a + exceptionInfoRequest, readMemoryRequest, writeMemoryRequest :: (ToJSON a, FromJSON b) => a -> ResponseCont b r -> TestDAP r launchRequest = requestWithArgs "launch" attachRequest = requestWithArgs "attach" @@ -322,7 +328,7 @@ exceptionInfoRequest = requestWithArgs "exceptionInfo" readMemoryRequest = requestWithArgs "readMemory" writeMemoryRequest = requestWithArgs "writeMemory" -terminateRequest, disconnectRequest :: Maybe Value -> ResponseCont Value a -> TestDAP a +terminateRequest, disconnectRequest :: (ToJSON a, FromJSON b) => Maybe a -> ResponseCont b r -> TestDAP r terminateRequest = customRequest "terminate" -- example: -- object @@ -335,10 +341,10 @@ terminateRequest = customRequest "terminate" -- waitEventFiltering $ eventMatch "terminated" disconnectRequest = customRequest "disconnect" -threadsRequest :: ResponseCont Value a -> TestDAP a -threadsRequest = customRequest "threads" Nothing +threadsRequest :: FromJSON b => ResponseCont b r -> TestDAP r +threadsRequest = customRequest "threads" (Nothing :: Maybe ()) -------------------------------------------------------------------------------- -requestWithArgs :: String -> Value -> ResponseCont Value a -> TestDAP a +requestWithArgs :: (ToJSON a, FromJSON b) => String -> a -> ResponseCont b r -> TestDAP r requestWithArgs command args = customRequest command (Just args) -- example: respondWithBody revReqNum [ "shellProcessId" .= ... ] @@ -351,7 +357,7 @@ respondWithBody seqNum command body = , "body" .= body ] -------------------------------------------------------------------------------- -customRequest :: String -> Maybe Value -> ResponseCont Value a -> TestDAP a +customRequest :: (ToJSON a, FromJSON b) => String -> Maybe a -> ResponseCont b r -> TestDAP r customRequest command args = do send $ [ "type" .= ("request" :: String) diff --git a/test/haskell/Test/DAP/Init.hs b/test/haskell/Test/DAP/Init.hs index b6397e20..4bbaf979 100644 --- a/test/haskell/Test/DAP/Init.hs +++ b/test/haskell/Test/DAP/Init.hs @@ -9,7 +9,6 @@ module Test.DAP.Init where ---------------------------------------------------------------------------- import Data.Maybe import Data.List (isInfixOf) -import Control.Concurrent import Control.Exception hiding (handle) import qualified Control.Exception as E import Network.Run.TCP @@ -30,8 +29,9 @@ import Data.Aeson.Types import Test.Tasty.HUnit (assertFailure) import DAP.Server (readPayload) import qualified Control.Monad.Catch -import Test.DAP.Messages.Parser import Test.Utils (withHermeticDir) +import DAP.Types (OutputEvent (..)) +import Test.DAP.Messages.Parser -------------------------------------------------------------------------------- -- * Launch the DAP server process (what we're testing) @@ -82,12 +82,12 @@ startTestDAPServer testDir flags = do -- | Prefer this to startTestDAPServer withTestDAPServer :: FilePath -> [String] -> (FilePath -> TestDAPServer -> IO a) -> IO a -withTestDAPServer dir flags check = do +withTestDAPServer dir flags check' = do keep_tmp_dirs <- maybe False read <$> lookupEnv "KEEP_TEMP_DIRS" withHermeticDir keep_tmp_dirs dir $ \test_dir -> bracket (startTestDAPServer test_dir flags) testDAPServerCleanup - (check test_dir) + (check' test_dir) getAvailablePort :: IO Int getAvailablePort = @@ -150,12 +150,12 @@ handleServerTestDAP = do payload <- nextPayload liftIO $ case parseMaybe parseType payload of Just "event" -> do - let mtxt = parseOutput payload + let mtxt = fromJSON @(Event OutputEvent) payload atomically $ do writeTChan clientEvents payload case mtxt of - Just txt -> modifyTVar' clientFullOutput (txt:) - Nothing -> pure () + Success (Event _ (Just txt)) -> modifyTVar' clientFullOutput (outputEventOutput txt:) + _ -> pure () Just "response" -> -- Fail immediately if the server reports failure, even if the test -- is blocked waiting for some other specific message. diff --git a/test/haskell/Test/DAP/Messages.hs b/test/haskell/Test/DAP/Messages.hs index 72af7a2b..30fe943d 100644 --- a/test/haskell/Test/DAP/Messages.hs +++ b/test/haskell/Test/DAP/Messages.hs @@ -18,6 +18,7 @@ import Data.IORef import DAP.Utils import Control.Concurrent.STM import qualified Data.Text as T +import Test.DAP.Messages.Parser ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- @@ -56,16 +57,17 @@ newtype TestDAP a = TestDAP { runTestDAP :: TestDAPClientContext -> IO a } -- * Message primitives -------------------------------------------------------------------------------- -type ResponseCont b a = Async b -> TestDAP a +type AsyncCont b a = Async b -> TestDAP a +type ResponseCont b a = Async (Response b) -> TestDAP a -- | Run an action with an Async in the continuation synchronously by simply -- waiting for the response. -sync :: (ResponseCont b b -> TestDAP b) -> TestDAP b +sync :: (ResponseCont b (Response b) -> TestDAP (Response b)) -> TestDAP (Response b) sync k = k (liftIO . wait) -- | Send message with next sequence number and expect a response (response -- value is given as async in continuation) -send :: [Pair] -> ResponseCont Value a -> TestDAP a +send :: forall b r. FromJSON b => [Pair] -> ResponseCont b r -> TestDAP r send message k = do ctx@TestDAPClientContext{..} <- ask seqNum <- liftIO $ atomicModifyIORef' clientNextSeqRef (\n -> (n + 1, n)) @@ -74,7 +76,10 @@ send message k = do encodeBaseProtocolMessage (object ("seq" .= seqNum : filter ((/= "seq") . fst) message)) withAsync (runTestDAP waitForResponse ctx) $ \v -> - runTestDAP (k v) ctx + runTestDAP (k $ (\r -> unwrap (fromJSON @(Response b) r) r) <$> v) ctx + where + unwrap (Error e) r = error ("send: Parsing 'Response' failed with " ++ show e ++ " for message: " ++ show r) + unwrap (Success x) _ = x -- | Reply to reverse request of given seq number reply :: Int -> [Pair] -> TestDAP () diff --git a/test/haskell/Test/DAP/Messages/Parser.hs b/test/haskell/Test/DAP/Messages/Parser.hs index c6cfce95..37d72f79 100644 --- a/test/haskell/Test/DAP/Messages/Parser.hs +++ b/test/haskell/Test/DAP/Messages/Parser.hs @@ -5,8 +5,7 @@ import Data.Aeson import Data.Aeson.Types import Data.Aeson.KeyMap import qualified Data.HashMap.Strict as H -import qualified Data.Foldable as F -import qualified Data.Text as T +import Test.DAP.Orphans () -------------------------------------------------------------------------------- -- * MessageMatch @@ -56,53 +55,23 @@ reverseRequestMatch commandExpected = -------------------------------------------------------------------------------- -- * Message parsers -------------------------------------------------------------------------------- --- TODO: ALL OF THESE SHOULD BE AUTOMATICALLY FROMJSON FOR RESPONSE TYPES!!!! -parseThreadIds :: Value -> Maybe [Int] -parseThreadIds = parseMaybe $ withObject "threads response" $ \o -> do - body <- o .: "body" - withObject "threads body" (\b -> do - threads <- b .: "threads" - withArray "threads" (\arr -> do - mapM (withObject "thread" (\t -> t .: "id")) $ F.toList arr - ) threads - ) body +data Response a = Response + { responseSuccess :: Bool + , responseBody :: Maybe a } -parseFramesIds :: Value -> Maybe [Int] -parseFramesIds = parseMaybe $ withObject "stackTrace response" $ \o -> do - body <- o .: "body" - withObject "stackTrace body" (\b -> do - frames <- b .: "stackFrames" - withArray "stackFrames" (\arr -> do - mapM (withObject "frame" (\f -> f .: "id")) $ F.toList arr - ) frames - ) body +data Event a = Event + { eventEvent :: String + , eventBody :: Maybe a } -parseScopes :: Value -> Maybe [(String, Bool)] -parseScopes = parseMaybe $ withObject "scopes response" $ \o -> do - body <- o .: "body" - withObject "scopes body" (\b -> do - scopes <- b .: "scopes" - withArray "scopes" (\arr -> - mapM - (withObject "scope" (\s -> do - name <- s .: "name" - expensive <- s .: "expensive" - pure (name, expensive) - )) - (F.toList arr) - ) scopes - ) body - -parseStoppedEventReason :: Value -> Maybe String -parseStoppedEventReason = parseMaybe $ withObject "stopped event" $ \o -> do - body <- o .: "body" - String reason <- body .: "reason" - pure $ T.unpack reason - -parseOutput :: Value -> Maybe T.Text -parseOutput = parseMaybe $ withObject "event" $ \ o -> do - body <- o .: "body" - String output <- body .: "output" - pure output +instance FromJSON a => FromJSON (Response a) where + parseJSON = withObject "parsing from .body" $ \o -> do + body <- o .:? "body" + success <- o .: "success" + pure $ Response success body +instance FromJSON a => FromJSON (Event a) where + parseJSON = withObject "parsing from .body" $ \o -> do + body <- o .:? "body" + event <- o .: "event" + pure $ Event event body diff --git a/test/haskell/Test/DAP/Orphans.hs b/test/haskell/Test/DAP/Orphans.hs new file mode 100644 index 00000000..da155626 --- /dev/null +++ b/test/haskell/Test/DAP/Orphans.hs @@ -0,0 +1,116 @@ +{-# OPTIONS_GHC -Wno-orphans #-} +{-# LANGUAGE OverloadedStrings #-} +module Test.DAP.Orphans () where + +import Data.Aeson +import DAP.Utils (genericParseJSONWithModifier, genericToJSONWithModifier) +import DAP + +-------------------------------------------------------------------------------- +-- * FromJSON instances for response body types (client parses server responses) +-------------------------------------------------------------------------------- + +instance FromJSON Breakpoint where + parseJSON = genericParseJSONWithModifier + +instance FromJSON PresentationHint where + parseJSON = genericParseJSONWithModifier + +instance FromJSON StackFrame where + parseJSON = genericParseJSONWithModifier + +instance FromJSON Scope where + parseJSON = genericParseJSONWithModifier + +instance FromJSON ScopePresentationHint where + parseJSON = withText "ScopePresentationHint" $ \t -> pure $ case t of + "arguments" -> ScopePresentationHintArguments + "locals" -> ScopePresentationHintLocals + "registers" -> ScopePresentationHintRegisters + other -> ScopePresentationHint other + +instance FromJSON StoppedEventReason where + parseJSON = withText "StoppedEventReason" $ \t -> case t of + "step" -> pure StoppedEventReasonStep + "breakpoint" -> pure StoppedEventReasonBreakpoint + "exception" -> pure StoppedEventReasonException + "pause" -> pure StoppedEventReasonPause + "entry" -> pure StoppedEventReasonEntry + "goto" -> pure StoppedEventReasonGoto + "function breakpoint" -> pure StoppedEventReasonFunctionBreakpoint + "data breakpoint" -> pure StoppedEventReasonDataBreakpoint + "instruction breakpoint" -> pure StoppedEventReasonInstructionBreakpoint + other -> fail $ "Unknown StoppedEventReason: " ++ show other + +instance FromJSON StoppedEvent where + parseJSON = genericParseJSONWithModifier + +instance FromJSON OutputEventCategory where + parseJSON = withText "OutputEventCategory" $ \t -> pure $ case t of + "console" -> OutputEventCategoryConsole + "important" -> OutputEventCategoryImportant + "stdout" -> OutputEventCategoryStdout + "stderr" -> OutputEventCategoryStderr + "telemetry" -> OutputEventCategoryTelemetry + other -> OutputEventCategory other + +instance FromJSON ThreadsResponse where + parseJSON = withObject "fromJSON:ThreadsResponse" $ \o -> do + ts <- o .: "threads" + pure $ ThreadsResponse ts + +instance FromJSON StackTraceResponse where + parseJSON = withObject "fromJSON:StackTraceResponse" $ \o -> do + fs <- o .: "stackFrames" + tot <- o .:? "totalFrames" + pure $ StackTraceResponse fs tot + +instance FromJSON ScopesResponse where + parseJSON = withObject "fromJSON:ScopesResponse" $ \o -> do + scs <- o .: "scopes" + pure $ ScopesResponse scs + +instance FromJSON EventGroup where + parseJSON = genericParseJSONWithModifier + +instance FromJSON OutputEvent where + parseJSON = genericParseJSONWithModifier + +-------------------------------------------------------------------------------- +-- * ToJSON instances for request argument types (client sends requests) +-------------------------------------------------------------------------------- + +instance ToJSON PathFormat where + toJSON Path = "path" + toJSON URI = "uri" + toJSON (PathFormat t) = toJSON t + +instance ToJSON InitializeRequestArguments where + toJSON = genericToJSON defaultOptions { omitNothingFields = True } + +instance ToJSON SetBreakpointsArguments where + toJSON = genericToJSONWithModifier + +instance ToJSON SourceBreakpoint where + toJSON = genericToJSONWithModifier + +instance ToJSON SteppingGranularity where + toJSON = genericToJSONWithModifier + +instance ToJSON NextArguments where + toJSON = genericToJSONWithModifier + +instance ToJSON StackFrameFormat where + toJSON = genericToJSONWithModifier + +instance ToJSON StackTraceArguments where + toJSON = genericToJSONWithModifier + +instance ToJSON ScopesArguments where + toJSON = genericToJSONWithModifier + +instance ToJSON ValueFormat where + toJSON = genericToJSONWithModifier + +instance ToJSON DisconnectArguments where + toJSON = genericToJSONWithModifier diff --git a/test/haskell/Test/Unit/DAP/LogMessage.hs b/test/haskell/Test/Unit/DAP/LogMessage.hs index e76c8471..7ba7b0b5 100644 --- a/test/haskell/Test/Unit/DAP/LogMessage.hs +++ b/test/haskell/Test/Unit/DAP/LogMessage.hs @@ -1,18 +1,16 @@ -- | 'logMessage'/'logPoints' tests -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE CPP #-} -module Test.Unit.DAP.LogMessage (logMessageTests,setupBreakpoints) where +module Test.Unit.DAP.LogMessage (logMessageTests, setupBreakpoints, mkBP) where import Control.Monad.IO.Class (liftIO) import Test.DAP import Test.Tasty import Test.Tasty.HUnit -import Test.Utils -import Test.DAP.Messages.Parser -import Data.Maybe (mapMaybe) +import Data.Maybe (fromMaybe) import qualified Data.Text as T +import DAP (SourceBreakpoint(..), defaultSourceBreakpoint, OutputEvent (..)) +import Test.DAP.Messages.Parser (Event(..)) logMessageTests :: TestTree logMessageTests = @@ -44,13 +42,13 @@ logMessageTests = simpleTest :: String -> String -> IO () simpleTest tmpl expected = - logMessageTestSetup [] [(18,Nothing,Just tmpl)] $ do + logMessageTestSetup [] [mkBP 18 Nothing (Just tmpl)] $ do assertOutput (T.pack expected) conditionTest :: IO () conditionTest = logMessageTestSetup [] bps $ do - events <- waitAccumulating Event "output" - let output = T.concat $ mapMaybe parseOutput events + events <- waitAccumulating EventTy "output" + let output = T.concat $ map (outputEventOutput . fromMaybe (error "conditionTest:fromMaybe") . eventBody) events liftIO $ assertBool "logged when False" $ not $ "DO NOT LOG" `T.isInfixOf` output @@ -58,11 +56,18 @@ conditionTest = logMessageTestSetup [] bps $ do "DO LOG" `T.isInfixOf` output return () where - bps = [(18,Just "False", Just "DO NOT LOG") - ,(19,Just "True", Just "DO LOG") + bps = [ mkBP 18 (Just "False") (Just "DO NOT LOG") + , mkBP 19 (Just "True") (Just "DO LOG") ] -logMessageTestSetup :: [String] -> [(Int, Maybe String, Maybe String)] -> TestDAP a -> IO () +mkBP :: Int -> Maybe String -> Maybe String -> SourceBreakpoint +mkBP line cond logMsg = defaultSourceBreakpoint + { sourceBreakpointLine = line + , sourceBreakpointCondition = T.pack <$> cond + , sourceBreakpointLogMessage = T.pack <$> logMsg + } + +logMessageTestSetup :: [String] -> [SourceBreakpoint] -> TestDAP a -> IO () logMessageTestSetup flags bps check = do withTestDAPServer "test/unit/T113" flags $ \test_dir server-> do @@ -73,10 +78,10 @@ logMessageTestSetup flags bps check = do -- | Let's you setup multiple breakpoints, with conditions and logs. -- Any events after configurationDone are left unconsumed. -setupBreakpoints :: FilePath -> [(Int, Maybe String, Maybe String)] -> TestDAP () +setupBreakpoints :: FilePath -> [SourceBreakpoint] -> TestDAP () setupBreakpoints testDir bps = do _ <- sync $ defaultLaunch testDir - _ <- waitFiltering Event "initialized" + waitFiltering_ EventTy "initialized" _ <- sync $ defaultSetBreakpoints testDir bps _ <- sync configurationDone pure () diff --git a/test/haskell/Test/Unit/DAP/Persistent.hs b/test/haskell/Test/Unit/DAP/Persistent.hs index 3d60e233..250574c9 100644 --- a/test/haskell/Test/Unit/DAP/Persistent.hs +++ b/test/haskell/Test/Unit/DAP/Persistent.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE CPP #-} {-# LANGUAGE ViewPatterns #-} module Test.Unit.DAP.Persistent (persistentTests) where @@ -10,10 +7,9 @@ import qualified Data.Text as T import Test.DAP import Test.Tasty import Test.Tasty.HUnit -import Test.Tasty.ExpectedFailure import Test.Utils -import Test.Unit.DAP.LogMessage (setupBreakpoints) -import qualified System.Process as P +import Test.Unit.DAP.LogMessage (setupBreakpoints, mkBP) +import qualified DAP import Control.Exception (bracket) import System.Environment (lookupEnv) @@ -60,7 +56,7 @@ withServerTestSetup' dirs0@(d0:_) flags check = do go keep acc (d:ds) server = withHermeticDir keep d $ \test_dir -> go keep (test_dir:acc) ds server -withBreakPoints :: [(Int, Maybe String, Maybe String)] -> TestDAP a -> (FilePath, TestDAPServer) -> IO () +withBreakPoints :: [DAP.SourceBreakpoint] -> TestDAP () -> (FilePath, TestDAPServer) -> IO () withBreakPoints bps check (test_dir,server) = withTestDAPServerClient False server $ do () <- setupBreakpoints test_dir bps @@ -101,24 +97,24 @@ testParallel' (unzip -> (dirs,bps)) flags k simpleSessions :: Int -> (FilePath, TestDAPServer) -> [IO ()] simpleSessions n x = - [withBreakPoints [(18,Nothing,Just msg)] check x + [withBreakPoints [mkBP 18 Nothing (Just msg)] check x | i <- [(0::Int)..n] , let msg = "MSG_" ++ show i , let check = do assertOutput (T.pack msg) - waitFiltering Event "exited" + waitFiltering_ EventTy "exited" ] simpleSessions' :: [Int] -> ([FilePath], TestDAPServer) -> [IO ()] simpleSessions' ls (dirs,server) = - [ withBreakPoints [ (line,Nothing,Just msg) - , (line+1,Nothing,Nothing) + [ withBreakPoints [ mkBP line Nothing (Just msg) + , mkBP (line+1) Nothing Nothing ] check (d,server) | (i,(line,d)) <- zip [(0::Int)..] $ zip ls dirs , let msg = "MSG_" ++ show i , let check = do assertOutput (T.pack msg) - waitFiltering Event "stopped" + waitFiltering_ EventTy "stopped" ] diff --git a/test/haskell/Test/Unit/DAP/RunInTerminal.hs b/test/haskell/Test/Unit/DAP/RunInTerminal.hs index 83164bca..c0354848 100644 --- a/test/haskell/Test/Unit/DAP/RunInTerminal.hs +++ b/test/haskell/Test/Unit/DAP/RunInTerminal.hs @@ -11,8 +11,9 @@ import System.IO import Test.DAP import Test.Tasty import Test.Tasty.HUnit +#ifdef mingw32_HOST_OS import Test.Tasty.ExpectedFailure -import Test.Utils +#endif import qualified Data.HashMap.Strict as H import qualified Data.Text as T import qualified System.Process as P @@ -73,7 +74,7 @@ runInTerminal1 flags = do liftIO $ hPutStrLn rit_in secret_in -- Only after writing should we receive the next "stopped" event - _ <- waitFiltering Event "stopped" + waitFiltering_ EventTy "stopped" -- To next line, which should be the "putStrLn" after the "getLine" next diff --git a/test/haskell/Test/Unit/DAP/Scopes.hs b/test/haskell/Test/Unit/DAP/Scopes.hs index 9656b36f..f1b6bf82 100644 --- a/test/haskell/Test/Unit/DAP/Scopes.hs +++ b/test/haskell/Test/Unit/DAP/Scopes.hs @@ -6,8 +6,12 @@ import Control.Monad.IO.Class (liftIO) import Test.DAP import Test.Tasty import Test.Tasty.HUnit +#ifdef mingw32_HOST_OS import Test.Tasty.ExpectedFailure -import Test.Utils (withHermeticDir) +#endif +import DAP (threadId, stackFrameId, scopeName, scopeExpensive) +import Data.List (find) +import qualified Data.Text as T scopesTests :: TestTree scopesTests = @@ -26,11 +30,11 @@ scopesExpensiveTest = withTestDAPServer "test/unit/T44" ["--disable-ipe-backtrac _ <- defaultHitBreakpoint test_dir 6 - threadId:_ <- threads - frameId:_ <- stackTrace threadId - scps <- scopes frameId + thread:_ <- threads + frame:_ <- stackTrace (threadId thread) + scps <- scopes (stackFrameId frame) - let lookupExpensive n = lookup n scps + let lookupExpensive n = scopeExpensive <$> find ((== T.pack n) . scopeName) scps liftIO $ assertEqual "Locals should not be expensive" (Just False) (lookupExpensive "Locals") liftIO $ assertEqual "Module should be expensive" (Just True) (lookupExpensive "Module") liftIO $ assertEqual "Globals should be expensive" (Just True) (lookupExpensive "Globals")