should-not-typecheck 1.0.1 → 2.0
raw patch · 5 files changed
+104/−11 lines, 5 filesdep +deepseq
Dependencies added: deepseq
Files
- CHANGELOG.md +14/−0
- README.md +49/−5
- should-not-typecheck.cabal +4/−1
- src/Test/ShouldNotTypecheck.hs +13/−3
- test/ShouldNotTypecheckSpec.hs +24/−2
+ CHANGELOG.md view
@@ -0,0 +1,14 @@+# `should-not-typecheck` changelog++## 2.0+* Changed API to require `NFData a` so we can fully evaluate expressions, rather than just converting to WHNF.++## 1.0.1+* Use `throwIO` instead of `throw` for exception ordering safety.++## 1.0+* Stabilise API at 1.0 release.+* Allow building on 7.6.3.++## 0.1.0.0+* Initial version.
README.md view
@@ -1,12 +1,10 @@-# should-not-typecheck [](https://travis-ci.org/CRogers/should-not-typecheck)--`should-not-typecheck` is a Haskell library which allows you to assert that an expression does not typecheck in your unit tests. It provides one function, `shouldNotTypecheck :: a -> Assertion`, which takes an expression and will fail the test if it typechecks. `shouldNotTypecheck` returns an HUnit `Assertion` (so it can be used with both `HUnit` and `hspec`).+# should-not-typecheck [](https://travis-ci.org/CRogers/should-not-typecheck) [](https://hackage.haskell.org/package/should-not-typecheck) -Avaliable on Hackage as [`should-not-typecheck`](https://hackage.haskell.org/package/should-not-typecheck).+`should-not-typecheck` is a Haskell library which allows you to assert that an expression does not typecheck in your tests. It provides one function, `shouldNotTypecheck`, which takes an expression and will fail the test if it typechecks. `shouldNotTypecheck` returns an HUnit `Assertion` (so it can be used with both `HUnit` and `hspec`). ## Example (hspec) -The secret sauce is the [Deferred Type Errors GHC extension](https://downloads.haskell.org/~ghc/7.10.1/docs/html/users_guide/defer-type-errors.html). This allows you to write a non-typechecking expression which will throw an exception at run time (rather than erroring out at compile time). `shouldNotTypecheck` tries to catch that exception and fails the test if no deferred type error is caught.+The secret sauce is the [Deferred Type Errors GHC extension](https://downloads.haskell.org/~ghc/7.10.1/docs/html/users_guide/defer-type-errors.html). This allows you to write a ill-typed expression which will throw an exception at run time (rather than erroring out at compile time). `shouldNotTypecheck` tries to catch that exception and fails the test if no deferred type error is caught. ```haskell {-# OPTIONS_GHC -fdefer-type-errors #-} -- Very important!@@ -24,6 +22,52 @@ ``` It can be used similarly with HUnit.++### `NFData a` constraint++Haskell is a lazy language - deferred type errors will not get evaluated unless we explicitly and deeply force (evaluate) the value. [`NFData`](https://hackage.haskell.org/package/deepseq-1.4.1.1/docs/Control-DeepSeq.html#t:NFData) is a typeclass from the [`deepseq`](https://hackage.haskell.org/package/deepseq) library which allows you to describe how to fully evaluate an expression (convert it to Normal Form). `shouldNotTypecheck` uses this typeclass to fully evaluate expressions passed to it. For vanilla Haskell types you only need to derive `Generic` and the `deepseq` class will handle it for you:++```haskell+{-# LANGUAGE DeriveGeneric #-}++import GHC.Generics (Generic)++data SomeType a = WithSome | DataConstructors a+ deriving Generic++instance NFData a => NFData (SomeType a)+```++In GHC 7.10 [`DeriveAnyClass` can be used](https://hackage.haskell.org/package/deepseq-1.4.1.1/docs/Control-DeepSeq.html#v:rnf) to make it even more succinct.++With `deepseq >= 1.4`, this autoderiving `Generic` option is included with the library. With `deepseq <= 1.3` you'll have to use the [`deepseq-generics`](https://hackage.haskell.org/package/deepseq-generics) library as well.++#### GADTs++With more complex datatypes, like GADTs and those existentially quantified, `DeriveGeneric` does not work. You will need to provide an instance for `NFData` yourself, but not to worry as it follows a pattern:++```haskell+{-# LANGUAGE GADTs #-}++import Control.DeepSeq (NFData)++data Expr t where+ IntVal :: Int -> Expr Int+ BoolVal :: Bool -> Expr Bool+ Add :: Expr Int -> Expr Int -> Expr Int++instance NFData (Expr t) where+ rnf expr = case expr of+ IntVal i -> rnf i -- call rnf on every subvalue+ BoolVal b -> rnf b+ Add l r -> rnf l `seq` rnf r -- and `seq` multiple values together++-- Now we can test expressions like:+badExpr = Add (IntVal 4) (BoolVal True)+-- do not typecheck!+```++If you forget to specify provide an `NFData` instance for a type `should-not-typecheck` should warn you. ## Motivation
should-not-typecheck.cabal view
@@ -1,5 +1,5 @@ name: should-not-typecheck-version: 1.0.1+version: 2.0 synopsis: A HUnit/hspec assertion library to verify that an expression does not typecheck description: For examples and an introduction to the library please take a look at the <https://github.com/CRogers/should-not-typecheck#should-not-typecheck- README> on github.@@ -13,6 +13,7 @@ build-type: Simple extra-source-files: README.md+ , CHANGELOG.md , stack.yaml cabal-version: >=1.10 tested-with: GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.1@@ -22,6 +23,7 @@ exposed-modules: Test.ShouldNotTypecheck build-depends: base >= 4.6 && < 5 , HUnit >= 1.2+ , deepseq >= 1.3 default-language: Haskell2010 test-suite tests@@ -33,6 +35,7 @@ , HUnit >= 1.2 , hspec >= 2.1 , hspec-expectations >= 0.6+ , deepseq default-language: Haskell2010 source-repository head
src/Test/ShouldNotTypecheck.hs view
@@ -1,9 +1,17 @@ module Test.ShouldNotTypecheck (shouldNotTypecheck) where +import Control.DeepSeq (force, NFData) import Control.Exception (evaluate, try, throwIO, ErrorCall(..)) import Data.List (isSuffixOf) import Test.HUnit.Lang (Assertion, assertFailure) +-- Taken from base-4.8.0.0:Data.List+isSubsequenceOf :: (Eq a) => [a] -> [a] -> Bool+isSubsequenceOf [] _ = True+isSubsequenceOf _ [] = False+isSubsequenceOf a@(x:a') (y:b) | x == y = isSubsequenceOf a' b+ | otherwise = isSubsequenceOf a b+ {-| Takes one argument, an expression that should not typecheck. It will fail the test if the expression does typecheck.@@ -11,11 +19,13 @@ See the <https://github.com/CRogers/should-not-typecheck#should-not-typecheck- README> for examples and more infomation. -}-shouldNotTypecheck :: a -> Assertion+shouldNotTypecheck :: NFData a => a -> Assertion shouldNotTypecheck a = do- result <- try (evaluate a)+ result <- try (evaluate $ force a) case result of Right _ -> assertFailure "Expected expression to not compile but it did compile" Left (ErrorCall msg) -> case isSuffixOf "(deferred type error)" msg of- True -> return ()+ True -> case isSubsequenceOf "No instance for" msg && isSubsequenceOf "NFData" msg of+ True -> assertFailure $ "Make sure the expression has an NFData instance! See docs at https://github.com/CRogers/should-not-typecheck#nfdata-a-constraint. Full error:\n" ++ msg+ False -> return () False -> throwIO (ErrorCall msg)
test/ShouldNotTypecheckSpec.hs view
@@ -1,8 +1,11 @@+{-# LANGUAGE GADTs, TemplateHaskell #-} {-# OPTIONS_GHC -fdefer-type-errors #-} module Main where +import Control.DeepSeq import Control.Exception+import GHC.Generics (Generic) import Test.Hspec import Test.Hspec.Expectations (expectationFailure) import Test.HUnit.Lang (performTestCase)@@ -26,6 +29,19 @@ True -> return () False -> expectationFailure "Incorrect exception propagated" +data Expr t where+ IntVal :: Int -> Expr Int+ BoolVal :: Bool -> Expr Bool+ Add :: Expr Int -> Expr Int -> Expr Int++instance NFData (Expr t) where+ rnf expr = case expr of+ IntVal i -> rnf i+ BoolVal b -> rnf b+ Add l r -> rnf l `seq` rnf r++data NoNFDataInstance = NoNFDataInstance+ main :: IO () main = hspec $ do describe "shouldNotCompile" $ do@@ -37,8 +53,14 @@ it "should throw an actual exception and not fail the assertion if the expression contains an non-HUnitFailure exception" $ do let exception = NoMethodError "lol"- shouldThrowException exception (shouldNotTypecheck (throw exception))+ shouldThrowException exception (shouldNotTypecheck (throw exception :: Int)) it "should propagate an actual exception and not fail the assertion if the expression contains a non-deferred ErrorCall exception" $ do let exception = ErrorCall "yay"- shouldThrowException exception (shouldNotTypecheck (throw exception))+ shouldThrowException exception (shouldNotTypecheck (throw exception :: Int))++ it "should not throw an assertion when an expression with more than one level of constructors is ill typed" $ do+ shouldNotTypecheck (Add (BoolVal True) (IntVal 4))++ it "should warn if an expression had a type error due to lack of NFData instance" $ do+ shouldFailAssertion (shouldNotTypecheck NoNFDataInstance)