-- |  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,
    concat,

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

    -- * Booleans
    true,
    false,

    -- * Collections
    ok,
    err,

    -- * Customizing

    -- | These functions will let you build your own expectations.
    pass,
    fail,
    onFail,

    -- * Fancy Expectations
    equalToContentsOf,
    withIO,
  )
where

import qualified Data.Text
import qualified Data.Text.IO
import qualified Debug
import qualified List
import List (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 qualified Test.Internal as Internal
import Test.Internal (Expectation)
import qualified Text.Show.Pretty
import Prelude (Eq, IO, Ord, Show, show)

-- | Run some IO and assert the value it produces.
--
-- If the IO throws an exception the test will fail.
withIO :: (a -> Expectation) -> IO a -> Expectation
withIO :: (a -> Expectation) -> IO a -> Expectation
withIO a -> Expectation
fn IO a
io =
  IO a -> Task Never a
forall a e. IO a -> Task e a
fromIO IO a
io
    Task Never a
-> (Task Never a -> Task Never TestResult) -> Task Never TestResult
forall a b. a -> (a -> b) -> b
|> (a -> Task Never TestResult)
-> Task Never a -> Task Never TestResult
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
andThen (Expectation -> Task Never TestResult
Internal.unExpectation (Expectation -> Task Never TestResult)
-> (a -> Expectation) -> a -> Task Never TestResult
forall b c a. (b -> c) -> (a -> b) -> a -> c
<< a -> Expectation
fn)
    Task Never TestResult
-> (Task Never TestResult -> Expectation) -> Expectation
forall a b. a -> (a -> b) -> b
|> Task Never TestResult -> Expectation
Internal.Expectation

-- | 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 :: Expectation
pass :: Expectation
pass = Task Never TestResult -> Expectation
Internal.Expectation (TestResult -> Task Never TestResult
forall a x. a -> Task x a
Task.succeed TestResult
Internal.Succeeded)

-- | 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 :: Text -> Expectation
fail :: Text -> Expectation
fail Text
msg =
  Text
msg
    Text -> (Text -> Failure) -> Failure
forall a b. a -> (a -> b) -> b
|> Text -> Failure
Internal.FailedAssertion
    Failure -> (Failure -> TestResult) -> TestResult
forall a b. a -> (a -> b) -> b
|> Failure -> TestResult
Internal.Failed
    TestResult
-> (TestResult -> Task Never TestResult) -> Task Never TestResult
forall a b. a -> (a -> b) -> b
|> TestResult -> Task Never TestResult
forall a x. a -> Task x a
Task.succeed
    Task Never TestResult
-> (Task Never TestResult -> Expectation) -> Expectation
forall a b. a -> (a -> b) -> b
|> Task Never TestResult -> Expectation
Internal.Expectation

-- | 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 :: Text -> Expectation -> Expectation
onFail :: Text -> Expectation -> Expectation
onFail Text
msg (Internal.Expectation Task Never TestResult
task) =
  Task Never TestResult
task
    Task Never TestResult
-> (Task Never TestResult -> Task Never TestResult)
-> Task Never TestResult
forall a b. a -> (a -> b) -> b
|> (TestResult -> TestResult)
-> Task Never TestResult -> Task Never TestResult
forall a b x. (a -> b) -> Task x a -> Task x b
Task.map
      ( \TestResult
res ->
          case TestResult
res of
            TestResult
Internal.Succeeded -> TestResult
Internal.Succeeded
            Internal.Failed Failure
_ -> Failure -> TestResult
Internal.Failed (Text -> Failure
Internal.FailedAssertion Text
msg)
      )
    Task Never TestResult
-> (Task Never TestResult -> Expectation) -> Expectation
forall a b. a -> (a -> b) -> b
|> Task Never TestResult -> Expectation
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 :: (Show a, Eq a) => a -> a -> Expectation
equal :: a -> a -> Expectation
equal = (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
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 :: (Show a, Eq a) => a -> a -> Expectation
notEqual :: a -> a -> Expectation
notEqual = (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
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 :: (Show a, Ord a) => a -> a -> Expectation
lessThan :: a -> a -> Expectation
lessThan = (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
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 :: (Show a, Ord a) => a -> a -> Expectation
atMost :: a -> a -> Expectation
atMost = (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
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 :: (Show a, Ord a) => a -> a -> Expectation
greaterThan :: a -> a -> Expectation
greaterThan = (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
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 :: (Show a, Ord a) => a -> a -> Expectation
atLeast :: a -> a -> Expectation
atLeast = (a -> a -> Bool) -> Text -> a -> a -> Expectation
forall a.
Show a =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert a -> a -> Bool
forall comparable.
Ord comparable =>
comparable -> comparable -> Bool
(<=) Text
"Expect.atLeast"

-- | 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 :: Bool -> Expectation
true :: Bool -> Expectation
true Bool
x = (Bool -> Bool -> Bool) -> Text -> Bool -> Bool -> Expectation
forall a.
Show a =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert Bool -> Bool -> Bool
(&&) Text
"Expect.true" Bool
x Bool
True

-- | 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 :: Bool -> Expectation
false :: Bool -> Expectation
false Bool
x = (Bool -> Bool -> Bool) -> Text -> Bool -> Bool -> Expectation
forall a.
Show a =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert Bool -> Bool -> Bool
xor Text
"Expect.false" Bool
x Bool
True

-- | 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 :: 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
          (subject -> Expectation
expectation subject
subject)
    )
    Expectation
pass
    List (subject -> Expectation)
expectations

-- | Combine multiple expectations into one. The resulting expectation is a
-- failure if any of the original expectations are a failure.
concat :: List Expectation -> Expectation
concat :: List Expectation -> Expectation
concat List Expectation
expectations =
  (Expectation -> Expectation -> Expectation)
-> Expectation -> List Expectation -> Expectation
forall a b. (a -> b -> b) -> b -> List a -> b
List.foldl
    ( \Expectation
expectation Expectation
acc ->
        Expectation -> Expectation -> Expectation
Internal.append
          Expectation
acc
          Expectation
expectation
    )
    Expectation
pass
    List 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 :: Show b => Result b a -> Expectation
ok :: Result b a -> Expectation
ok Result b a
res =
  case Result b a
res of
    Ok a
_ -> Expectation
pass
    Err b
message -> Text -> Expectation
fail (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 :: 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 -> Text -> Expectation
fail (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
_ -> Expectation
pass

-- | 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 :: Text -> Text -> Expectation
equalToContentsOf :: Text -> Text -> Expectation
equalToContentsOf Text
filepath' Text
actual =
  Task Never TestResult -> Expectation
Internal.Expectation (Task Never TestResult -> Expectation)
-> Task Never TestResult -> Expectation
forall a b. (a -> b) -> a -> b
<| do
    let filepath :: String
filepath = Text -> String
Data.Text.unpack Text
filepath'
    Bool
exists <- IO Bool -> Task Never Bool
forall a e. IO a -> Task e a
fromIO (IO Bool -> Task Never Bool) -> IO Bool -> Task Never Bool
forall a b. (a -> b) -> a -> b
<| do
      Bool -> String -> IO ()
Directory.createDirectoryIfMissing Bool
True (String -> String
FilePath.takeDirectory String
filepath)
      String -> IO Bool
Directory.doesFileExist String
filepath
    if Bool
exists
      then do
        Text
expected <- IO Text -> Task Never Text
forall a e. IO a -> Task e a
fromIO (String -> IO Text
Data.Text.IO.readFile String
filepath)
        (UnescapedShow -> UnescapedShow -> Bool)
-> Text -> UnescapedShow -> UnescapedShow -> Expectation
forall a.
Show a =>
(a -> a -> Bool) -> Text -> a -> a -> Expectation
assert
          UnescapedShow -> UnescapedShow -> Bool
forall a. Eq a => a -> a -> Bool
(==)
          Text
"Expect.equalToContentsOf"
          (Text -> UnescapedShow
UnescapedShow Text
expected)
          (Text -> UnescapedShow
UnescapedShow Text
actual)
          Expectation
-> (Expectation -> Task Never TestResult) -> Task Never TestResult
forall a b. a -> (a -> b) -> b
|> Expectation -> Task Never TestResult
Internal.unExpectation
      else do
        IO () -> Task Never ()
forall a e. IO a -> Task e a
fromIO (String -> Text -> IO ()
Data.Text.IO.writeFile String
filepath Text
actual)
        Expectation -> Task Never TestResult
Internal.unExpectation Expectation
pass

-- 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 :: 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
actual a
expected =
  if a -> a -> Bool
pred a
actual a
expected
    then Expectation
pass
    else Task Never TestResult -> Expectation
Internal.Expectation (Task Never TestResult -> Expectation)
-> Task Never TestResult -> Expectation
forall a b. (a -> b) -> a -> b
<| do
      Maybe (Window Int)
window <- IO (Maybe (Window Int)) -> Task Never (Maybe (Window Int))
forall a e. IO a -> Task e 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
      Config -> PrettyShow a -> PrettyShow a -> Text
forall a. Show a => Config -> a -> a -> Text
Diff.pretty
        Config :: Maybe Text -> Wrapping -> 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
          }
        (a -> PrettyShow a
forall a. a -> PrettyShow a
PrettyShow a
expected)
        (a -> PrettyShow a
forall a. a -> PrettyShow a
PrettyShow a
actual)
        Text -> (Text -> Expectation) -> Expectation
forall a b. a -> (a -> b) -> b
|> Text -> Expectation
fail
        Expectation
-> (Expectation -> Task Never TestResult) -> Task Never TestResult
forall a b. a -> (a -> b) -> b
|> Expectation -> Task Never TestResult
Internal.unExpectation

fromIO :: Prelude.IO a -> Task e a
fromIO :: IO a -> Task e a
fromIO IO a
io = (LogHandler -> IO (Result e a)) -> Task e a
forall x a. (LogHandler -> IO (Result x a)) -> Task x a
Platform.Internal.Task (\LogHandler
_ -> (a -> Result e a) -> IO a -> IO (Result e a)
forall (m :: * -> *) a value.
Functor m =>
(a -> value) -> m a -> m value
map a -> Result e a
forall error value. value -> Result error value
Ok IO a
io)

newtype PrettyShow a = PrettyShow a

instance Show a => Show (PrettyShow a) where
  show :: PrettyShow a -> String
show (PrettyShow a
x) = a -> String
forall a. Show a => a -> String
Text.Show.Pretty.ppShow a
x