{-# OPTIONS_GHC -Wall -Werror -funbox-strict-fields #-} -- | 'Reporter' for running HUnit tests and reporting results as -- JUnit-style XML reports. This uses the hexpat library for XML -- generation. This module also contains functions for creating the -- various nodes in a JUnit XML report. module Test.HUnitPlus.XML( -- * XML Generation propertyElem, propertiesElem, systemOutElem, systemErrElem, failureElem, errorElem, testcaseElem, skippedTestElem, testSuiteElem, testSuitesElem, -- * Reporter xmlReporter ) where import Data.Map(Map) import Data.Time import Data.Word import Network.HostName import System.Locale import Test.HUnitPlus.Reporting(Reporter(..), State(..), Counts(..), defaultReporter, showPath) import Text.XML.Expat.Tree import qualified Data.Map as Map -- | Generate an element for a property definition propertyElem :: (String, String) -- ^ The name/value pair -> Node String String propertyElem (name, value) = Element { eName = "property", eChildren = [], eAttributes = [("name", name), ("value", value)] } -- | Generate an element for a set of property definitions propertiesElem :: [(String, String)] -- ^ A list of name/value pairs to make into properties -> Node String String propertiesElem props = Element { eName = "properties", eAttributes = [], eChildren = map propertyElem props } -- | Generate an element representing output to stdout systemOutElem :: String -- ^ The stdout output -> Node String String systemOutElem content = Element { eName = "system-out", eAttributes = [], eChildren = [Text content] } -- | Generate an element representing output to stderr systemErrElem :: String -- ^ The stderr output -> Node String String systemErrElem content = Element { eName = "system-err", eAttributes = [], eChildren = [Text content] } -- | Generate an element representing a test failure. failureElem :: String -- ^ A message associated with the failure -> Node String String failureElem message = Element { eAttributes = [("message", message)], eName = "failure", eChildren = [] } -- | Generate an element representing an error in a test. errorElem :: String -- ^ A message associated with the error -> Node String String errorElem message = Element { eAttributes = [("message", message)], eName = "error", eChildren = [] } -- | Generate an element for a single test case. testcaseElem :: String -- ^ The name of the test -> String -- ^ The path to the test (reported as \"classname\") -> Word -- ^ The number of assertions in the test -> Double -- ^ The execution time of the test -> [Node String String] -- ^ Elements representing the events that happened -- during test execution. -> Node String String testcaseElem name classname assertions time children = Element { eName = "testcase", eChildren = children, eAttributes = [("name", name), ("classname", classname), ("assertions", show assertions), ("time", show time)] } -- | Generate an element for a skipped test case skippedTestElem :: String -- ^ The name of the test -> String -- ^ The path of the test -> Node String String skippedTestElem name classname = let skippedElem = Element { eName = "skipped", eAttributes = [], eChildren = [] } in Element { eAttributes = [("name", name), ("classname", classname)], eName = "testcase", eChildren = [skippedElem] } -- | Generate an element for a test suite run testSuiteElem :: String -- ^ The name of the test suite -> Map String String -- ^ The properties defined for this suite -> Word -- ^ The number of tests -> Word -- ^ The number of failures -> Word -- ^ The number of errors -> Word -- ^ The number of skipped tests -> String -- ^ The hostname of the machine on which this was run -> UTCTime -- ^ The timestamp at which time this was run -> Double -- ^ The execution time for the test suite -> [Node String String] -- ^ The testcases and output nodes for the test suite -> Node String String testSuiteElem name propmap tests failures errors skipped hostname timestamp time content = let contentWithProps = case Map.assocs propmap of [] -> content props -> propertiesElem props : content timestr = formatTime defaultTimeLocale "%c" timestamp in Element { eName = "testsuite", eChildren = contentWithProps, eAttributes = [("name", name), ("hostname", hostname), ("timestamp", timestr), ("time", show time), ("tests", show tests), ("failures", show failures), ("errors", show errors), ("skipped", show skipped)] } -- | Generate the top-level element containing all test suites testSuitesElem :: Double -- ^ The execution time of all suites -> [Node String String] -- ^ Elements representing all the test suites -> Node String String testSuitesElem time suites = Element { eName = "testsuites", eChildren = suites, eAttributes = [("time", show time)] } -- | A reporter that generates JUnit XML reports xmlReporter :: Reporter [[Node String String]] xmlReporter = let reportStart = return [[]] reportEnd time _ [suites] = return [[testSuitesElem time (reverse suites)]] reportEnd _ _ _ = fail "Extra information on node stack" reportStartSuite _ stack = return ([] : stack) reportEndSuite time State { stName = name, stOptions = options, stCounts = Counts { cCases = cases, cErrors = errors, cFailures = failures, cSkipped = skipped } } (events : rest : stack) = do hostname <- getHostName timestamp <- getCurrentTime return ((testSuiteElem name options cases failures errors skipped hostname timestamp time (reverse events) : rest) : stack) reportEndSuite _ _ stack = fail ("Node stack underflow in end suite.\n" ++ show stack) reportStartCase _ stack = return ([] : stack) reportEndCase time State { stName = name, stPath = testpath, stCounts = Counts { cCaseAsserts = asserts } } (events : rest : stack) = return ((testcaseElem name (showPath testpath) asserts time (reverse events) : rest) : stack) reportEndCase _ _ _ = fail "Node stack underflow in end case" reportSkipCase State { stName = name, stPath = testpath } (rest : stack) = return ((skippedTestElem name (showPath testpath) : rest) : stack) reportSkipCase _ _ = fail "Node stack underflow in skip case" reportFailure msg _ (rest : stack) = return ((failureElem msg : rest) : stack) reportFailure _ _ _ = fail "Node stack underflow in report failure" reportError msg _ (rest : stack) = return ((errorElem msg : rest) : stack) reportError _ _ _ = fail "Node stack underflow in report error" reportSystemOut msg _ (rest : stack) = return ((systemOutElem msg : rest) : stack) reportSystemOut _ _ _ = fail "Node stack underflow in system out" reportSystemErr msg _ (rest : stack) = return ((systemErrElem msg : rest) : stack) reportSystemErr _ _ _ = fail "Node stack underflow in system err" in defaultReporter { reporterStart = reportStart, reporterEnd = reportEnd, reporterStartSuite = reportStartSuite, reporterEndSuite = reportEndSuite, reporterStartCase = reportStartCase, reporterEndCase = reportEndCase, reporterSkipCase = reportSkipCase, reporterFailure = reportFailure, reporterError = reportError, reporterSystemOut = reportSystemOut, reporterSystemErr = reportSystemErr }