packages feed

scientist-0.0.0.0: tests/Scientist/Experiment/RunSpec.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Scientist.Experiment.RunSpec
  ( spec
  ) where

import Scientist.Test

import Data.Either (partitionEithers)
import qualified Data.List.NonEmpty as NE
import Scientist.Candidate
import Scientist.Control
import Scientist.Duration
import Scientist.Experiment
import Scientist.Experiment.Run
import Scientist.Result
import UnliftIO.Concurrent
import UnliftIO.Exception (fromException, throwString)

data ExampleResult = A | B | C
  deriving stock (Eq, Show)

spec :: Spec
spec = do
  describe "experimentRun" $ do
    it "is ResultSkipped based on runIf" $ do
      result <-
        experimentRunInternal
        $ setExperimentRunIf False
        $ setExperimentTry (pure A)
        $ newExperiment "test" (pure A)

      expectSkippedWith result A

    it "is ResultSkipped based on enabled" $ do
      result <-
        experimentRunInternal
        $ setExperimentEnabled (pure False)
        $ setExperimentTry (pure A)
        $ newExperiment "test" (pure A)

      expectSkippedWith result A

    it "is ResultSkipped when no Candidates present" $ do
      result <- experimentRunInternal $ newExperiment "test" (pure A)

      expectSkippedWith result A

    it "is ResultMatched if all candidates match" $ do
      result <-
        experimentRunInternal
        $ setExperimentCompare experimentCompareEq
        $ setExperimentTry (pure A)
        $ setExperimentTry (pure A)
        $ newExperiment "test" (A <$ threadDelay (100 * 1000))

      expectMatched result $ \rd -> do
        resultDetailsExperimentName rd `shouldBe` "test"

        let control = resultDetailsControl rd
        resultControlValue control `shouldBe` A
        resultControlDuration control `shouldSatisfy` isDurationNear 0.100

        let
          (failed, succeeded) =
            partitionEithers
              $ map resultCandidateValue
              $ NE.toList
              $ resultDetailsCandidates rd

        succeeded `shouldMatchList` [A, A]
        length failed `shouldBe` 0

    it "is ResultIgnored if any candidates are ignored" $ do
      let
        ignoreB _ = \case
          Right (Candidate B) -> True
          _ -> False

      result <-
        experimentRunInternal
        $ setExperimentIgnore ignoreB
        $ setExperimentTry (pure C)
        $ setExperimentTry (pure B)
        $ newExperiment "test" (pure A)

      expectIgnored result $ \rd -> do
        resultDetailsExperimentName rd `shouldBe` "test"

        let control = resultDetailsControl rd
        resultControlValue control `shouldBe` A

        let
          (failed, succeeded) =
            partitionEithers
              $ map resultCandidateValue
              $ NE.toList
              $ resultDetailsCandidates rd

        succeeded `shouldMatchList` [B, C]
        length failed `shouldBe` 0

    it "is ResultMismatched if any candidates mismatched" $ do
      result <-
        experimentRunInternal
        $ setExperimentCompare experimentCompareEq
        $ setExperimentTry (pure B)
        $ setExperimentTry (pure A)
        $ newExperiment "test" (pure A)

      expectMismatched result $ \rd -> do
        resultDetailsExperimentName rd `shouldBe` "test"

        let control = resultDetailsControl rd
        resultControlValue control `shouldBe` A

        let
          (failed, succeeded) =
            partitionEithers
              $ map resultCandidateValue
              $ NE.toList
              $ resultDetailsCandidates rd

        succeeded `shouldMatchList` [A, B]
        length failed `shouldBe` 0

    it "rescues exceptions in the Candidate branch" $ do
      result <-
        experimentRunInternal
        $ setExperimentTry (throwString "boom")
        $ newExperiment "test" (pure A)

      expectMismatched result $ \rd -> do
        resultDetailsExperimentName rd `shouldBe` "test"

        let
          control = resultDetailsControl rd
          mCandidateException =
            either fromException (const Nothing)
              $ resultCandidateValue
              $ NE.head
              $ resultDetailsCandidates rd

        resultControlValue control `shouldBe` A
        mCandidateException `shouldSatisfyMaybe` isStringException "boom"

    it "does not rescue exceptions in the Control branch" $ do
      experimentRunInternal (newExperiment "test" (throwString "boom"))
        `shouldThrowString` "boom"

    it "does not rescue exceptions in publishing" $ do
      experimentRunInternal
          (setExperimentPublish (\_ -> throwString "boom")
          $ setExperimentTry (pure B)
          $ newExperiment "test" (pure A)
          )
        `shouldThrowString` "boom"

    it "can be configured to rescue exceptions in publishing" $ do
      result <-
        experimentRunInternal
        $ setExperimentOnException (\_ -> pure ())
        $ setExperimentPublish (\_ -> throwString "boom")
        $ setExperimentTry (pure B)
        $ newExperiment "test" (pure A)

      expectMismatched result $ \_ -> pure ()

    it "supports implicitly named candidates" $ do
      result <-
        experimentRunInternal
        $ setExperimentCompare experimentCompareEq
        $ setExperimentTry (pure A)
        $ setExperimentTry (pure A)
        $ setExperimentTry (pure A)
        $ newExperiment "test" (pure A)

      expectMatched result $ \rd -> do
        resultControlName (resultDetailsControl rd) `shouldBe` "control"

        map resultCandidateName (NE.toList $ resultDetailsCandidates rd)
          `shouldMatchList` ["candidate", "candidate-1", "candidate-2"]

    it "supports explicitly named candidates" $ do
      result <-
        experimentRunInternal
        $ setExperimentCompare experimentCompareEq
        $ setExperimentTryNamed "who" (pure A)
        $ setExperimentTryNamed "what" (pure A)
        $ setExperimentTryNamed "when" (pure A)
        $ newExperiment "test" (pure A)

      expectMatched result $ \rd -> do
        resultControlName (resultDetailsControl rd) `shouldBe` "control"

        map resultCandidateName (NE.toList $ resultDetailsCandidates rd)
          `shouldMatchList` ["who", "what", "when"]

expectSkippedWith :: (Eq a, Show a) => Result c a b -> a -> IO ()
expectSkippedWith result a =
  expectSkipped result $ \(Control b) -> b `shouldBe` a

expectSkipped :: Result c a b -> (Control a -> IO ()) -> IO ()
expectSkipped result f = case result of
  ResultSkipped x -> f x
  _ -> expectationFailure "Expected result to be Skipped"

expectMatched :: Result c a b -> (ResultDetails c a b -> IO ()) -> IO ()
expectMatched result f = case result of
  ResultMatched rd -> f rd
  _ -> expectationFailure "Expected result to be Matched"

expectIgnored :: Result c a b -> (ResultDetails c a b -> IO ()) -> IO ()
expectIgnored result f = case result of
  ResultIgnored rd -> f rd
  _ -> expectationFailure "Expected result to be Ignored"

expectMismatched :: Result c a b -> (ResultDetails c a b -> IO ()) -> IO ()
expectMismatched result f = case result of
  ResultMismatched rd -> f rd
  _ -> expectationFailure "Expected result to be Mismatched"

isDurationNear :: Duration -> Duration -> Bool
isDurationNear x = isWithinOf x 0.050