packages feed

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

{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

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

import Prelude

import Control.Monad (void)
import Data.Fixed (Micro, Nano)
import Data.Maybe (isJust)
import Data.Text (Text, unpack)
import Data.Text qualified as T
import Data.Time.ISO8601 (parseISO8601)
import Example qualified
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Test.Hspec.JUnit.Config
import Test.Hspec.JUnit.Formatter qualified as Formatter
import Test.Hspec.Runner qualified as Hspec
import Text.Read (readMaybe)
import Text.XML qualified as XML
import Text.XML.Cursor

spec :: Spec
spec = do
  context "ExampleSpec" $ do
    doc <- runIO $ fromDocument <$> renderJUnitXml testConfig Example.spec

    context "time attributes" $ do
      it "sums correctly on testsuites and testsuite" $ do
        let
          -- Do the summation at Nano...
          readNano :: Text -> Nano
          readNano = maybe 0 realToFrac . readMaybe @Double . unpack

          -- ...but truncate to Micro for comparison
          readTimes :: [[Text]] -> Micro
          readTimes = realToFrac . sum . concatMap (map readNano)

          suitesTime :: Micro
          suitesTime =
            readTimes
              $ doc
                $| element "testsuites"
                &| attribute "time"

          suiteTimes :: Micro
          suiteTimes =
            readTimes
              $ doc
                $| element "testsuites"
                &/ element "testsuite"
                &| attribute "time"

          casesTimes :: Micro
          casesTimes =
            readTimes
              $ doc
                $| element "testsuites"
                &/ element "testsuite"
                &/ element "testcase"
                &| attribute "time"

        suitesTime `shouldSatisfy` (> 0)
        suitesTime `shouldBe` suiteTimes
        suitesTime `shouldBe` casesTimes

    context "timestamp attributes" $ do
      it "has the same timestamp on testsuites and testsuite" $ do
        let
          [suitesTimestamp] =
            concatMap (map (parseISO8601 . unpack))
              $ doc
                $| element "testsuites"
                &| attribute "timestamp"

          [suite1Timestamp, suite2Timestamp] =
            concatMap (map (parseISO8601 . unpack))
              $ doc
                $| element "testsuites"
                &/ element "testsuite"
                &| attribute "timestamp"

        suitesTimestamp `shouldSatisfy` isJust
        suite1Timestamp `shouldSatisfy` isJust
        suite2Timestamp `shouldSatisfy` isJust
        suite1Timestamp `shouldBe` suitesTimestamp
        suite2Timestamp `shouldBe` suitesTimestamp

    context "testsuites node" $ do
      it "produces one, with our package name" $ do
        ( doc
            $| element "testsuites"
            &| attribute "name"
          )
          `shouldBe` [["hspec-junit-format"]]

    context "testsuite nodes" $ do
      it "produces two" $ do
        ( doc
            $| element "testsuites"
            &/ element "testsuite"
          )
          `shouldSatisfy` (== 2) . length

      it "has name" $ do
        ( doc
            $| element "testsuites"
            &/ element "testsuite"
            &| attribute "name"
          )
          `shouldBe` [ ["Some section"]
                     , ["Some section/A grouped context"]
                     ]

    context "testcase nodes" $ do
      it "has time" $ do
        ( doc
            $| element "testsuites"
            &/ element "testsuite"
            &/ element "testcase"
            &| attribute "time"
          )
          `shouldSatisfy` (== 6) . length

      it "has timestamp" $ do
        ( doc
            $| element "testsuites"
            &/ element "testsuite"
            &/ element "testcase"
            &| attribute "timestamp"
          )
          `shouldSatisfy` (== 6) . length

      it "has classname" $ do
        ( doc
            $| element "testsuites"
            &/ element "testsuite"
            &/ element "testcase"
            &| attribute "classname"
          )
          `shouldBe` ( replicate 2 ["Some section"]
                         <> replicate 4 ["Some section/A grouped context"]
                     )

      it "has file" $ do
        ( doc
            $| element "testsuites"
            &/ element "testsuite"
            &/ element "testcase"
            &| attribute "file"
          )
          `shouldBe` replicate 6 ["tests/Example.hs"]

      it "has name" $ do
        ( doc
            $| element "testsuites"
            &/ element "testsuite"
            &/ element "testcase"
            &| attribute "name"
          )
          `shouldBe` [ ["has an expectation"]
                     , ["has a failure"]
                     , ["happens in a group"]
                     , ["also happens in a group"]
                     , ["gets skipped"]
                     , ["throws a colourful exception"]
                     ]

    context "first testsuite" $ do
      let [testsuite, _] = doc $| element "testsuites" &/ element "testsuite"

      it "has correct counts" $ do
        (testsuite $| attribute "errors") `shouldBe` ["0"]
        (testsuite $| attribute "failures") `shouldBe` ["1"]
        (testsuite $| attribute "skipped") `shouldBe` ["0"]
        (testsuite $| attribute "tests") `shouldBe` ["2"]

      it "produces two testcase nodes" $ do
        (testsuite $/ element "testcase") `shouldSatisfy` (== 2) . length

      context "first testcase" $ do
        let [testcase, _] = testsuite $/ element "testcase"

        it "has line" $ do
          (testcase $| attribute "line") `shouldBe` ["16"]

        it "has name" $ do
          (testcase $| attribute "name") `shouldBe` ["has an expectation"]

        it "has no child nodes" $ do
          (testcase $| child &| node) `shouldBe` []

      context "second testcase" $ do
        let [_, testcase] = testsuite $/ element "testcase"

        it "has line" $ do
          (testcase $| attribute "line") `shouldBe` ["19"]

        it "has name" $ do
          (testcase $| attribute "name") `shouldBe` ["has a failure"]

        it "has a failure reported" $ do
          ( testcase
              $/ element "failure"
              &/ content
            )
            `shouldBe` [ T.unlines
                           [ "Expected: \"False\""
                           , " but got: \"True\""
                           ]
                       ]

    context "second testsuite" $ do
      let [_, testsuite] =
            doc
              $| element "testsuites"
              &/ element "testsuite"

      it "has correct counts" $ do
        (testsuite $| attribute "errors") `shouldBe` ["1"]
        (testsuite $| attribute "failures") `shouldBe` ["0"]
        (testsuite $| attribute "skipped") `shouldBe` ["1"]
        (testsuite $| attribute "tests") `shouldBe` ["4"]

      it "produces four testcase nodes" $ do
        (testsuite $/ element "testcase") `shouldSatisfy` (== 4) . length

      context "first testcase" $ do
        let [testcase, _, _, _] = testsuite $/ element "testcase"

        it "has line" $ do
          (testcase $| attribute "line") `shouldBe` ["22"]

        it "has name" $ do
          (testcase $| attribute "name") `shouldBe` ["happens in a group"]

        it "has no child nodes" $ do
          (testcase $| child &| node) `shouldBe` []

      context "second testcase" $ do
        let [_, testcase, _, _] = testsuite $/ element "testcase"

        it "has line" $ do
          (testcase $| attribute "line") `shouldBe` ["24"]

        it "has name" $ do
          (testcase $| attribute "name") `shouldBe` ["also happens in a group"]

        it "has no child nodes" $ do
          (testcase $| child &| node) `shouldBe` []

      context "third testcase" $ do
        let [_, _, testcase, _] = testsuite $/ element "testcase"

        it "has line" $ do
          (testcase $| attribute "line") `shouldBe` ["27"]

        it "has name" $ do
          (testcase $| attribute "name") `shouldBe` ["gets skipped"]

        it "has a skipped node" $ do
          ( testcase
              $/ element "skipped"
              &| attribute "message"
            )
            `shouldBe` [["some reason"]]

      context "fourth testcase" $ do
        let [_, _, _, testcase] = testsuite $/ element "testcase"

        it "has line" $ do
          pendingWith "Newer base returns line 29"
          (testcase $| attribute "line") `shouldBe` ["28"]

        it "has name" $ do
          (testcase $| attribute "name") `shouldBe` ["throws a colourful exception"]

        context "error content" $ do
          let [el] = testcase $/ element "error"

          it "strips escapes from a message attribute" $ do
            -- Annoying, but Hspec has tied our hands here
            (el $| attribute "type") `shouldBe` ["SomeException"]
            (el $| attribute "message") `shouldBe` ["ColourfulException"]

          it "does not strip escapes in content" $ do
            let [c] = el $/ content

            take 1 (T.lines c) `shouldBe` ["\ESC[32mColour\ESC[31mful\ESC[0mException"]

  context "ExampleSpec with prefixing" $ do
    let testConfig' = setJUnitConfigSourcePathPrefix "lol/monorepo" testConfig
    doc <- runIO $ fromDocument <$> renderJUnitXml testConfig' Example.spec

    it "prefixes testcase file attributes" $ do
      ( doc
          $| element "testsuites"
          &/ element "testsuite"
          &/ element "testcase"
          &| attribute "file"
        )
        `shouldBe` replicate 6 ["lol/monorepo/tests/Example.hs"]

  context "ExampleSpec with dropConsoleFormatting" $ do
    let testConfig' = setJUnitConfigDropConsoleFormatting True testConfig
    doc <- runIO $ fromDocument <$> renderJUnitXml testConfig' Example.spec

    it "drops console formatting in text nodes" $ do
      let [c] =
            doc
              $| element "testsuites"
              &/ element "testsuite"
              &/ element "testcase"
              &/ element "error"
              &/ content

      take 1 (T.lines c) `shouldBe` ["ColourfulException"]

testConfig :: JUnitConfig
testConfig = defaultJUnitConfig "hspec-junit-format"

renderJUnitXml :: JUnitConfig -> Spec -> IO XML.Document
renderJUnitXml baseConfig x = do
  withSystemTempDirectory "" $ \tmp -> do
    let junitConfig =
          setJUnitConfigOutputDirectory tmp
            $ setJUnitConfigOutputName "test.xml" baseConfig

    (config, forest) <-
      Hspec.evalSpec Hspec.defaultConfig $ Formatter.use junitConfig x

    void $ Hspec.runSpecForest forest config

    XML.readFile XML.def $ tmp </> "test.xml"