module Test.Hspec.JUnit.Format ( junit ) 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.Api.Format.V1 import Test.Hspec.JUnit.Config as Config import Test.Hspec.JUnit.Render (renderJUnit) import qualified Test.Hspec.JUnit.Schema as Schema import Text.XML.Stream.Render (def, renderBytes) junit :: JUnitConfig -> FormatConfig -> IO Format junit :: JUnitConfig -> FormatConfig -> IO Format junit JUnitConfig junitConfig FormatConfig _config = Format -> IO Format forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure (Format -> IO Format) -> Format -> IO Format forall a b. (a -> b) -> a -> b $ \case Event Started -> () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () GroupStarted Path _ -> () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () GroupDone Path _ -> () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () Progress Path _ Progress _ -> () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () ItemStarted Path _ -> () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () ItemDone Path _ Item _ -> () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () Done [(Path, Item)] paths -> do UTCTime time <- IO UTCTime getCurrentTime let (String directory, String _) = String -> (String, String) splitFileName String file Bool -> String -> IO () createDirectoryIfMissing Bool True String directory let groups :: [(Text, [(Text, Item)])] groups = [(Path, Item)] -> [(Text, [(Text, Item)])] groupItems [(Path, Item)] paths output :: Suites output = Schema.Suites { suitesName :: Text suitesName = Text suiteName , suitesSuites :: [Suite] suitesSuites = [(Text, [(Text, Item)])] groups [(Text, [(Text, Item)])] -> ((Text, [(Text, Item)]) -> Suite) -> [Suite] forall (f :: * -> *) a b. Functor f => f a -> (a -> b) -> f b <&> \(Text group, [(Text, Item)] items) -> do let suite :: [TestCase] -> Suite suite [TestCase] xs = Schema.Suite { suiteName :: Text suiteName = Text group , suiteTimestamp :: UTCTime suiteTimestamp = UTCTime time , suiteCases :: [TestCase] suiteCases = [TestCase] xs } [TestCase] -> Suite suite ([TestCase] -> Suite) -> [TestCase] -> Suite forall a b. (a -> b) -> a -> b $ (Text -> Item -> TestCase) -> (Text, Item) -> TestCase forall a b c. (a -> b -> c) -> (a, b) -> c uncurry ((String -> String) -> Text -> Text -> Item -> TestCase itemToTestCase String -> String applyPrefix Text group) ((Text, Item) -> TestCase) -> [(Text, Item)] -> [TestCase] forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> [(Text, Item)] items } ConduitT () Void (ResourceT IO) () -> IO () forall (m :: * -> *) r. MonadUnliftIO m => ConduitT () Void (ResourceT m) r -> m r runConduitRes (ConduitT () Void (ResourceT IO) () -> IO ()) -> ConduitT () Void (ResourceT IO) () -> IO () forall a b. (a -> b) -> a -> b $ [Suites] -> ConduitT () Suites (ResourceT IO) () forall (m :: * -> *) a i. Monad m => [a] -> ConduitT i a m () sourceList [Suites output] ConduitT () Suites (ResourceT IO) () -> ConduitT Suites Void (ResourceT IO) () -> ConduitT () Void (ResourceT IO) () forall (m :: * -> *) a b c r. Monad m => ConduitT a b m () -> ConduitT b c m r -> ConduitT a c m r .| Bool -> ConduitT Suites Event (ResourceT IO) () forall (m :: * -> *). MonadThrow m => Bool -> ConduitT Suites Event m () renderJUnit Bool dropConsoleFormatting ConduitT Suites Event (ResourceT IO) () -> ConduitT Event Void (ResourceT IO) () -> ConduitT Suites Void (ResourceT IO) () forall (m :: * -> *) a b c r. Monad m => ConduitT a b m () -> ConduitT b c m r -> ConduitT a c m r .| RenderSettings -> ConduitT Event ByteString (ResourceT IO) () forall (m :: * -> *). PrimMonad m => RenderSettings -> ConduitT Event ByteString m () renderBytes RenderSettings forall a. Default a => a def ConduitT Event ByteString (ResourceT IO) () -> ConduitT ByteString Void (ResourceT IO) () -> ConduitT Event Void (ResourceT IO) () forall (m :: * -> *) a b c r. Monad m => ConduitT a b m () -> ConduitT b c m r -> ConduitT a c m r .| String -> ConduitT ByteString Void (ResourceT IO) () forall (m :: * -> *) o. MonadResource m => String -> ConduitT ByteString o m () sinkFile String file where file :: String file = JUnitConfig -> String getJUnitConfigOutputFile JUnitConfig junitConfig suiteName :: Text suiteName = JUnitConfig -> Text getJUnitConfigSuiteName JUnitConfig junitConfig applyPrefix :: String -> String applyPrefix = JUnitConfig -> String -> String getJUnitPrefixSourcePath JUnitConfig junitConfig dropConsoleFormatting :: Bool dropConsoleFormatting = JUnitConfig -> Bool getJUnitConfigDropConsoleFormatting JUnitConfig junitConfig groupItems :: [(Path, Item)] -> [(Text, [(Text, Item)])] groupItems :: [(Path, Item)] -> [(Text, [(Text, Item)])] groupItems = Map Text [(Text, Item)] -> [(Text, [(Text, Item)])] forall k a. Map k a -> [(k, a)] Map.toList (Map Text [(Text, Item)] -> [(Text, [(Text, Item)])]) -> ([(Path, Item)] -> Map Text [(Text, Item)]) -> [(Path, Item)] -> [(Text, [(Text, Item)])] forall b c a. (b -> c) -> (a -> b) -> a -> c . ([(Text, Item)] -> [(Text, Item)] -> [(Text, Item)]) -> [(Text, [(Text, Item)])] -> Map Text [(Text, Item)] forall k a. Ord k => (a -> a -> a) -> [(k, a)] -> Map k a Map.fromListWith [(Text, Item)] -> [(Text, Item)] -> [(Text, Item)] forall a. Semigroup a => a -> a -> a (<>) ([(Text, [(Text, Item)])] -> Map Text [(Text, Item)]) -> ([(Path, Item)] -> [(Text, [(Text, Item)])]) -> [(Path, Item)] -> Map Text [(Text, Item)] forall b c a. (b -> c) -> (a -> b) -> a -> c . ((Path, Item) -> (Text, [(Text, Item)])) -> [(Path, Item)] -> [(Text, [(Text, Item)])] forall a b. (a -> b) -> [a] -> [b] forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b fmap (Path, Item) -> (Text, [(Text, Item)]) forall {b}. (Path, b) -> (Text, [(Text, b)]) group where group :: (Path, b) -> (Text, [(Text, b)]) group (([String] path, String name), b item) = (Text -> [Text] -> Text T.intercalate Text "/" ([Text] -> Text) -> [Text] -> Text forall a b. (a -> b) -> a -> b $ String -> Text pack (String -> Text) -> [String] -> [Text] forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> [String] path, [(String -> Text pack String name, b item)]) itemToTestCase :: (FilePath -> FilePath) -> Text -> Text -> Item -> Schema.TestCase itemToTestCase :: (String -> String) -> Text -> Text -> Item -> TestCase itemToTestCase String -> String applyPrefix Text group Text name Item item = Schema.TestCase { testCaseLocation :: Maybe Location testCaseLocation = (String -> String) -> Location -> Location toSchemaLocation String -> String applyPrefix (Location -> Location) -> Maybe Location -> Maybe Location forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> (Item -> Maybe Location itemResultLocation Item item Maybe Location -> Maybe Location -> Maybe Location forall a. Maybe a -> Maybe a -> Maybe a forall (f :: * -> *) a. Alternative f => f a -> f a -> f a <|> Item -> Maybe Location itemLocation Item item) , testCaseClassName :: Text testCaseClassName = Text group , testCaseName :: Text testCaseName = Text name , testCaseDuration :: Double testCaseDuration = Seconds -> Double unSeconds (Seconds -> Double) -> Seconds -> Double forall a b. (a -> b) -> a -> b $ Item -> Seconds itemDuration Item item , testCaseResult :: Maybe Result testCaseResult = case Item -> Result itemResult Item item of Result Success -> Maybe Result forall a. Maybe a Nothing Pending Maybe Location mLocation Maybe String mMessage -> Result -> Maybe Result forall a. a -> Maybe a Just (Result -> Maybe Result) -> Result -> Maybe Result forall a b. (a -> b) -> a -> b $ Text -> Result Schema.Skipped (Text -> Result) -> Text -> Result forall a b. (a -> b) -> a -> b $ Maybe Location -> Text -> Text prefixLocation Maybe Location mLocation (Text -> Text) -> Text -> Text forall a b. (a -> b) -> a -> b $ Text -> Text prefixInfo (Text -> Text) -> Text -> Text forall a b. (a -> b) -> a -> b $ Text -> (String -> Text) -> Maybe String -> Text forall b a. b -> (a -> b) -> Maybe a -> b maybe Text "" String -> Text pack Maybe String mMessage Failure Maybe Location mLocation FailureReason reason -> Result -> Maybe Result forall a. a -> Maybe a Just (Result -> Maybe Result) -> Result -> Maybe Result forall a b. (a -> b) -> a -> b $ Text -> Text -> Result Schema.Failure Text "error" (Text -> Result) -> Text -> Result forall a b. (a -> b) -> a -> b $ Maybe Location -> Text -> Text prefixLocation Maybe Location mLocation (Text -> Text) -> Text -> Text forall a b. (a -> b) -> a -> b $ Text -> Text prefixInfo (Text -> Text) -> Text -> Text forall a b. (a -> b) -> a -> b $ FailureReason -> Text reasonToText FailureReason reason } where prefixLocation :: Maybe Location -> Text -> Text prefixLocation Maybe Location mLocation Text str = case Maybe Location mLocation of Maybe Location Nothing -> Text str Just Location {Int String locationFile :: String locationLine :: Int locationColumn :: Int locationFile :: Location -> String locationLine :: Location -> Int locationColumn :: Location -> Int ..} -> [Text] -> Text forall a. Monoid a => [a] -> a mconcat [ String -> Text pack (String -> Text) -> String -> Text forall a b. (a -> b) -> a -> b $ String -> String applyPrefix String locationFile , Text ":" , String -> Text pack (String -> Text) -> String -> Text forall a b. (a -> b) -> a -> b $ Int -> String forall a. Show a => a -> String show Int locationLine , Text ":" , String -> Text pack (String -> Text) -> String -> Text forall a b. (a -> b) -> a -> b $ Int -> String forall a. Show a => a -> String show Int locationColumn , Text "\n" ] Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text str prefixInfo :: Text -> Text prefixInfo Text str | Text -> Bool T.null (Text -> Bool) -> Text -> Bool forall a b. (a -> b) -> a -> b $ Text -> Text T.strip (Text -> Text) -> Text -> Text forall a b. (a -> b) -> a -> b $ String -> Text pack (String -> Text) -> String -> Text forall a b. (a -> b) -> a -> b $ Item -> String itemInfo Item item = Text str | Bool otherwise = String -> Text pack (Item -> String itemInfo Item item) Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text "\n\n" Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text str itemResultLocation :: Item -> Maybe Location itemResultLocation :: Item -> Maybe Location itemResultLocation Item item = case Item -> Result itemResult Item item of Result Success -> Maybe Location forall a. Maybe a Nothing Pending Maybe Location mLocation Maybe String _ -> Maybe Location mLocation Failure Maybe Location mLocation FailureReason _ -> Maybe Location mLocation toSchemaLocation :: (FilePath -> FilePath) -> Location -> Schema.Location toSchemaLocation :: (String -> String) -> Location -> Location toSchemaLocation String -> String applyPrefix Location {Int String locationFile :: Location -> String locationLine :: Location -> Int locationColumn :: Location -> Int locationFile :: String locationLine :: Int locationColumn :: Int ..} = Schema.Location { locationFile :: String Schema.locationFile = String -> String applyPrefix String locationFile , locationLine :: Natural Schema.locationLine = Int -> Natural forall a b. (Integral a, Num b) => a -> b fromIntegral (Int -> Natural) -> Int -> Natural forall a b. (a -> b) -> a -> b $ Int -> Int -> Int forall a. Ord a => a -> a -> a max Int 0 Int locationLine } unSeconds :: Seconds -> Double unSeconds :: Seconds -> Double unSeconds (Seconds Double x) = Double x reasonToText :: FailureReason -> Text reasonToText :: FailureReason -> Text reasonToText = \case Error Maybe String _ SomeException err -> String -> Text pack (String -> Text) -> String -> Text forall a b. (a -> b) -> a -> b $ SomeException -> String forall a. Show a => a -> String show SomeException err FailureReason NoReason -> Text "no reason" Reason String err -> String -> Text pack String err ExpectedButGot Maybe String preface String expected String actual -> [Text] -> Text T.unlines ([Text] -> Text) -> [Text] -> Text forall a b. (a -> b) -> a -> b $ String -> Text pack (String -> Text) -> [String] -> [Text] forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> String -> Maybe String -> String forall a. a -> Maybe a -> a fromMaybe String "" Maybe String preface String -> [String] -> [String] forall a. a -> [a] -> [a] : ( Text -> String -> [String] forall a. Show a => Text -> a -> [String] foundLines Text "expected" String expected [String] -> [String] -> [String] forall a. Semigroup a => a -> a -> a <> Text -> String -> [String] forall a. Show a => Text -> a -> [String] foundLines Text " but got" String actual ) foundLines :: Show a => Text -> a -> [String] foundLines :: forall a. Show a => Text -> a -> [String] foundLines Text msg a found = case [Text] lines' of [] -> [] Text first : [Text] rest -> Text -> String unpack (Text msg Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text ": " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text first) String -> [String] -> [String] forall a. a -> [a] -> [a] : (Text -> String unpack (Text -> String) -> (Text -> Text) -> Text -> String forall b c a. (b -> c) -> (a -> b) -> a -> c . (Int -> Text -> Text T.replicate Int 9 Text " " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <>) (Text -> String) -> [Text] -> [String] forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> [Text] rest) where lines' :: [Text] lines' = Text -> [Text] T.lines (Text -> [Text]) -> (String -> Text) -> String -> [Text] forall b c a. (b -> c) -> (a -> b) -> a -> c . String -> Text pack (String -> [Text]) -> String -> [Text] forall a b. (a -> b) -> a -> b $ a -> String forall a. Show a => a -> String show a found