packages feed

sydtest-0.23.0.2: src/Test/Syd/ReRun.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-unused-pattern-binds -Wno-unused-imports #-}

module Test.Syd.ReRun (withRerunByReport) where

import Autodocodec
import Control.Monad.Writer
import qualified Data.ByteString as SB
import qualified Data.ByteString.Lazy as LB
import Data.Map (Map)
import qualified Data.Map as M
import Data.Monoid
import Data.Text (Text)
import GHC.Generics (Generic)
import Path
import Path.IO
import Test.Syd.Def
import Test.Syd.OptParse
import Test.Syd.Run
import Test.Syd.SpecDef
import Test.Syd.SpecForest

withRerunByReport ::
  Settings ->
  (TestDefM outers inner r -> IO (Timed ResultForest)) ->
  TestDefM outers inner r ->
  IO (Timed ResultForest)
withRerunByReport sets func spec =
  if settingSkipPassed sets
    then do
      mReport <- readReport sets
      resultForest <- func (filterByMReport mReport spec)
      let newReport = collectReport sets resultForest
      let combinedReport = maybe newReport (`combineReport` newReport) mReport
      writeReport sets combinedReport
      pure resultForest
    else func spec

filterByMReport :: Maybe ReportForest -> TestDefM outers inner r -> TestDefM outers inner r
filterByMReport = maybe id filterByReport

filterByReport :: ReportForest -> TestDefM outers inner r -> TestDefM outers inner r
filterByReport report =
  censor
    ( \testForest ->
        -- Don't filter anything if everything was removed because it passed.
        -- This should be the final step that reruns all the tests because
        let (filteredResult, All allPassedOrSkipped) = runWriter (goF report testForest)
         in if allPassedOrSkipped
              then testForest
              else filteredResult
    )
  where
    goF :: ReportForest -> TestForest o i -> Writer All (TestForest o i)
    goF forest = mapM (goT forest)
    goT :: ReportForest -> TestTree o i -> Writer All (TestTree o i)
    goT forest t = case t of
      DefSpecifyNode description _ _ -> do
        case M.lookup description forest of
          Nothing -> do
            -- New test, definitely run it.
            tell $ All False
            pure t
          Just (ReportBranch _) -> do
            -- "it" turned into "describe": new, definitely run.
            tell $ All False
            pure t
          Just (ReportNode passed) -> do
            -- Don't rerun if it's already passed.
            tell $ All passed
            pure $
              if passed
                then DefPendingNode description (Just "Skipped passed test")
                else t
      DefPendingNode {} -> do
        -- Keep the pending node, it doesn't hurt.
        tell $ All True
        pure t
      DefDescribeNode description f ->
        case M.lookup description forest of
          Nothing -> do
            -- New branch, or a branch that wasn't run because of a filter,
            -- definitely run it.
            pure t
          Just (ReportNode _) -> do
            -- "describe" turned into "it": new, definitely run.
            pure t
          Just (ReportBranch deeperForest) ->
            DefDescribeNode description <$> goF deeperForest f
      DefSetupNode func f -> DefSetupNode func <$> goF forest f
      DefBeforeAllNode func f -> DefBeforeAllNode func <$> goF forest f
      DefBeforeAllWithNode func f -> DefBeforeAllWithNode func <$> goF forest f
      DefWrapNode func f -> DefWrapNode func <$> goF forest f
      DefAroundAllNode func f -> DefAroundAllNode func <$> goF forest f
      DefAroundAllWithNode func f -> DefAroundAllWithNode func <$> goF forest f
      DefAfterAllNode func f -> DefAfterAllNode func <$> goF forest f
      DefParallelismNode x f -> DefParallelismNode x <$> goF forest f
      DefRandomisationNode x f -> DefRandomisationNode x <$> goF forest f
      DefTimeoutNode func f -> DefTimeoutNode func <$> goF forest f
      DefRetriesNode func f -> DefRetriesNode func <$> goF forest f
      DefFlakinessNode x f -> DefFlakinessNode x <$> goF forest f
      DefExpectationNode x f -> DefExpectationNode x <$> goF forest f

readReport :: Settings -> IO (Maybe ReportForest)
readReport settings = do
  reportFile <- getReportFile settings
  mContents <- forgivingAbsence $ SB.readFile (fromAbsFile reportFile)
  case mContents of
    Nothing -> pure Nothing
    Just contents ->
      case eitherDecodeJSONViaCodec (LB.fromStrict contents) of
        Left _ ->
          -- If we cant decode the file, just pretend it wasn't there.
          pure Nothing
        Right report -> pure (Just report)

writeReport :: Settings -> ReportForest -> IO ()
writeReport settings report = do
  reportFile <- getReportFile settings
  ensureDir (parent reportFile)
  SB.writeFile (fromAbsFile reportFile) (SB.toStrict (encodeJSONViaCodec report))

getReportFile :: Settings -> IO (Path Abs File)
getReportFile setting = case settingReportFile setting of
  Just fp -> pure fp
  Nothing -> do
    cacheDir <- getXdgDir XdgCache (Just [reldir|sydtest|])
    resolveFile cacheDir "sydtest-report.json"

collectReport :: Settings -> Timed ResultForest -> ReportForest
collectReport settings = goF . timedValue
  where
    goF :: ResultForest -> ReportForest
    goF = M.unions . map goT
    goT :: ResultTree -> Map Text ReportTree
    goT = \case
      DescribeNode description f -> M.singleton description (ReportBranch (goF f))
      SubForestNode f -> goF f
      PendingNode _ _ -> M.empty
      SpecifyNode testReportDescription TDef {..} ->
        let report = timedValue testDefVal
            passed = not $ testRunReportFailed settings report
         in M.singleton testReportDescription (ReportNode passed)

combineReport :: ReportForest -> ReportForest -> ReportForest
combineReport = goF
  where
    goF :: ReportForest -> ReportForest -> ReportForest
    goF = M.unionWith goT
    goT :: ReportTree -> ReportTree -> ReportTree
    goT oldT newT = case (oldT, newT) of
      (ReportNode _, ReportNode newPassed) ->
        -- We could do '||' here but ignoring the old value is more accurate
        -- because the whole point is that we skip passed tests.
        ReportNode newPassed
      (ReportBranch oldForest, ReportBranch newForest) -> ReportBranch $ goF oldForest newForest
      _ -> newT

type ReportForest = Map Text ReportTree

data ReportTree
  = ReportNode !Bool
  | ReportBranch !ReportForest
  deriving stock (Show, Eq, Generic)

instance HasCodec ReportTree where
  codec = named "ReportTree" $ dimapCodec f g $ eitherCodec codec codec
    where
      f = \case
        Left n -> ReportNode n
        Right ts -> ReportBranch ts
      g = \case
        ReportNode n -> Left n
        ReportBranch n -> Right n