packages feed

doctest 0.6.1 → 0.7.0

raw patch · 18 files changed

+606/−241 lines, 18 filesdep +doctestdep +hspec-discoverdep +silentlydep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: doctest, hspec-discover, silently

Dependency ranges changed: base

API changes (from Hackage documentation)

- Test.DocTest: data DocTest
- Test.DocTest: data Interpreter
- Test.DocTest: firstExpression :: DocTest -> String
- Test.DocTest: getDocTests :: [String] -> [String] -> IO [DocTest]
- Test.DocTest: sourcePath :: DocTest -> FilePath
- Test.DocTest: toAssertion :: Interpreter -> DocTest -> Assertion
- Test.DocTest: withInterpreter :: [String] -> (Interpreter -> IO a) -> IO a
+ Test.DocTest: doctest :: [String] -> IO ()

Files

doctest.cabal view
@@ -1,5 +1,5 @@ name:             doctest-version:          0.6.1+version:          0.7.0 synopsis:         Test interactive Haskell examples description:      The doctest program checks examples in source code comments.                   It is modeled after doctest for Python@@ -30,13 +30,18 @@   hs-source-dirs:       src   other-modules:-      Interpreter+      Extract+    , GhcUtil+    , Interpreter+    , Location     , Options-    , DocTest     , Parse-    , Extract-    , GhcUtil     , Paths_doctest+    , Property+    , Report+    , Run+    , Type+    , Util   build-depends:       base        >= 4.0  && < 4.6     , ghc         >= 6.12 && < 7.6@@ -45,7 +50,7 @@     , directory     , process     , ghc-paths   == 0.1.*-    , HUnit       == 1.2.*+    , transformers  executable doctest   main-is:@@ -53,32 +58,20 @@   ghc-options:       -Wall -threaded   hs-source-dirs:-      src-  other-modules:-      Interpreter-    , Options-    , DocTest-    , Parse-    , Extract-    , GhcUtil-    , Paths_doctest+      driver   build-depends:       base        >= 4.0  && < 4.6-    , ghc         >= 6.12 && < 7.6-    , syb-    , deepseq-    , directory-    , process-    , ghc-paths   == 0.1.*-    , HUnit       == 1.2.*+    , doctest  test-suite spec-  type:-      exitcode-stdio-1.0   main-is:       Spec.hs+  type:+      exitcode-stdio-1.0   ghc-options:       -Wall -Werror -threaded+  cpp-options:+      -DTEST   hs-source-dirs:       src, test   build-depends:@@ -89,8 +82,23 @@     , directory     , process     , ghc-paths   == 0.1.*+    , transformers     , HUnit       == 1.2.*+    , hspec-discover     , hspec-shouldbe-    , transformers     , stringbuilder+    , silently     , filepath++test-suite doctests+  main-is:+      doctests.hs+  type:+      exitcode-stdio-1.0+  ghc-options:+      -Wall -Werror -threaded+  hs-source-dirs:+      test+  build-depends:+      base+    , doctest
+ driver/Main.hs view
@@ -0,0 +1,6 @@+module Main (main) where+import           Test.DocTest+import           System.Environment (getArgs)++main :: IO ()+main = getArgs >>= doctest
− src/DocTest.hs
@@ -1,44 +0,0 @@-module DocTest (-    getDocTests-  , DocTest(..)-  , Interaction(..)-  , toTestCase-  , toAssertion-  ) where--import Test.HUnit (Test(..), assertEqual, Assertion)-import qualified Interpreter-import Parse---toTestCase :: Interpreter.Interpreter -> DocTest -> Test-toTestCase repl test = TestLabel sourceFile $ TestCase $ toAssertion repl test-  where-    -- FIXME: use source location here-    sourceFile = moduleName test---- |--- Execute all expressions from given 'DocTest' in given--- 'Interpreter.Interpreter' and verify the output.------ The interpreter state is zeroed with @:reload@ before executing the--- expressions.  This means that you can reuse the same--- 'Interpreter.Interpreter' for several calls to `toAssertion`.-toAssertion :: Interpreter.Interpreter -> DocTest -> Assertion-toAssertion repl test = do-  _ <- Interpreter.eval repl $ ":reload"-  _ <- Interpreter.eval repl $ ":m *" ++ moduleName test-  mapM_ interactionToAssertion $ interactions test-  where-    interactionToAssertion x = do-      result' <- Interpreter.eval repl exampleExpression-      assertEqual ("expression `" ++ exampleExpression ++ "'")-        exampleResult $ lines result'-      where-        exampleExpression = expression x-        exampleResult     = map subBlankLines $ result x--        -- interpret lines that only contain the string "<BLANKLINE>" as an-        -- empty line-        subBlankLines "<BLANKLINE>" = ""-        subBlankLines line          = line
src/Extract.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveDataTypeable, StandaloneDeriving, DeriveFunctor #-} module Extract (Module(..), extract) where  import           Prelude hiding (mod, catch)@@ -9,14 +9,17 @@ import           Control.DeepSeq (deepseq, NFData(rnf)) import           Data.Generics -import           GHC hiding (flags, Module)+import           GHC hiding (flags, Module, Located) import           NameSet (NameSet) import           Coercion (Coercion) import           FastString (unpackFS) import           Digraph (flattenSCCs)  import           GhcUtil (withGhc)+import           Location hiding (unLoc) +import           Util (convertDosLineEndings)+ -- | A wrapper around `SomeException`, to allow for a custom `Show` instance. newtype ExtractError = ExtractError SomeException   deriving Typeable@@ -40,12 +43,14 @@ instance Exception ExtractError  -- | Documentation for a module grouped together with the modules name.-data Module = Module {-  moduleName          :: String-, moduleDocumentation :: [String]-} deriving (Eq, Show)+data Module a = Module {+  moduleName    :: String+, moduleContent :: [a]+} deriving (Eq, Functor) -instance NFData Module where+deriving instance Show a => Show (Module a)++instance NFData a => NFData (Module a) where   rnf (Module name docs) = name `deepseq` docs `deepseq` ()  -- | Parse a list of modules.@@ -53,7 +58,7 @@       -> [String] -- ^ files/modules       -> IO [TypecheckedModule] parse flags modules = withGhc flags $ do-  mapM (flip guessTarget Nothing) modules >>= setTargets+  mapM (`guessTarget` Nothing) modules >>= setTargets   mods <- depanal [] False   let sortedMods = flattenSCCs (topSortModuleGraph False mods Nothing)   reverse <$> mapM (parseModule >=> typecheckModule >=> loadModule) sortedMods@@ -64,10 +69,10 @@ -- those modules (possibly indirect). extract :: [String] -- ^ flags         -> [String] -- ^ files/modules-        -> IO [Module]+        -> IO [Module (Located String)] extract flags modules = do   mods <- parse flags modules-  let docs = map extractFromModule (map tm_parsed_module mods)+  let docs = map (fmap (fmap convertDosLineEndings) . extractFromModule . tm_parsed_module) mods    (docs `deepseq` return docs) `catches` [       -- Re-throw AsyncException, otherwise execution will not terminate on@@ -79,15 +84,15 @@     ]  -- | Extract all docstrings from given module and attach the modules name.-extractFromModule :: ParsedModule -> Module+extractFromModule :: ParsedModule -> Module (Located String) extractFromModule m = Module name docs   where-    docs = map unLoc (docStringsFromModule m)+    docs = docStringsFromModule m     name = (moduleNameString . GHC.moduleName . ms_mod . pm_mod_summary) m  -- | Extract all docstrings from given module. docStringsFromModule :: ParsedModule -> [Located String]-docStringsFromModule mod = map (fmap unpackDocString) docs+docStringsFromModule mod = map (toLocated . fmap unpackDocString) docs   where     source   = (unLoc . pm_parsed_source) mod 
src/GhcUtil.hs view
@@ -1,20 +1,21 @@ {-# LANGUAGE CPP #-} module GhcUtil (withGhc) where -import            Control.Exception+import           Control.Exception+import           Control.Monad (void) -import            GHC.Paths (libdir)-import            GHC hiding (flags)-import            DynFlags (dopt_set)+import           GHC.Paths (libdir)+import           GHC hiding (flags)+import           DynFlags (dopt_set) -import            MonadUtils (liftIO)-import            System.Exit (exitFailure)+import           MonadUtils (liftIO)+import           System.Exit (exitFailure)  #if __GLASGOW_HASKELL__ < 702-import            StaticFlags (v_opt_C_ready)-import            Data.IORef (writeIORef)+import           StaticFlags (v_opt_C_ready)+import           Data.IORef (writeIORef) #else-import            StaticFlags (saveStaticFlagGlobals, restoreStaticFlagGlobals)+import           StaticFlags (saveStaticFlagGlobals, restoreStaticFlagGlobals) #endif  @@ -55,7 +56,7 @@   (dynflags, rest, _) <- (setHaddockMode `fmap` getSessionDynFlags) >>= flip parseDynamicFlags flags   case rest of     x : _ -> error ("Unrecognized GHC option: " ++ unLoc x)-    _     -> setSessionDynFlags dynflags >> return ()+    _     -> void (setSessionDynFlags dynflags)  setHaddockMode :: DynFlags -> DynFlags setHaddockMode dynflags = (dopt_set dynflags Opt_Haddock) {
src/Interpreter.hs view
@@ -1,19 +1,20 @@ module Interpreter (   Interpreter , eval+, safeEval , withInterpreter ) where -import System.IO-import System.Process-import System.Exit-import System.Directory (getPermissions, executable)-import Control.Monad(when)-import Control.Exception (bracket)-import Data.Char-import Data.List+import           System.IO+import           System.Process+import           System.Exit+import           System.Directory (getPermissions, executable)+import           Control.Monad (when, unless)+import           Control.Exception hiding (handle)+import           Data.Char+import           Data.List -import GHC.Paths (ghc)+import           GHC.Paths (ghc)  -- | Truly random marker, used to separate expressions. --@@ -35,7 +36,7 @@   -- in a perfect world this permission check should never fail, but I know of   -- at least one case where it did..   x <- getPermissions ghc-  when (not $ executable x) $ do+  unless (executable x) $ do     fail $ ghc ++ " is not executable!"    (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess $ (proc ghc myFlags) {std_in = CreatePipe, std_out = CreatePipe, std_err = UseHandle stdout}@@ -128,7 +129,7 @@ getResult :: Interpreter -> IO String getResult repl = do   line <- hGetLine stdout_-  if isSuffixOf marker line+  if marker `isSuffixOf` line     then       return $ stripMarker line     else do@@ -146,3 +147,17 @@ eval repl expr = do   putExpression repl expr   getResult repl++-- | Evaluate an expression; return a Left value on executions.+--+-- Exceptions may e.g. be caused on unterminated multiline expressions.+safeEval :: Interpreter -> String -> IO (Either String String)+safeEval repl expression = (Right `fmap` Interpreter.eval repl expression) `catches` [+  -- Re-throw AsyncException, otherwise execution will not terminate on+  -- SIGINT (ctrl-c).  All AsyncExceptions are re-thrown (not just+  -- UserInterrupt) because all of them indicate severe conditions and+  -- should not occur during normal test runs.+  Handler $ \e -> throw (e :: AsyncException),++  Handler $ \e -> (return . Left . show) (e :: SomeException)+  ]
+ src/Location.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE CPP, DeriveFunctor #-}+module Location where++import           Control.DeepSeq (deepseq, NFData(rnf))+import           SrcLoc hiding (Located)+import qualified SrcLoc as GHC+import           FastString (unpackFS)++#if __GLASGOW_HASKELL__ < 702+import           Outputable (showPpr)+#endif++-- | A thing with a location attached.+data Located a = Located Location a+  deriving (Eq, Show, Functor)++instance NFData a => NFData (Located a) where+  rnf (Located loc a) = loc `deepseq` a `deepseq` ()++-- | Convert a GHC located thing to a located thing.+toLocated :: GHC.Located a -> Located a+toLocated (L loc a) = Located (toLocation loc) a++-- | Discard location information.+unLoc :: Located a -> a+unLoc (Located _ a) = a++-- | Add dummy location information.+noLocation :: a -> Located a+noLocation = Located (UnhelpfulLocation "<no location info>")++-- | A line number.+type Line = Int++-- | A combination of file name and line number.+data Location = UnhelpfulLocation String | Location FilePath Line+  deriving Eq++instance Show Location where+  show (UnhelpfulLocation s) = s+  show (Location file line)  = file ++ ":" ++ show line++instance NFData Location where+  rnf (UnhelpfulLocation str) = str `deepseq` ()+  rnf (Location file line)    = file `deepseq` line `deepseq` ()++-- |+-- Create a list from a location, by repeatedly increasing the line number by+-- one.+enumerate :: Location -> [Location]+enumerate loc = case loc of+  UnhelpfulLocation _ -> repeat loc+  Location file line  -> map (Location file) [line ..]++-- | Convert a GHC source span to a location.+toLocation :: SrcSpan -> Location+#if __GLASGOW_HASKELL__ < 702+toLocation loc+  | isGoodSrcLoc start = Location (unpackFS $ srcLocFile start) (srcLocLine start)+  | otherwise          = (UnhelpfulLocation . showPpr) start+  where+    start = srcSpanStart loc+#else+toLocation loc = case loc of+  UnhelpfulSpan str -> UnhelpfulLocation (unpackFS str)+  RealSrcSpan sp    -> Location (unpackFS . srcSpanFile $ sp) (srcSpanStartLine sp)+#endif
− src/Main.hs
@@ -1,41 +0,0 @@-module Main where--import Test.HUnit (runTestTT, Test(..), Counts(..))-import System.Exit (exitSuccess, exitFailure)-import System.IO--import Parse-import Options-import DocTest--import qualified Interpreter--main :: IO ()-main = do-  (options, files) <- getOptions-  let ghciArgs = ghcOptions options ++ files--  -- get examples from Haddock comments-  docTests <- getDocTests (ghcOptions options) files--  let (tCount, iCount) = (length docTests, length (concatMap interactions docTests))-  hPutStrLn stderr (formatTestAndInteractionCount tCount iCount)--  if DumpOnly `elem` options-    then do-      -- dump to stdout-      print docTests-    else do-      -- map to unit tests-      Interpreter.withInterpreter ghciArgs $ \repl -> do-        let tests = TestList $ map (toTestCase repl) docTests-        Counts _ _ errCount failCount <- runTestTT tests-        if errCount == 0 && failCount == 0-          then exitSuccess-          else exitFailure---formatTestAndInteractionCount :: Int -> Int -> String-formatTestAndInteractionCount 1 1           = "There is one test, with one single interaction."-formatTestAndInteractionCount 1 iCount      = "There is one test, with " ++ show iCount ++ " interactions."-formatTestAndInteractionCount tCount iCount = "There are " ++ show tCount ++ " tests, with " ++ show iCount ++ " total interactions."
src/Options.hs view
@@ -1,19 +1,18 @@ module Options (   Option(..)-, getOptions+, parseOptions , ghcOptions ) where -import Control.Monad (when)-import System.Environment (getArgs)-import System.Exit (exitSuccess, exitFailure)-import System.IO (hPutStr, stderr)+import           Control.Monad (when, unless)+import           System.Exit (exitSuccess, exitFailure)+import           System.IO (hPutStr, stderr) -import System.Console.GetOpt+import           System.Console.GetOpt -import Paths_doctest (version)-import Data.Version (showVersion)-import Config as GHC+import           Paths_doctest (version)+import           Data.Version (showVersion)+import           Config as GHC  data Option = Help             | Version@@ -37,9 +36,8 @@   ]  -getOptions :: IO ([Option], [String])-getOptions = do-  args  <- getArgs+parseOptions :: [String] -> IO ([Option], [String])+parseOptions args = do   let (options, modules, errors) = getOpt Permute (documentedOptions ++ undocumentedOptions) args    when (Help `elem` options) $ do@@ -51,7 +49,7 @@     putStrLn ("using version " ++ GHC.cProjectVersion ++ " of the GHC API")     exitSuccess -  when ((not . null) errors) $ do+  unless (null errors) $ do     tryHelp $ head errors    when (null modules) $ do
src/Parse.hs view
@@ -1,10 +1,14 @@ module Parse (-  DocTest(..)-, Interaction(..)+  Module (..)+, DocTest (..)+, Interaction (..)+, Expression+, ExpectedResult , getDocTests  -- * exported for testing-, parse+, parseInteractions+, parseProperties ) where  import           Data.Char (isSpace)@@ -12,54 +16,73 @@ import           Data.Maybe (fromMaybe)  import           Extract--data DocTest = DocExample {-  moduleName    :: String-, interactions  :: [Interaction]-} deriving (Eq, Show)+import           Location +data DocTest = Example [Located Interaction] | Property (Located Expression)+  deriving (Eq, Show) -data Interaction = Interaction {-  expression :: String    -- ^ example expression-, result     :: [String]  -- ^ expected result-} deriving (Eq, Show)+type Expression = String+type ExpectedResult = [String] +data Interaction = Interaction Expression ExpectedResult+  deriving (Eq, Show)  -- | -- Extract 'DocTest's from all given modules and all modules included by the -- given modules.-getDocTests :: [String]     -- ^ List of GHC flags-            -> [String]     -- ^ File or module names-            -> IO [DocTest] -- ^ Extracted 'DocTest's+getDocTests+  :: [String]             -- ^ List of GHC flags+  -> [String]             -- ^ File or module names+  -> IO [Module DocTest]  -- ^ Extracted 'DocTest's getDocTests flags modules = do   mods <- extract flags modules-  return (concatMap moduleToDocTest mods)+  return (filter (not . null . moduleContent) $ map parseModule mods) --- | Convert a `Module` to a list of `DocTest`s.-moduleToDocTest :: Module -> [DocTest]-moduleToDocTest (Module name docs) = (map (DocExample name) . filter (not . null) . map parse) docs+-- | Convert documentation to `Example`s.+parseModule :: Module (Located String) -> Module DocTest+parseModule (Module name docs) = Module name (properties ++ examples)+  where+    examples = (map Example . filter (not . null) . map parseInteractions) docs+    properties = [] -- (map Property . concatMap parseProperties) docs --- | Extract all interactions from given Haddock documentation.-parse :: String -> [Interaction]-parse input = go (map (reverse . dropWhile ((==) '\r') . reverse) $ lines input)+-- | Extract all properties from given Haddock comment.+parseProperties :: Located String -> [Located Expression]+parseProperties (Located loc input) = go $ zipWith Located (enumerate loc) (lines input)   where-    isPrompt = isPrefixOf ">>>" . dropWhile isSpace-    isBlankLine  = null . dropWhile isSpace+    isPrompt :: Located String -> Bool+    isPrompt = isPrefixOf "prop>" . dropWhile isSpace . unLoc++    go xs = case dropWhile (not . isPrompt) xs of+      prop:rest -> stripPrompt `fmap` prop : go rest+      [] -> []++    stripPrompt = strip . drop 5 . dropWhile isSpace++-- | Extract all interactions from given Haddock comment.+parseInteractions :: Located String -> [Located Interaction]+parseInteractions (Located loc input) = go $ zipWith Located (enumerate loc) (lines input)+  where+    isPrompt :: Located String -> Bool+    isPrompt = isPrefixOf ">>>" . dropWhile isSpace . unLoc++    isBlankLine :: Located String -> Bool+    isBlankLine  = null . dropWhile isSpace . unLoc++    isEndOfInteraction :: Located String -> Bool     isEndOfInteraction x = isPrompt x || isBlankLine x -    go :: [String] -> [Interaction]-    go xs =-      case dropWhile (not . isPrompt) xs of-        prompt:rest ->-          let -            (ys,zs) = break isEndOfInteraction rest-          in-            toInteraction prompt ys : go zs-        _ -> []+    go :: [Located String] -> [Located Interaction]+    go xs = case dropWhile (not . isPrompt) xs of+      prompt:rest ->+        let+          (ys,zs) = break isEndOfInteraction rest+        in+          toInteraction prompt ys : go zs+      [] -> []  -- | Create an `Interaction`, strip superfluous whitespace as appropriate.-toInteraction :: String -> [String] -> Interaction-toInteraction x xs =+toInteraction :: Located String -> [Located String] -> Located Interaction+toInteraction (Located loc x) xs = Located loc $   Interaction     (strip $ drop 3 e)  -- we do not care about leading and trailing                         -- whitespace in expressions, so drop them@@ -73,7 +96,7 @@     --     -- 3. interpret lines that only contain the string "<BLANKLINE>" as an     -- empty line-    result_ = map (substituteBlankLine . tryStripPrefix prefix) xs+    result_ = map (substituteBlankLine . tryStripPrefix prefix . unLoc) xs       where         tryStripPrefix pre ys = fromMaybe ys $ stripPrefix pre ys 
+ src/Property.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE CPP #-}+module Property (+  runProperty+#ifdef TEST+, freeVariables+, parseNotInScope+#endif+) where++import           Data.List++import           Util+import           Interpreter (Interpreter)+import qualified Interpreter+import           Type+import           Location+import           Parse++runProperty :: Interpreter -> Located Expression -> IO DocTestResult+runProperty repl p@(Located _ expression) = do+  _ <- Interpreter.eval repl "import Test.QuickCheck (quickCheck, (==>))"+  r <- closeTerm expression >>= (Interpreter.safeEval repl . quickCheck)+  case r of+    Left err -> do+      return (Error p err)+    Right res+      | "OK, passed" `isInfixOf` res -> return Success+      | otherwise -> do+          let msg =  stripEnd (takeWhileEnd (/= '\b') res)+          return (PropertyFailure p msg)+  where+    quickCheck term = "quickCheck (" ++ term ++ ")"++    -- | Find all free variables in given term, and close it by abstrating over+    -- them.+    closeTerm :: String -> IO String+    closeTerm term = do+      r <- freeVariables repl (quickCheck term)+      case r of+        []   -> return term+        vars -> return ("\\" ++ unwords vars ++ "-> (" ++ term ++ ")")++-- | Find all free variables in given term.+--+-- GHCi is used to detect free variables.+freeVariables :: Interpreter -> String -> IO [String]+freeVariables repl term = do+  r <- Interpreter.safeEval repl (":type " ++ term)+  return (either (const []) (nub . parseNotInScope) r)++-- | Parse and return all variables that are not in scope from a ghc error+-- message.+--+-- >>> parseNotInScope "<interactive>:4:1: Not in scope: `foo'"+-- ["foo"]+parseNotInScope :: String -> [String]+parseNotInScope = nub . map extractVariable . filter ("Not in scope: " `isInfixOf`) . lines+  where+    -- | Extract variable name from a "Not in scope"-error.+    extractVariable :: String -> String+    extractVariable = unquote . takeWhileEnd (/= ' ')++    -- | Remove quotes from given name, if any.+    unquote ('`':xs) = init xs+    unquote xs       = xs
+ src/Report.hs view
@@ -0,0 +1,192 @@+{-# LANGUAGE CPP #-}+module Report (+  runModules+, Summary(..)++#ifdef TEST+, Report+, ReportState (..)+, report+, report_+, reportFailure+, runProperty+, DocTestResult (..)+#endif+) where++import           Prelude hiding (putStr, putStrLn, error)+import           Data.Monoid+import           Control.Monad+import           Text.Printf (printf)+import           System.IO (hPutStrLn, hPutStr, stderr, hIsTerminalDevice)+import           Data.Char++import           Control.Monad.Trans.State+import           Control.Monad.IO.Class++import           Interpreter (Interpreter)+import qualified Interpreter+import           Parse+import           Location+import           Type+import           Property++-- | Summary of a test run.+data Summary = Summary {+  sExamples :: Int+, sTried    :: Int+, sErrors   :: Int+, sFailures :: Int+}++-- | Format a summary.+instance Show Summary where+  show (Summary examples tried errors failures) =+    printf "Examples: %d  Tried: %d  Errors: %d  Failures: %d" examples tried errors failures++-- | Sum up summaries.+instance Monoid Summary where+  mempty = Summary 0 0 0 0+  (Summary x1 x2 x3 x4) `mappend` (Summary y1 y2 y3 y4) = Summary (x1 + y1) (x2 + y2) (x3 + y3) (x4 + y4)++-- |+-- Run all examples from given modules, return true if there were+-- errors/failures.+runModules :: Int -> Interpreter -> [Module DocTest] -> IO Bool+runModules exampleCount repl modules = do+  isInteractive <- hIsTerminalDevice stderr+  ReportState _ _ s <- (`execStateT` ReportState 0 isInteractive mempty {sExamples = exampleCount}) $ do+    forM_ modules $ runModule repl++    -- report final summary+    gets (show . reportStateSummary) >>= report++  return (sErrors s /= 0 || sFailures s /= 0)++-- | A monad for generating test reports.+type Report = StateT ReportState IO++data ReportState = ReportState {+  reportStateCount        :: Int     -- ^ characters on the current line+, reportStateInteractive  :: Bool    -- ^ should intermediate results be printed?+, reportStateSummary      :: Summary -- ^ test summary+}++-- | Add output to the report.+report :: String -> Report ()+report msg = do+  overwrite msg++  -- add a newline, this makes the output permanent+  liftIO $ hPutStrLn stderr ""+  modify (\st -> st {reportStateCount = 0})++-- | Add intermediate output to the report.+--+-- This will be overwritten by subsequent calls to `report`/`report_`.+-- Intermediate out may not contain any newlines.+report_ :: String -> Report ()+report_ msg = do+  f <- gets reportStateInteractive+  when f $ do+    overwrite msg+    modify (\st -> st {reportStateCount = length msg})++-- | Add output to the report, overwrite any intermediate out.+overwrite :: String -> Report ()+overwrite msg = do+  n <- gets reportStateCount+  let str | 0 < n     = "\r" ++ msg ++ replicate (n - length msg) ' '+          | otherwise = msg+  liftIO (hPutStr stderr str)++-- | Run all examples from given module.+runModule :: Interpreter -> Module DocTest -> Report ()+runModule repl (Module name examples) = do+  forM_ examples $ \e -> do++    -- report intermediate summary+    gets (show . reportStateSummary) >>= report_++    r <- liftIO $ runDocTest repl name e+    case r of+      Success ->+        success+      Error (Located loc expression) err -> do+        report (printf "### Error in %s: expression `%s'" (show loc) expression)+        report err+        error+      InteractionFailure (Located loc (Interaction expression expected)) actual -> do+        report (printf "### Failure in %s: expression `%s'" (show loc) expression)+        reportFailure expected actual+        failure+      PropertyFailure (Located loc expression) msg -> do+        report (printf "### Failure in %s: expression `%s'" (show loc) expression)+        report msg+        failure+  where+    success = updateSummary (Summary 0 1 0 0)+    failure = updateSummary (Summary 0 1 0 1)+    error   = updateSummary (Summary 0 1 1 0)++    updateSummary summary = do+      ReportState n f s <- get+      put (ReportState n f $ s `mappend` summary)++reportFailure :: [String] -> [String] -> Report ()+reportFailure expected actual = do+  outputLines "expected: " expected+  outputLines " but got: " actual+  where++    -- print quotes if any line ends with trailing whitespace+    printQuotes = any isSpace (map last . filter (not . null) $ expected ++ actual)++    -- use show to escape special characters in output lines if any output line+    -- contains any unsafe character+    escapeOutput = any (not . isSafe) (concat $ expected ++ actual)++    isSafe :: Char -> Bool+    isSafe c = c == ' ' || (isPrint c && (not . isSpace) c)++    outputLines message l_ = case l of+      x:xs -> do+        report (message ++ x)+        let padding = replicate (length message) ' '+        forM_ xs $ \y -> report (padding ++ y)+      []   ->+        report message+      where+        l | printQuotes || escapeOutput = map show l_+          | otherwise                   = l_++runDocTest :: Interpreter -> String -> DocTest -> IO DocTestResult+runDocTest repl module_ docTest = do+  _ <- Interpreter.eval repl   ":reload"+  _ <- Interpreter.eval repl $ ":m *" ++ module_+  case docTest of+    Example xs -> runExample repl xs+    Property p -> runProperty repl p++-- |+-- Execute all expressions from given example in given+-- 'Interpreter' and verify the output.+--+-- The interpreter state is zeroed with @:reload@ before executing the+-- expressions.  This means that you can reuse the same+-- 'Interpreter' for several calls to `runExample`.+runExample :: Interpreter -> [Located Interaction] -> IO DocTestResult+runExample repl = go+  where+    go (i@(Located loc (Interaction expression expected)) : xs) = do+      r <- fmap lines `fmap` Interpreter.safeEval repl expression+      case r of+        Left err -> do+          return (Error (Located loc expression) err)+        Right actual -> do+          if expected /= actual+            then+              return (InteractionFailure i actual)+            else+              go xs+    go [] = return Success
+ src/Run.hs view
@@ -0,0 +1,63 @@+module Run (doctest) where+import           Data.Monoid+import           Control.Monad (when)+import           System.Exit (exitFailure)+import           System.IO++import           Parse+import           Options+import           Report+import qualified Interpreter++doctest :: [String] -> IO ()+-- | Run doctest with given list of arguments.+--+-- Example:+--+-- >>> doctest ["--optghc=-iexample/src", "example/src/Example.hs"]+-- There are 2 tests, with 2 total interactions.+-- Examples: 2  Tried: 2  Errors: 0  Failures: 0+--+-- This can be used to create a Cabal test suite that runs doctest for your+-- project.+doctest args = do+  (options, files) <- parseOptions args+  let ghciArgs = ghcOptions options ++ files++  -- get examples from Haddock comments+  modules <- getDocTests (ghcOptions options) files++  let c = (mconcat . map count) modules+  hPrint stderr c++  if DumpOnly `elem` options+    then do+      -- dump to stdout+      print modules+    else do+      -- run tests+      Interpreter.withInterpreter ghciArgs $ \repl -> do+        r <- runModules (exampleCount c) repl modules+        when r exitFailure+  where+    exampleCount (Count n _) = n++-- | Number of examples and interactions.+data Count = Count Int {- example count -} Int {- interaction count -}++instance Monoid Count where+  mempty = Count 0 0+  (Count x1 y1) `mappend` (Count x2 y2) = Count (x1 + x2) (y1 + y2)++instance Show Count where+  show (Count 1 1)           = "There is one test, with one single interaction."+  show (Count 1 iCount)      = "There is one test, with " ++ show iCount ++ " interactions."+  show (Count tCount iCount) = "There are " ++ show tCount ++ " tests, with " ++ show iCount ++ " total interactions."++-- | Count number of examples and interactions in given module.+count :: Module DocTest -> Count+count (Module _ examples) = (mconcat . map f) examples+  where+    f :: DocTest -> Count+    f (Example x)  = Count 1 (length x)+    f (Property _) = Count 1 1
src/Test/DocTest.hs view
@@ -1,35 +1,5 @@--- |--- An experimental API for extracting and executing `DocTest`s.------ Use 'getDocTests' to extract 'DocTest' examples from Haddock comments.  To--- verify that the examples work turn them into 'Test.HUnit.Assertion's, using--- 'withInterpreter' and 'toAssertion'.  After this just wrap the newly minted--- assertions into something suitable for your favorite test framework. module Test.DocTest (-    DocTest-  , getDocTests-  , sourcePath-  , firstExpression-  , toAssertion-  , Interpreter-  , withInterpreter-  ) where--import           DocTest-import           Interpreter (Interpreter)-import qualified Interpreter---- | Instead of what the name suggests, this returns the module name, not the--- file name.-sourcePath :: DocTest -> FilePath-sourcePath = moduleName-{-# DEPRECATED sourcePath "this function will vanish in a future release" #-}--firstExpression :: DocTest -> String-firstExpression test = expression $ head $ interactions test+  doctest+) where -withInterpreter-  :: [String]               -- ^ List of flags, passed to GHC-  -> (Interpreter -> IO a)  -- ^ Action to run-  -> IO a                   -- ^ Result of action-withInterpreter = Interpreter.withInterpreter+import           Run
+ src/Type.hs view
@@ -0,0 +1,12 @@+module Type where++import           Location+import           Parse++-- | The result of evaluating an interaction.+data DocTestResult =+    Success+  | InteractionFailure (Located Interaction) [String]+  | PropertyFailure (Located Expression) String+  | Error (Located Expression) String+  deriving (Eq, Show)
+ src/Util.hs view
@@ -0,0 +1,26 @@+module Util where+import           Data.Char++convertDosLineEndings :: String -> String+convertDosLineEndings = go+  where+    go input = case input of+      '\r':'\n':xs -> '\n' : go xs++      -- Haddock comments from source files with dos line endings end with a+      -- CR, so we strip that, too.+      "\r"         -> ""++      x:xs         -> x : go xs+      ""           -> ""++-- | Return the longest suffix of elements that satisfy a given predicate.+takeWhileEnd :: (a -> Bool) -> [a] -> [a]+takeWhileEnd p = reverse . takeWhile p . reverse++-- | Remove trailing white space from a string.+--+-- >>> stripEnd "foo   "+-- "foo"+stripEnd :: String -> String+stripEnd = reverse . dropWhile isSpace . reverse
test/Spec.hs view
@@ -1,15 +1,1 @@-module Main (main) where--import           Test.Hspec.ShouldBe--import qualified ExtractSpec-import qualified ParseSpec-import qualified InterpreterSpec-import qualified MainSpec--main :: IO ()-main = hspecX $ do-  describe "ExtractSpec"      ExtractSpec.spec-  describe "ParseSpec"        ParseSpec.spec-  describe "InterpreterSpec"  InterpreterSpec.spec-  describe "MainSpec"         MainSpec.spec+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/doctests.hs view
@@ -0,0 +1,13 @@+module Main where++import           Test.DocTest++main :: IO ()+main = doctest [+    "--optghc=-packageghc"+  , "--optghc=-isrc"+  , "--optghc=-idist/build/autogen/"+  , "--optghc=-optP-include"+  , "--optghc=-optPdist/build/autogen/cabal_macros.h"+  , "src/Run.hs"+  ]