{-# LANGUAGE RankNTypes #-}

-- | A library to create @Expectation@s, which describe a claim to be tested.
--
-- = Quick Reference
--
-- - 'equal' @(arg2 == arg1)@
-- - 'notEqual' @(arg2 /= arg1)@
-- - 'lessThan' @(arg2 < arg1)@
-- - 'atMost' @(arg2 <= arg1)@
-- - 'greaterThan' @(arg2 > arg1)@
-- - 'atLeast' @(arg2 >= arg1)@
-- - 'true' @(arg == True)@
-- - 'false' @(arg == False)@
module Expect
  ( -- * Basic Expectations
    Expectation,
    equal,
    notEqual,
    all,
    equalToContentsOf,

    -- * Floating Point Comparisons
    FloatingPointTolerance (..),
    within,
    notWithin,

    -- * Numeric Comparisons
    lessThan,
    atMost,
    greaterThan,
    atLeast,

    -- * Booleans
    true,
    false,

    -- * Collections
    ok,
    err,

    -- * Customizing
    pass,
    fail,
    onFail,
    fromResult,

    -- * Testing tasks
    succeeds,
    fails,
    andCheck,

    -- * Fancy Expectations
    fromIO,
    Internal.Expectation',
    around,
  )
where

import qualified Data.Text
import qualified Data.Text.IO
import qualified Debug
import qualified GHC.Stack as Stack
import qualified List
import NriPrelude
import qualified Platform.Internal
import qualified Pretty.Diff as Diff
import qualified System.Console.Terminal.Size as Terminal
import qualified System.Directory as Directory
import qualified System.FilePath as FilePath
import qualified Task
import Test.Internal (Expectation)
import qualified Test.Internal as Internal
import qualified Text.Show.Pretty
import qualified Prelude

-- | Always passes.
--
-- > import Json.Decode exposing (decodeString, int)
-- > import Test exposing (test)
-- > import Expect
-- >
-- >
-- > test "Json.Decode.int can decode the number 42." <|
-- >     \_ ->
-- >         case decodeString int "42" of
-- >             Ok _ ->
-- >                 Expect.pass
-- >
-- >             Err err ->
-- >                 Expect.fail err
pass :: Stack.HasCallStack => Expectation
pass :: Expectation
pass = (HasCallStack => Text -> () -> Expectation)
-> Text -> () -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> () -> Expectation
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass Text
"Expect.pass" ()

-- | Fails with the given message.
--
-- > import Json.Decode exposing (decodeString, int)
-- > import Test exposing (test)
-- > import Expect
-- >
-- >
-- > test "Json.Decode.int can decode the number 42." <|
-- >     \_ ->
-- >         case decodeString int "42" of
-- >             Ok _ ->
-- >                 Expect.pass
-- >
-- >             Err err ->
-- >                 Expect.fail err
fail :: Stack.HasCallStack => Text -> Expectation
fail :: Text -> Expectation
fail Text
msg =
  (HasCallStack => Text -> Text -> Expectation)
-> Text -> Text -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> Text -> Expectation
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion Text
"Expect.fail" Text
msg

-- | If the given expectation fails, replace its failure message with a custom one.
--
-- > "something"
-- >     |> Expect.equal "something else"
-- >     |> Expect.onFail "thought those two strings would be the same"
onFail :: Stack.HasCallStack => Text -> Expectation -> Expectation
onFail :: Text -> Expectation -> Expectation
onFail Text
msg (Internal.Expectation Task Failure ()
task) =
  Task Failure ()
task
    Task Failure ()
-> (Task Failure () -> Task Failure ()) -> Task Failure ()
forall a b. a -> (a -> b) -> b
|> (Failure -> Task Failure ()) -> Task Failure () -> Task Failure ()
forall x y a. (x -> Task y a) -> Task x a -> Task y a
Task.onError
      ( \Failure
_ ->
          (HasCallStack => Text -> Text -> Expectation)
-> Text -> Text -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> Text -> Expectation
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion Text
"Expect.onFail" Text
msg
            Expectation -> (Expectation -> Task Failure ()) -> Task Failure ()
forall a b. a -> (a -> b) -> b
|> Expectation -> Task Failure ()
forall a. Expectation' a -> Task Failure a
Internal.unExpectation
      )
    Task Failure () -> (Task Failure () -> Expectation) -> Expectation
forall a b. a -> (a -> b) -> b
|> Task Failure () -> Expectation
forall a. Task Failure a -> Expectation' a
Internal.Expectation

-- | Passes if the arguments are equal.
--
-- > Expect.equal 0 (List.length [])
-- >
-- > -- Passes because (0 == 0) is True
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because the expected value didn't split the space in "Betty Botter"
-- > Text.split " " "Betty Botter bought some butter"
-- >     |> Expect.equal [ "Betty Botter", "bought", "some", "butter" ]
-- >
-- > {-
-- >
-- > [ "Betty", "Botter", "bought", "some", "butter" ]
-- > ╷
-- > │ Expect.equal
-- > ╵
-- > [ "Betty Botter", "bought", "some", "butter" ]
-- >
-- > -}
equal :: (Stack.HasCallStack, Show a, Eq a) => a -> a -> Expectation
equal :: a -> a -> Expectation
equal = (HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation)
-> (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
forall a. Eq a => a -> a -> Bool
(==) Text
"Expect.equal"

-- | Passes if the arguments are not equal.
--
-- > -- Passes because (11 /= 100) is True
-- > 90 + 10
-- >     |> Expect.notEqual 11
-- >
-- >
-- > -- Fails because (100 /= 100) is False
-- > 90 + 10
-- >     |> Expect.notEqual 100
-- >
-- > {-
-- >
-- > 100
-- > ╷
-- > │ Expect.notEqual
-- > ╵
-- > 100
-- >
-- > -}
notEqual :: (Stack.HasCallStack, Show a, Eq a) => a -> a -> Expectation
notEqual :: a -> a -> Expectation
notEqual = (HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation)
-> (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
forall a. Eq a => a -> a -> Bool
(/=) Text
"Expect.notEqual"

-- | Passes if the second argument is less than the first.
--
-- > Expect.lessThan 1 (List.length [])
-- >
-- > -- Passes because (0 < 1) is True
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because (0 < -1) is False
-- > List.length []
-- >     |> Expect.lessThan -1
-- >
-- >
-- > {-
-- >
-- > 0
-- > ╷
-- > │ Expect.lessThan
-- > ╵
-- > -1
-- >
-- > -}
lessThan :: (Stack.HasCallStack, Show a, Ord a) => a -> a -> Expectation
lessThan :: a -> a -> Expectation
lessThan = (HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation)
-> (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
(>) Text
"Expect.lessThan"

-- | Passes if the second argument is less than or equal to the first.
--
-- > Expect.atMost 1 (List.length [])
-- >
-- > -- Passes because (0 <= 1) is True
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because (0 <= -3) is False
-- > List.length []
-- >     |> Expect.atMost -3
-- >
-- > {-
-- >
-- > 0
-- > ╷
-- > │ Expect.atMost
-- > ╵
-- > -3
-- >
-- > -}
atMost :: (Stack.HasCallStack, Show a, Ord a) => a -> a -> Expectation
atMost :: a -> a -> Expectation
atMost = (HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation)
-> (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
(>=) Text
"Expect.atMost"

-- | Passes if the second argument is greater than the first.
--
-- > Expect.greaterThan -2 List.length []
-- >
-- > -- Passes because (0 > -2) is True
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because (0 > 1) is False
-- > List.length []
-- >     |> Expect.greaterThan 1
-- >
-- > {-
-- >
-- > 0
-- > ╷
-- > │ Expect.greaterThan
-- > ╵
-- > 1
-- >
-- > -}
greaterThan :: (Stack.HasCallStack, Show a, Ord a) => a -> a -> Expectation
greaterThan :: a -> a -> Expectation
greaterThan = (HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation)
-> (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
(<) Text
"Expect.greaterThan"

-- | Passes if the second argument is greater than or equal to the first.
--
-- > Expect.atLeast -2 (List.length [])
-- >
-- > -- Passes because (0 >= -2) is True
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because (0 >= 3) is False
-- > List.length []
-- >     |> Expect.atLeast 3
-- >
-- > {-
-- >
-- > 0
-- > ╷
-- > │ Expect.atLeast
-- > ╵
-- > 3
-- >
-- > -}
atLeast :: (Stack.HasCallStack, Show a, Ord a) => a -> a -> Expectation
atLeast :: a -> a -> Expectation
atLeast = (HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation)
-> (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
(<=) Text
"Expect.atLeast"

-- | A type to describe how close a floating point number must be to the
-- expected value for the test to pass. This may be specified as absolute or
-- relative.
--
-- 'AbsoluteOrRelative' tolerance uses a logical OR between the absolute
-- (specified first) and relative tolerance. If you want a logical AND, use
-- 'Expect.all'.
data FloatingPointTolerance
  = Absolute Float
  | Relative Float
  | AbsoluteOrRelative Float Float
  deriving (Int -> FloatingPointTolerance -> ShowS
[FloatingPointTolerance] -> ShowS
FloatingPointTolerance -> String
(Int -> FloatingPointTolerance -> ShowS)
-> (FloatingPointTolerance -> String)
-> ([FloatingPointTolerance] -> ShowS)
-> Show FloatingPointTolerance
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [FloatingPointTolerance] -> ShowS
$cshowList :: [FloatingPointTolerance] -> ShowS
show :: FloatingPointTolerance -> String
$cshow :: FloatingPointTolerance -> String
showsPrec :: Int -> FloatingPointTolerance -> ShowS
$cshowsPrec :: Int -> FloatingPointTolerance -> ShowS
Show)

-- | Passes if the second and third arguments are equal within a tolerance
-- specified by the first argument. This is intended to avoid failing because
-- of minor inaccuracies introduced by floating point arithmetic.
--
-- > -- Fails because 0.1 + 0.2 == 0.30000000000000004 (0.1 is non-terminating in base 2)
-- > 0.1 + 0.2 |> Expect.equal 0.3
-- >
-- > -- So instead write this test, which passes
-- > 0.1 + 0.2 |> Expect.within (Absolute 0.000000001) 0.3
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because 3.14 is not close enough to pi
-- > 3.14 |> Expect.within (Absolute 0.0001) pi
-- >
-- > {-
-- >
-- > 3.14
-- > ╷
-- > │ Expect.within Absolute 0.0001
-- > ╵
-- > 3.141592653589793
-- >
-- > -}
within :: (Stack.HasCallStack) => FloatingPointTolerance -> Float -> Float -> Expectation
within :: FloatingPointTolerance -> Float -> Float -> Expectation
within FloatingPointTolerance
tolerance =
  (HasCallStack =>
 (Float -> Float -> Bool) -> Text -> Float -> Float -> Expectation)
-> (Float -> Float -> Bool)
-> Text
-> Float
-> Float
-> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
    HasCallStack =>
(Float -> Float -> Bool) -> Text -> Float -> Float -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert
    (FloatingPointTolerance -> Float -> Float -> Bool
withinHelper FloatingPointTolerance
tolerance)
    (Text
"Expect.within " Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ String -> Text
Data.Text.pack (FloatingPointTolerance -> String
forall a. Show a => a -> String
Prelude.show FloatingPointTolerance
tolerance))

-- | Passes if (and only if) a call to within with the same arguments would have failed.
notWithin :: (Stack.HasCallStack) => FloatingPointTolerance -> Float -> Float -> Expectation
notWithin :: FloatingPointTolerance -> Float -> Float -> Expectation
notWithin FloatingPointTolerance
tolerance =
  (HasCallStack =>
 (Float -> Float -> Bool) -> Text -> Float -> Float -> Expectation)
-> (Float -> Float -> Bool)
-> Text
-> Float
-> Float
-> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
    HasCallStack =>
(Float -> Float -> Bool) -> Text -> Float -> Float -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert
    (\Float
expected Float
actual -> Bool -> Bool
not (FloatingPointTolerance -> Float -> Float -> Bool
withinHelper FloatingPointTolerance
tolerance Float
expected Float
actual))
    (Text
"Expect.notWithin " Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ String -> Text
Data.Text.pack (FloatingPointTolerance -> String
forall a. Show a => a -> String
Prelude.show FloatingPointTolerance
tolerance))

withinHelper :: FloatingPointTolerance -> Float -> Float -> Bool
withinHelper :: FloatingPointTolerance -> Float -> Float -> Bool
withinHelper FloatingPointTolerance
tolerance Float
expected Float
actual =
  case FloatingPointTolerance
tolerance of
    Absolute Float
absTolerance -> Float -> Float
forall number. Num number => number -> number
abs (Float
actual Float -> Float -> Float
forall number. Num number => number -> number -> number
- Float
expected) Float -> Float -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
<= Float
absTolerance
    Relative Float
relTolerance -> Float -> Float
forall number. Num number => number -> number
abs (Float
actual Float -> Float -> Float
forall number. Num number => number -> number -> number
- Float
expected) Float -> Float -> Float
/ Float
expected Float -> Float -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
<= Float
relTolerance
    AbsoluteOrRelative Float
absTolerance Float
relTolerance ->
      FloatingPointTolerance -> Float -> Float -> Bool
withinHelper (Float -> FloatingPointTolerance
Absolute Float
absTolerance) Float
expected Float
actual
        Bool -> Bool -> Bool
|| FloatingPointTolerance -> Float -> Float -> Bool
withinHelper (Float -> FloatingPointTolerance
Relative Float
relTolerance) Float
expected Float
actual

-- | Passes if the argument is 'True', and otherwise fails with the given message.
--
-- > Expect.true "Expected the list to be empty." (List.isEmpty [])
-- >
-- > -- Passes because (List.isEmpty []) is True
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because List.isEmpty returns False, but we expect True.
-- > List.isEmpty [ 42 ]
-- >     |> Expect.true "Expected the list to be empty."
-- >
-- > {-
-- >
-- > Expected the list to be empty.
-- >
-- > -}
true :: Stack.HasCallStack => Bool -> Expectation
true :: Bool -> Expectation
true Bool
x =
  if Bool
x
    then (HasCallStack => Text -> () -> Expectation)
-> Text -> () -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> () -> Expectation
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass Text
"Expect.true" ()
    else (HasCallStack => Text -> Text -> Expectation)
-> Text -> Text -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> Text -> Expectation
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion Text
"Expect.true" Text
"I expected a True but got False"

-- | Passes if the argument is 'False', and otherwise fails with the given message.
--
-- > Expect.false "Expected the list not to be empty." (List.isEmpty [ 42 ])
-- >
-- > -- Passes because (List.isEmpty [ 42 ]) is False
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because (List.isEmpty []) is True
-- > List.isEmpty []
-- >     |> Expect.false "Expected the list not to be empty."
-- >
-- > {-
-- >
-- > Expected the list not to be empty.
-- >
-- > -}
false :: Stack.HasCallStack => Bool -> Expectation
false :: Bool -> Expectation
false Bool
x =
  if Bool
x
    then (HasCallStack => Text -> Text -> Expectation)
-> Text -> Text -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> Text -> Expectation
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion Text
"Expect.false" Text
"I expected a False but got True"
    else (HasCallStack => Text -> () -> Expectation)
-> Text -> () -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> () -> Expectation
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass Text
"Expect.false" ()

-- | Passes if each of the given functions passes when applied to the subject.
--
-- Passing an empty list is assumed to be a mistake, so Expect.all [] will always return a failed expectation no matter what else it is passed.
--
-- > Expect.all
-- >     [ Expect.greaterThan -2
-- >     , Expect.lessThan 5
-- >     ]
-- >     (List.length [])
-- > -- Passes because (0 > -2) is True and (0 < 5) is also True
--
-- Failures resemble code written in pipeline style, so you can tell which argument is which:
--
-- > -- Fails because (0 < -10) is False
-- > List.length []
-- >     |> Expect.all
-- >         [ Expect.greaterThan -2
-- >         , Expect.lessThan -10
-- >         , Expect.equal 0
-- >         ]
-- > {-
-- > 0
-- > ╷
-- > │ Expect.lessThan
-- > ╵
-- > -10
-- > -}
all :: Stack.HasCallStack => List (subject -> Expectation) -> subject -> Expectation
all :: List (subject -> Expectation) -> subject -> Expectation
all List (subject -> Expectation)
expectations subject
subject =
  ((subject -> Expectation) -> Expectation -> Expectation)
-> Expectation -> List (subject -> Expectation) -> Expectation
forall a b. (a -> b -> b) -> b -> List a -> b
List.foldl
    ( \subject -> Expectation
expectation Expectation
acc ->
        Expectation -> Expectation -> Expectation
Internal.append
          Expectation
acc
          ((HasCallStack => subject -> Expectation) -> subject -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack subject -> Expectation
HasCallStack => subject -> Expectation
expectation subject
subject)
    )
    Expectation
HasCallStack => Expectation
pass
    List (subject -> Expectation)
expectations

-- | Passes if the Result is an Ok rather than Err. This is useful for tests where you expect not to see an error, but you don't care what the actual result is.
--
-- (Tip: If your function returns a Maybe instead, consider Expect.notEqual Nothing.)
--
-- > -- Passes
-- > String.toInt "not an int"
-- >     |> Expect.err
--
-- Test failures will be printed with the unexpected Ok value contrasting with any Err.
--
-- > -- Fails
-- > String.toInt "20"
-- >     |> Expect.err
-- >
-- > {-
-- >
-- > Ok 20
-- > ╷
-- > │ Expect.err
-- > ╵
-- > Err _
-- >
-- > -}
ok :: (Stack.HasCallStack, Show b) => Result b a -> Expectation
ok :: Result b a -> Expectation
ok Result b a
res =
  case Result b a
res of
    Ok a
_ ->
      (HasCallStack => Text -> () -> Expectation)
-> Text -> () -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
        HasCallStack => Text -> () -> Expectation
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass
        Text
"Expect.ok"
        ()
    Err b
message ->
      (HasCallStack => Text -> Text -> Expectation)
-> Text -> Text -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
        HasCallStack => Text -> Text -> Expectation
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion
        Text
"Expect.ok"
        (Text
"I expected a Ok but got Err (" Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ b -> Text
forall a. Show a => a -> Text
Debug.toString b
message Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ Text
")")

-- | Passes if the Result is an Err rather than Ok. This is useful for tests where you expect to get an error but you don't care what the actual error is.
--
-- (Tip: If your function returns a Maybe instead, consider Expect.equal Nothing.)
--
-- > -- Passes
-- > String.toInt "not an int"
-- >     |> Expect.err
--
-- Test failures will be printed with the unexpected Ok value contrasting with any Err.
--
-- > -- Fails
-- > String.toInt "20"
-- >     |> Expect.err
-- >
-- > {-
-- >
-- > Ok 20
-- > ╷
-- > │ Expect.err
-- > ╵
-- > Err _
-- >
-- > -}
err :: (Stack.HasCallStack, Show a) => Result b a -> Expectation
err :: Result b a -> Expectation
err Result b a
res =
  case Result b a
res of
    Ok a
value ->
      (HasCallStack => Text -> Text -> Expectation)
-> Text -> Text -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
        HasCallStack => Text -> Text -> Expectation
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion
        Text
"Expect.err"
        (Text
"I expected a Err but got Ok (" Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ a -> Text
forall a. Show a => a -> Text
Debug.toString a
value Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ Text
")")
    Err b
_ ->
      (HasCallStack => Text -> () -> Expectation)
-> Text -> () -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
        HasCallStack => Text -> () -> Expectation
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass
        Text
"Expect.err"
        ()

-- | Check if a string is equal to the contents of a file.
--
-- > Debug.toString complicatedObject
-- >     |> Expect.equalToContentsOf "golden-results/complicated-object.txt"
--
-- If the file does not exist it will be created and the test will pass.
-- Subsequent runs will check the test output matches the now existing file.
--
-- This can be useful when checking big strings, like for example JSON
-- encodings. When a test fails we can throw away the file, rerun the test, and
-- use @git diff golden-results/complicated-object.txt@ to check whether the
-- changes are acceptable.
equalToContentsOf :: Stack.HasCallStack => Text -> Text -> Expectation
equalToContentsOf :: Text -> Text -> Expectation
equalToContentsOf Text
filepath' Text
actual = do
  let filepath :: String
filepath = Text -> String
Data.Text.unpack Text
filepath'
  Bool
exists <-
    IO Bool -> Expectation' Bool
forall a. IO a -> Expectation' a
fromIO (IO Bool -> Expectation' Bool) -> IO Bool -> Expectation' Bool
forall a b. (a -> b) -> a -> b
<| do
      Bool -> String -> IO ()
Directory.createDirectoryIfMissing Bool
True (ShowS
FilePath.takeDirectory String
filepath)
      String -> IO Bool
Directory.doesFileExist String
filepath
  if Bool
exists
    then do
      Text
expected <- IO Text -> Expectation' Text
forall a. IO a -> Expectation' a
fromIO (String -> IO Text
Data.Text.IO.readFile String
filepath)
      (HasCallStack =>
 (UnescapedShow -> UnescapedShow -> Bool)
 -> Text -> UnescapedShow -> UnescapedShow -> Expectation)
-> (UnescapedShow -> UnescapedShow -> Bool)
-> Text
-> UnescapedShow
-> UnescapedShow
-> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
        HasCallStack =>
(UnescapedShow -> UnescapedShow -> Bool)
-> Text -> UnescapedShow -> UnescapedShow -> Expectation
forall a.
(HasCallStack, Show a) =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert
        UnescapedShow -> UnescapedShow -> Bool
forall a. Eq a => a -> a -> Bool
(==)
        (Text
"Expect.equalToContentsOf " Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ Text
filepath')
        (Text -> UnescapedShow
UnescapedShow Text
expected)
        (Text -> UnescapedShow
UnescapedShow Text
actual)
    else do
      IO () -> Expectation
forall a. IO a -> Expectation' a
fromIO (String -> Text -> IO ()
Data.Text.IO.writeFile String
filepath Text
actual)
      (HasCallStack => Text -> () -> Expectation)
-> Text -> () -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
        HasCallStack => Text -> () -> Expectation
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass
        (Text
"Expect.equalToContentsOf " Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ Text
filepath')
        ()

-- By default we will compare values with each other after they have been
-- passed to @show@. Unfortunately @show@ for the @Text@ type escapes special
-- characters, so a string like this:
--
--    Hi there,
--    newline!
--
-- Is rendered in test output as this:
--
--    \"Hi there,\nnewline!\"
--
-- And then test output looks all garbled.
--
-- This newtype wrapper for @Text@ makes the show instance render it without
-- escaping any character, resulting in cleaner test output!
newtype UnescapedShow = UnescapedShow Text deriving (UnescapedShow -> UnescapedShow -> Bool
(UnescapedShow -> UnescapedShow -> Bool)
-> (UnescapedShow -> UnescapedShow -> Bool) -> Eq UnescapedShow
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: UnescapedShow -> UnescapedShow -> Bool
$c/= :: UnescapedShow -> UnescapedShow -> Bool
== :: UnescapedShow -> UnescapedShow -> Bool
$c== :: UnescapedShow -> UnescapedShow -> Bool
Eq)

instance Show UnescapedShow where
  show :: UnescapedShow -> String
show (UnescapedShow Text
text) = Text -> String
Data.Text.unpack Text
text

assert :: (Stack.HasCallStack, Show a) => (a -> a -> Bool) -> Text -> a -> a -> Expectation
assert :: (a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
pred Text
funcName a
expected a
actual =
  if a -> a -> Bool
pred a
expected a
actual
    then (HasCallStack => Text -> () -> Expectation)
-> Text -> () -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> () -> Expectation
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass Text
funcName ()
    else do
      Maybe (Window Int)
window <- IO (Maybe (Window Int)) -> Expectation' (Maybe (Window Int))
forall a. IO a -> Expectation' a
fromIO IO (Maybe (Window Int))
forall n. Integral n => IO (Maybe (Window n))
Terminal.size
      let terminalWidth :: Int
terminalWidth = case Maybe (Window Int)
window of
            Just Terminal.Window {Int
width :: forall a. Window a -> a
width :: Int
Terminal.width} -> Int
width Int -> Int -> Int
forall number. Num number => number -> number -> number
- Int
4 -- indentation
            Maybe (Window Int)
Nothing -> Int
80
      let expectedText :: Text
expectedText = String -> Text
Data.Text.pack (a -> String
forall a. Show a => a -> String
Text.Show.Pretty.ppShow a
actual)
      let actualText :: Text
actualText = String -> Text
Data.Text.pack (a -> String
forall a. Show a => a -> String
Text.Show.Pretty.ppShow a
expected)
      let numLines :: Text -> Int
numLines Text
text = List Text -> Int
forall a. List a -> Int
List.length (Text -> List Text
Data.Text.lines Text
text)
      (HasCallStack => Text -> Text -> Expectation)
-> Text -> Text -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack HasCallStack => Text -> Text -> Expectation
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion Text
funcName
        (Text -> Expectation) -> Text -> Expectation
forall a b. (a -> b) -> a -> b
<| Config -> Text -> Text -> Text
Diff.pretty
          Config :: Maybe Text -> Wrapping -> MultilineContext -> Config
Diff.Config
            { separatorText :: Maybe Text
Diff.separatorText = Text -> Maybe Text
forall a. a -> Maybe a
Just Text
funcName,
              wrapping :: Wrapping
Diff.wrapping = Int -> Wrapping
Diff.Wrap Int
terminalWidth,
              multilineContext :: MultilineContext
Diff.multilineContext =
                if Text -> Int
numLines Text
expectedText Int -> Int -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
< Int
6 Bool -> Bool -> Bool
&& Text -> Int
numLines Text
actualText Int -> Int -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
< Int
6
                  then MultilineContext
Diff.FullContext
                  else Int -> Text -> MultilineContext
Diff.Surrounding Int
2 Text
"..."
            }
          Text
expectedText
          Text
actualText

-- | Convert an IO type to an expectation. Useful if you need to call a function
-- in Haskell's base library or an external library in a test.
fromIO :: Prelude.IO a -> Internal.Expectation' a
fromIO :: IO a -> Expectation' a
fromIO IO a
io =
  (LogHandler -> IO (Result Failure a)) -> Task Failure a
forall x a. (LogHandler -> IO (Result x a)) -> Task x a
Platform.Internal.Task (\LogHandler
_ -> (a -> Result Failure a) -> IO a -> IO (Result Failure a)
forall (m :: * -> *) a value.
Functor m =>
(a -> value) -> m a -> m value
map a -> Result Failure a
forall error value. value -> Result error value
Ok IO a
io)
    Task Failure a
-> (Task Failure a -> Expectation' a) -> Expectation' a
forall a b. a -> (a -> b) -> b
|> Task Failure a -> Expectation' a
forall a. Task Failure a -> Expectation' a
Internal.Expectation

-- | Used for making matchers
-- expectOneItem :: Expectation' [a] -> Expectation' a
-- expectOneItem t = do
--   xs <- t
--   case xs of
--     [x] -> Ok x
--     _ -> Err ("Expected one item, but got " ++ Debug.toString (List.length xs) ++ ".")
--   |> Expect.fromResult
fromResult :: (Stack.HasCallStack, Show b) => Result b a -> Internal.Expectation' a
fromResult :: Result b a -> Expectation' a
fromResult (Ok a
a) =
  (HasCallStack => Text -> a -> Expectation' a)
-> Text -> a -> Expectation' a
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
    HasCallStack => Text -> a -> Expectation' a
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass
    Text
"Expect.fromResult"
    a
a
fromResult (Err b
msg) =
  (HasCallStack => Text -> Text -> Expectation' a)
-> Text -> Text -> Expectation' a
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
    HasCallStack => Text -> Text -> Expectation' a
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion
    Text
"Expect.fromResult"
    (b -> Text
forall a. Show a => a -> Text
Debug.toString b
msg)

-- | Check a task returns an expected value.
--
-- > test "Greetings are friendly" <| \_ -> do
-- >     getGreeting
-- >         |> andCheck (Expect.equal "Hi!")
andCheck :: (Stack.HasCallStack, Show err) => (a -> Expectation) -> Task err a -> Internal.Expectation
andCheck :: (a -> Expectation) -> Task err a -> Expectation
andCheck a -> Expectation
expectation Task err a
task = do
  a
x <- Task err a -> Expectation' a
forall err a.
(HasCallStack, Show err) =>
Task err a -> Expectation' a
succeeds Task err a
task
  (HasCallStack => a -> Expectation) -> a -> Expectation
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack a -> Expectation
HasCallStack => a -> Expectation
expectation a
x

-- | Check a task succeeds.
--
-- > test "solve rubicskube" <| \_ -> do
-- >     solveRubicsKube
-- >         |> succeeds
succeeds :: (Stack.HasCallStack, Show err) => Task err a -> Internal.Expectation' a
succeeds :: Task err a -> Expectation' a
succeeds Task err a
task =
  Task err a
task
    Task err a -> (Task err a -> Task Failure a) -> Task Failure a
forall a b. a -> (a -> b) -> b
|> (err -> Task Failure a) -> Task err a -> Task Failure a
forall x y a. (x -> Task y a) -> Task x a -> Task y a
Task.onError
      ( \err
message ->
          (HasCallStack => Text -> Text -> Expectation' a)
-> Text -> Text -> Expectation' a
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
            HasCallStack => Text -> Text -> Expectation' a
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion
            Text
"Expect.succeeds"
            (err -> Text
forall a. Show a => a -> Text
Debug.toString err
message)
            Expectation' a
-> (Expectation' a -> Task Failure a) -> Task Failure a
forall a b. a -> (a -> b) -> b
|> Expectation' a -> Task Failure a
forall a. Expectation' a -> Task Failure a
Internal.unExpectation
      )
    Task Failure a
-> (Task Failure a -> Task Failure a) -> Task Failure a
forall a b. a -> (a -> b) -> b
|> (a -> Task Failure a) -> Task Failure a -> Task Failure a
forall a x b. (a -> Task x b) -> Task x a -> Task x b
Task.andThen
      ( \a
a ->
          (HasCallStack => Text -> a -> Expectation' a)
-> Text -> a -> Expectation' a
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
            HasCallStack => Text -> a -> Expectation' a
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass
            Text
"Expect.succeeds"
            a
a
            Expectation' a
-> (Expectation' a -> Task Failure a) -> Task Failure a
forall a b. a -> (a -> b) -> b
|> Expectation' a -> Task Failure a
forall a. Expectation' a -> Task Failure a
Internal.unExpectation
      )
    Task Failure a
-> (Task Failure a -> Expectation' a) -> Expectation' a
forall a b. a -> (a -> b) -> b
|> Task Failure a -> Expectation' a
forall a. Task Failure a -> Expectation' a
Internal.Expectation

-- | Check a task fails.
--
-- > test "chemistry experiment" <| \_ -> do
-- >     mixRedAndGreenLiquids
-- >         |> fails
fails :: (Stack.HasCallStack, Show a) => Task err a -> Internal.Expectation' err
fails :: Task err a -> Expectation' err
fails Task err a
task =
  Task err a
task
    Task err a
-> (Task err a -> Task err (Result Text err))
-> Task err (Result Text err)
forall a b. a -> (a -> b) -> b
|> (a -> Result Text err) -> Task err a -> Task err (Result Text err)
forall a b x. (a -> b) -> Task x a -> Task x b
Task.map (\a
succ -> Text -> Result Text err
forall error value. error -> Result error value
Err (Text
"Expected failure but succeeded with " Text -> Text -> Text
forall appendable.
Semigroup appendable =>
appendable -> appendable -> appendable
++ a -> Text
forall a. Show a => a -> Text
Debug.toString a
succ))
    Task err (Result Text err)
-> (Task err (Result Text err) -> Task Failure (Result Text err))
-> Task Failure (Result Text err)
forall a b. a -> (a -> b) -> b
|> (err -> Task Failure (Result Text err))
-> Task err (Result Text err) -> Task Failure (Result Text err)
forall x y a. (x -> Task y a) -> Task x a -> Task y a
Task.onError (\err
err' -> Result Text err -> Task Failure (Result Text err)
forall a x. a -> Task x a
Task.succeed (err -> Result Text err
forall error value. value -> Result error value
Ok err
err'))
    Task Failure (Result Text err)
-> (Task Failure (Result Text err) -> Task Failure err)
-> Task Failure err
forall a b. a -> (a -> b) -> b
|> (Result Text err -> Task Failure err)
-> Task Failure (Result Text err) -> Task Failure err
forall a x b. (a -> Task x b) -> Task x a -> Task x b
Task.andThen
      ( \Result Text err
res ->
          Expectation' err -> Task Failure err
forall a. Expectation' a -> Task Failure a
Internal.unExpectation
            (Expectation' err -> Task Failure err)
-> Expectation' err -> Task Failure err
forall a b. (a -> b) -> a -> b
<| case Result Text err
res of
              Ok err
a ->
                (HasCallStack => Text -> err -> Expectation' err)
-> Text -> err -> Expectation' err
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
                  HasCallStack => Text -> err -> Expectation' err
forall a. HasCallStack => Text -> a -> Expectation' a
Internal.pass
                  Text
"Expect.fails"
                  err
a
              Err Text
msg ->
                (HasCallStack => Text -> Text -> Expectation' err)
-> Text -> Text -> Expectation' err
forall a. HasCallStack => (HasCallStack => a) -> a
Stack.withFrozenCallStack
                  HasCallStack => Text -> Text -> Expectation' err
forall a. HasCallStack => Text -> Text -> Expectation' a
Internal.failAssertion
                  Text
"Expect.fails"
                  (Text -> Text
forall a. Show a => a -> Text
Debug.toString Text
msg)
      )
    Task Failure err
-> (Task Failure err -> Expectation' err) -> Expectation' err
forall a b. a -> (a -> b) -> b
|> Task Failure err -> Expectation' err
forall a. Task Failure a -> Expectation' a
Internal.Expectation

-- | This can be used to create custom test functions that contain some setup
-- and teardown logic, for example to make tests run in a database transaction
-- that gets rolled back afterwards.
--
-- > dbTest ::
-- >   Stack.HasCallStack =>
-- >   Text ->
-- >   (Db.Connection -> Expect.Expectation) ->
-- >   Test.Test
-- > dbTest description body =
-- >   Stack.withFrozenCallStack Test.test description <| \_ -> do
-- >     Expect.around
-- >       ( \task' -> do
-- >           conn <- Db.getConnection
-- >           Platform.finally
-- >             (task' conn)
-- >             (Db.rollback conn)
-- >       )
-- >       body
around ::
  (forall e a. (arg -> Task e a) -> Task e a) ->
  (arg -> Expectation) ->
  Expectation
around :: (forall e a. (arg -> Task e a) -> Task e a)
-> (arg -> Expectation) -> Expectation
around forall e a. (arg -> Task e a) -> Task e a
runTask arg -> Expectation
runExpectation =
  Task Failure () -> Expectation
forall a. Task Failure a -> Expectation' a
Internal.Expectation
    ( (arg -> Task Failure ()) -> Task Failure ()
forall e a. (arg -> Task e a) -> Task e a
runTask
        ( \arg
arg ->
            arg -> Expectation
runExpectation arg
arg
              Expectation -> (Expectation -> Task Failure ()) -> Task Failure ()
forall a b. a -> (a -> b) -> b
|> Expectation -> Task Failure ()
forall a. Expectation' a -> Task Failure a
Internal.unExpectation
        )
    )