quickcheck-silent-0.11.0.12: src/Test/QuickCheck/Silent.hs
{-# OPTIONS_GHC -Wall -Werror #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
{-# LANGUAGE DeriveDataTypeable #-}
--------------------------------------------------------------------------------
-- |
-- Copyright : (c) 2026 SPISE MISU ApS
-- License : LGPL-3.0-only
-- Maintainer : SPISE MISU <mail+hackage@spisemisu.com>
-- Stability : experimental
--
-- Testing with QuickCheck in silence
--------------------------------------------------------------------------------
module Test.QuickCheck.Silent
( -- * Declaring tests
Test (Case, Group)
, Label
-- * Property without IO
, SilentProp ()
, property
, silent
, withNumTests
-- * Running tests
, JSON
, Status(..)
, Result(..)
, quickCheckSilent
, quickCheckSilentSuite
, quickCheckSilentSuiteJSON
, isSuccess
)
where
--------------------------------------------------------------------------------
import Data.Data ( Data )
import Data.List ( isPrefixOf )
import qualified Test.QuickCheck as QC
import Test.QuickCheck
( Property
, Testable
, chatty
, stdArgs
)
import Internal.GaloisInc.Text.JSON.Generic ( encodeJSON )
--------------------------------------------------------------------------------
-- | A `Testable` silent property.
newtype SilentProp =
SilentProp
{ property :: Property
-- ^ Unwrap the the silent property.
}
--------------------------------------------------------------------------------
-- | A name or description for a case or a subtree of the @Test@s.
type Label = String
-- | The basic structure used to create an annotated tree of test cases.
data Test
-- | A set of @Test@s sharing the same level in the hierarchy.
= Group !Label ![Test]
-- | A single, independent test case composed.
| Case !Label !SilentProp
--------------------------------------------------------------------------------
-- | Status represents the outcome of a test
data Status
-- | A successful test run
= Success
-- | A failed test run
| Failure
-- | Given up
| Desisted
-- | A property that should have failed did not
| NoFailure
deriving (Data, Show)
-- | Result represents the test result
data Result =
Result
{ status :: !Status
-- ^ Outcome of the test
, numTests :: !Int
-- ^ Number of tests performed
, numDiscarded :: !Int
-- ^ Number of tests skipped
, output :: !String
-- ^ Non-printed output
, reason :: !String
-- ^ If the property failed, why?
}
deriving (Data, Show)
data LabelResultJSON =
LabelResultJSON
{ label :: !Label
, result :: !Result
}
deriving (Data, Show)
-- | A 'JSON' payload string that represents the test 'Result' with its
-- respective 'Label'
--
-- > { "label": "…", "result": { "status": "…", …, "reason": "…" } }
type JSON = String
--------------------------------------------------------------------------------
-- | Convert a `Testable` thing, without 'System.IO.IO' effects, to a silent
-- property.
silent
:: Testable prop
=> prop
-> SilentProp
silent =
SilentProp . QC.property
-- | Configures how many times a silent property will be tested.
--
-- For example,
--
-- > quickCheckSilent [withNumTests 1000 p]
--
-- will test @p@ up to 1000 times.
withNumTests
:: Testable prop
=> Int
-> prop
-> SilentProp
withNumTests n =
SilentProp . QC.withNumTests n
--------------------------------------------------------------------------------
-- | Tests a sequence of silent properties, producing a list of results, without
-- printing them to 'System.IO.stdout'.
quickCheckSilent
:: [SilentProp]
-> IO [Result]
quickCheckSilent =
mapM (\ p -> (aux . property) p >>= pure . result_)
where
aux =
QC.quickCheckWithResult $ stdArgs { chatty = False }
-- | Tests a suite of silent properties, producing a list of results with their
-- respective labels and without printing them to 'System.IO.stdout'.
--
-- For example,
--
-- > check =
-- > quickCheckSilentSuite sep pat tcs
-- > where
-- > sep = '.'
-- > pat = "Test.QuickCheck.Silent"
-- > tcs =
-- > Group "Test"
-- > [ Group "QuickCheck"
-- > [ Group "Silent"
-- > [ Case "foo" (withNumTests 100 p)
-- > , Case "bar" (withNumTests 1000 q)
-- > ]
-- > , Group "OtherModule"
-- > [ Case "baz" (withNumTests 10 r)
-- > ]
-- > ]
-- > ]
--
-- will test both @p@ and @q@, but not @r@.
--
-- The provided @pattern@, will be checked if it 'Data.List.isPrefixOf' of each
-- @label@:
--
-- > pattern `isPrefixOf` label
--
-- @NOTE@: To test all cases, just provide an empty string as @pattern@ as it's
-- a prefix for all possible @labels@.
quickCheckSilentSuite
:: Char
-> String
-> Test
-> IO [(Label, Result)]
quickCheckSilentSuite sep pat suite =
mapM
( \ (l, sp) ->
(aux . property) sp >>= \ r ->
pure (l, result_ r)
)
$ filter ( \(l, _) -> pat `isPrefixOf` l)
$ dfs [] suite
where
dfs [ ] (Group l ts) = concatMap (dfs ( l)) ts
dfs acc (Group l ts) = concatMap (dfs (acc ++ [sep] ++ l)) ts
dfs [ ] (Case l p) = [ ( l, p)]
dfs acc (Case l p) = [ (acc ++ [sep] ++ l, p)]
aux =
QC.quickCheckWithResult $ stdArgs { chatty = False }
-- | Same behavior as 'quickCheckSilentSuite', but, producing a 'JSON' data
-- payload instead.
quickCheckSilentSuiteJSON
:: Char
-> String
-> Test
-> IO [JSON]
quickCheckSilentSuiteJSON sep pat suite =
( \ lrs ->
map
( \ (l, r) ->
encodeJSON $ LabelResultJSON l r
)
lrs
)
<$> quickCheckSilentSuite sep pat suite
-- | Check if the test run result was a success
isSuccess :: Result -> Bool
isSuccess Result { status = Success } = True
isSuccess ___________________________ = False
--------------------------------------------------------------------------------
-- HELPERS
result_
:: QC.Result
-> Result
result_ res =
case res of
QC.Success t d _ _ _ o ->
Result
{ status = Success
, Test.QuickCheck.Silent.numTests = t
, Test.QuickCheck.Silent.numDiscarded = d
, Test.QuickCheck.Silent.output = o
, Test.QuickCheck.Silent.reason = []
}
QC.GaveUp t d _ _ _ o ->
Result
{ status = Desisted
, Test.QuickCheck.Silent.numTests = t
, Test.QuickCheck.Silent.numDiscarded = d
, Test.QuickCheck.Silent.output = o
, Test.QuickCheck.Silent.reason = []
}
QC.Failure t d _ _ _ _ _ r _ o _ _ _ _ ->
Result
{ status = Failure
, Test.QuickCheck.Silent.numTests = t
, Test.QuickCheck.Silent.numDiscarded = d
, Test.QuickCheck.Silent.output = o
, Test.QuickCheck.Silent.reason = r
}
QC.NoExpectedFailure t d _ _ _ o ->
Result
{ status = NoFailure
, Test.QuickCheck.Silent.numTests = t
, Test.QuickCheck.Silent.numDiscarded = d
, Test.QuickCheck.Silent.output = o
, Test.QuickCheck.Silent.reason = []
}