|
| 1 | +{-# LANGUAGE ApplicativeDo #-} |
| 2 | +{-# LANGUAGE OverloadedStrings #-} |
| 3 | +{-# LANGUAGE RecordWildCards #-} |
| 4 | +{-# LANGUAGE TypeApplications #-} |
| 5 | + |
| 6 | +import Cabal.Plan |
| 7 | +import Control.Monad (guard, unless) |
| 8 | +import Data.Aeson qualified as JSON |
| 9 | +import Data.Map.Strict qualified as Map |
| 10 | +import Data.Text qualified as T |
| 11 | +import Data.Traversable (for) |
| 12 | +import LogResults |
| 13 | +import Options.Applicative hiding (Failure) |
| 14 | +import Parse |
| 15 | +import System.Console.Terminal.Size qualified as TS |
| 16 | +import System.Directory (doesDirectoryExist, doesFileExist) |
| 17 | +import System.Exit (die) |
| 18 | +import System.FilePath ((<.>), (</>)) |
| 19 | +import System.IO (hPutStrLn, stderr) |
| 20 | + |
| 21 | +data Options = Options |
| 22 | + { optVerbosity :: Int |
| 23 | + , optProjectDir :: FilePath |
| 24 | + , optOutput :: FilePath |
| 25 | + } |
| 26 | + deriving (Show) |
| 27 | + |
| 28 | +main :: IO () |
| 29 | +main = do |
| 30 | + cols <- maybe 100 TS.width <$> TS.size |
| 31 | + |
| 32 | + let counter = fmap length . many . flag' () |
| 33 | + |
| 34 | + Options {..} <- |
| 35 | + customExecParser |
| 36 | + (prefs $ columns cols) |
| 37 | + ( info |
| 38 | + ( helper <*> do |
| 39 | + optVerbosity <- |
| 40 | + counter $ |
| 41 | + help "Increase output verbosity (repeatable)" |
| 42 | + <> short 'v' |
| 43 | + <> long "verbose" |
| 44 | + optProjectDir <- |
| 45 | + strOption $ |
| 46 | + help "The project directory, or a subdirectory of it" |
| 47 | + <> short 'p' |
| 48 | + <> long "project" |
| 49 | + <> metavar "DIR" |
| 50 | + <> value "." |
| 51 | + <> showDefaultWith id |
| 52 | + optOutput <- |
| 53 | + strOption $ |
| 54 | + help "Write output to FILE" |
| 55 | + <> short 'o' |
| 56 | + <> long "output" |
| 57 | + <> metavar "FILE" |
| 58 | + <> value "/dev/stdout" |
| 59 | + <> showDefaultWith id |
| 60 | + pure Options {..} |
| 61 | + ) |
| 62 | + (fullDesc <> header "Extract failure information from Cabal test logs") |
| 63 | + ) |
| 64 | + |
| 65 | + let trace n = if optVerbosity >= n then hPutStrLn stderr else const mempty |
| 66 | + |
| 67 | + -- Avoid confusing behaviour from `findProjectRoot` |
| 68 | + doesDirectoryExist optProjectDir |
| 69 | + >>= (`unless` die ("Project directory " <> optProjectDir <> " doesn't exist")) |
| 70 | + |
| 71 | + root <- |
| 72 | + findProjectRoot optProjectDir |
| 73 | + >>= maybe (die $ "Can't find project root in " <> optProjectDir) pure |
| 74 | + |
| 75 | + plan <- findAndDecodePlanJson $ ProjectRelativeToDir root |
| 76 | + |
| 77 | + let |
| 78 | + targetLogs = do |
| 79 | + -- List monad |
| 80 | + unit <- Map.elems $ pjUnits plan |
| 81 | + guard $ uType unit == UnitTypeLocal |
| 82 | + Just dir <- [uDistDir unit] |
| 83 | + comp@(CompNameTest tName) <- Map.keys (uComps unit) |
| 84 | + let |
| 85 | + pId = uPId unit |
| 86 | + PkgId pName _ = pId |
| 87 | + PkgName name = pName |
| 88 | + target = name <> ":" <> dispCompNameTarget pName comp |
| 89 | + file = dir </> "test" </> T.unpack (dispPkgId pId <> "-" <> tName) <.> "log" |
| 90 | + pure (target, file) |
| 91 | + |
| 92 | + trace 1 $ show (length targetLogs) <> " Cabal targets found" |
| 93 | + |
| 94 | + targetFailures <- |
| 95 | + for targetLogs $ \(target, file) -> do |
| 96 | + exists <- doesFileExist file |
| 97 | + failures <- |
| 98 | + if exists |
| 99 | + then do |
| 100 | + trace 2 $ "Examining " <> file |
| 101 | + parseLog file |
| 102 | + else |
| 103 | + pure mempty |
| 104 | + pure (target, failures) |
| 105 | + |
| 106 | + let logResults = Map.fromList $ filter (not . null . snd) targetFailures |
| 107 | + |
| 108 | + trace 1 $ show (Map.size logResults) <> " logs with failures found" |
| 109 | + |
| 110 | + JSON.encodeFile @LogResults optOutput logResults |
0 commit comments