Skip to content

Commit 4e9396c

Browse files
authored
Merge pull request #6740 from ulidtko/maint/repl-tests
Cleanup integration tests for `stack repl`
2 parents 53c71cd + 3a939d5 commit 4e9396c

File tree

9 files changed

+171
-142
lines changed

9 files changed

+171
-142
lines changed

stack.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@ executable stack-integration-test
628628
main-is: IntegrationSpec.hs
629629
other-modules:
630630
StackTest
631+
StackTest.Repl
631632
Paths_stack
632633
autogen-modules:
633634
Paths_stack

tests/integration/lib/StackTest.hs

Lines changed: 5 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ module StackTest
1313
, stackCleanFull
1414
, stackIgnoreException
1515
, stackErr
16-
, Repl
17-
, ReplConnection (..)
18-
, nextPrompt
19-
, replCommand
20-
, replGetChar
21-
, replGetLine
22-
, runRepl
23-
, repl
2416
, stackStderr
2517
, stackCheckStderr
2618
, stackErrStderr
@@ -49,15 +41,11 @@ module StackTest
4941
, superslow
5042
) where
5143

52-
import Control.Monad ( forever, unless, void, when )
53-
import Control.Monad.IO.Class ( liftIO )
54-
import Control.Monad.Trans.Reader ( ReaderT, ask, runReaderT )
55-
import Control.Concurrent ( forkIO )
44+
import Control.Monad ( unless, void, when )
5645
import Control.Exception
57-
( Exception (..), IOException, bracket_, catch, throw
46+
( Exception (..), IOException, bracket_, catch
5847
, throwIO
5948
)
60-
import Data.Maybe ( fromMaybe )
6149
import GHC.Stack ( HasCallStack )
6250
import System.Environment ( getEnv, lookupEnv )
6351
import System.Directory
@@ -66,14 +54,12 @@ import System.Directory
6654
, setCurrentDirectory
6755
)
6856
import System.IO
69-
( BufferMode (..), Handle, IOMode (..), hGetChar, hGetLine
70-
, hPutChar, hPutStr, hPutStrLn, hSetBuffering, stderr
71-
, withFile
57+
( hPutStr, hPutStrLn, stderr
7258
)
7359
import System.IO.Error
74-
( isDoesNotExistError, isEOFError )
60+
( isDoesNotExistError )
7561
import System.Process
76-
( CreateProcess (..), StdStream (..), createProcess, proc
62+
( CreateProcess (..), createProcess, proc
7763
, readCreateProcessWithExitCode, readProcessWithExitCode
7864
, shell, waitForProcess
7965
)
@@ -149,74 +135,6 @@ stackErr args = do
149135
ec <- stack' args
150136
when (ec == ExitSuccess) $ error "stack was supposed to fail, but didn't"
151137

152-
type Repl = ReaderT ReplConnection IO
153-
154-
data ReplConnection = ReplConnection
155-
{ replStdin :: Handle
156-
, replStdout :: Handle
157-
}
158-
159-
nextPrompt :: Repl ()
160-
nextPrompt = do
161-
(ReplConnection _ replStdoutHandle) <- ask
162-
c <- liftIO $ hGetChar replStdoutHandle
163-
if c == '>'
164-
then do
165-
-- Skip next character
166-
void $ liftIO $ hGetChar replStdoutHandle
167-
else nextPrompt
168-
169-
replCommand :: String -> Repl ()
170-
replCommand cmd = do
171-
(ReplConnection replStdinHandle _) <- ask
172-
liftIO $ hPutStrLn replStdinHandle cmd
173-
174-
replGetLine :: Repl String
175-
replGetLine = ask >>= liftIO . hGetLine . replStdout
176-
177-
replGetChar :: Repl Char
178-
replGetChar = ask >>= liftIO . hGetChar . replStdout
179-
180-
runRepl ::
181-
HasCallStack
182-
=> FilePath
183-
-> [String]
184-
-> ReaderT ReplConnection IO ()
185-
-> IO ExitCode
186-
runRepl cmd args actions = do
187-
logInfo $ "Running: " ++ cmd ++ " " ++ unwords (map showProcessArgDebug args)
188-
(Just rStdin, Just rStdout, Just rStderr, ph) <-
189-
createProcess (proc cmd args)
190-
{ std_in = CreatePipe
191-
, std_out = CreatePipe
192-
, std_err = CreatePipe
193-
}
194-
hSetBuffering rStdin NoBuffering
195-
hSetBuffering rStdout NoBuffering
196-
hSetBuffering rStderr NoBuffering
197-
-- Log stack repl's standard error output
198-
tempDir <- if isWindows
199-
then fromMaybe "" <$> lookupEnv "TEMP"
200-
else pure "/tmp"
201-
let tempLogFile = tempDir ++ "/stderr"
202-
_ <- forkIO $ withFile tempLogFile WriteMode $ \logFileHandle -> do
203-
hSetBuffering logFileHandle NoBuffering
204-
forever $
205-
catch
206-
(hGetChar rStderr >>= hPutChar logFileHandle)
207-
(\e -> unless (isEOFError e) $ throw e)
208-
runReaderT actions (ReplConnection rStdin rStdout)
209-
waitForProcess ph
210-
211-
repl :: HasCallStack => [String] -> Repl () -> IO ()
212-
repl args action = do
213-
stackExe' <- stackExe
214-
ec <- runRepl stackExe' ("repl":args) action
215-
unless (ec == ExitSuccess) $ pure ()
216-
-- TODO: Understand why the exit code is 1 despite running GHCi tests
217-
-- successfully.
218-
-- else error $ "Exited with exit code: " ++ show ec
219-
220138
stackStderr :: HasCallStack => [String] -> IO (ExitCode, String)
221139
stackStderr args = do
222140
stackExe' <- stackExe
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
{- |
2+
Integration-test helpers & fixtures for testing `stack repl`
3+
-}
4+
module StackTest.Repl
5+
( Repl
6+
, ReplConnection (..)
7+
, nextPrompt
8+
, replCommand
9+
, replGetChar
10+
, replGetLine
11+
, stackRepl
12+
-- * Reexport
13+
, module StackTest
14+
) where
15+
16+
import Control.Exception (SomeException, catch, displayException, finally)
17+
import Control.Monad ((>=>), unless, when)
18+
import Control.Monad.IO.Class (liftIO)
19+
import Control.Monad.Trans (lift)
20+
import Control.Monad.Trans.Reader
21+
import Control.Monad.Trans.State qualified as State
22+
import Data.Maybe (fromMaybe)
23+
import Data.Foldable (toList)
24+
import Data.Sequence as Seq (Seq(Empty), (|>), fromList)
25+
import GHC.Stack (HasCallStack)
26+
import System.Directory (removeFile)
27+
import System.Environment (lookupEnv)
28+
import System.Exit (ExitCode (..), exitFailure)
29+
import System.IO
30+
( BufferMode (NoBuffering, LineBuffering), Handle, IOMode (ReadMode)
31+
, hClose, hGetChar, hGetContents', hGetLine, hPutStrLn, hSetBuffering
32+
, openTempFile
33+
, withFile
34+
)
35+
import System.Process
36+
( CreateProcess (std_err, std_in, std_out)
37+
, StdStream (CreatePipe, UseHandle)
38+
, createProcess, proc, waitForProcess
39+
)
40+
41+
import StackTest
42+
43+
type Repl = ReaderT ReplConnection IO
44+
45+
data ReplConnection = ReplConnection
46+
{ replStdin :: Handle
47+
, replStdout :: Handle
48+
}
49+
50+
replCommand :: String -> Repl ()
51+
replCommand cmd = do
52+
(ReplConnection replStdinHandle _) <- ask
53+
-- echo what we send to the test's stdout
54+
liftIO . putStrLn $ "____> " <> cmd
55+
liftIO $ hPutStrLn replStdinHandle cmd
56+
57+
replGetChar :: Repl Char
58+
replGetChar = asks replStdout >>= liftIO . hGetChar
59+
60+
replGetLine :: Repl String
61+
replGetLine = ask >>= liftIO . hGetLine . replStdout
62+
63+
nextPrompt :: Repl ()
64+
nextPrompt = State.evalStateT poll Seq.Empty where
65+
poll = do
66+
c <- lift (asks replStdout) >>= liftIO . hGetChar
67+
State.modify (|> c)
68+
when (c == '\n') $ do
69+
State.get >>= liftIO . putStr . ("ghci> " ++) . toList
70+
State.put Seq.Empty
71+
buf <- State.get
72+
unless (buf == Seq.fromList "ghci> ")
73+
poll
74+
75+
runRepl
76+
:: HasCallStack
77+
=> FilePath
78+
-> [String]
79+
-> Repl ()
80+
-> IO ExitCode
81+
runRepl cmd args actions = do
82+
(stderrBufPath, stderrBufHandle) <- openTempStderrBufferFile
83+
hSetBuffering stderrBufHandle NoBuffering
84+
85+
logInfo $ "Running: " ++ cmd ++ " " ++ unwords (map showProcessArgDebug args) ++ "\n\
86+
\ with stderr in " ++ stderrBufPath
87+
88+
-- launch the GHCi subprocess, grab its FD handles and process handle
89+
(Just rStdin, Just rStdout, Nothing, ph) <-
90+
createProcess (proc cmd args)
91+
{ std_in = CreatePipe
92+
, std_out = CreatePipe
93+
, std_err = UseHandle stderrBufHandle
94+
}
95+
hSetBuffering rStdin LineBuffering
96+
hSetBuffering rStdout NoBuffering
97+
98+
-- run the test script which is to talk to the GHCi subprocess.
99+
runReaderT actions (ReplConnection rStdin rStdout)
100+
-- the nested actions script may fail in arbitrary ways; handle that here,
101+
-- attaching the subprocess stderr as relevant context
102+
`catch` \(e :: SomeException) -> do
103+
putStrLn "=============================="
104+
putStrLn "EXCEPTION in test: "
105+
putStrLn . quote $ displayException e
106+
putStrLn "------[ stderr of repl ]------"
107+
withFile stderrBufPath ReadMode $ hGetContents' >=> putStr . quote
108+
putStrLn "=============================="
109+
`finally` do
110+
hClose stderrBufHandle
111+
removeFile stderrBufPath
112+
113+
-- once done with the test, signal EOF on stdin for clean termination of ghci
114+
hClose rStdin
115+
-- read out the exit-code
116+
waitForProcess ph
117+
118+
-- | Roll a bicycle, rather than just `import Path.IO (getTempDir, openTempFile)`,
119+
-- because it's a hassle to use anything beyond base & boot libs here.
120+
openTempStderrBufferFile :: IO (FilePath, Handle)
121+
openTempStderrBufferFile = getTempDir >>= (`openTempFile` "err.log") where
122+
getTempDir | isWindows = fromMaybe "" <$> lookupEnv "TEMP"
123+
| otherwise = pure "/tmp"
124+
125+
-- | Testing helper to exercise `stack repl`.
126+
stackRepl :: HasCallStack => [String] -> Repl () -> IO ()
127+
stackRepl args action = do
128+
stackExe' <- stackExe
129+
ec <- runRepl stackExe' ("repl" : "--ghci-options=-ignore-dot-ghci" : args) action
130+
unless (ec == ExitSuccess) $ do
131+
putStrLn $ "repl exited with " <> show ec
132+
exitFailure
133+
134+
quote :: String -> String
135+
quote = unlines . map ("> " <>) . lines

tests/integration/run-single-test.sh

100644100755
File mode changed.

tests/integration/run-sort-tests.sh

100644100755
File mode changed.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import StackTest
1+
import StackTest.Repl
22

33
main :: IO ()
44
main = do
55
stack ["build", "--ghc-options=-ddump-simpl -ddump-asm -DBAR -DBAZ"]
6-
repl ["--ghc-options=-ddump-simpl -ddump-asm"] (pure ())
6+
stackRepl ["--ghc-options=-ddump-simpl -ddump-asm"] (pure ())

tests/integration/tests/3926-ghci-with-sublibraries/Main.hs

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,36 @@
1-
import Control.Concurrent
21
import Control.Monad.IO.Class
32
import Control.Monad
43
import Data.List
5-
import StackTest
64

7-
main :: IO ()
8-
main
9-
| isWindows =
10-
putStrLn "This test was disabled on Windows on 25 June 2023 (see \
11-
\https://github.com/commercialhaskell/stack/issues/6170)."
12-
| otherwise = do
13-
stack ["clean"] -- to make sure we can load the code even after a clean
14-
copy "src/Lib.v1" "src/Lib.hs"
15-
copy "src-internal/Internal.v1" "src-internal/Internal.hs"
16-
stack ["build"] -- need a build before ghci at the moment, see #4148
17-
forkIO fileEditingThread
18-
replThread
19-
20-
replThread :: IO ()
21-
replThread = repl [] $ do
22-
-- The command must be issued before searching the output for the next prompt,
23-
-- otherwise, on Windows from msys2-20230526, `stack repl` encounters a EOF
24-
-- and terminates gracefully.
25-
replCommand ":main"
26-
nextPrompt
27-
line <- replGetLine
28-
let expected = "hello world"
29-
when (line /= expected) $
30-
error $
31-
"Main module didn't load correctly.\n"
32-
<> "Expected: " <> expected <> "\n"
33-
<> "Actual : " <> line <> "\n"
34-
liftIO $ threadDelay 1000000 -- wait for an edit of the internal library
35-
reloadAndTest "testInt" "42" "Internal library didn't reload."
36-
liftIO $ threadDelay 1000000 -- wait for an edit of the internal library
37-
reloadAndTest "testStr" "\"OK\"" "Main library didn't reload."
5+
import StackTest.Repl
386

39-
fileEditingThread :: IO ()
40-
fileEditingThread = do
41-
threadDelay 1000000
42-
-- edit the internal library and pure to ghci
43-
copy "src-internal/Internal.v2" "src-internal/Internal.hs"
44-
threadDelay 1000000
45-
-- edit the internal library and end thread, returning to ghci
46-
copy "src/Lib.v2" "src/Lib.hs"
7+
main :: IO ()
8+
main = do
9+
stack ["clean"] -- to make sure we can load the code even after a clean
10+
copy "src/Lib.v1" "src/Lib.hs"
11+
copy "src-internal/Internal.v1" "src-internal/Internal.hs"
12+
stack ["build"] -- need a build before ghci at the moment, see #4148
13+
stackRepl [] $ do
14+
nextPrompt
15+
replCommand ":main"
16+
line <- replGetLine
17+
let expected = "hello world"
18+
when (line /= expected) $
19+
error $
20+
"Main module didn't load correctly.\n"
21+
<> "Expected: " <> expected <> "\n"
22+
<> "Actual : " <> line <> "\n"
23+
liftIO $ copy "src-internal/Internal.v2" "src-internal/Internal.hs"
24+
reloadAndTest "testInt" "42" "Internal library didn't reload."
25+
liftIO $ copy "src/Lib.v2" "src/Lib.hs"
26+
reloadAndTest "testStr" "\"OK\"" "Main library didn't reload."
4727

4828
reloadAndTest :: String -> String -> String -> Repl ()
4929
reloadAndTest cmd exp err = do
5030
reload
5131
replCommand cmd
5232
line <- replGetLine
33+
liftIO . putStrLn $ line
5334
unless (exp `isSuffixOf` line) $ error err
5435

5536
reload :: Repl ()

tests/integration/tests/4270-files-order/Main.hs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import Control.Monad
22
import StackTest
3+
import StackTest.Repl
34

45
main :: IO ()
56
main = do
67
stack ["build"]
7-
repl [] $ do
8-
-- The command must be issued before searching the output for the next
9-
-- prompt, otherwise, on Windows from msys2-20230526, `stack repl`
10-
-- encounters a EOF and terminates gracefully.
11-
replCommand "putStrLn greeting"
8+
stackRepl [] $ do
129
nextPrompt
10+
replCommand "putStrLn greeting"
1311
line <- replGetLine
1412
let expected = "Hello, world!"
1513
when (line /= expected) $

tests/integration/tests/module-added-multiple-times/Main.hs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import Control.Monad
2-
import Data.List
3-
import StackTest
2+
import StackTest.Repl
43

54
main :: IO ()
6-
main = repl [] $ do
7-
-- The command must be issued before searching the output for the next prompt,
8-
-- otherwise, on Windows from msys2-20230526, `stack repl` encounters a EOF
9-
-- and terminates gracefully.
10-
replCommand ":main"
5+
main = stackRepl [] $ do
116
nextPrompt
7+
replCommand ":main"
128
line <- replGetLine
139
let expected = "Hello World!"
1410
when (line /= expected) $

0 commit comments

Comments
 (0)