packages feed

hspec-junit-formatter-1.2.0.0: tests/Test/Hspec/JUnit/FormatterSpec.hs

{-# LANGUAGE CPP #-}

module Test.Hspec.JUnit.FormatterSpec
  ( spec
  ) where

import Prelude

import Control.Monad (void)
import Data.Char (isSpace)
import Data.List (isInfixOf, isPrefixOf)
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Example qualified
import System.FilePath ((<.>), (</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Test.Hspec.Golden
import Test.Hspec.JUnit.Config
import Test.Hspec.JUnit.Formatter qualified as Formatter
import Test.Hspec.Runner
import Text.XML qualified as XML

spec :: Spec
spec = do
  it "matches golden file" $ do
    junitGolden "default" id

  it "matches golden file with prefixing" $ do
    junitGolden "prefixed" $ setJUnitConfigSourcePathPrefix "lol/monorepo"

-- | Run @Example.spec@ and compare XML to a golden file
junitGolden
  :: String
  -- ^ Unique name
  -> (JUnitConfig -> JUnitConfig)
  -- ^ Any modification to make to the 'JUnitConfig' before running
  -> IO (Golden XML.Document)
junitGolden name modifyConfig = do
  actual <- withSystemTempDirectory "" $ \tmp -> do
    let junitConfig =
          modifyConfig
            $ setJUnitConfigOutputDirectory tmp
            $ setJUnitConfigOutputName "test.xml"
            $ defaultJUnitConfig "hspec-junit-format"

    runSpec' $ Formatter.use junitConfig Example.spec
    readNormalizedXML $ tmp </> "test.xml"

  pure
    Golden
      { output = actual
      , encodePretty = show
      , writeToFile = XML.writeFile XML.def
      , readFromFile = readNormalizedXML
      , goldenFile =
          "tests" </> "golden" </> name <> "-" <> ghcSuffix <.> "xml"
      , actualFile = Nothing
      , failFirstTime = False
      }

runSpec' :: Spec -> IO ()
runSpec' x = do
  (config, forest) <- evalSpec defaultConfig x
  void $ runSpecForest forest config

readNormalizedXML :: FilePath -> IO XML.Document
readNormalizedXML = fmap normalizeDoc . XML.readFile XML.def

normalizeDoc :: XML.Document -> XML.Document
normalizeDoc = removeWhitespaceNodes . removeTimeAttributes . normalizeErrorMessages

removeWhitespaceNodes :: XML.Document -> XML.Document
removeWhitespaceNodes doc =
  doc
    { XML.documentRoot = go $ XML.documentRoot doc
    }
 where
  go el =
    el {XML.elementNodes = concatMap filterWhitespace $ XML.elementNodes el}

  filterWhitespace :: XML.Node -> [XML.Node]
  filterWhitespace = \case
    XML.NodeElement el -> [XML.NodeElement $ go el]
    XML.NodeContent c | T.all isSpace c -> []
    n -> [n]

-- | Remove volatile attributes so they don't invalidate comparison
removeTimeAttributes :: XML.Document -> XML.Document
removeTimeAttributes =
  removeAttributesByName "time" . removeAttributesByName "timestamp"

removeAttributesByName :: XML.Name -> XML.Document -> XML.Document
removeAttributesByName name doc =
  doc
    { XML.documentRoot = go $ XML.documentRoot doc
    }
 where
  go el =
    el
      { XML.elementAttributes = Map.delete name $ XML.elementAttributes el
      , XML.elementNodes = map (onNodeElement go) $ XML.elementNodes el
      }

  onNodeElement f = \case
    XML.NodeElement el -> XML.NodeElement $ f el
    n -> n

normalizeErrorMessages :: XML.Document -> XML.Document
normalizeErrorMessages doc =
  doc
    { XML.documentRoot = go $ XML.documentRoot doc
    }
 where
  go el =
    el
      { XML.elementNodes =
          map (onNodeElement go . normalizeErrorContent . normalizeLineNumbers)
            $ XML.elementNodes el
      }

  normalizeErrorContent :: XML.Node -> XML.Node
  normalizeErrorContent = \case
    XML.NodeElement el
      | XML.elementName el == XML.Name "failure" Nothing Nothing ->
          XML.NodeElement
            $ el {XML.elementNodes = map stripLocationPrefix $ XML.elementNodes el}
      | XML.elementName el == XML.Name "skipped" Nothing Nothing ->
          XML.NodeElement
            $ el {XML.elementNodes = map stripLocationPrefix $ XML.elementNodes el}
      | otherwise -> XML.NodeElement el
    n -> n

  normalizeLineNumbers :: XML.Node -> XML.Node
  normalizeLineNumbers = \case
    XML.NodeElement el
      | XML.elementName el == XML.Name "testcase" Nothing Nothing ->
          let
            attrs = XML.elementAttributes el
            normalizedAttrs = Map.adjust normalizeLineAttr (XML.Name "line" Nothing Nothing) attrs
          in
            XML.NodeElement $ el {XML.elementAttributes = normalizedAttrs}
      | otherwise -> XML.NodeElement el
    n -> n

  normalizeLineAttr :: T.Text -> T.Text
  normalizeLineAttr lineText
    | lineText == "29" = "28" -- Normalize line 29 to 28 for version compatibility
    | otherwise = lineText

  stripLocationPrefix :: XML.Node -> XML.Node
  stripLocationPrefix = \case
    XML.NodeContent content ->
      let
        contentText = T.unpack content
        normalizedContent = case lines contentText of
          (firstLine : rest)
            | ( "tests/Example.hs:" `isPrefixOf` firstLine
                  || "lol/monorepo/tests/Example.hs:" `isPrefixOf` firstLine
              )
                && "\n" `isInfixOf` contentText ->
                unlines rest
          _ -> contentText
        trimmedContent = case reverse normalizedContent of
          '\n' : rest -> reverse rest
          _ -> normalizedContent
      in
        XML.NodeContent $ T.pack trimmedContent
    n -> n

  onNodeElement f = \case
    XML.NodeElement el -> XML.NodeElement $ f el
    n -> n

-- GHC can change certain aspects, mainly about source-location, so we can
-- incorpate that by tracking separate Golden files as necessary
ghcSuffix :: String
#if __GLASGOW_HASKELL__ >= 900
ghcSuffix = "ghc-9"
#elif __GLASGOW_HASKELL__ >= 800
ghcSuffix = "ghc-8"
#else
-- Fail to compile on other GHCs
#endif