mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
e752010a83
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7125 GitOrigin-RevId: fa051a4942a882efe6e3d673eab084b94ec8dd90
223 lines
7.0 KiB
Haskell
223 lines
7.0 KiB
Haskell
module Hasura.FeatureMatrix (render, parseLogs, extractFeatures, renderFeatureMatrix) where
|
|
|
|
import Control.Applicative
|
|
import Control.Monad.Except
|
|
import Control.Monad.State
|
|
import Data.Aeson
|
|
import Data.Aeson.Types
|
|
import Data.Attoparsec.ByteString as Atto
|
|
import Data.ByteString (ByteString)
|
|
import Data.ByteString.Lazy qualified as LBS
|
|
import Data.Foldable
|
|
import Data.Map.Strict (Map)
|
|
import Data.Map.Strict qualified as M
|
|
import Data.Maybe
|
|
import Data.Set qualified as Set
|
|
import Data.Text (Text)
|
|
import Data.Text qualified as T
|
|
import Lucid
|
|
import Text.RawString.QQ (r)
|
|
import Prelude hiding (interact)
|
|
|
|
type FeatureName = Text
|
|
|
|
type Features = Map FeatureName [TestRun]
|
|
|
|
data TestRun = TestRun
|
|
{ -- Extra data we capture but do not currently use.
|
|
_testRunRequirement :: Text,
|
|
testRunGroups :: [Text],
|
|
testRunPassed :: Bool,
|
|
-- Extra data we capture but do not currently use.
|
|
_testRunHspecItem :: HspecItem
|
|
}
|
|
|
|
data HspecEventItemDone = HspecEventItemDone
|
|
{ heidGroups :: [Text],
|
|
heidItem :: HspecItem,
|
|
heidRequirement :: Text
|
|
}
|
|
|
|
{- Decode things from events such as:
|
|
|
|
{
|
|
"event_tag": "ItemDone",
|
|
"groups": [
|
|
"Test.HelloWorld",
|
|
"Postgres",
|
|
"Example test"
|
|
],
|
|
"item": {
|
|
"duration": 1.1321041419996618,
|
|
"info": "",
|
|
"location": {
|
|
"column": 5,
|
|
"file": "test/Test/HelloWorldSpec.hs",
|
|
"line": 60
|
|
},
|
|
"result": {
|
|
"result": "Success"
|
|
}
|
|
},
|
|
"requirement": "Works as expected",
|
|
"type": "Hspec Event"
|
|
}
|
|
-}
|
|
|
|
instance FromJSON HspecEventItemDone where
|
|
parseJSON = withObject "Hspec Event ItemDone" \o -> do
|
|
let tags = parseMaybe (\o' -> (,) <$> o' .: "type" <*> o' .: "event_tag") o
|
|
unless
|
|
(tags == Just ("Hspec Event" :: Text, "ItemDone" :: Text))
|
|
(fail "Not a Hspec Event ItemDone")
|
|
HspecEventItemDone <$> o .: "groups" <*> o .: "item" <*> o .: "requirement"
|
|
|
|
data HspecItem = HspecItem
|
|
{ hiResult :: HspecItemResult
|
|
}
|
|
|
|
instance FromJSON HspecItem where
|
|
parseJSON =
|
|
withObject
|
|
"Hspec Item"
|
|
( \o -> HspecItem <$> o .: "result"
|
|
)
|
|
|
|
data HspecItemResult = HIRSuccess | HIRPending Text | HIRFailure Value -- Failures have details that we don't care about just yet.
|
|
|
|
instance FromJSON HspecItemResult where
|
|
parseJSON =
|
|
withObject
|
|
"Hspec Item Result"
|
|
( \o -> do
|
|
result :: String <- o .: "result"
|
|
case result of
|
|
"Success" -> pure HIRSuccess
|
|
"Failure" -> HIRFailure <$> (o .: "reason")
|
|
"Pending" -> HIRPending <$> (o .: "message")
|
|
_ -> fail $ "Unknown result type: " ++ result
|
|
)
|
|
|
|
render :: ByteString -> ByteString
|
|
render input =
|
|
let parsedLogs = parseLogs input
|
|
features = runExcept . flip execStateT mempty . traverse extractFeatures <$> parsedLogs
|
|
in renderFeatureMatrix features
|
|
|
|
parseLogs :: ByteString -> Atto.Result [Value]
|
|
parseLogs = noPartial . Atto.parse (json `sepBy` (string "\n") <* (option () (void (string "\n")) *> endOfInput))
|
|
|
|
noPartial :: Atto.Result a -> Atto.Result a
|
|
noPartial (Partial c) = c "" -- You signal Attoparsec that you're done by
|
|
-- giving an empty string to partial results. A bit weird...
|
|
noPartial result = result
|
|
|
|
extractFeatures :: Value -> (StateT Features (Except Text)) ()
|
|
extractFeatures v = do
|
|
case parseEither parseJSON v of
|
|
-- Skip this entry, as it's uninteresting.
|
|
Left "Error in $: Not a Hspec Event ItemDone" -> return ()
|
|
-- We do want to report other errors though, because those are indicative
|
|
-- of format changes that we need to catch in order to ensure we're
|
|
-- reporting correctly.
|
|
Left err -> throwError (T.pack err)
|
|
Right (itemDone :: HspecEventItemDone) -> do
|
|
case testRun itemDone of
|
|
Nothing -> return ()
|
|
Just run -> modify (M.unionWith (<>) (M.singleton (featureName itemDone) [run]))
|
|
where
|
|
featureName :: HspecEventItemDone -> FeatureName
|
|
featureName HspecEventItemDone {..} = case heidGroups of
|
|
[] -> "empty??"
|
|
name : _ -> T.takeWhile (/= '.') $ fromMaybe name (T.stripPrefix "Test." name)
|
|
|
|
testRun :: HspecEventItemDone -> Maybe TestRun
|
|
testRun HspecEventItemDone {..} =
|
|
TestRun heidRequirement
|
|
<$> pure heidGroups
|
|
<*> case hiResult heidItem of
|
|
HIRSuccess -> Just True
|
|
HIRPending {} -> Nothing -- We don't care about pending items
|
|
HIRFailure {} -> Just False
|
|
<*> pure heidItem
|
|
|
|
-- | Output the rendered feature matrix to stdout.
|
|
renderFeatureMatrix :: Atto.Result (Either Text Features) -> ByteString
|
|
renderFeatureMatrix =
|
|
LBS.toStrict . renderBS . \case
|
|
Fail unconsumed contexts errorMsg ->
|
|
reportHead do
|
|
h1_ "Failed to decode json"
|
|
pre_ (toHtml errorMsg)
|
|
pre_ (toHtml unconsumed)
|
|
mapM_ (pre_ . toHtml) contexts
|
|
Partial {} ->
|
|
reportHead do
|
|
h1_ "Failed to decode json (`Partial` result, impossible?)"
|
|
Done unconsumed (Left err) ->
|
|
reportHead do
|
|
h1_ "Failed to gather feature information"
|
|
pre_ (toHtml err)
|
|
h1_ "Maybe unconsumed input remains"
|
|
pre_ (toHtml unconsumed)
|
|
Done "" (Right features) ->
|
|
reportHead do
|
|
h1_ reportTitle
|
|
table_ $ traverse_ featureRow (M.toList features)
|
|
Done unconsumed (Right _) ->
|
|
reportHead do
|
|
h1_ "Parsing succeded, but unconsumed input remains (impossible?)"
|
|
pre_ (toHtml unconsumed)
|
|
where
|
|
reportHead :: Html () -> Html ()
|
|
reportHead content =
|
|
doctypehtml_ do
|
|
head_ do
|
|
meta_ [charset_ "utf-8"]
|
|
title_ reportTitle
|
|
style_
|
|
[r|
|
|
body {
|
|
max-width: 900px;
|
|
margin: auto;
|
|
font-family: sans-serif;
|
|
}
|
|
h1 { text-align: center; }
|
|
table {
|
|
width: 70%;
|
|
margin: auto;
|
|
font-size: 20px;
|
|
}
|
|
td { padding: 5px; padding-left: 15px; padding-right: 15px; }
|
|
tr { border-bottom: 1px solid #dadada; }
|
|
tr:hover { background-color: #eee; }
|
|
|]
|
|
body_ content
|
|
|
|
reportTitle = "Feature Matrix Compatibility Report"
|
|
|
|
featureRow :: (FeatureName, [TestRun]) -> Html ()
|
|
featureRow (name, runs) = tr_ $ do
|
|
td_ (toHtml name)
|
|
td_ [style_ "text-align: right", title_ (runNames runs)] (toHtml $ runsStatus runs)
|
|
|
|
-- Construct a sensible list of indications of which tests have been run.
|
|
runNames :: [TestRun] -> Text
|
|
runNames =
|
|
T.unlines
|
|
. Set.toList
|
|
. Set.fromList
|
|
. filter (not . T.null)
|
|
. map (\testRun -> runsStatus [testRun] <> " " <> inlineGroups (testRunGroups $ testRun))
|
|
|
|
inlineGroups :: [Text] -> Text
|
|
inlineGroups groups =
|
|
T.intercalate
|
|
"."
|
|
(filter (/= "Postgres") $ tail groups)
|
|
|
|
runsStatus :: [TestRun] -> Text
|
|
runsStatus runs
|
|
| all testRunPassed runs = "✅"
|
|
| otherwise = "❌"
|