Skip to content

Commit 7566cf6

Browse files
committed
Check where Cabal writes output to
Tests can now specify files with a `.stdout` or `.stderr` suffix to assert where Cabal writes output to. `.out` will continue to work for testing what output Cabal generates ignoring which file descriptor that output is sent to. This commit also simplifies the `test{,Normalized}{Actual,Expect}File{Out,Stdout,Stderr}` family of functions
1 parent cff9b1a commit 7566cf6

File tree

18 files changed

+188
-61
lines changed

18 files changed

+188
-61
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module M where
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
main :: IO ()
2+
main = fail "Setup called despite `build-type:Simple`"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# cabal build
2+
Resolving dependencies...
3+
Build profile: -w ghc-<GHCVER> -O1
4+
In order, the following will be built:
5+
- my-0 (lib) (first run)
6+
Configuring library for my-0..
7+
Preprocessing library for my-0..
8+
Building library for my-0..
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
packages: .
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Resolving dependencies...
2+
Build profile: -w ghc-<GHCVER> -O1
3+
In order, the following will be built:
4+
- my-0 (lib) (first run)
5+
Configuring library for my-0..
6+
Preprocessing library for my-0..
7+
Building library for my-0..

cabal-testsuite/PackageTests/FileDescriptors/cabal.stdout

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Test.Cabal.Prelude
2+
3+
main :: IO ()
4+
main = cabalTest $ cabal "build" ["all"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
cabal-version: 3.0
2+
name: my
3+
version: 0
4+
-- tests that output goes to the correct file descriptors
5+
6+
library
7+
exposed-modules: M
8+
build-depends: base
9+
default-language: Haskell2010
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Setup configure
2+
Configuring LibV09-0.1...
3+
# Setup build
4+
Preprocessing library for LibV09-0.1..
5+
Building library for LibV09-0.1..
6+
Preprocessing test suite 'LibV09-Deadlock' for LibV09-0.1..
7+
Building test suite 'LibV09-Deadlock' for LibV09-0.1..
8+
# Setup test
9+
Running 1 test suites...
10+
Test suite LibV09-Deadlock: RUNNING...
11+
Test suite LibV09-Deadlock: FAIL
12+
Test suite logged to: setup-deadlock.cabal.dist/work/dist/test/LibV09-0.1-LibV09-Deadlock.log
13+
0 of 1 test suites (0 of 1000 test cases) passed.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Setup configure
2+
Configuring LibV09-0.1...
3+
# Setup build
4+
Preprocessing library for LibV09-0.1..
5+
Building library for LibV09-0.1..
6+
Preprocessing test suite 'LibV09-Deadlock' for LibV09-0.1..
7+
Building test suite 'LibV09-Deadlock' for LibV09-0.1..
8+
# Setup test
9+
Running 1 test suites...
10+
Test suite LibV09-Deadlock: RUNNING...
11+
Test suite LibV09-Deadlock: FAIL
12+
Test suite logged to:
13+
setup-deadlock.dist/work/dist/test/LibV09-0.1-LibV09-Deadlock.log
14+
0 of 1 test suites (0 of 1000 test cases) passed.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Setup configure
2+
Configuring LibV09-0.1...
3+
# Setup build
4+
Preprocessing library for LibV09-0.1..
5+
Building library for LibV09-0.1..
6+
Preprocessing test suite 'LibV09-Deadlock' for LibV09-0.1..
7+
Building test suite 'LibV09-Deadlock' for LibV09-0.1..
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Setup configure
2+
Configuring LibV09-0.1...
3+
# Setup build
4+
Preprocessing library for LibV09-0.1..
5+
Building library for LibV09-0.1..
6+
Preprocessing test suite 'LibV09-Deadlock' for LibV09-0.1..
7+
Building test suite 'LibV09-Deadlock' for LibV09-0.1..

cabal-testsuite/PackageTests/TestSuiteTests/LibV09/tests/Deadlock.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ import Distribution.TestSuite
55
import Lib
66

77
tests :: IO [Test]
8-
tests = return [nullt x | x <- [1 .. 1000]]
8+
tests = return [nullt x | x <- [1 .. 3]]

cabal-testsuite/src/Test/Cabal/DecodeShowBuildInfo.hs

+8-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ recordBuildInfo pkgName cname = do
3333
recordMode RecordAll $ do
3434
recordHeader ["show-build-info", prettyShow pkgName, prettyShow cname]
3535
buildInfo <- liftIO $ readFile fp
36-
recordLog $ Result ExitSuccess "build --enable-build-info" buildInfo
36+
recordLog $ Result
37+
{ resultExitCode = ExitSuccess
38+
, resultCommand = "build --enable-build-info"
39+
-- TODO: Consider if these three fields are instantiated correctly.
40+
, resultOut = buildInfo
41+
, resultStdout = buildInfo
42+
, resultStderr = mempty
43+
}
3744

3845
-- | Decode the given filepath into a 'BuildInfo'.
3946
--

cabal-testsuite/src/Test/Cabal/Monad.hs

+42-30
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module Test.Cabal.Monad (
3535
testSourceCopyDir,
3636
testCabalDir,
3737
testUserCabalConfigFile,
38+
FileDescriptor(..),
3839
testActualFile,
3940
-- * Skipping tests
4041
skip,
@@ -351,23 +352,29 @@ runTestM mode m = withSystemTempDirectory "cabal-testsuite" $ \tmp_dir -> do
351352

352353
check_expect accept = do
353354
env <- getTestEnv
354-
actual_raw <- liftIO $ readFileOrEmpty (testActualFile env)
355-
expect <- liftIO $ readFileOrEmpty (testExpectFile env)
356355
norm_env <- mkNormalizerEnv
357-
let actual = normalizeOutput norm_env actual_raw
358-
when (words actual /= words expect) $ do
359-
-- First try whitespace insensitive diff
360-
let actual_fp = testNormalizedActualFile env
361-
expect_fp = testNormalizedExpectFile env
362-
liftIO $ writeFile actual_fp actual
363-
liftIO $ writeFile expect_fp expect
364-
liftIO $ putStrLn "Actual output differs from expected:"
365-
b <- diff ["-uw"] expect_fp actual_fp
366-
unless b . void $ diff ["-u"] expect_fp actual_fp
367-
if accept
368-
then do liftIO $ putStrLn "Accepting new output."
369-
liftIO $ writeFileNoCR (testExpectFile env) actual
370-
else liftIO $ exitWith (ExitFailure 1)
356+
[Out, Stdout, Stderr] `forM_` \fd -> do
357+
exists <- liftIO $ doesFileExist $ testFile NotNormalized Expect fd env
358+
when exists $ do
359+
actual <- normalizeOutput norm_env <$> liftIO (readFileOrEmpty (testFile NotNormalized Actual fd env))
360+
expect <- liftIO $ readFileOrEmpty (testFile NotNormalized Expect fd env)
361+
when (words actual /= words expect) $ do
362+
-- First try whitespace insensitive diff
363+
let
364+
actualFile = testFile Normalized Actual fd env
365+
expectFile = testFile Normalized Expect fd env
366+
liftIO $ do
367+
writeFile expectFile actual
368+
writeFile actualFile expect
369+
putStrLn "Actual output differs from expected:"
370+
b <- diff ["-uw"] expectFile actualFile
371+
unless b . void $ diff ["-u"] expectFile actualFile
372+
liftIO $
373+
if accept
374+
then do
375+
putStrLn "Accepting new output."
376+
writeFileNoCR (testFile NotNormalized Expect Stdout env) actual
377+
else exitWith (ExitFailure 1)
371378

372379
readFileOrEmpty :: FilePath -> IO String
373380
readFileOrEmpty f = readFile f `E.catch` \e ->
@@ -596,18 +603,23 @@ testCabalDir env = testHomeDir env </> ".cabal"
596603
testUserCabalConfigFile :: TestEnv -> FilePath
597604
testUserCabalConfigFile env = testCabalDir env </> "config"
598605

599-
-- | The file where the expected output of the test lives
600-
testExpectFile :: TestEnv -> FilePath
601-
testExpectFile env = testSourceDir env </> testName env <.> "out"
606+
data Expected = Expect | Actual deriving (Show, Eq, Ord, Enum, Read, Bounded)
607+
data Normalized = Normalized | NotNormalized deriving (Show, Eq, Ord, Enum, Read, Bounded)
608+
data FileDescriptor = Out | Stdout | Stderr deriving (Show, Eq, Ord, Enum, Read, Bounded)
602609

603-
-- | Where we store the actual output
604-
testActualFile :: TestEnv -> FilePath
605-
testActualFile env = testWorkDir env </> testName env <.> "comp.out"
606-
607-
-- | Where we will write the normalized actual file (for diffing)
608-
testNormalizedActualFile :: TestEnv -> FilePath
609-
testNormalizedActualFile env = testActualFile env <.> "normalized"
610-
611-
-- | Where we will write the normalized expected file (for diffing)
612-
testNormalizedExpectFile :: TestEnv -> FilePath
613-
testNormalizedExpectFile env = testWorkDir env </> testName env <.> "out.normalized"
610+
testFile :: Normalized -> Expected -> FileDescriptor -> TestEnv -> FilePath
611+
testFile n e f = \env -> sourceDir env </> testName env <.> suffix
612+
where
613+
suffix
614+
= case e of { Expect -> "" ; Actual -> "comp" }
615+
<.> case f of { Out -> "out" ; Stdout -> "stdout" ; Stderr -> "stderr" }
616+
<.> case n of { Normalized -> "normalized" ; NotNormalized -> "" }
617+
sourceDir env = case (e, n) of
618+
(_, Normalized) -> testWorkDir env
619+
(Actual, _) -> testWorkDir env
620+
_ -> testSourceDir env
621+
{-# INLINE testFile #-}
622+
623+
testActualFile :: FileDescriptor -> TestEnv -> FilePath
624+
testActualFile fd env = testFile NotNormalized Actual fd env
625+
{-# INLINE testActualFile #-}

cabal-testsuite/src/Test/Cabal/Prelude.hs

+28-16
Original file line numberDiff line numberDiff line change
@@ -563,14 +563,17 @@ withRepo repo_dir m = do
563563
requireSuccess :: Result -> TestM Result
564564
requireSuccess r@Result { resultCommand = cmd
565565
, resultExitCode = exitCode
566-
, resultOutput = output } = withFrozenCallStack $ do
566+
, resultStdout
567+
, resultStderr } = withFrozenCallStack $ do
567568
env <- getTestEnv
568569
when (exitCode /= ExitSuccess && not (testShouldFail env)) $
569570
assertFailure $ "Command " ++ cmd ++ " failed.\n" ++
570-
"Output:\n" ++ output ++ "\n"
571+
"Stdout:\n" ++ resultStdout ++ "\n" ++
572+
"Stderr:\n" ++ resultStderr ++ "\n"
571573
when (exitCode == ExitSuccess && testShouldFail env) $
572574
assertFailure $ "Command " ++ cmd ++ " succeeded.\n" ++
573-
"Output:\n" ++ output ++ "\n"
575+
"Stdout:\n" ++ resultStdout ++ "\n" ++
576+
"Stderr:\n" ++ resultStderr ++ "\n"
574577
return r
575578

576579
initWorkDir :: TestM ()
@@ -595,21 +598,30 @@ recordHeader args = do
595598
initWorkDir
596599
liftIO $ putStr str_header
597600
liftIO $ C.appendFile (testWorkDir env </> "test.log") header
598-
liftIO $ C.appendFile (testActualFile env) header
601+
liftIO $ C.appendFile (testActualFile Out env) header
599602

600603
recordLog :: Result -> TestM ()
601-
recordLog res = do
604+
recordLog Result{resultOut, resultStdout, resultStderr, resultCommand} = do
602605
env <- getTestEnv
603606
let mode = testRecordMode env
604607
initWorkDir
605608
liftIO $ C.appendFile (testWorkDir env </> "test.log")
606-
(C.pack $ "+ " ++ resultCommand res ++ "\n"
607-
++ resultOutput res ++ "\n\n")
608-
liftIO . C.appendFile (testActualFile env) . C.pack . testRecordNormalizer env $
609-
case mode of
610-
RecordAll -> unlines (lines (resultOutput res))
611-
RecordMarked -> getMarkedOutput (resultOutput res)
609+
(C.pack $ "+ " ++ resultCommand ++ "\n"
610+
++ "OUT\n" <> resultOut ++ "\n"
611+
++ "STDOUT\n" <> resultStdout ++ "\n"
612+
++ "STDERR\n" <> resultStderr ++ "\n"
613+
++ "\n")
614+
let report f txt
615+
= liftIO
616+
. C.appendFile f
617+
. C.pack
618+
. testRecordNormalizer env $ case mode of
619+
RecordAll -> unlines (lines txt)
620+
RecordMarked -> getMarkedOutput txt
612621
DoNotRecord -> ""
622+
report (testActualFile Out env) resultOut
623+
report (testActualFile Stdout env) resultStdout
624+
report (testActualFile Stderr env) resultStderr
613625

614626
getMarkedOutput :: String -> String -- trailing newline
615627
getMarkedOutput out = unlines (go (lines out) False)
@@ -669,7 +681,7 @@ shouldNotExist path =
669681
assertRegex :: MonadIO m => String -> String -> Result -> m ()
670682
assertRegex msg regex r =
671683
withFrozenCallStack $
672-
let out = resultOutput r
684+
let out = resultStdout r
673685
in assertBool (msg ++ ",\nactual output:\n" ++ out)
674686
(out =~ regex)
675687

@@ -695,14 +707,14 @@ assertOutputContains needle result =
695707
withFrozenCallStack $
696708
unless (needle `isInfixOf` (concatOutput output)) $
697709
assertFailure $ " expected: " ++ needle
698-
where output = resultOutput result
710+
where output = resultStdout result
699711

700712
assertOutputDoesNotContain :: MonadIO m => WithCallStack (String -> Result -> m ())
701713
assertOutputDoesNotContain needle result =
702714
withFrozenCallStack $
703715
when (needle `isInfixOf` (concatOutput output)) $
704716
assertFailure $ "unexpected: " ++ needle
705-
where output = resultOutput result
717+
where output = resultStdout result
706718

707719
assertFindInFile :: MonadIO m => WithCallStack (String -> FilePath -> m ())
708720
assertFindInFile needle path =
@@ -897,7 +909,7 @@ withSourceCopy m = do
897909
let cwd = testCurrentDir env
898910
dest = testSourceCopyDir env
899911
r <- git' "ls-files" ["--cached", "--modified"]
900-
forM_ (lines (resultOutput r)) $ \f -> do
912+
forM_ (lines (resultStdout r)) $ \f -> do
901913
unless (isTestFile f) $ do
902914
liftIO $ createDirectoryIfMissing True (takeDirectory (dest </> f))
903915
liftIO $ copyFile (cwd </> f) (dest </> f)
@@ -923,7 +935,7 @@ getIPID :: String -> TestM String
923935
getIPID pn = do
924936
r <- ghcPkg' "field" ["--global", pn, "id"]
925937
-- Don't choke on warnings from ghc-pkg
926-
case mapMaybe (stripPrefix "id: ") (lines (resultOutput r)) of
938+
case mapMaybe (stripPrefix "id: ") (lines (resultStdout r)) of
927939
-- ~/.cabal/store may contain multiple versions of single package
928940
-- we pick first one. It should work
929941
(x:_) -> return (takeWhile (not . Char.isSpace) x)

cabal-testsuite/src/Test/Cabal/Run.hs

+33-13
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,18 @@ import System.IO
1515
import System.Exit
1616
import System.Directory
1717
import System.FilePath
18+
import Data.Foldable (traverse_)
1819

1920
-- | The result of invoking the command line.
2021
data Result = Result
2122
{ resultExitCode :: ExitCode
2223
, resultCommand :: String
23-
, resultOutput :: String
24+
-- | Output sent to any file descriptor.
25+
, resultOut :: String
26+
-- | Output sent to stdout.
27+
, resultStdout :: String
28+
-- | Output sent to stderr.
29+
, resultStderr :: String
2430
} deriving Show
2531

2632
-- | Run a command, streaming its output to stdout, and return a 'Result'
@@ -46,22 +52,23 @@ run _verbosity mb_cwd env_overrides path0 args input = do
4652

4753
mb_env <- getEffectiveEnvironment env_overrides
4854
putStrLn $ "+ " ++ showCommandForUser path args
49-
(readh, writeh) <- Compat.createPipe
50-
hSetBuffering readh LineBuffering
51-
hSetBuffering writeh LineBuffering
52-
let drain = do
53-
r <- hGetContents readh
54-
putStr r -- forces the output
55-
hClose readh
55+
(readstdout, writestdout) <- Compat.createPipe
56+
(readstderr, writestderr) <- Compat.createPipe
57+
(readall, writeall) <- Compat.createPipe
58+
traverse_ (`hSetBuffering` LineBuffering) [ stdout, readstdout, writestdout, readstderr, writestderr, readall, writeall ]
59+
let mkDrain h = do
60+
r <- hGetContents' h
61+
hPutStr writeall r
5662
return r
57-
withAsync drain $ \sync -> do
63+
withAsync (mkDrain readstdout) $ \syncstdout -> do
64+
withAsync (mkDrain readstderr) $ \syncstderr -> do
5865

5966
let prc = (proc path args)
6067
{ cwd = mb_cwd
6168
, env = mb_env
6269
, std_in = case input of { Just _ -> CreatePipe; Nothing -> Inherit }
63-
, std_out = UseHandle writeh
64-
, std_err = UseHandle writeh
70+
, std_out = UseHandle writestdout
71+
, std_err = UseHandle writestderr
6572
}
6673
(stdin_h, _, _, procHandle) <- createProcess prc
6774

@@ -74,10 +81,23 @@ run _verbosity mb_cwd env_overrides path0 args input = do
7481

7582
-- wait for the program to terminate
7683
exitcode <- waitForProcess procHandle
77-
out <- wait sync
84+
rStdout <- wait syncstdout
85+
rStderr <- wait syncstderr
86+
hClose writeall
87+
88+
rAll <- hGetContents' readall
7889

7990
return Result {
8091
resultExitCode = exitcode,
8192
resultCommand = showCommandForUser path args,
82-
resultOutput = out
93+
resultOut = rAll,
94+
resultStdout = rStdout,
95+
resultStderr = rStderr
8396
}
97+
98+
-- `hGetContents'` is in since base-4.15.0.0 -- which we don't have.
99+
hGetContents' :: Handle -> IO String
100+
hGetContents' h = do
101+
v <- hGetContents h
102+
length v `seq` hClose h
103+
pure v

changelog.d/issue-7790

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
synopsis: Test where Cabal writes output to
2+
packages: Cabal
3+
issues: #7790

0 commit comments

Comments
 (0)