packages feed

hspec-junit-formatter-1.1.1.0: library/Test/Hspec/JUnit.hs

{-# LANGUAGE CPP #-}

module Test.Hspec.JUnit
  {-# DEPRECATED "Use Test.Hspec.JUnit.Formatter" #-}
  ( -- * Runners
    hspecJUnit
  , hspecJUnitWith

    -- * Directly modifying 'Config'
  , configWithJUnit
  , configWithJUnitAvailable

    -- * Actual format function
  , junitFormat

    -- * Configuration
  , module Test.Hspec.JUnit.Config
  ) where

import Prelude

import Control.Applicative ((<|>))
import Data.Conduit (runConduitRes, (.|))
import Data.Conduit.Combinators (sinkFile)
import Data.Conduit.List (sourceList)
import Data.Functor ((<&>))
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text, pack, unpack)
import qualified Data.Text as T
import Data.Time (getCurrentTime)
import System.Directory (createDirectoryIfMissing)
import System.FilePath (splitFileName)
import Test.Hspec.Core.Format
  ( Event (..)
  , FailureReason (..)
  , Format
  , FormatConfig
  , Item (..)
  , Location (..)
  , Path
  , Result (..)
  , Seconds (..)
  )
import Test.Hspec.Core.Runner (Config (..), defaultConfig, hspecWith)
import Test.Hspec.Core.Spec (Spec)
import Test.Hspec.JUnit.Config
import Test.Hspec.JUnit.Config.Env
import Test.Hspec.JUnit.Render (renderJUnit)
import qualified Test.Hspec.JUnit.Schema as Schema
import Text.XML.Stream.Render (def, renderBytes)

-- | Like 'hspec' but adds JUNit functionality
--
-- To actually /use/ the JUnit format, you must set @JUNIT_ENABLED=1@ in the
-- environment; by default, this function just behaves like 'hspec'.
--
-- Running tests with @--test-arguments="-f junit"@ also works.
--
-- All configuration of the JUnit report occurs through environment variables.
--
-- See "Test.Hspec.JUnit.Config" and "Test.Hspec.JUnit.Config.Env".
hspecJUnit :: Spec -> IO ()
hspecJUnit = hspecJUnitWith defaultConfig

-- | 'hspecJUnit' but built on a non-default 'Config'
hspecJUnitWith :: Config -> Spec -> IO ()
hspecJUnitWith config spec = do
  junitEnabled <- envJUnitEnabled
  junitConfig <- envJUnitConfig

  let
    modify = if junitEnabled then configWithJUnit junitConfig else id
    base = configWithJUnitAvailable junitConfig config

  hspecWith (modify base) spec

-- | Modify an Hspec 'Config' to use 'junitFormat'
configWithJUnit :: JUnitConfig -> Config -> Config
configWithJUnit junitConfig config =
  config {configFormat = Just $ junitFormat junitConfig}

-- | Modify an Hspec 'Config' to have the 'junitFormat' /available/
--
-- Adds @junit@ to the list of available options for @-f, --format@.
configWithJUnitAvailable :: JUnitConfig -> Config -> Config
configWithJUnitAvailable junitConfig config =
  config
    { configAvailableFormatters =
        configAvailableFormatters config <> [("junit", junitFormat junitConfig)]
    }

-- | Hspec 'configFormat' that generates a JUnit report
junitFormat :: JUnitConfig -> FormatConfig -> IO Format
junitFormat junitConfig _config = pure $ \case
  Started -> pure ()
  GroupStarted _ -> pure ()
  GroupDone _ -> pure ()
  Progress _ _ -> pure ()
  ItemStarted _ -> pure ()
  ItemDone _ _ -> pure ()
  Done paths -> do
    time <- getCurrentTime

    let (directory, _) = splitFileName file
    createDirectoryIfMissing True directory

    let
      groups = groupItems paths
      output =
        Schema.Suites
          { suitesName = suiteName
          , suitesSuites =
              groups <&> \(group, items) -> do
                let suite xs =
                      Schema.Suite
                        { suiteName = group
                        , suiteTimestamp = time
                        , suiteCases = xs
                        }
                suite $ uncurry (itemToTestCase applyPrefix group) <$> items
          }

    runConduitRes $
      sourceList [output]
        .| renderJUnit dropConsoleFormatting
        .| renderBytes def
        .| sinkFile file
 where
  file = getJUnitConfigOutputFile junitConfig
  suiteName = getJUnitConfigSuiteName junitConfig
  applyPrefix = getJUnitPrefixSourcePath junitConfig
  dropConsoleFormatting = getJUnitConfigDropConsoleFormatting junitConfig

groupItems :: [(Path, Item)] -> [(Text, [(Text, Item)])]
groupItems = Map.toList . Map.fromListWith (<>) . fmap group
 where
  group ((path, name), item) =
    (T.intercalate "/" $ pack <$> path, [(pack name, item)])

itemToTestCase
  :: (FilePath -> FilePath) -> Text -> Text -> Item -> Schema.TestCase
itemToTestCase applyPrefix group name item =
  Schema.TestCase
    { testCaseLocation =
        toSchemaLocation applyPrefix
          <$> (itemResultLocation item <|> itemLocation item)
    , testCaseClassName = group
    , testCaseName = name
    , testCaseDuration = unSeconds $ itemDuration item
    , testCaseResult = case itemResult item of
        Success -> Nothing
        Pending mLocation mMessage ->
          Just $
            Schema.Skipped $
              prefixLocation mLocation $
                prefixInfo $
                  maybe
                    ""
                    pack
                    mMessage
        Failure mLocation reason ->
          Just $
            Schema.Failure "error" $
              prefixLocation mLocation $
                prefixInfo $
                  reasonToText reason
    }
 where
  prefixLocation mLocation str = case mLocation of
    Nothing -> str
    Just Location {..} ->
      mconcat
        [ pack $ applyPrefix locationFile
        , ":"
        , pack $ show locationLine
        , ":"
        , pack $ show locationColumn
        , "\n"
        ]
        <> str
  prefixInfo str
    | T.null $ T.strip $ pack $ itemInfo item = str
    | otherwise = pack (itemInfo item) <> "\n\n" <> str

itemResultLocation :: Item -> Maybe Location
itemResultLocation item = case itemResult item of
  Success -> Nothing
  Pending mLocation _ -> mLocation
  Failure mLocation _ -> mLocation

toSchemaLocation :: (FilePath -> FilePath) -> Location -> Schema.Location
toSchemaLocation applyPrefix Location {..} =
  Schema.Location
    { Schema.locationFile = applyPrefix locationFile
    , Schema.locationLine = fromIntegral $ max 0 locationLine
    }

unSeconds :: Seconds -> Double
unSeconds (Seconds x) = x

foundLines :: Show a => Text -> a -> [String]
foundLines msg found = case lines' of
  [] -> []
  first : rest ->
    unpack (msg <> ": " <> first) : (unpack . (T.replicate 9 " " <>) <$> rest)
 where
  lines' = T.lines . pack $ show found

{- FOURMOLU_DISABLE -}
reasonToText :: FailureReason -> Text
reasonToText = \case
  Error _ err -> pack $ show err
  NoReason -> "no reason"
  Reason err -> pack err
#if MIN_VERSION_hspec_core(2,11,0)
  ColorizedReason err -> pack err
#endif
  ExpectedButGot preface expected actual ->
    T.unlines
      $ pack
      <$> fromMaybe "" preface
      : (foundLines "expected" expected
        <> foundLines " but got" actual
        )
{- FOURMOLU_ENABLE -}