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