I’ve come across a simple problem that I can’t solve nicely. I have a main :: IO () in one file, and I want to write a test suite that runs this main and inspects its standard output while it is running. The method to capture the standard output should be fairly portable (at least Linux and Mac, bonus points for Windows).
So far the only idea I had was to use turtle or similar, call cabal run externally and pipe it into a file or a further helper function. This feels super brittle, though, and involves stepping way to far out of the context.
If I came up against this problem I would look really hard for a way to rewrite main :: IO () into something yields its output into an iterator/pipe/conduit. Would that be possible in your case?
Unfortunately not. The situation is a similar one, I want to automatically test against a student’s solution to a programming task. It has to be simple and realistic, so I can’t use another type than IO.
The knob library looks useful, maybe I can just duplicate stdout to such a handle…
Thanks, I’ll test that! I was wondering already how test frameworks manage to suppress IO. This package seems to be part of hspec, so I have a good feeling about its maintenance state.
Although I’m wondering whether I can do IO at the same time! From the docs I have the feeling that if I do forkIO $ silence _, I can’t write to stdout in the foreground thread. Maybe I can still write to stderr…
At least on Linux, silently seems to work for me! And in fact I can still write to stderr and capture the stdout generated by the given IO The docs seem to claim that this works for Linux, Mac and Windows.
Fundamentally, you want to redirect stdout and stderr to other file descriptors.
To do this, you can create another file descriptor, which will get a position like 3 (typically stdout is 1 and stderr is 2)
Then, you close stdout (1).
And dup (see man dup) your fd (3).
Dup guarantees the duplicate file descriptor takes the lowest available position, which is 1 (because we closed stdout)
Therefore, when you invoke someMain and it writes to stdout, it is really writing to file descriptor 1, which is your file
import System.Posix.IO
import System.Posix.Files
main = do
f <- createFile "mytestout" (foldr unionFileModes nullFileMode [ownerReadMode, ownerWriteMode])
closeFd stdOutput
dup f
-- When someMain writes to stdout it is really writing to @f@
someMain
someMain = do
putStrLn "Writing to stdout!”
module Main (main) where
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck.Modifiers
import Run
import qualified StudentMainModule as T -- read a sequence of numbers and write its sum
main :: IO ()
main = hspec $ do
describe "Executando o programa" $ do
it "com quantidade negativa" $ do
let xs = []
out <- runWithInput T.main (unlines (map show (-10 : xs)))
shouldContain out ("Soma dos números digitados: " ++ show (sum xs))
prop "com quantidade não negativa" $ do
\xs -> do
out <- runWithInput T.main (unlines (map show (length xs : xs)))
shouldContain out ("Soma dos números digitados: " ++ show (sum xs))