packages feed

hspec-formatter-github-0.1.0.0: src/Test/Hspec/Formatters/GithubAction/Internal.hs

{-# LANGUAGE StrictData #-}
{-# LANGUAGE OverloadedStrings #-}

-- | This module is internal! Use at your own risk. Breaking changes to
-- this module will not necessarily be reflected in PVP versions.
module Test.Hspec.Formatters.GithubAction.Internal where

import Prelude
import Control.Applicative
import Test.Hspec.Core.Util (joinPath)
import Test.Hspec.Api.Formatters.V2

-- | This option enhances a given 'Formatter' with annotations that will
-- show up on the correct test item failure. This allows you to use the
-- test output you want, and additionally get Github Action annotations.
withGithubActionFormatter :: Formatter -> Formatter
withGithubActionFormatter fmtr = fmtr
  { formatterItemDone = \path item -> do
      formatterItemDone fmtr path item
      emitGithubActionAnnotation path item
  }

-- | A representation of the Github Actions error format.
data ErrorCommand = ErrorCommand
    { title   :: Maybe String
    , file    :: Maybe String
    , line    :: Maybe Int
    , col     :: Maybe Int
    , message :: String
    }

-- | Make a suitable error annotation from an hspec failure.
--
-- Not clear what to do with the Maybe Location here: do we use the one from
-- the Item, or this one? What if both or neither are available?
-- Also not clear whether to use itemInfo.
errorCommandFor :: Path -> Item -> Maybe Location -> FailureReason -> ErrorCommand
errorCommandFor specPath@(_nesting, requirement) item mFailureLocation reason = ErrorCommand
    { -- requirement is used because it should always be non-empty and meaningful
      title = Just requirement
    , file = locationFile   <$> mloc
    , line = locationLine   <$> mloc
    , col  = locationColumn <$> mloc
    , message = unlines (messageHeaderLines ++ messageBodyLines)
    }
  where
    -- prefer the location of the actual failure. if that is not present,
    -- fall  back to the test definition location
    mloc = mFailureLocation <|> itemLocation item
    -- Use the path to give a message header, so that the message is never empty.
    -- Empty messages seem to cause github actions to ignore the annotation.
    messageHeaderLines :: [String]
    messageHeaderLines = joinPath specPath : if itemInfo item == "" then [] else [itemInfo item]

    messageBodyLines :: [String]
    messageBodyLines = case reason of
        NoReason -> []
        Reason str -> [str]
        ExpectedButGot preface expected actual ->
            let bodyLines =
                    [ mconcat ["expected: ", expected]
                    , mconcat ["     got: ", actual  ]
                    ]
                headerLines = maybe [] (\x -> [x]) preface
            in  headerLines ++ bodyLines
        Error preface err ->
            let bodyLines = [show err]
                headerLines = maybe [] (\x -> [x]) preface
            in  headerLines ++ bodyLines

-- | The github actions command format.
formatErrorCommand :: ErrorCommand -> String
formatErrorCommand ec = mconcat
    [ "\n::error "
    , "title=", maybe "" escapeProperty (title ec), ","
    , "file=", maybe "" escapeProperty (file ec), ","
    , "line=", maybe "" show (line ec), ","
    , "col=", maybe "" show (col ec)
    , "::"
    , escapeData (message ec)
    -- FIXME should check if on windows and use \r\n, as this is what the
    -- github library does.
    , "\n"
    ]

{-
function escapeData(s: any): string {
  return toCommandValue(s)
    .replace(/%/g, '%25')
    .replace(/\r/g, '%0D')
    .replace(/\n/g, '%0A')
}
function escapeProperty(s: any): string {
  return toCommandValue(s)
    .replace(/%/g, '%25')
    .replace(/\r/g, '%0D')
    .replace(/\n/g, '%0A')
    .replace(/:/g, '%3A')
    .replace(/,/g, '%2C')
}
-}

escapeData :: String -> String
escapeData = (>>= replace)
  where
    replace '%'  = "%25"
    replace '\r' = "%0D"
    replace '\n' = "%0A"
    replace c    = [c]

escapeProperty :: String -> String
escapeProperty = (>>= replace)
  where
    replace '%'  = "%25"
    replace '\r' = "%0D"
    replace '\n' = "%0A"
    replace ':'  = "%3A"
    replace ','  = "%2C"
    replace c    = [c]

-- | If you want to extend
emitGithubActionAnnotation :: Path -> Item -> FormatM ()
emitGithubActionAnnotation path item = case itemResult item of
    Success -> pure ()
    Pending _ _ -> pure ()
    Failure mLoc reason -> write . formatErrorCommand $ errorCommandFor path item mLoc reason