about summary refs log tree commit diff
path: root/third_party/bazel/rules_haskell/tools/coverage-reports/Main.hs
blob: 5ab4dafdefc105b28871e7773ca17b81c9804767 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

import Control.Monad (forM_)

import Control.Arrow.ListArrow (runLA)
import Data.Either.Utils (maybeToEither)
import Data.List (find)
import Data.List.Safe (head, tail)
import Data.List.Utils (split)
import Data.Tree.NTree.TypeDefs (NTree(..))
import Prelude hiding (head, tail)
import System.Console.CmdArgs.Implicit (Data, Typeable, cmdArgs)
import System.Directory (createDirectoryIfMissing)
import System.Exit (exitFailure)
import System.FilePath (FilePath, (</>), takeDirectory)
import qualified Text.XML.HXT.Arrow.ReadDocument as XML
import Text.XML.HXT.DOM.QualifiedName (localPart)
import Text.XML.HXT.DOM.TypeDefs (XNode(..), XmlTree)
import Text.XML.HXT.XPath.XPathEval (getXPath, getXPathSubTrees)

data Args = Args
  { testlog :: FilePath
  , destdir :: FilePath
  } deriving (Data, Typeable)

data ReportFile = ReportFile
  { content :: String
  , filename :: FilePath
  } deriving (Show)

main :: IO ()
main = do
  Args {testlog, destdir} <- cmdArgs $ Args {testlog = "", destdir = ""}
  if testlog == ""
    then putStrLn noTestlogError >> exitFailure
    else do
      fileContents <- readFile testlog
      let xmlTrees = runLA XML.xreadDoc fileContents
      let rootTree = find isRoot xmlTrees
      case rootTree of
        Nothing -> do
          putStrLn "Invalid XML format for testlog."
          exitFailure
        Just tree -> do
          let reportFiles = generateReportFiles tree
          case reportFiles of
            Right reports ->
              forM_ reports $ \ReportFile {content, filename} -> do
                putStrLn $ concat ["Creating ", show $ destdir </> filename]
                createDirectoryIfMissing
                  True
                  (destdir </> takeDirectory filename)
                writeFile (destdir </> filename) content
            Left err -> do
              putStrLn err
              exitFailure

generateReportFiles :: XmlTree -> Either String [ReportFile]
generateReportFiles doc =
  let testSuites = getXPath "/testsuites/testsuite" doc
   in concat <$> sequence (reportsForTestCase <$> testSuites)

reportsForTestCase :: XmlTree -> Either String [ReportFile]
reportsForTestCase testSuite = do
  caseName <-
    extractAttr =<<
    maybeToEither
      "Couldn't find testcase name."
      (head (getXPathSubTrees "/testsuite/testcase/@name" testSuite))
  let coverageOutputDirectory = takeDirectory caseName
  testOutput <-
    extractText =<<
    maybeToEither
      "Couldn't find system output."
      (head (getXPathSubTrees "/testsuite/system-out" testSuite))
  htmlPortion <-
    maybeToEither
      ("Couldn't find HTML report section in test case " ++ caseName ++ ".")
      (head =<< tail (split testOutputSeparator testOutput))
  let coverageReportPartXmlTrees = runLA XML.hreadDoc htmlPortion
  traverse
    (coveragePartToReportFile coverageOutputDirectory)
    coverageReportPartXmlTrees

coveragePartToReportFile :: FilePath -> XmlTree -> Either String ReportFile
coveragePartToReportFile parentDirectory reportPart = do
  filename <-
    extractAttr =<<
    maybeToEither
      "Couldn't find report part name."
      (head (getXPathSubTrees "/coverage-report-part/@name" reportPart))
  content <- extractText reportPart
  return $
    ReportFile
      { content = content
      , filename = "coverage-reports" </> parentDirectory </> filename
      }

noTestlogError :: String
noTestlogError =
  unlines
    [ "ERROR: You must specify the testlog XML file location with --testlog."
    , "It is found inside the bazel-testlog, in the respective"
    , "folder for the test you're interested in."
    , "This must be after having run 'bazel coverage'."
    ]

isRoot :: XmlTree -> Bool
isRoot tree =
  case tree of
    NTree (XTag name _) _ -> localPart name == "testsuites"
    _ -> False

extractAttr :: XmlTree -> Either String String
extractAttr tree =
  case tree of
    NTree (XAttr _) [NTree (XText value) []] -> pure value
    _ -> Left "Couldn't extract attribute from test XML."

extractText :: XmlTree -> Either String String
extractText tree =
  let treeToText :: XmlTree -> String -> String
      treeToText textTree acc =
        case textTree of
          (NTree (XText value) _) -> acc ++ value
          _ -> ""
   in case tree of
        NTree (XTag _ _) textTree -> pure $ foldr treeToText "" textTree
        _ -> Left "Couldn't extract text from test XML."

testOutputSeparator :: String
testOutputSeparator = "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"