diff --git a/package.yaml b/package.yaml index 5f4548dc..d81947a8 100644 --- a/package.yaml +++ b/package.yaml @@ -45,6 +45,7 @@ dependencies: - yaml - casing - temporary + - ghc ghc-options: -Wall -threaded diff --git a/sensei.cabal b/sensei.cabal index 061e932e..e7c7f175 100644 --- a/sensei.cabal +++ b/sensei.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.36.0. +-- This file has been generated from package.yaml by hpack version 0.37.0. -- -- see: https://github.com/sol/hpack @@ -25,6 +25,7 @@ executable seito Client Config EventQueue + GHC.Diagnostic HTTP Imports Input @@ -61,6 +62,7 @@ executable seito , directory , filepath , fsnotify ==0.4.* + , ghc , http-client >=0.5.0 , http-types , mtl @@ -83,6 +85,7 @@ executable sensei Client Config EventQueue + GHC.Diagnostic HTTP Imports Input @@ -119,6 +122,7 @@ executable sensei , directory , filepath , fsnotify ==0.4.* + , ghc , http-client >=0.5.0 , http-types , mtl @@ -141,6 +145,7 @@ executable sensei-web Client Config EventQueue + GHC.Diagnostic HTTP Imports Input @@ -177,6 +182,7 @@ executable sensei-web , directory , filepath , fsnotify ==0.4.* + , ghc , http-client >=0.5.0 , http-types , mtl @@ -200,6 +206,7 @@ test-suite spec Client Config EventQueue + GHC.Diagnostic HTTP Imports Input @@ -214,6 +221,7 @@ test-suite spec ClientSpec ConfigSpec EventQueueSpec + GHC.DiagnosticSpec Helper HTTPSpec Language.Haskell.GhciWrapperSpec @@ -254,6 +262,7 @@ test-suite spec , directory , filepath , fsnotify ==0.4.* + , ghc , hspec >=2.9.0 , hspec-contrib >=0.5.2 , hspec-wai diff --git a/src/GHC/Diagnostic.hs b/src/GHC/Diagnostic.hs new file mode 100644 index 00000000..b3c6cddb --- /dev/null +++ b/src/GHC/Diagnostic.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE DeriveAnyClass #-} +module GHC.Diagnostic where + +import Prelude hiding (span) +import Imports hiding (empty) +import GHC.Generics +import Data.Aeson (ToJSON(..), FromJSON(..), decode) +import Data.ByteString.Lazy (fromStrict) + +import GHC.Types.SrcLoc +import GHC.Types.Error hiding (Severity, Diagnostic) +import GHC.Utils.Error hiding (Severity, Diagnostic) +import qualified GHC.Utils.Error as GHC +import GHC.Utils.Outputable + +data Diagnostic = Diagnostic { + version :: String +, ghcVersion :: String +, span :: Maybe Span +, severity :: Severity +, code :: Maybe Int +, message :: [String] +, hints :: [String] +} deriving (Eq, Show, Generic, ToJSON, FromJSON) + +data Span = Span { + file :: FilePath +, start :: Location +, end :: Location +} deriving (Eq, Show, Generic, ToJSON, FromJSON) + +data Location = Location { + line :: Int +, column :: Int +} deriving (Eq, Show, Generic, ToJSON, FromJSON) + +data Severity = Warning | Error + deriving (Eq, Show, Generic, ToJSON, FromJSON) + +parse :: ByteString -> Maybe Diagnostic +parse = decode . fromStrict + +toSrcSpan :: Maybe Span -> SrcSpan +toSrcSpan = \ case + Nothing -> UnhelpfulSpan UnhelpfulNoLocationInfo + Just span -> mkSrcSpan (loc span.start) (loc span.end) + where + loc l = mkSrcLoc (fromString span.file) l.line l.column + +format :: Diagnostic -> ByteString +format = diagnosticToSDoc >>> showSDocUnsafe >>> encodeUtf8 + + +diagnosticToSDoc :: Diagnostic -> SDoc +diagnosticToSDoc diagnostic = result $+$ blankLine $+$ blankLine + where + result :: SDoc + result = mkLocMessageWarningGroups printWarningGroups msg_class span formatted + + printWarningGroups :: Bool + -- printWarningGroups = True + printWarningGroups = False + + formatted :: SDoc + -- formatted = updSDocContext (\_ -> ctx) messageWithHints + formatted = messageWithHints + + msg_class :: MessageClass + msg_class = MCDiagnostic severity reason code + + reason :: ResolvedDiagnosticReason + -- reason = undefined -- msg.errMsgReason + reason = ResolvedDiagnosticReason ErrorWithoutFlag + + -- style :: PprStyle + -- style = defaultErrStyle + + -- opts :: DiagOpts + -- opts = emptyDiagOpts -- FIXME + + -- ctx :: SDocContext + -- ctx = (diag_ppr_ctx opts) { sdocStyle = style } + + span :: SrcSpan + span = toSrcSpan diagnostic.span + + severity :: GHC.Severity + severity = case diagnostic.severity of + Warning -> SevWarning + Error -> SevError + + + code :: Maybe DiagnosticCode + code = DiagnosticCode "GHC" . fromIntegral <$> diagnostic.code + + hints :: [SDoc] + hints = map verbose diagnostic.hints + + message :: SDoc + message = bulleted $ map verbose diagnostic.message + + messageWithHints :: SDoc + messageWithHints = case hints of + [] -> message + [h] -> message $$ hang (text "Suggested fix:") 2 h + hs -> message $$ hang (text "Suggested fixes:") 2 (bulleted hs) + +bulleted :: [SDoc] -> SDoc +bulleted = formatBulleted . mkDecorated + +verbose :: String -> SDoc +verbose = foldr ($+$) empty . map text . lines + +-- FIXME: check performance impact diff --git a/src/HTTP.hs b/src/HTTP.hs index 333e0832..e52cf5e4 100644 --- a/src/HTTP.hs +++ b/src/HTTP.hs @@ -11,9 +11,10 @@ module HTTP ( #endif ) where -import Imports hiding (encodeUtf8) +import Imports hiding (strip, encodeUtf8) import System.Directory +import Data.Aeson (ToJSON(..), encode) import qualified Data.ByteString.Lazy as L import Data.Text.Lazy.Encoding (encodeUtf8) import Network.Wai @@ -22,6 +23,7 @@ import Network.Wai.Handler.Warp (runSettingsSocket, defaultSettings) import Network.Socket import qualified Trigger +import GHC.Diagnostic socketName :: FilePath -> String socketName dir = dir ".sensei.sock" @@ -35,8 +37,8 @@ newSocket = socket AF_UNIX Stream 0 withSocket :: (Socket -> IO a) -> IO a withSocket = bracket newSocket close -withServer :: FilePath -> IO (Trigger.Result, String) -> IO a -> IO a -withServer dir trigger = withApplication dir (app trigger) +withServer :: FilePath -> IO (Trigger.Result, String, [Diagnostic]) -> IO a -> IO a +withServer dir = withApplication dir . app withApplication :: FilePath -> Application -> IO a -> IO a withApplication dir application action = do @@ -59,8 +61,12 @@ withThread asyncAction action = do takeMVar mvar return r -app :: IO (Trigger.Result, String) -> Application -app trigger request respond = trigger >>= textPlain +app :: IO (Trigger.Result, String, [Diagnostic]) -> Application +app getLastResult request respond = case pathInfo request of + ["diagnostics"] -> do + (_, _, diagnostics) <- getLastResult + respond $ json diagnostics + _ -> getLastResult >>= textPlain where color :: Either ByteString Bool color = case join $ lookup "color" $ queryString request of @@ -69,8 +75,8 @@ app trigger request respond = trigger >>= textPlain Just "true" -> Right True Just value -> Left $ "invalid value for color: " <> urlEncode True value - textPlain :: (Trigger.Result, FilePath) -> IO ResponseReceived - textPlain (result, xs) = case color of + textPlain :: (Trigger.Result, FilePath, [Diagnostic]) -> IO ResponseReceived + textPlain (result, xs, _diagnostics) = case color of Left err -> respond $ responseLBS status400 [(hContentType, "text/plain")] (L.fromStrict err) Right c -> respond $ responseLBS status [(hContentType, "text/plain")] (encodeUtf8 . fromString $ strip xs) where @@ -84,6 +90,12 @@ app trigger request respond = trigger >>= textPlain Trigger.Failure -> status500 Trigger.Success -> status200 +json :: ToJSON a => a -> Response +json value = responseLBS + status200 + [("Content-Type", "application/json")] + (encode value) + -- | -- Remove terminal sequences. stripAnsi :: String -> String diff --git a/src/Imports.hs b/src/Imports.hs index afbeb70d..2305da21 100644 --- a/src/Imports.hs +++ b/src/Imports.hs @@ -11,7 +11,7 @@ import Data.Functor as Imports ((<&>)) import Data.Bifunctor as Imports import Data.Char as Imports import Data.Either as Imports -import Data.List as Imports +import Data.List as Imports hiding (span) import Data.Maybe as Imports import Data.String as Imports import Data.ByteString.Char8 as Imports (ByteString, pack, unpack) @@ -26,6 +26,10 @@ import Control.Monad.IO.Class as Imports import System.IO (Handle) import GHC.IO.Handle.Internals (wantReadableHandle_) +import Data.Version as Imports (Version(..), showVersion, makeVersion) +import qualified Data.Version as Version +import Text.ParserCombinators.ReadP + import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -54,4 +58,12 @@ encodeUtf8 :: String -> ByteString encodeUtf8 = T.encodeUtf8 . T.pack decodeUtf8 :: ByteString -> String -decodeUtf8 = T.unpack . T.decodeUtf8 +decodeUtf8 = T.unpack . T.decodeUtf8Lenient + +strip :: String -> String +strip = reverse . dropWhile isSpace . reverse . dropWhile isSpace + +parseVersion :: String -> Maybe Version +parseVersion xs = case [v | (v, "") <- readP_to_S Version.parseVersion xs] of + [v] -> Just v + _ -> Nothing diff --git a/src/Language/Haskell/GhciWrapper.hs b/src/Language/Haskell/GhciWrapper.hs index 64e92f27..e075785b 100644 --- a/src/Language/Haskell/GhciWrapper.hs +++ b/src/Language/Haskell/GhciWrapper.hs @@ -14,7 +14,7 @@ module Language.Haskell.GhciWrapper ( , reload #ifdef TEST -, extractReloadStatus +, extractReloadDiagnostics , extractNothing #endif ) where @@ -29,8 +29,10 @@ import System.Process hiding (createPipe) import System.Exit (exitFailure) import Util (isWritableByOthers) +import ReadHandle hiding (getResult) import qualified ReadHandle -import ReadHandle (ReadHandle, toReadHandle, Extract(..), partialMessageStartsWithOneOf) +import GHC.Diagnostic (Diagnostic) +import qualified GHC.Diagnostic as Diagnostic data Config = Config { configIgnoreDotGhci :: Bool @@ -81,17 +83,25 @@ new startupFile Config{..} envDefaults args_ = do env <- sanitizeEnv <$> getEnvironment let + ghc :: String + ghc = fromMaybe "ghc" $ lookup "SENSEI_GHC" env + + ghcVersion <- parseVersion . strip <$> readProcess ghc ["--numeric-version"] "" + + let + diagnosticsAsJson :: [String] -> [String] + diagnosticsAsJson + | ghcVersion < Just (makeVersion [9,10]) = id + | otherwise = ("-fdiagnostics-as-json" :) + mandatoryArgs :: [String] mandatoryArgs = ["-fshow-loaded-modules", "--interactive"] args :: [String] - args = "-ghci-script" : startupFile : args_ ++ catMaybes [ + args = "-ghci-script" : startupFile : diagnosticsAsJson args_ ++ catMaybes [ if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing ] ++ mandatoryArgs - ghc :: String - ghc = fromMaybe "ghc" $ lookup "SENSEI_GHC" env - (stdoutReadEnd, stdoutWriteEnd) <- createPipe (Just stdin_, Nothing, Nothing, processHandle ) <- createProcess (proc ghc args) { @@ -120,6 +130,7 @@ new startupFile Config{..} envDefaults args_ = do Just _ -> exitFailure Nothing -> return interpreter where + checkDotGhci :: IO () checkDotGhci = unless configIgnoreDotGhci $ do let dotGhci = fromMaybe "" configWorkingDirectory ".ghci" isWritableByOthers dotGhci >>= \ case @@ -131,18 +142,19 @@ new startupFile Config{..} envDefaults args_ = do , "" ] + setMode :: Handle -> IO () setMode h = do hSetBinaryMode h False hSetBuffering h LineBuffering hSetEncoding h utf8 - printStartupMessages :: Interpreter -> IO (String, [ReloadStatus]) - printStartupMessages interpreter = evalVerbose extractReloadStatus interpreter "" + printStartupMessages :: Interpreter -> IO (String, [Either ReloadStatus Diagnostic]) + printStartupMessages interpreter = evalVerbose extractReloadDiagnostics interpreter "" close :: Interpreter -> IO () close Interpreter{..} = do hClose hIn - ReadHandle.drain extractNothing readHandle echo + ReadHandle.drain extractReloadDiagnostics readHandle echo hClose hOut e <- waitForProcess process when (e /= ExitSuccess) $ do @@ -154,6 +166,9 @@ putExpression Interpreter{hIn = stdin} e = do ByteString.hPut stdin ReadHandle.marker hFlush stdin +extractReloadDiagnostics :: Extract (Either ReloadStatus Diagnostic) +extractReloadDiagnostics = extractReloadStatus <+> extractDiagnostics + data ReloadStatus = Ok | Failed deriving (Eq, Show) @@ -168,6 +183,12 @@ extractReloadStatus = Extract { ok = "Ok, modules loaded: " failed = "Failed, modules loaded: " +extractDiagnostics :: ReadHandle.Extract Diagnostic +extractDiagnostics = ReadHandle.Extract { + isPartialMessage = ByteString.isPrefixOf "{" +, parseMessage = fmap (id &&& Diagnostic.format) . Diagnostic.parse +} + extractNothing :: Extract () extractNothing = Extract { isPartialMessage = const False @@ -186,7 +207,7 @@ eval ghci = fmap fst . evalVerbose extractNothing ghci {echo = silent} evalVerbose :: Extract a -> Interpreter -> String -> IO (String, [a]) evalVerbose extract ghci expr = putExpression ghci expr >> getResult extract ghci -reload :: Interpreter -> IO (String, ReloadStatus) -reload ghci = evalVerbose extractReloadStatus ghci ":reload" <&> second \ case - [Ok] -> Ok - _ -> Failed +reload :: Interpreter -> IO (String, (ReloadStatus, [Diagnostic])) +reload ghci = evalVerbose extractReloadDiagnostics ghci ":reload" <&> second \ case + (partitionEithers -> ([Ok], diagnostics)) -> (Ok, diagnostics) + (partitionEithers ->(_, diagnostics)) -> (Failed, diagnostics) diff --git a/src/ReadHandle.hs b/src/ReadHandle.hs index d32b97c1..6b2f2304 100644 --- a/src/ReadHandle.hs +++ b/src/ReadHandle.hs @@ -4,6 +4,7 @@ module ReadHandle ( , toReadHandle , marker , Extract(..) +, (<+>) , partialMessageStartsWith , partialMessageStartsWithOneOf , getResult @@ -82,6 +83,12 @@ data Extract a = Extract { , parseMessage :: ByteString -> Maybe (a, ByteString) } +(<+>) :: Extract a -> Extract b -> Extract (Either a b) +(<+>) a b = Extract { + isPartialMessage = \ input -> a.isPartialMessage input || b.isPartialMessage input +, parseMessage = \ input -> first Left <$> a.parseMessage input <|> first Right <$> b.parseMessage input +} + partialMessageStartsWith :: ByteString -> ByteString -> Bool partialMessageStartsWith prefix chunk = ByteString.isPrefixOf chunk prefix || ByteString.isPrefixOf prefix chunk diff --git a/src/Run.hs b/src/Run.hs index c15070f4..a1378927 100644 --- a/src/Run.hs +++ b/src/Run.hs @@ -26,6 +26,7 @@ import qualified Input import Pager (pager) import Util import Config +import GHC.Diagnostic waitForever :: IO () waitForever = forever $ threadDelay 10000000 @@ -85,7 +86,7 @@ run args = do defaultRunArgs :: IO RunArgs defaultRunArgs = do queue <- newQueue - lastOutput <- newMVar (Trigger.Success, "") + lastOutput <- newMVar (Trigger.Success, "", []) return RunArgs { ignoreConfig = False , dir = "" @@ -100,7 +101,7 @@ data RunArgs = RunArgs { ignoreConfig :: Bool , dir :: FilePath , args :: [String] -, lastOutput :: MVar (Result, String) +, lastOutput :: MVar (Result, String, [Diagnostic]) , queue :: EventQueue , sessionConfig :: Session.Config , withSession :: forall r. Session.Config -> [String] -> (Session.Session -> IO r) -> IO r @@ -119,16 +120,16 @@ runWith RunArgs {..} = do addCleanupAction :: IO () -> IO () addCleanupAction cleanupAction = atomicModifyIORef' cleanup $ \ action -> (action >> cleanupAction, ()) - saveOutput :: IO (Trigger.Result, String) -> IO () + saveOutput :: IO (Trigger.Result, String, [Diagnostic]) -> IO () saveOutput action = do runCleanupAction result <- modifyMVar lastOutput $ \ _ -> (id &&& id) <$> action case result of - (HookFailed, _output) -> pass - (Failure, output) -> config.senseiHooksOnFailure >>= \ case + (HookFailed, _output, _diagnostics) -> pass + (Failure, output, _diagnostics) -> config.senseiHooksOnFailure >>= \ case HookSuccess -> pager output >>= addCleanupAction HookFailure message -> hPutStrLn stderr message - (Success, _output) -> config.senseiHooksOnSuccess >>= \ case + (Success, _output, _diagnostics) -> config.senseiHooksOnSuccess >>= \ case HookSuccess -> pass HookFailure message -> hPutStrLn stderr message diff --git a/src/Session.hs b/src/Session.hs index fb4d1ec7..9ed970c1 100644 --- a/src/Session.hs +++ b/src/Session.hs @@ -36,6 +36,7 @@ import qualified Language.Haskell.GhciWrapper as Interpreter import Util import Options +import GHC.Diagnostic data Session = Session { interpreter :: Interpreter @@ -63,7 +64,7 @@ withSession config args action = do where (ghciArgs, hspecArgs) = splitArgs args -reload :: MonadIO m => Session -> m (String, ReloadStatus) +reload :: MonadIO m => Session -> m (String, (ReloadStatus, [Diagnostic])) reload session = liftIO $ Interpreter.reload session.interpreter data Summary = Summary { diff --git a/src/Trigger.hs b/src/Trigger.hs index 132d6025..b75a42ac 100644 --- a/src/Trigger.hs +++ b/src/Trigger.hs @@ -27,6 +27,7 @@ import Util import Config (Hook, HookResult(..)) import Session (Session, ReloadStatus(..), isFailure, isSuccess, hspecPreviousSummary, resetSummary) import qualified Session +import GHC.Diagnostic data Hooks = Hooks { beforeReload :: Hook @@ -42,7 +43,7 @@ defaultHooks = Hooks { data Result = HookFailed | Failure | Success deriving (Eq, Show) -triggerAll :: Session -> Hooks -> IO (Result, String) +triggerAll :: Session -> Hooks -> IO (Result, String, [Diagnostic]) triggerAll session hooks = do resetSummary session trigger session hooks @@ -55,18 +56,18 @@ removeProgress xs = case break (== '\r') xs of dropLastLine :: String -> String dropLastLine = reverse . dropWhile (/= '\n') . reverse -type Trigger = ExceptT Result (WriterT String IO) +type Trigger = ExceptT Result (WriterT (String, [Diagnostic]) IO) -trigger :: Session -> Hooks -> IO (Result, String) +trigger :: Session -> Hooks -> IO (Result, String, [Diagnostic]) trigger session hooks = runWriterT (runExceptT go) >>= \ case - (Left result, output) -> return (result, output) - (Right (), output) -> return (Success, output) + (Left result, (output, diagnostics)) -> return (result, output, diagnostics) + (Right (), (output, diagnostics)) -> return (Success, output, diagnostics) where go :: Trigger () go = do runHook hooks.beforeReload - (output, r) <- Session.reload session - tell output + (output, (r, err)) <- Session.reload session + tell (output, err) case r of Failed -> do echo $ withColor Red "RELOADING FAILED" <> "\n" @@ -93,7 +94,8 @@ trigger session hooks = runWriterT (runExceptT go) >>= \ case runSpec :: IO String -> Trigger () runSpec hspec = do - liftIO hspec >>= tell . removeProgress + r <- removeProgress <$> liftIO hspec + tell (r, []) result <- hspecPreviousSummary session unless (isSuccess result) abort @@ -104,5 +106,5 @@ trigger session hooks = runWriterT (runExceptT go) >>= \ case echo :: String -> Trigger () echo message = do - tell message + tell (message, []) liftIO $ Session.echo session message diff --git a/test/ClientSpec.hs b/test/ClientSpec.hs index b413fce5..d2eddc0b 100644 --- a/test/ClientSpec.hs +++ b/test/ClientSpec.hs @@ -16,7 +16,7 @@ withFailure = withServer Trigger.Failure (withColor Red "failure") withServer :: Trigger.Result -> String -> (FilePath -> IO a) -> IO a withServer result text action = do withTempDirectory $ \ dir -> do - HTTP.withServer dir (return (result, text)) $ do + HTTP.withServer dir (return (result, text, [])) $ do action dir spec :: Spec diff --git a/test/GHC/DiagnosticSpec.hs b/test/GHC/DiagnosticSpec.hs new file mode 100644 index 00000000..31e2f21e --- /dev/null +++ b/test/GHC/DiagnosticSpec.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE BlockArguments #-} +module GHC.DiagnosticSpec (spec) where + +import Helper hiding (diagnostic) + +import System.Process + +import GHC.Diagnostic + +test :: HasCallStack => FilePath -> Spec +test name = it name $ do + err <- translate <$> ghc ["-fno-diagnostics-show-caret"] + Just diagnostic <- parse . encodeUtf8 <$> ghc ["-fdiagnostics-as-json"] + decodeUtf8 (format diagnostic) `shouldBe` err + where + ghc :: [String] -> IO String + ghc args = do + let + process :: CreateProcess + process = proc "ghc" (args ++ ["test/assets" name "Foo.hs"]) + (_, _, err) <- readCreateProcessWithExitCode process "" + return err + + translate :: String -> String + translate = map \ case + '‘' -> '`' + '’' -> '\'' + '•' -> '*' + c -> c + +ftest :: HasCallStack => FilePath -> Spec +ftest = focus . test + +_ignore :: HasCallStack => FilePath -> Spec +_ignore = ftest + +spec :: Spec +spec = do + describe "format" $ do + test "variable-not-in-scope" + test "variable-not-in-scope-perhaps-use" + test "use-BlockArguments" + test "non-existing" diff --git a/test/HTTPSpec.hs b/test/HTTPSpec.hs index 2c922c82..6f173b16 100644 --- a/test/HTTPSpec.hs +++ b/test/HTTPSpec.hs @@ -1,39 +1,64 @@ module HTTPSpec (spec) where +import Prelude hiding (span) import Helper +import Data.Aeson (encode) +import Data.ByteString.Lazy (toStrict) import Test.Hspec.Wai import qualified System.Console.ANSI as Ansi import HTTP import qualified Trigger + spec :: Spec spec = do describe "app" $ do - with (return $ app $ return (Trigger.Success, withColor Green "hello")) $ do - it "returns 200 on success" $ do - get "/" `shouldRespondWith` fromString (withColor Green "hello") + describe "/" $ do + context "on success" $ do + with (return $ app $ return (Trigger.Success, withColor Green "hello", [])) $ do + it "returns 200 on success" $ do + get "/" `shouldRespondWith` fromString (withColor Green "hello") + + context "with ?color" $ do + it "keeps terminal sequences" $ do + get "/?color" `shouldRespondWith` fromString (withColor Green "hello") + + context "with ?color=true" $ do + it "keeps terminal sequences" $ do + get "/?color=true" `shouldRespondWith` fromString (withColor Green "hello") + + context "with ?color=false" $ do + it "removes terminal sequences" $ do + get "/?color=false" `shouldRespondWith` "hello" + + context "with an in invalid value for ?color" $ do + it "returns status 400" $ do + get "/?color=some%20value" `shouldRespondWith` 400 { matchBody = "invalid value for color: some%20value" } + + context "on failure" $ do + with (return $ app $ return (Trigger.Failure, "hello", [])) $ do + it "return 500" $ do + get "/" `shouldRespondWith` 500 - context "with ?color" $ do - it "keeps terminal sequences" $ do - get "/?color" `shouldRespondWith` fromString (withColor Green "hello") + describe "/diagnostics" $ do + let + start :: Location + start = Location 23 42 - context "with ?color=true" $ do - it "keeps terminal sequences" $ do - get "/?color=true" `shouldRespondWith` fromString (withColor Green "hello") + span :: Span + span = Span "Foo.hs" start start - context "with ?color=false" $ do - it "removes terminal sequences" $ do - get "/?color=false" `shouldRespondWith` "hello" + err :: Diagnostic + err = Diagnostic "" "" (Just span) Error Nothing [] [] - context "with an in invalid value for ?color" $ do - it "returns status 400" $ do - get "/?color=some%20value" `shouldRespondWith` 400 { matchBody = "invalid value for color: some%20value" } + err_str :: String + err_str = decodeUtf8 . toStrict $ encode [err] - with (return $ app $ return (Trigger.Failure, "hello")) $ do - it "return 500 on failure" $ do - get "/" `shouldRespondWith` 500 + with (return $ app $ return (Trigger.Failure, "", [err])) $ do + it "returns thc diagnostics" $ do + get "/diagnostics" `shouldRespondWith` (fromString err_str) describe "stripAnsi" $ do it "removes ANSI color sequences" $ do diff --git a/test/Helper.hs b/test/Helper.hs index 72d1ca3e..1c37e0d0 100644 --- a/test/Helper.hs +++ b/test/Helper.hs @@ -13,8 +13,15 @@ module Helper ( , withColor , timeout + +, Diagnostic(..) +, Span(..) +, Location(..) +, Severity(..) +, diagnostic ) where +import Prelude hiding (span) import Imports import System.Directory as Imports @@ -31,6 +38,8 @@ import Run () import Util import Language.Haskell.GhciWrapper (Config(..)) +import GHC.Diagnostic + timeout :: IO a -> IO (Maybe a) timeout action = lookupEnv "CI" >>= \ case Nothing -> System.Timeout.timeout 5_000_000 action @@ -90,3 +99,14 @@ failingSpec = unlines [ , " it \"foo\" True" , " it \"bar\" False" ] + +diagnostic :: Severity -> Diagnostic +diagnostic severity = Diagnostic { + version = "1.0" +, ghcVersion = "ghc-9.10.1" +, span = Nothing +, severity +, code = Nothing +, message = [] +, hints = [] +} diff --git a/test/Language/Haskell/GhciWrapperSpec.hs b/test/Language/Haskell/GhciWrapperSpec.hs index 54bad2df..69263c86 100644 --- a/test/Language/Haskell/GhciWrapperSpec.hs +++ b/test/Language/Haskell/GhciWrapperSpec.hs @@ -134,16 +134,37 @@ spec = do writeFile file "module Foo where" action file + failingModule :: String -> IO () + failingModule file = writeFile file $ unlines [ + "module Foo where" + , "foo = bar" + ] + it "indicates success" do withModule \ file -> do withInterpreter [file] \ ghci -> do - Interpreter.reload ghci `shouldReturn` ("", Ok) + Interpreter.reload ghci `shouldReturn` ("", (Ok, [])) it "indicates failure" do withModule \ file -> do withInterpreter [file] \ ghci -> do - writeFile file $ unlines [ - "module Foo where" - , "foo = bar" - ] - snd <$> Interpreter.reload ghci `shouldReturn` Failed + failingModule file + snd <$> Interpreter.reload ghci `shouldReturn` (Failed, [ +#if __GLASGOW_HASKELL__ >= 910 + (diagnostic Error) { + span = Just $ Span file (Location 2 7) (Location 2 10) + , code = Just 88464 + , message = ["Variable not in scope: bar"] + } +#endif + ]) + + context "with -fno-diagnostics-as-json" $ do + it "does not extract diagnostics" do +#if __GLASGOW_HASKELL__ < 910 + pending +#endif + withModule \ file -> do + withInterpreter ["-fno-diagnostics-as-json", file] \ ghci -> do + failingModule file + snd <$> Interpreter.reload ghci `shouldReturn` (Failed, []) diff --git a/test/ReadHandleSpec.hs b/test/ReadHandleSpec.hs index 2aa4169d..0a2cd429 100644 --- a/test/ReadHandleSpec.hs +++ b/test/ReadHandleSpec.hs @@ -1,15 +1,21 @@ module ReadHandleSpec (spec) where +import Prelude hiding (span) import Helper import Test.QuickCheck import qualified Data.ByteString as ByteString import Session (Summary(..), extractSummary) -import Language.Haskell.GhciWrapper (extractReloadStatus) +import Language.Haskell.GhciWrapper (extractReloadDiagnostics) import ReadHandle +import qualified Data.Aeson as Aeson +import Data.ByteString.Lazy (toStrict) + +import qualified GHC.Diagnostic as Diagnostic + chunkByteString :: (Int, Int) -> ByteString -> Gen [ByteString] chunkByteString size = go where @@ -50,7 +56,7 @@ spec = do describe "drain" $ do it "drains all remaining input" $ do h <- fakeHandle ["foo", marker, "bar", marker, "baz", marker, ""] - withSpy (drain extractReloadStatus h) `shouldReturn` ["foo", "bar", "baz"] + withSpy (drain extractReloadDiagnostics h) `shouldReturn` ["foo", "bar", "baz"] describe "getResult" $ do context "with a single result" $ do @@ -59,14 +65,14 @@ spec = do it "returns result" $ do withSpy $ \ echo -> do h <- fakeHandle input - getResult extractReloadStatus h echo `shouldReturn` ("foobarbaz", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foobarbaz", []) `shouldReturn` ["foo", "bar", "baz"] context "with chunks of arbitrary size" $ do it "returns result" $ do withRandomChunkSizes input $ \ h -> do fmap mconcat . withSpy $ \ echo -> do - getResult extractReloadStatus h echo `shouldReturn` ("foobarbaz", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foobarbaz", []) `shouldReturn` "foobarbaz" context "with extractSummary" $ do @@ -110,52 +116,100 @@ spec = do getResult extract h echo `shouldReturn` ("foo\nbar\nSummary baz\n", []) `shouldReturn` "foo\nbar\nSummary baz\n" + context "" $ do + let + extract = extractReloadDiagnostics { + parseMessage = \ xs -> flip (,) "" <$> Diagnostic.parse xs + } + + it "" $ do + let + start :: Location + start = Location 23 42 + + span :: Span + span = Span "Foo.hs" start start + + err :: Diagnostic + err = Diagnostic "" "" (Just span) Error Nothing [] [] + + bar :: ByteString + bar = toStrict $ Aeson.encode err + + foo :: [ByteString] + foo = [ + "foo\n" + , "bar\n" + , bar <> "\n" + , "baz\n" + , marker + ] + withRandomChunkSizes foo $ \ h -> do + fmap mconcat . withSpy $ \ echo -> do + getResult extract h echo `shouldReturn` ("foo\nbar\nbaz\n", [err]) + `shouldReturn` "foo\nbar\nbaz\n" + + it "" $ do + let + foo :: [ByteString] + foo = [ + "foo\n" + , "bar\n" + , "{..." + , marker + ] + withRandomChunkSizes foo $ \ h -> do + fmap mconcat . withSpy $ \ echo -> do + getResult extract h echo `shouldReturn` ("foo\nbar\n{...", []) + `shouldReturn` "foo\nbar\n{..." + + context "with multiple results" $ do let input = ["foo", marker, "bar", marker, "baz", marker] it "returns one result at a time" $ do withSpy $ \ echo -> do h <- fakeHandle input - getResult extractReloadStatus h echo `shouldReturn` ("foo", []) - getResult extractReloadStatus h echo `shouldReturn` ("bar", []) - getResult extractReloadStatus h echo `shouldReturn` ("baz", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foo", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("bar", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("baz", []) `shouldReturn` ["foo", "bar", "baz"] context "with chunks of arbitrary size" $ do it "returns one result at a time" $ do withRandomChunkSizes input $ \ h -> do fmap mconcat . withSpy $ \ echo -> do - getResult extractReloadStatus h echo `shouldReturn` ("foo", []) - getResult extractReloadStatus h echo `shouldReturn` ("bar", []) - getResult extractReloadStatus h echo `shouldReturn` ("baz", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foo", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("bar", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("baz", []) `shouldReturn` "foobarbaz" context "when a chunk that contains a marker ends with a partial marker" $ do it "correctly gives the marker precedence over the partial marker" $ do withSpy $ \ echo -> do h <- fakeHandle ["foo" <> marker <> "bar" <> partialMarker, ""] - getResult extractReloadStatus h echo `shouldReturn` ("foo", []) - getResult extractReloadStatus h echo `shouldReturn` ("bar" <> partialMarker, []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foo", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("bar" <> partialMarker, []) `shouldReturn` ["foo", "bar", partialMarker] context "on EOF" $ do it "returns all remaining input" $ do withSpy $ \ echo -> do h <- fakeHandle ["foo", "bar", "baz", ""] - getResult extractReloadStatus h echo `shouldReturn` ("foobarbaz", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foobarbaz", []) `shouldReturn` ["foo", "bar", "baz"] context "with a partialMarker at the end" $ do it "includes the partial marker in the output" $ do withSpy $ \ echo -> do h <- fakeHandle ["foo", "bar", "baz", partialMarker, ""] - getResult extractReloadStatus h echo `shouldReturn` ("foobarbaz" <> partialMarker, []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foobarbaz" <> partialMarker, []) `shouldReturn` ["foo", "bar", "baz", partialMarker] context "after a marker" $ do it "returns all remaining input" $ do withSpy $ \ echo -> do h <- fakeHandle ["foo", "bar", "baz", marker, "qux", ""] - getResult extractReloadStatus h echo `shouldReturn` ("foobarbaz", []) - getResult extractReloadStatus h echo `shouldReturn` ("qux", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("foobarbaz", []) + getResult extractReloadDiagnostics h echo `shouldReturn` ("qux", []) `shouldReturn` ["foo", "bar", "baz", "qux"] diff --git a/test/SessionSpec.hs b/test/SessionSpec.hs index 58cefd14..ad333588 100644 --- a/test/SessionSpec.hs +++ b/test/SessionSpec.hs @@ -36,11 +36,6 @@ spec = do withSession ["-XOverloadedStrings", "-Wall", "-Werror"] $ \ Session{..} -> do eval interpreter "23 :: Int" `shouldReturn` "23\n" - describe "reload" $ do - it "reloads" $ do - withSession [] $ \ session -> do - Session.reload session `shouldReturn` ("", Ok) - describe "hasSpec" $ around withSomeSpec $ do context "when module contains spec" $ do it "returns True" $ \ name -> do diff --git a/test/TriggerSpec.hs b/test/TriggerSpec.hs index 9be4a195..207a886d 100644 --- a/test/TriggerSpec.hs +++ b/test/TriggerSpec.hs @@ -39,10 +39,14 @@ trigger :: Session -> IO (Result, [String]) trigger session = triggerWithHooks session defaultHooks triggerWithHooks :: Session -> Hooks -> IO (Result, [String]) -triggerWithHooks session hooks = fmap normalize <$> Trigger.trigger session hooks +triggerWithHooks session hooks = do + (result, output, _) <- Trigger.trigger session hooks + return (result, normalize output) triggerAll :: Session -> IO (Result, [String]) -triggerAll session = fmap normalize <$> Trigger.triggerAll session defaultHooks +triggerAll session = do + (result, output, _) <- Trigger.triggerAll session defaultHooks + return (result, normalize output) requiresHspecMeta :: IO () -> IO () requiresHspecMeta action = try action >>= \ case diff --git a/test/assets/use-BlockArguments/Foo.hs b/test/assets/use-BlockArguments/Foo.hs new file mode 100644 index 00000000..de194c98 --- /dev/null +++ b/test/assets/use-BlockArguments/Foo.hs @@ -0,0 +1,5 @@ +{-# LANGUAGE NoBlockArguments #-} +module BlockArguments.Foo where + +foo :: IO () +foo = id do return () diff --git a/test/assets/variable-not-in-scope-perhaps-use/Foo.hs b/test/assets/variable-not-in-scope-perhaps-use/Foo.hs new file mode 100644 index 00000000..851426f6 --- /dev/null +++ b/test/assets/variable-not-in-scope-perhaps-use/Foo.hs @@ -0,0 +1,2 @@ +module Foo where +foo = filter_ diff --git a/test/assets/variable-not-in-scope/Foo.hs b/test/assets/variable-not-in-scope/Foo.hs new file mode 100644 index 00000000..5f2becf5 --- /dev/null +++ b/test/assets/variable-not-in-scope/Foo.hs @@ -0,0 +1,2 @@ +module Foo where +foo = bar diff --git a/test/assets/variable-not-in-scope/Spec.hs b/test/assets/variable-not-in-scope/Spec.hs new file mode 100644 index 00000000..5f2becf5 --- /dev/null +++ b/test/assets/variable-not-in-scope/Spec.hs @@ -0,0 +1,2 @@ +module Foo where +foo = bar