Skip to content

Commit c27e80a

Browse files
Only start GHC once per thread
1 parent 0b415ae commit c27e80a

File tree

9 files changed

+88
-69
lines changed

9 files changed

+88
-69
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ jobs:
7070
- "9.4.7"
7171
- "9.2.8"
7272
- "9.0.2"
73-
- "8.10.7"
7473
exclude:
7574
# Newer macOSs don't have the right LLVM to compile our dependencies
7675
- os: macOS-latest

CHANGES.markdown

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Account for `default-language` sections in Cabal files ([#85](https://github.com/martijnbastiaan/doctest-parallel/issues/85))
33
* Add support for Cabal 3.14 ([#88](https://github.com/martijnbastiaan/doctest-parallel/pull/88))
44
* Add parallel parsing on Linux/macOS. The GHC API is now used to call the parser directly, which allows parallel parsing. On Windows, files will be parsed sequentially still due to the GHC API locking files. ([#85](https://github.com/martijnbastiaan/doctest-parallel/issues/89))
5-
* Drop support for GHC 8.4, 8.6, and 8.8
5+
* Drop support for GHC < 9
66

77
# 0.3.1.1
88
* Add support for GHC 9.12 (loosened bounds in Hackage revision)

doctest-parallel.cabal

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ author: Martijn Bastiaan <martijn@hmbastiaan.nl>
1717
maintainer: Martijn Bastiaan <martijn@hmbastiaan.nl>
1818
build-type: Simple
1919
tested-with:
20-
GHC == 8.10.7
21-
, GHC == 9.0.2
20+
GHC == 9.0.2
2221
, GHC == 9.2.8
2322
, GHC == 9.4.7
2423
, GHC == 9.6.3
@@ -101,7 +100,7 @@ library
101100
, directory
102101
, exceptions
103102
, filepath
104-
, ghc >=8.2 && <9.13
103+
, ghc >=9.0 && <9.13
105104
, ghc-exactprint
106105
, ghc-paths >=0.1.0.9
107106
, process

scripts/build_all.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22
set -e
33

4-
GHCS=( "8.10.7" "9.0.2" "9.2.8" "9.4.8" "9.6.6" "9.8.1" "9.10.1" )
4+
GHCS=( "9.0.2" "9.2.8" "9.4.8" "9.6.6" "9.8.1" "9.10.1" )
55

66
cabal update
77

src/Test/DocTest/Internal/Extract.hs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88
{-# LANGUAGE ScopedTypeVariables #-}
99
{-# LANGUAGE ViewPatterns #-}
1010

11-
module Test.DocTest.Internal.Extract (Module(..), isEmptyModule, extract, eraseConfigLocation) where
11+
module Test.DocTest.Internal.Extract
12+
( Module(..)
13+
, isEmptyModule
14+
, extract
15+
, extractIO
16+
, eraseConfigLocation
17+
) where
1218
import Prelude hiding (mod, concat)
1319
import Control.DeepSeq (NFData, deepseq)
14-
import Control.Exception
20+
import Control.Exception (AsyncException, throw, throwIO, fromException)
1521
import Control.Monad
22+
import Control.Monad.Catch (catches, SomeException, Exception, Handler (Handler))
1623
import Data.Generics (Data, extQ, mkQ, everythingBut)
1724
import Data.List (partition, isPrefixOf)
1825
import Data.List.Extra (trim, splitOn)
@@ -168,40 +175,43 @@ findModulePath importPaths modName = do
168175
-- | Parse a list of modules. Can throw an `ModuleNotFoundError` if a module's
169176
-- source file cannot be found. Can throw a `SourceError` if an error occurs
170177
-- while parsing.
171-
parse :: [String] -> String -> IO ParsedSource
172-
parse args modName = do
178+
parse :: String -> Ghc ParsedSource
179+
parse modName = do
173180
-- Find all specified modules on disk
174-
withGhc args $ do
175-
importPaths0 <- importPaths <$> getDynFlags
176-
path <- liftIO $ findModulePath importPaths0 modName
177-
178-
-- LANGUAGE pragmas can influence how a file is parsed. For example, CPP
179-
-- means we need to preprocess the file before parsing it. We use GHC's
180-
-- `getOptionsFromFile` to parse these pragmas and then feed them as options
181-
-- to the "real" parser.
182-
dynFlags0 <- getDynFlags
181+
importPaths0 <- importPaths <$> getDynFlags
182+
path <- liftIO $ findModulePath importPaths0 modName
183+
184+
-- LANGUAGE pragmas can influence how a file is parsed. For example, CPP
185+
-- means we need to preprocess the file before parsing it. We use GHC's
186+
-- `getOptionsFromFile` to parse these pragmas and then feed them as options
187+
-- to the "real" parser.
188+
dynFlags0 <- getDynFlags
183189
#if __GLASGOW_HASKELL__ < 904
184-
flagsFromFile <-
190+
flagsFromFile <-
185191
#else
186-
(_, flagsFromFile) <-
192+
(_, flagsFromFile) <-
187193
#endif
188-
liftIO $ getOptionsFromFile (initParserOpts dynFlags0) path
189-
(dynFlags1, _, _) <- parseDynamicFilePragma dynFlags0 flagsFromFile
194+
liftIO $ getOptionsFromFile (initParserOpts dynFlags0) path
195+
(dynFlags1, _, _) <- parseDynamicFilePragma dynFlags0 flagsFromFile
190196

191197
#if MIN_VERSION_ghc_exactprint(1,3,0)
192-
result <- parseModuleEpAnnsWithCppInternal defaultCppOptions dynFlags1 path
198+
result <- parseModuleEpAnnsWithCppInternal defaultCppOptions dynFlags1 path
193199
#else
194-
result <- parseModuleApiAnnsWithCppInternal defaultCppOptions dynFlags1 path
200+
result <- parseModuleApiAnnsWithCppInternal defaultCppOptions dynFlags1 path
195201
#endif
196202

197-
case result of
198-
Left errs -> throwErrors errs
203+
case result of
204+
Left errs -> throwErrors errs
199205
#if MIN_VERSION_ghc_exactprint(1,3,0)
200-
Right (_cppComments, _dynFlags, parsedSource) -> pure parsedSource
206+
Right (_cppComments, _dynFlags, parsedSource) -> pure parsedSource
201207
#else
202-
Right (_apiAnns, _cppComments, _dynFlags, parsedSource) -> pure parsedSource
208+
Right (_apiAnns, _cppComments, _dynFlags, parsedSource) -> pure parsedSource
203209
#endif
204210

211+
-- | Like `extract`, but runs in the `IO` monad given GHC parse arguments.
212+
extractIO :: [String] -> String -> IO (Module (Located String))
213+
extractIO parseArgs modName = withGhc parseArgs $ extract modName
214+
205215
-- | Extract all docstrings from given list of files/modules.
206216
--
207217
-- This includes the docstrings of all local modules that are imported from
@@ -210,9 +220,9 @@ parse args modName = do
210220
-- Can throw `ExtractError` if an error occurs while extracting the docstrings,
211221
-- or a `SourceError` if an error occurs while parsing the module. Can throw a
212222
-- `ModuleNotFoundError` if a module's source file cannot be found.
213-
extract :: [String] -> String -> IO (Module (Located String))
214-
extract args modName = do
215-
mod <- parse args modName
223+
extract :: String -> Ghc (Module (Located String))
224+
extract modName = do
225+
mod <- parse modName
216226
let
217227
docs0 = extractFromModule modName mod
218228
docs1 = fmap convertDosLineEndings <$> docs0
@@ -223,7 +233,7 @@ extract args modName = do
223233
-- UserInterrupt) because all of them indicate severe conditions and
224234
-- should not occur during normal operation.
225235
Handler (\e -> throw (e :: AsyncException))
226-
, Handler (throwIO . ExtractError)
236+
, Handler (liftIO . throwIO . ExtractError)
227237
]
228238

229239
-- | Extract all docstrings from given module and attach the modules name.

src/Test/DocTest/Internal/Parse.hs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Test.DocTest.Internal.Parse (
1111
, ExpectedLine (..)
1212
, LineChunk (..)
1313
, getDocTests
14+
, getDocTestsIO
1415

1516
-- * exported for testing
1617
, parseInteractions
@@ -24,7 +25,9 @@ import Data.Maybe
2425
import Data.String
2526

2627
import Test.DocTest.Internal.Extract
28+
import Test.DocTest.Internal.GhcUtil (withGhc)
2729
import Test.DocTest.Internal.Location
30+
import GHC (Ghc)
2831

2932

3033
data DocTest = Example Expression ExpectedResult | Property Expression
@@ -47,10 +50,13 @@ type ExpectedResult = [ExpectedLine]
4750

4851
type Interaction = (Expression, ExpectedResult)
4952

53+
-- | Extract 'DocTest's from given module
54+
getDocTestsIO :: [String] -> String -> IO (Module [Located DocTest])
55+
getDocTestsIO parseArgs mod_ = withGhc parseArgs $ parseModule <$> extract mod_
5056

5157
-- | Extract 'DocTest's from given module
52-
getDocTests :: [String] -> String -> IO (Module [Located DocTest])
53-
getDocTests args mod_ = parseModule <$> extract args mod_
58+
getDocTests :: String -> Ghc (Module [Located DocTest])
59+
getDocTests mod_ = parseModule <$> extract mod_
5460

5561
-- | Convert documentation to `Example`s.
5662
parseModule :: Module (Located String) -> Module [Located DocTest]

src/Test/DocTest/Internal/Runner.hs

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ module Test.DocTest.Internal.Runner where
88
import Prelude hiding (putStr, putStrLn, error)
99

1010
import Control.Concurrent (Chan, writeChan, readChan, newChan, forkIO, ThreadId, myThreadId, MVar, newMVar)
11-
import Control.Exception (SomeException, catch)
11+
import Control.Exception (SomeException)
1212
import Control.Monad hiding (forM_)
13+
import Control.Monad.Catch (catch)
1314
import Data.Foldable (forM_)
1415
import Data.Function (on)
1516
import Data.List (sortBy)
@@ -30,15 +31,18 @@ import Test.DocTest.Internal.Options
3031
, cfgRandomizeOrder, cfgImplicitModuleImport, parseLocatedModuleOptions)
3132
import Test.DocTest.Internal.Location
3233
import qualified Test.DocTest.Internal.Property as Property
33-
import Test.DocTest.Internal.Runner.Example
34-
import Test.DocTest.Internal.Logging (LogLevel (..), formatLog, shouldLog, getThreadName)
3534
import Test.DocTest.Internal.Extract (isEmptyModule)
35+
import Test.DocTest.Internal.GhcUtil (withGhc)
36+
import Test.DocTest.Internal.Logging (LogLevel (..), formatLog, shouldLog, getThreadName)
37+
import Test.DocTest.Internal.Runner.Example
3638

3739
import System.IO.CodePage (withCP65001)
3840
import Control.Monad.Extra (whenM)
41+
import GHC (Ghc)
3942

4043
#ifdef mingw32_HOST_OS
41-
import Control.Concurrent.MVar (withMVar)
44+
import Control.Concurrent.MVar (putMVar, takeMVar)
45+
import Control.Monad.Catch (finally)
4246
#endif
4347

4448
#if __GLASGOW_HASKELL__ < 804
@@ -101,7 +105,8 @@ runModules modConfig nThreads implicitPrelude parseArgs evalArgs modules = do
101105
(input, output) <-
102106
makeThreadPool
103107
(fromMaybe nCores nThreads)
104-
(runModule modConfig implicitPrelude ghcLock parseArgs evalArgs)
108+
parseArgs
109+
(runModule modConfig implicitPrelude ghcLock evalArgs)
105110

106111
-- Send instructions to threads
107112
liftIO (mapM_ (writeChan input) modules)
@@ -207,31 +212,30 @@ runModule
207212
-> MVar ()
208213
-- ^ GHC lock
209214
-> [String]
210-
-- ^ Parse GHC arguments
211-
-> [String]
212215
-- ^ Eval GHCi arguments
213216
-> Chan (ThreadId, ReportUpdate)
214217
-> ModuleName
215-
-> IO ()
216-
runModule modConfig0 implicitPrelude ghcLock parseArgs evalArgs output modName = do
217-
threadId <- myThreadId
218+
-> Ghc ()
219+
runModule modConfig0 implicitPrelude ghcLock evalArgs output modName = do
220+
threadId <- liftIO myThreadId
218221
let update r = writeChan output (threadId, r)
219222

220-
mod_@(Module module_ setup examples0 modArgs) <-
223+
mod_@(Module module_ setup examples0 modArgs) <- do
221224
#ifdef mingw32_HOST_OS
222225
-- XXX: Cannot use multiple GHC APIs at the same time on Windows
223-
withMVar ghcLock $ \() ->
226+
liftIO $ takeMVar ghcLock
227+
getDocTests modName `finally` liftIO (putMVar ghcLock ())
224228
#else
225-
ghcLock `seq`
229+
ghcLock `seq` getDocTests modName
226230
#endif
227-
getDocTests parseArgs modName
228-
update (UpdateModuleParsed modName (count (Module module_ setup examples0 modArgs)))
231+
232+
liftIO $ update (UpdateModuleParsed modName (count (Module module_ setup examples0 modArgs)))
229233
let modConfig2 = parseLocatedModuleOptions modName modConfig0 modArgs
230234

231235
unless (isEmptyModule mod_) $
232236
case modConfig2 of
233237
Left (loc, flag) ->
234-
update (UpdateOptionError loc flag)
238+
liftIO $ update (UpdateOptionError loc flag)
235239

236240
Right modConfig3 -> do
237241
let
@@ -267,7 +271,7 @@ runModule modConfig0 implicitPrelude ghcLock parseArgs evalArgs output modName =
267271

268272

269273
let logger = update . UpdateLog Debug
270-
Interpreter.withInterpreter logger evalArgs $ \repl -> withCP65001 $ do
274+
liftIO $ Interpreter.withInterpreter logger evalArgs $ \repl -> withCP65001 $ do
271275
-- Try to import this module, if it fails, something is off
272276
importResult <-
273277
case importModule of
@@ -292,7 +296,7 @@ runModule modConfig0 implicitPrelude ghcLock parseArgs evalArgs output modName =
292296
update (UpdateImportError module_ importResult)
293297

294298
-- Signal main thread a module has been tested
295-
update UpdateModuleDone
299+
liftIO $ update UpdateModuleDone
296300

297301
data ReportUpdate
298302
= UpdateSuccess FromSetup
@@ -316,18 +320,19 @@ data ReportUpdate
316320

317321
makeThreadPool ::
318322
Int ->
319-
(Chan (ThreadId, ReportUpdate) -> ModuleName -> IO ()) ->
323+
[String] ->
324+
(Chan (ThreadId, ReportUpdate) -> ModuleName -> Ghc ()) ->
320325
IO (Chan ModuleName, Chan (ThreadId, ReportUpdate))
321-
makeThreadPool nThreads mutator = do
326+
makeThreadPool nThreads parseArgs mutator = do
322327
input <- newChan
323328
output <- newChan
324329
forM_ [1..nThreads] $ \_ ->
325-
forkIO $ forever $ do
326-
modName <- readChan input
327-
threadId <- myThreadId
330+
forkIO $ withGhc parseArgs $ forever $ do
331+
modName <- liftIO $ readChan input
332+
threadId <- liftIO myThreadId
328333
catch
329334
(mutator output modName)
330-
(\e -> writeChan output (threadId, UpdateInternalError modName e))
335+
(\e -> liftIO $ writeChan output (threadId, UpdateInternalError modName e))
331336
return (input, output)
332337

333338
reportModuleParsed :: (?verbosity::LogLevel, ?threadId::ThreadId) => ModuleName -> Int -> Report ()

test/ExtractSpec.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import System.FilePath
2121

2222
shouldGive :: HasCallStack => (String, String) -> Module String -> Assertion
2323
(d, m) `shouldGive` expected = do
24-
r <- fmap unLoc `fmap` extract ["-i" ++ dir] m
24+
r <- fmap unLoc `fmap` extractIO ["-i" ++ dir] m
2525
eraseConfigLocation r `shouldBe` eraseConfigLocation expected
2626
where
2727
dir = "test/extract" </> d
@@ -65,7 +65,7 @@ spec = do
6565
("setup", "Foo") `shouldGive` (mod_ "Foo" [" foo", " bar", " baz"]){moduleSetup=Just "\n some setup code"}
6666

6767
it "fails on invalid flags" $ do
68-
extract ["--foobar"] "test/Foo.hs" `shouldThrow` (\e -> case e of UsageError "unrecognized option `--foobar'" -> True; _ -> False)
68+
extractIO ["--foobar"] "test/Foo.hs" `shouldThrow` (\e -> case e of UsageError "unrecognized option `--foobar'" -> True; _ -> False)
6969

7070
describe "extract (regression tests)" $ do
7171
it "works with infix operators" $ do

test/ParseSpec.hs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,17 @@ shouldGive action expected = map (fmap $ map unLoc) `fmap` fmap pure action `sho
3030

3131
spec :: Spec
3232
spec = do
33-
describe "getDocTests" $ do
33+
describe "getDocTestsIO" $ do
3434
it "extracts properties from a module" $ do
35-
getDocTests ["-itest/parse/property"] "Fib" `shouldGive` do
35+
getDocTestsIO ["-itest/parse/property"] "Fib" `shouldGive` do
3636
module_ "Fib" $ do
3737
group $ do
3838
prop_ "foo"
3939
prop_ "bar"
4040
prop_ "baz"
4141

4242
it "extracts examples from a module" $ do
43-
getDocTests ["-itest/parse/simple"] "Fib" `shouldGive` do
43+
getDocTestsIO ["-itest/parse/simple"] "Fib" `shouldGive` do
4444
module_ "Fib" $ do
4545
group $ do
4646
ghci "putStrLn \"foo\""
@@ -51,7 +51,7 @@ spec = do
5151
"baz"
5252

5353
it "extracts examples from documentation for non-exported names" $ do
54-
getDocTests ["-itest/parse/non-exported"] "Fib" `shouldGive` do
54+
getDocTestsIO ["-itest/parse/non-exported"] "Fib" `shouldGive` do
5555
module_ "Fib" $ do
5656
group $ do
5757
ghci "putStrLn \"foo\""
@@ -62,7 +62,7 @@ spec = do
6262
"baz"
6363

6464
it "extracts multiple examples from a module" $ do
65-
getDocTests ["-itest/parse/multiple-examples"] "Foo" `shouldGive` do
65+
getDocTestsIO ["-itest/parse/multiple-examples"] "Foo" `shouldGive` do
6666
module_ "Foo" $ do
6767
group $ do
6868
ghci "foo"
@@ -72,17 +72,17 @@ spec = do
7272
"42"
7373

7474
it "returns an empty list, if documentation contains no examples" $ do
75-
getDocTests ["-itest/parse/no-examples"] "Fib" >>= (`shouldSatisfy` isEmptyModule)
75+
getDocTestsIO ["-itest/parse/no-examples"] "Fib" >>= (`shouldSatisfy` isEmptyModule)
7676

7777
it "sets setup code to Nothing, if it does not contain any tests" $ do
78-
getDocTests ["-itest/parse/setup-empty"] "Foo" `shouldGive` do
78+
getDocTestsIO ["-itest/parse/setup-empty"] "Foo" `shouldGive` do
7979
module_ "Foo" $ do
8080
group $ do
8181
ghci "foo"
8282
"23"
8383

8484
it "keeps modules that only contain setup code" $ do
85-
getDocTests ["-itest/parse/setup-only"] "Foo" `shouldGive` do
85+
getDocTestsIO ["-itest/parse/setup-only"] "Foo" `shouldGive` do
8686
tell [Module "Foo" (Just [Example "foo" ["23"]]) [] []]
8787

8888
describe "parseInteractions (an internal function)" $ do

0 commit comments

Comments
 (0)