packages feed

tasty-discover-5.2.0: test/DiscoverTest.hs

{-# LANGUAGE CPP                 #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- HLINT ignore "Avoid reverse" -}
{- HLINT ignore "Redundant reverse" -}

module DiscoverTest where

import Data.ByteString.Lazy (ByteString)
import Data.List
import Data.Maybe (listToMaybe)
import Data.String (IsString(..))
import GHC.Generics (Generic)
import System.Console.ANSI (Color(..), ColorIntensity(..), ConsoleLayer(..), SGR(..), setSGRCode)
import Test.Hspec (shouldBe)
import Test.Hspec.Core.Spec (Spec, describe, it)
import Test.Tasty
import Test.Tasty.Discover (Flavored, flavored, skip, platform, applySkips, evaluatePlatformExpression)
import Test.Tasty.Golden
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck hiding (Property, property)

import qualified Hedgehog            as H
import qualified Hedgehog.Gen        as G
import qualified Hedgehog.Range      as R
import qualified Test.Tasty          as TT
import qualified Test.Tasty.Discover as TD
import qualified Test.Tasty.Hedgehog as TH
import qualified Test.Tasty.HUnit    as HU

------------------------------------------------------------------------------------------------

unit_listCompare :: IO ()
unit_listCompare = [1 :: Int, 2, 3] `compare` [1,2] @?= GT

------------------------------------------------------------------------------------------------

prop_additionCommutative :: Int -> Int -> Bool
prop_additionCommutative a b = a + b == b + a

------------------------------------------------------------------------------------------------

scprop_sortReverse :: [Int] -> Bool
scprop_sortReverse list = sort list == sort (reverse list)

------------------------------------------------------------------------------------------------

spec_prelude :: Spec
spec_prelude = describe "Prelude.head" $ do
  it "returns the first element of a list" $ do
    listToMaybe [23 ..] `shouldBe` Just (23 :: Int)

------------------------------------------------------------------------------------------------

test_addition :: TestTree
test_addition = testProperty "Addition commutes" $ \(a :: Int) (b :: Int) -> a + b == b + a

------------------------------------------------------------------------------------------------

test_multiplication :: [TestTree]
test_multiplication =
  [ testProperty "Multiplication commutes" $ \(a :: Int) (b :: Int) -> a * b == b * a
  , testProperty "One is identity" $ \(a :: Int) -> a == a
  ]

------------------------------------------------------------------------------------------------

test_generateTree :: IO TestTree
test_generateTree = do
  let input = "Some input"
  pure $ testCase input $ pure ()

------------------------------------------------------------------------------------------------

test_generateTrees :: IO [TestTree]
test_generateTrees = pure (map (\s -> testCase s $ pure ()) ["First input", "Second input"])

------------------------------------------------------------------------------------------------
-- How to simultaneously support tasty-hedgehog <1.2 and ^>1.2 using a custom test

newtype Property = Property
  { unProperty :: H.Property
  }

instance TD.Tasty Property where
  tasty info (Property p) = do
    let name = TD.nameOf info
    let mkTestTree =
#if MIN_VERSION_tasty_hedgehog(1, 2, 0)
          TH.testPropertyNamed name (fromString (TD.descriptionOf info))
#else
          TH.testProperty name
#endif
        yellowText text = setSGRCode [SetColor Foreground Vivid Yellow] ++ text ++ setSGRCode [Reset]
    -- Apply skip functionality if SkipTest option is True
    pure $ askOption $ \(TD.SkipTest shouldSkip) ->
      if shouldSkip
        then testCase (TD.nameOf info ++ " " ++ yellowText "[SKIPPED]") (pure ())
        else mkTestTree p

property :: HasCallStack => H.PropertyT IO () -> Property
property = Property . H.property

tasty_reverse :: Property
tasty_reverse = property $ do
  xs <- H.forAll $ G.list (R.linear 0 100) G.alpha
  reverse (reverse xs) H.=== xs

tasty_skip_me :: Flavored Property
tasty_skip_me =
  flavored skip $ property $ do
    H.failure

-- Skipping guideline (read this first):
--   To skip a tasty_ test at the TestTree level (so it shows [SKIPPED] and
--   doesn’t run), use:  flavored skip $ ...
--   Applying skip directly to a TestTree won’t skip it at that outer level; the
--   test itself must check SkipTest via askOption.
--
-- Implementation details (for the curious):
--   Applying skip to an already-constructed TestTree sets a Tasty option on the
--   subtree. The test body can observe it (as asserted below), but the Tasty
--   instance used by tasty_ functions may have already decided how to wrap the
--   node, so you won’t see a top-level [SKIPPED] placeholder. Wrapping with
--   flavored skip applies the transformation earlier, letting the instance see
--   SkipTest and short-circuit with a skipped placeholder.
tasty_tree_of_testCase_not_skipped :: TT.TestTree
tasty_tree_of_testCase_not_skipped =
  skip $ askOption $ \(TD.SkipTest shouldSkip) ->
    HU.testCase "This test is not skipped at the TestTree level" $
      HU.assertBool "Expected SkipTest to be True inside TestTree" shouldSkip

------------------------------------------------------------------------------------------------
-- Platform expression tests

-- Test basic platform matching
unit_platformExpression_linux :: Assertion  
unit_platformExpression_linux =
  evaluatePlatformExpression "linux" "linux" @?= True

unit_platformExpression_darwin :: Assertion
unit_platformExpression_darwin =
  evaluatePlatformExpression "darwin" "darwin" @?= True

unit_platformExpression_windows :: Assertion
unit_platformExpression_windows =
  evaluatePlatformExpression "windows" "mingw32" @?= True

unit_platformExpression_mingw32 :: Assertion
unit_platformExpression_mingw32 =
  evaluatePlatformExpression "mingw32" "mingw32" @?= True

-- Test negation
unit_platformExpression_not_linux :: Assertion
unit_platformExpression_not_linux =
  evaluatePlatformExpression "!linux" "darwin" @?= True

unit_platformExpression_not_linux_false :: Assertion
unit_platformExpression_not_linux_false =
  evaluatePlatformExpression "!linux" "linux" @?= False

unit_platformExpression_not_darwin :: Assertion
unit_platformExpression_not_darwin =
  evaluatePlatformExpression "!darwin" "linux" @?= True

unit_platformExpression_not_darwin_false :: Assertion
unit_platformExpression_not_darwin_false =
  evaluatePlatformExpression "!darwin" "darwin" @?= False

unit_platformExpression_not_windows :: Assertion
unit_platformExpression_not_windows =
  evaluatePlatformExpression "!windows" "linux" @?= True

unit_platformExpression_not_windows_false :: Assertion
unit_platformExpression_not_windows_false =
  evaluatePlatformExpression "!windows" "mingw32" @?= False

unit_platformExpression_not_mingw32 :: Assertion
unit_platformExpression_not_mingw32 =
  evaluatePlatformExpression "!mingw32" "linux" @?= True

unit_platformExpression_not_mingw32_false :: Assertion
unit_platformExpression_not_mingw32_false =
  evaluatePlatformExpression "!mingw32" "mingw32" @?= False

unit_platformExpression_not_unix :: Assertion
unit_platformExpression_not_unix =
  evaluatePlatformExpression "!unix" "mingw32" @?= True

unit_platformExpression_not_unix_false_linux :: Assertion
unit_platformExpression_not_unix_false_linux =
  evaluatePlatformExpression "!unix" "linux" @?= False

unit_platformExpression_not_unix_false_darwin :: Assertion
unit_platformExpression_not_unix_false_darwin =
  evaluatePlatformExpression "!unix" "darwin" @?= False

-- Test conjunction (AND)
unit_platformExpression_and_true :: Assertion
unit_platformExpression_and_true =
  evaluatePlatformExpression "!windows & !darwin" "linux" @?= True

unit_platformExpression_and_false :: Assertion
unit_platformExpression_and_false =
  evaluatePlatformExpression "!windows & !darwin" "darwin" @?= False

unit_platformExpression_and_false_windows :: Assertion
unit_platformExpression_and_false_windows =
  evaluatePlatformExpression "!windows & !darwin" "mingw32" @?= False

unit_platformExpression_and_both_positive :: Assertion
unit_platformExpression_and_both_positive =
  evaluatePlatformExpression "unix & !windows" "linux" @?= True

unit_platformExpression_and_both_positive_false :: Assertion
unit_platformExpression_and_both_positive_false =
  evaluatePlatformExpression "unix & !windows" "mingw32" @?= False

unit_platformExpression_and_three_terms :: Assertion
unit_platformExpression_and_three_terms =
  evaluatePlatformExpression "!windows & !darwin & !mingw32" "linux" @?= True

-- Test disjunction (OR)
unit_platformExpression_or_true :: Assertion
unit_platformExpression_or_true =
  evaluatePlatformExpression "linux | darwin" "linux" @?= True

unit_platformExpression_or_true_darwin :: Assertion
unit_platformExpression_or_true_darwin =
  evaluatePlatformExpression "linux | darwin" "darwin" @?= True

unit_platformExpression_or_false :: Assertion
unit_platformExpression_or_false =
  evaluatePlatformExpression "linux | darwin" "mingw32" @?= False

unit_platformExpression_or_windows_mingw32 :: Assertion
unit_platformExpression_or_windows_mingw32 =
  evaluatePlatformExpression "windows | linux" "mingw32" @?= True

unit_platformExpression_or_three_platforms :: Assertion
unit_platformExpression_or_three_platforms =
  evaluatePlatformExpression "linux | darwin | windows" "darwin" @?= True

-- Test unix special case
unit_platformExpression_unix_linux :: Assertion
unit_platformExpression_unix_linux =
  evaluatePlatformExpression "unix" "linux" @?= True

unit_platformExpression_unix_darwin :: Assertion
unit_platformExpression_unix_darwin =
  evaluatePlatformExpression "unix" "darwin" @?= True

unit_platformExpression_unix_windows :: Assertion
unit_platformExpression_unix_windows =
  evaluatePlatformExpression "unix" "mingw32" @?= False

-- Test complex expressions
unit_platformExpression_complex1 :: Assertion
unit_platformExpression_complex1 =
  evaluatePlatformExpression "!windows & !darwin" "linux" @?= True

unit_platformExpression_complex2 :: Assertion  
unit_platformExpression_complex2 =
  evaluatePlatformExpression "linux | darwin" "freebsd" @?= False

-- Test edge cases
unit_platformExpression_unknown :: Assertion
unit_platformExpression_unknown =
  evaluatePlatformExpression "unknown_platform" "linux" @?= False

unit_platformExpression_empty :: Assertion
unit_platformExpression_empty =
  evaluatePlatformExpression "" "linux" @?= True  -- Should default to True on parse failure

unit_platformExpression_unknown_current :: Assertion
unit_platformExpression_unknown_current =
  evaluatePlatformExpression "linux" "freebsd" @?= False

unit_platformExpression_unix_freebsd :: Assertion
unit_platformExpression_unix_freebsd =
  evaluatePlatformExpression "unix" "freebsd" @?= False  -- unix only matches linux and darwin

unit_platformExpression_not_unix_freebsd :: Assertion
unit_platformExpression_not_unix_freebsd =
  evaluatePlatformExpression "!unix" "freebsd" @?= True  -- freebsd is not unix (in our definition)

------------------------------------------------------------------------------------------------
-- Platform-Specific Test Examples
-- 
-- The `platform` function allows you to conditionally run tests based on the current platform.
-- It takes a platform expression string and a TestTree, and returns a TestTree that will only
-- run if the expression evaluates to true for the current platform.
--
-- The `platform` function works best with `tasty_` prefixed tests when using tasty-discover.
-- For custom tests that don't use the `tasty_` prefix, you can apply `platform` directly to TestTrees.
--
-- Platform expression syntax:
--   - Platform names: "linux", "darwin", "windows", "mingw32", "unix"
--   - Logical operators: "&" (AND), "|" (OR), "!" (NOT)
--   - Parentheses for grouping (future enhancement)
--   - Special platform "unix" matches both "linux" and "darwin"
--   - Platform name "windows" is mapped to "mingw32" (the actual System.Info.os value)
--
-- Examples:
--   platform "linux" test          -- Run only on Linux
--   platform "!windows" test       -- Run on all platforms except Windows  
--   platform "!windows & !darwin" test -- Run on platforms that are neither Windows nor Darwin
--   platform "linux | darwin" test     -- Run on Linux or Darwin (Unix-like systems)
--   platform "unix" test               -- Run on Unix-like systems (Linux or Darwin)

-- Simple platform-specific tests using tasty_ prefix
tasty_linuxOnly :: TestTree
tasty_linuxOnly = platform "linux" $ testCase "Linux-specific functionality" $ do
  -- This test only runs on Linux
  pure ()

tasty_notWindows :: TestTree  
tasty_notWindows = platform "!windows" $ testCase "Non-Windows functionality" $ do
  -- This test runs on all platforms except Windows
  pure ()

tasty_unixLike :: TestTree
tasty_unixLike = platform "unix" $ testCase "Unix-like systems" $ do
  -- This test runs on Linux and Darwin (Unix-like systems)
  pure ()

-- Complex platform expressions
tasty_complexPlatform1 :: TestTree
tasty_complexPlatform1 = platform "!windows & !darwin" $ testCase "Neither Windows nor Darwin" $ do
  -- This test runs on platforms that are neither Windows nor Darwin (e.g., Linux)
  pure ()

tasty_complexPlatform2 :: TestTree
tasty_complexPlatform2 = platform "linux | darwin" $ testCase "Linux or Darwin only" $ do
  -- This test runs on either Linux or Darwin, but not Windows
  pure ()

-- Property tests with platform filtering
tasty_platformSpecific :: TestTree
tasty_platformSpecific = platform "!windows" $ testProperty "Property that doesn't work on Windows" $ 
  \(x :: Int) -> x + 0 == x

-- You can also combine platform filtering with other test transformations
-- using the Flavored type and function composition:

tasty_platformAndSkip :: TestTree  
tasty_platformAndSkip = platform "linux" $ skip $ testCase "Linux test that's also skipped" $ do
  -- This would only run on Linux, but it's also skipped, so it never actually runs
  pure ()

-- Test groups with platform filtering
tasty_platformGroup :: TestTree
tasty_platformGroup = platform "unix" $ testGroup "Unix-only tests" 
  [ testCase "Unix test 1" $ pure ()
  , testCase "Unix test 2" $ pure ()
  , testProperty "Unix property" $ \(x :: Int) -> x >= 0 || x < 0
  ]

-- For more advanced use cases, you can use Flavored with platform filtering
-- This allows you to work with custom test types that have Tasty instances

tasty_platformFlavored :: Flavored TestTree
tasty_platformFlavored = flavored (platform "!windows") $ testCase "Advanced platform test" $ do
  -- This uses the Flavored pattern to apply platform filtering
  pure ()

-- You can also create platform-specific custom Property tests
tasty_platformProperty :: Flavored Property
tasty_platformProperty = flavored (platform "unix") $ property $ do
  -- This hedgehog property only runs on Unix-like systems
  x <- H.forAll $ G.int (R.linear 1 100)
  x H.=== x

-- Helper function to make testProperty respect SkipTest option
testPropertySkippable :: Testable a => String -> a -> TestTree
testPropertySkippable name prop = askOption $ \(TD.SkipTest shouldSkip) ->
  if shouldSkip
    then testCase (name ++ " " ++ yellowText "[SKIPPED]") (pure ())
    else testProperty name prop
  where
    yellowText text = setSGRCode [SetColor Foreground Vivid Yellow] ++ text ++ setSGRCode [Reset]

tasty_testTree_no_darwin :: Flavored (IO TestTree)
tasty_testTree_no_darwin =
  flavored (platform "!darwin") $ pure $ applySkips $ testGroup "Non-Darwin group"
    [ testProperty "Always succeeds" $ \(x :: Int) -> x == x
    , testCase "Another test" $ pure ()
    , testProperty "Yet another" $ \(x :: Int) -> x >= 0 || x < 0
    ]

------------------------------------------------------------------------------------------------
-- How to use the latest version of tasty-hedgehog

hprop_reverse :: H.Property
hprop_reverse = H.property $ do
  xs <- H.forAll $ G.list (R.linear 0 100) G.alpha
  reverse (reverse xs) H.=== xs

------------------------------------------------------------------------------------------------
-- How to add custom support for golden tests.

data GoldenTest = GoldenTest FilePath (IO ByteString)
  deriving stock (Generic)

instance TD.Tasty GoldenTest where
  tasty info (GoldenTest fp act) = pure $ goldenVsString (TD.descriptionOf info) fp act

tasty_goldenTest :: GoldenTest
tasty_goldenTest = GoldenTest "test/SubMod/example.golden" $ return "test"