packages feed

tricorder-0.6.0.0: src/Tricorder/CLI/App/Test/SuiteView.hs

-- | The full, unprojected view of a test suite that @tricorder test suites@
-- selects fields from.
module Tricorder.CLI.App.Test.SuiteView
    ( SuiteView (..)
    , Status (..)
    , CaseView (..)
    , Outcome (..)
    , schema
    , defaultFields
    , selectSuites
    )
where

import Atelier.Types.JSON.QuietSnake (QuietSnake (..))
import Data.Aeson (ToJSON (..))
import Tricorder.CLI.Command.TestFilter (TestFilter)
import Tricorder.FieldMask (Field (..), FieldMask (..))

import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Tricorder.CLI.Command.TestFilter qualified as TestFilter
import Tricorder.FieldMask.Schema qualified as FieldMask (Schema (..))
import Tricorder.FieldMask.Schema qualified as FieldMask.Schema

import Tricorder.Build.Duration (Duration)
import Tricorder.Session.TestTarget (TestTarget)
import Tricorder.TestOutput (stripGhciNoise)

import Tricorder.Build.Test qualified as Test
import Tricorder.Session.TestTarget qualified as TestTarget


-- | Fields that do not apply to a suite's 'Status' are 'Nothing', and are
-- left out of the output.
data SuiteView = SuiteView
    { name :: Text
    , status :: Status
    , error :: Maybe Text
    , passedCases :: Maybe Int
    , totalCases :: Maybe Int
    , durationMs :: Maybe Duration
    , output :: Maybe Text
    , cases :: Maybe [CaseView]
    }
    deriving stock (Eq, Generic, Show)
    deriving (ToJSON) via QuietSnake SuiteView


data Status = Running | Errored | Passed | Failed
    deriving stock (Eq, Generic, Show)
    deriving (ToJSON) via QuietSnake Status


data CaseView = CaseView
    { description :: Text
    , outcome :: Outcome
    , failure :: Maybe Text
    }
    deriving stock (Eq, Generic, Show)
    deriving (ToJSON) via QuietSnake CaseView


data Outcome = CasePassed | CaseFailed
    deriving stock (Eq, Generic, Show)


instance ToJSON Outcome where
    toJSON = \case
        CasePassed -> "passed"
        CaseFailed -> "failed"


-- | Every field of a 'SuiteView', as named in its JSON encoding.
schema :: FieldMask.Schema
schema =
    FieldMask.Schema
        [ FieldMask.Schema.leaf "name"
        , FieldMask.Schema.leaf "status"
        , FieldMask.Schema.leaf "error"
        , FieldMask.Schema.leaf "passed_cases"
        , FieldMask.Schema.leaf "total_cases"
        , FieldMask.Schema.leaf "duration_ms"
        , FieldMask.Schema.leaf "output"
        , FieldMask.Schema.nested
            "cases"
            [ FieldMask.Schema.leaf "description"
            , FieldMask.Schema.leaf "outcome"
            , FieldMask.Schema.leaf "failure"
            ]
        ]


-- | A summary of each suite, leaving out the (potentially large) test output
-- and test cases.
defaultFields :: FieldMask
defaultFields =
    FieldMask
        $ (\name -> Field name Nothing)
            <$> "name" :| ["status", "error", "passed_cases", "total_cases", "duration_ms"]


-- | Pick out the requested suites, or all of them when none are requested.
-- Fails with the requested suites that do not exist.
--
-- 'TestFilter.FailedOnly' keeps only failed suites, and only the failed test
-- cases within them.
selectSuites
    :: [TestTarget] -> TestFilter -> Test.Suites -> Either (NonEmpty TestTarget) [SuiteView]
selectSuites requested testFilter (Test.Suites suites) = do
    selected <- case requested of
        [] -> Right $ Map.toList suites
        _ -> case nonEmpty missing of
            Just notFound -> Left notFound
            Nothing -> Right [(target, suite) | target <- ordNub requested, Just suite <- [Map.lookup target suites]]
    pure $ uncurry toView <$> filter (keepSuite . snd) selected
  where
    missing = filter (`Map.notMember` suites) requested

    keepSuite suite = case testFilter of
        TestFilter.FailedOnly -> Test.isFailedRun suite
        TestFilter.All -> True

    keepCase testCase = case testFilter of
        TestFilter.FailedOnly -> Test.caseFailed testCase
        TestFilter.All -> True

    toView target suite =
        let view =
                SuiteView
                    { name = TestTarget.render target
                    , status = Running
                    , error = Nothing
                    , passedCases = Nothing
                    , totalCases = Nothing
                    , durationMs = Nothing
                    , output = Nothing
                    , cases = Nothing
                    }
        in  case suite of
                Test.SuiteRunning _ -> view
                Test.SuiteErrored err -> view {status = Errored, error = Just err.message}
                Test.SuiteCompleted completion ->
                    view
                        { status = if completion.passed then Passed else Failed
                        , passedCases = Just $ length $ filter (not . Test.caseFailed) completion.testCases
                        , totalCases = Just $ length completion.testCases
                        , durationMs = completion.duration
                        , output = Just $ T.unlines $ stripGhciNoise $ T.lines completion.output
                        , cases = Just $ toCaseView <$> filter keepCase completion.testCases
                        }

    toCaseView testCase = case testCase.outcome of
        Test.Passed -> CaseView testCase.description CasePassed Nothing
        Test.Failed reason -> CaseView testCase.description CaseFailed (Just reason)