packages feed

nri-prelude-0.5.0.2: src/Test/Reporter/Junit.hs

{-# LANGUAGE NumericUnderscores #-}

-- | Module for presenting test results as a Junit XML file.
--
-- Lifted in large part from: https://github.com/stoeffel/tasty-test-reporter
module Test.Reporter.Junit
  ( report,
  )
where

import qualified Control.Exception.Safe as Exception
import qualified Data.ByteString as BS
import qualified Data.Text
import qualified Data.Text.Encoding as TE
import qualified GHC.Stack as Stack
import qualified List
import NriPrelude
import qualified Platform
import qualified System.Directory as Directory
import qualified System.FilePath as FilePath
import qualified Test.Internal as Internal
import qualified Test.Reporter.Internal
import qualified Text
import qualified Text.Colour
import qualified Text.XML.JUnit as JUnit
import qualified Tuple
import qualified Prelude

report :: FilePath.FilePath -> Internal.SuiteResult -> Prelude.IO ()
report path result = do
  createPathDirIfMissing path
  results <- testResults result
  JUnit.writeXmlReport path results

testResults :: Internal.SuiteResult -> Prelude.IO (List JUnit.TestSuite)
testResults result =
  case result of
    Internal.AllPassed passed ->
      List.map renderPassed passed
        |> Prelude.pure
    Internal.OnlysPassed passed skipped ->
      Prelude.pure
        ( List.map renderSkipped skipped
            ++ List.map renderPassed passed
        )
    Internal.PassedWithSkipped passed skipped ->
      Prelude.pure
        ( List.map renderSkipped skipped
            ++ List.map renderPassed passed
        )
    Internal.TestsFailed passed skipped failed -> do
      srcLocs <-
        List.map (map Tuple.second) failed
          |> Prelude.traverse Test.Reporter.Internal.readSrcLoc
      let renderedFailed = List.map2 renderFailed failed srcLocs
      Prelude.pure
        ( renderedFailed
            ++ List.map renderSkipped skipped
            ++ List.map renderPassed passed
        )
    Internal.NoTestsInSuite -> Prelude.pure []

renderPassed :: Internal.SingleTest Platform.TracingSpan -> JUnit.TestSuite
renderPassed test =
  JUnit.passed (Internal.name test)
    |> JUnit.time (duration (Internal.body test))
    |> JUnit.inSuite (suiteName test)

renderSkipped :: Internal.SingleTest Internal.NotRan -> JUnit.TestSuite
renderSkipped test =
  JUnit.skipped (Internal.name test)
    |> JUnit.inSuite (suiteName test)

renderFailed ::
  Internal.SingleTest (Platform.TracingSpan, Internal.Failure) ->
  Maybe (Stack.SrcLoc, BS.ByteString) ->
  JUnit.TestSuite
renderFailed test maybeSrcLoc =
  case Internal.body test of
    (tracingSpan, Internal.FailedAssertion msg _) ->
      let msg' = case maybeSrcLoc of
            Nothing -> msg
            Just (loc, src) ->
              Test.Reporter.Internal.renderSrcLoc loc src
                |> Text.Colour.renderChunksBS Text.Colour.WithoutColours
                |> TE.decodeUtf8
                |> (\srcStr -> srcStr ++ "\n" ++ msg)
       in JUnit.failed (Internal.name test)
            |> JUnit.stderr msg'
            |> ( case stackFrame test of
                   Nothing -> identity
                   Just frame -> JUnit.failureStackTrace [frame]
               )
            |> JUnit.time (duration tracingSpan)
            |> JUnit.inSuite (suiteName test)
    (tracingSpan, Internal.ThrewException err) ->
      JUnit.errored (Internal.name test)
        |> JUnit.errorMessage "This test threw an exception."
        |> JUnit.stderr (Data.Text.pack (Exception.displayException err))
        |> ( case stackFrame test of
               Nothing -> identity
               Just frame -> JUnit.errorStackTrace [frame]
           )
        |> JUnit.time (duration tracingSpan)
        |> JUnit.inSuite (suiteName test)
    (tracingSpan, Internal.TookTooLong) ->
      JUnit.errored (Internal.name test)
        |> JUnit.errorMessage "This test timed out."
        |> ( case stackFrame test of
               Nothing -> identity
               Just frame -> JUnit.errorStackTrace [frame]
           )
        |> JUnit.time (duration tracingSpan)
        |> JUnit.inSuite (suiteName test)
    (tracingSpan, Internal.TestRunnerMessedUp msg) ->
      JUnit.errored (Internal.name test)
        |> JUnit.errorMessage
          ( Text.join
              "\n"
              [ "Test runner encountered an unexpected error:",
                msg,
                "",
                "This is a bug.",
                "If you have some time to report the bug it would be much appreciated!",
                "You can do so here: https://github.com/NoRedInk/haskell-libraries/issues"
              ]
          )
        |> ( case stackFrame test of
               Nothing -> identity
               Just frame -> JUnit.errorStackTrace [frame]
           )
        |> JUnit.time (duration tracingSpan)
        |> JUnit.inSuite (suiteName test)

suiteName :: Internal.SingleTest a -> Text
suiteName test =
  Internal.describes test
    |> Text.join " - "

stackFrame :: Internal.SingleTest a -> Maybe Text
stackFrame test =
  Internal.loc test
    |> map
      ( \loc ->
          Data.Text.pack
            ( Stack.srcLocFile loc
                ++ ":"
                ++ Prelude.show (Stack.srcLocStartLine loc)
            )
      )

duration :: Platform.TracingSpan -> Float
duration test =
  let duration' = Platform.finished test - Platform.started test
   in Prelude.fromIntegral (Platform.inMicroseconds duration') / 1000_000

createPathDirIfMissing :: FilePath.FilePath -> Prelude.IO ()
createPathDirIfMissing path = do
  dirPath <- map FilePath.takeDirectory (Directory.canonicalizePath path)
  Directory.createDirectoryIfMissing True dirPath