packages feed

inspection-testing 0.1.2 → 0.2

raw patch · 6 files changed

+202/−58 lines, 6 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Test.Inspection: Failure :: String -> Result
+ Test.Inspection: Success :: String -> Result
+ Test.Inspection: [storeResult] :: Obligation -> Maybe String
+ Test.Inspection: data Result
+ Test.Inspection: inspectTest :: Obligation -> Q Exp
+ Test.Inspection: instance GHC.Show.Show Test.Inspection.Result
- Test.Inspection: Obligation :: Name -> Property -> Maybe String -> Bool -> Maybe Loc -> Obligation
+ Test.Inspection: Obligation :: Name -> Property -> Maybe String -> Bool -> Maybe Loc -> Maybe String -> Obligation

Files

ChangeLog.md view
@@ -1,5 +1,10 @@ # Revision history for inspection-testing +## 0.2 -- 2018-01-17++* With `$(inspectTest obligation)` you can now get the result of inspection+  testing at run-time, for integration into your test suite.+ ## 0.1.2 -- 2017-11-20  * Make `(==-)` a bit more liberal, and look through variable redefinitions that
Test/Inspection.hs view
@@ -17,6 +17,8 @@      -- * Registering obligations     inspect,+    inspectTest,+    Result(..),     -- * Defining obligations     Obligation(..), mkObligation, Property(..),     (===), (==-), (=/=), hasNoType,@@ -24,8 +26,9 @@  import Control.Monad import Language.Haskell.TH-import Language.Haskell.TH.Syntax (getQ, putQ, liftData)+import Language.Haskell.TH.Syntax (getQ, putQ, liftData, addTopDecls) import Data.Data+import GHC.Exts (lazy)  import Data.Maybe (fromMaybe) import qualified Data.Set as S@@ -38,7 +41,7 @@   1. enable the @TemplateHaskell@ langauge extension  2. load the plugin using @-fplugin Test.Inspection.Plugin@- 3. declare your proof obligations using 'inspect'+ 3. declare your proof obligations using 'inspect' or 'inspectTest'  An example module is @@ -79,9 +82,13 @@         -- ^ An optional name for the test     , expectFail  :: Bool         -- ^ Do we expect this property to fail?+        -- (Only used by 'inspect', not by 'inspectTest')     , srcLoc :: Maybe Loc         -- ^ The source location where this obligation is defined.         -- This is filled in by 'inspect'.+    , storeResult :: Maybe String+        -- ^ If this is 'Nothing', then report errors during compilation.+        -- Otherwise, update the top-level definition with this name.     }     deriving Data @@ -120,6 +127,7 @@     , testName = Nothing     , srcLoc = Nothing     , expectFail = False+    , storeResult = Nothing     }  -- | Convenience function to declare two functions to be equal@@ -153,14 +161,49 @@  -- The exported TH functions --- | As seen in the example above, the entry point to inspection testing is the--- 'inspect' function, to which you pass an 'Obligation'.-inspect :: Obligation -> Q [Dec]-inspect obl = do+inspectCommon :: AnnTarget -> Obligation -> Q [Dec]+inspectCommon annTarget obl = do     loc <- location     annExpr <- liftData (obl { srcLoc = Just loc })     rememberDs <- concat <$> mapM rememberName (allLocalNames obl)-    pure $ PragmaD (AnnP ModuleAnnotation annExpr) : rememberDs+    pure $ PragmaD (AnnP annTarget annExpr) : rememberDs++-- | As seen in the example above, the entry point to inspection testing is the+-- 'inspect' function, to which you pass an 'Obligation'.+-- It will report test failures at compile time.+inspect :: Obligation -> Q [Dec]+inspect = inspectCommon ModuleAnnotation++-- | The result of 'inspectTest', which a more or less helpful text message+data Result = Failure String | Success String+    deriving Show++didNotRunPluginError :: Result+didNotRunPluginError = lazy (error "Test.Inspection.Plugin did not run")+{-# NOINLINE didNotRunPluginError #-}++-- | This is a variant that allows compilation to succeed in any case,+-- and instead indicates the result as a value of type 'Result',+-- which allows seamless integration into test frameworks.+--+-- This variant ignores the 'expectFail' field of the obligation. Instead,+-- it is expected that you use the corresponding functionality in your test+-- framework (e.g. tasty-expected-failure)+inspectTest :: Obligation -> Q Exp+inspectTest obl = do+    nameS <- genName+    name <- newName nameS+    anns <- inspectCommon (ValueAnnotation name) obl+    addTopDecls $+        [ SigD name (ConT ''Result)+        , ValD (VarP name) (NormalB (VarE 'didNotRunPluginError)) []+        , PragmaD (InlineP name NoInline FunLike AllPhases)+        ] ++ anns+    return $ VarE name+  where+    genName = do+        (r,c) <- loc_start <$> location+        return $ "inspect_" ++ show r ++ "_" ++ show c  -- We need to ensure that names refernced in obligations are kept alive -- We do so by annotating them with 'KeepAlive'
Test/Inspection/Internal.hs view
@@ -6,7 +6,9 @@ -- Portability : GHC specifc -- {-# LANGUAGE DeriveDataTypeable #-}-module Test.Inspection.Internal (KeepAlive(..)) where+module Test.Inspection.Internal+    ( KeepAlive(..)+    ) where  import Data.Data 
Test/Inspection/Plugin.hs view
@@ -3,42 +3,46 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE TemplateHaskell #-} module Test.Inspection.Plugin (plugin) where  import Control.Monad import System.Exit import Data.Either import Data.Maybe+import Data.Bifunctor import qualified Data.Map.Strict as M import qualified Language.Haskell.TH.Syntax as TH  import GhcPlugins hiding (SrcLoc)  import Test.Inspection.Internal (KeepAlive(..))-import Test.Inspection (Obligation(..), Property(..))+import Test.Inspection (Obligation(..), Property(..), Result(..)) import Test.Inspection.Core --- | The plugin. It supports the option @-fplugin-opt=Test.Inspection.Plugin=keep-going@ to+-- | The plugin. It supports the option @-fplugin-opt=Test.Inspection.Plugin:keep-going@ to -- ignore a failing build. plugin :: Plugin plugin = defaultPlugin { installCoreToDos = install }  data UponFailure = AbortCompilation | KeepGoing deriving Eq +data ResultTarget = PrintAndAbort | StoreAt Name+ install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo] install args passes = return $ passes ++ [pass]   where-    pass = CoreDoPluginPass "Test.Inspection" (proofPass upon_failure)+    pass = CoreDoPluginPass "Test.Inspection.Plugin" (proofPass upon_failure)     upon_failure | "keep-going" `elem` args = KeepGoing                  | otherwise                = AbortCompilation  -extractObligations :: ModGuts -> (ModGuts, [Obligation])-extractObligations guts = (guts { mg_rules = rules', mg_anns = anns_clean }, obligations)+extractObligations :: ModGuts -> (ModGuts, [(ResultTarget, Obligation)])+extractObligations guts = (guts', obligations)   where-    rules' = mg_rules guts     (anns', obligations) = partitionMaybe findObligationAnn (mg_anns guts)     anns_clean = filter (not . isKeepAliveAnn) anns'+    guts' = guts { mg_anns = anns_clean }  isKeepAliveAnn :: Annotation -> Bool isKeepAliveAnn (Annotation (NamedTarget _) payload)@@ -47,10 +51,13 @@ isKeepAliveAnn _     = False -findObligationAnn :: Annotation -> Maybe Obligation+findObligationAnn :: Annotation -> Maybe (ResultTarget, Obligation) findObligationAnn (Annotation (ModuleTarget _) payload)     | Just obl <- fromSerialized deserializeWithData payload-    = Just obl+    = Just (PrintAndAbort, obl)+findObligationAnn (Annotation (NamedTarget n) payload)+    | Just obl <- fromSerialized deserializeWithData payload+    = Just (StoreAt n, obl) findObligationAnn _     = Nothing @@ -74,34 +81,48 @@     | moduleNameString (moduleName mod) == TH.modString m = TH.occString occ showTHName _ n = show n -data Stat = ExpSuccess | ExpFailure | UnexpSuccess | UnexpFailure+data Stat = ExpSuccess | ExpFailure | UnexpSuccess | UnexpFailure | StoredResult     deriving (Enum, Eq, Ord, Bounded) type Stats = M.Map Stat Int +type Updates = [(Name, Result)]+ tick :: Stat -> Stats tick s = M.singleton s 1 -checkObligation :: ModGuts -> Obligation -> CoreM Stats-checkObligation guts obl = do+checkObligation :: ModGuts -> (ResultTarget, Obligation) -> CoreM (Updates, Stats)+checkObligation guts (reportTarget, obl) = do      res <- checkProperty guts (target obl) (property obl)+    case reportTarget of+        PrintAndAbort -> do+            category <- case (res, expectFail obl) of+                -- Property holds+                (Nothing, False) -> do+                    putMsgS $ prettyObligation (mg_module guts) obl expSuccess+                    return ExpSuccess+                (Nothing, True) -> do+                    putMsgS $ prettyObligation (mg_module guts) obl unexpSuccess+                    return UnexpSuccess+                -- Property does not hold+                (Just reportDoc, False) -> do+                    putMsgS $ prettyObligation (mg_module guts) obl unexpFailure+                    putMsg $ reportDoc+                    return UnexpFailure+                (Just _, True) -> do+                    putMsgS $ prettyObligation (mg_module guts) obl expFailure+                    return ExpFailure+            return ([], tick category)+        StoreAt name -> do+            dflags <- getDynFlags+            let result = case res of+                    Nothing -> Success $ showSDoc dflags $+                        text (prettyObligation (mg_module guts) obl expSuccess)+                    Just reportMsg -> Failure $ showSDoc dflags $+                        text (prettyObligation (mg_module guts) obl unexpFailure) $$+                        reportMsg+            pure ([(name, result)], tick StoredResult) -    case (res, expectFail obl) of-        -- Property holds-        (Nothing, False) -> do-            putMsgS $ prettyObligation (mg_module guts) obl expSuccess-            return (tick ExpSuccess)-        (Nothing, True) -> do-            putMsgS $ prettyObligation (mg_module guts) obl unexpSuccess-            return (tick UnexpSuccess)-        -- Property does not hold-        (Just reportMsg, False) -> do-            putMsgS $ prettyObligation (mg_module guts) obl unexpFailure-            reportMsg-            return (tick UnexpFailure)-        (Just _, True) -> do-            putMsgS $ prettyObligation (mg_module guts) obl expFailure-            return (tick ExpFailure)   where     expSuccess   = "passed."     unexpSuccess = "passed unexpectedly!"@@ -109,7 +130,7 @@     expFailure   = "failed expectedly."  -type Result =  Maybe (CoreM ())+type CheckResult = Maybe SDoc  lookupNameInGuts :: ModGuts -> Name -> Maybe (Var, CoreExpr) lookupNameInGuts guts n = listToMaybe@@ -118,7 +139,15 @@     , getName v == n     ] -checkProperty :: ModGuts -> TH.Name -> Property -> CoreM Result+updateNameInGuts :: Name -> CoreExpr -> ModGuts -> ModGuts+updateNameInGuts n expr guts =+    guts {mg_binds = map (updateNameInGut n expr) (mg_binds guts) }++updateNameInGut :: Name -> CoreExpr -> CoreBind -> CoreBind+updateNameInGut n e (NonRec v _) | getName v == n = NonRec v e+updateNameInGut _ _ bind                          = bind++checkProperty :: ModGuts -> TH.Name -> Property -> CoreM CheckResult checkProperty guts thn1 (EqualTo thn2 ignore_types) = do     Just n1 <- thNameToGhcName thn1     Just n2 <- thNameToGhcName thn2@@ -141,13 +170,11 @@           -- OK if they have the same expression           then return Nothing           -- Not ok if the expression differ-          else pure . Just $ putMsg $-            pprSliceDifference slice1 slice2+          else pure . Just $ pprSliceDifference slice1 slice2        -- Not ok if both names are bound externally        | Nothing <- p1        , Nothing <- p2-       -> pure . Just $ do-            putMsg $ ppr n1 <+> text " and " <+> ppr n2 <+>+       -> pure . Just $ ppr n1 <+> text " and " <+> ppr n2 <+>                 text "are different external names"   where     binds = flattenBinds (mg_binds guts)@@ -156,23 +183,39 @@     Just n <- thNameToGhcName thn     Just t <- thNameToGhcName tht     case lookupNameInGuts guts n of-        Nothing -> pure . Just $ do-            putMsg $ ppr n <+> text "is not a local name"+        Nothing -> pure . Just $ ppr n <+> text "is not a local name"         Just (v, _) -> case freeOfType (slice binds v) t of-            Just _ -> pure . Just $ putMsg $ pprSlice (slice binds v)+            Just _ -> pure . Just $ pprSlice (slice binds v)             Nothing -> pure Nothing   where binds = flattenBinds (mg_binds guts)  checkProperty guts thn NoAllocation = do     Just n <- thNameToGhcName thn     case lookupNameInGuts guts n of-        Nothing -> pure . Just $ do-            putMsg $ ppr n <+> text "is not a local name"+        Nothing -> pure . Just $ ppr n <+> text "is not a local name"         Just (v, _) -> case doesNotAllocate (slice binds v) of-            Just (v',e') -> pure . Just $ putMsg $ nest 4 (ppr v' <+> text "=" <+> ppr e')+            Just (v',e') -> pure . Just $ nest 4 (ppr v' <+> text "=" <+> ppr e')             Nothing -> pure Nothing   where binds = flattenBinds (mg_binds guts) +storeResults :: Updates -> ModGuts -> CoreM ModGuts+storeResults = flip (foldM (flip (uncurry go)))+  where+    go :: Name -> Result -> ModGuts -> CoreM ModGuts+    go name res guts = do+        e <- resultToExpr res+        pure $ updateNameInGuts name e guts++dcExpr :: TH.Name -> CoreM CoreExpr+dcExpr thn = do+    Just name <- thNameToGhcName thn+    dc <- lookupDataCon name+    pure $ Var (dataConWrapId dc)++resultToExpr :: Result -> CoreM CoreExpr+resultToExpr (Success s) = App <$> dcExpr 'Success <*> mkStringExpr s+resultToExpr (Failure s) = App <$> dcExpr 'Failure <*> mkStringExpr s+ proofPass :: UponFailure -> ModGuts -> CoreM ModGuts proofPass upon_failure guts = do     dflags <- getDynFlags@@ -180,9 +223,12 @@         warnMsg $ fsep $ map text $ words "Test.Inspection: Compilation without -O detected. Expect optimizations to fail."      let (guts', obligations) = extractObligations guts-    stats <- M.unionsWith (+) <$> mapM (checkObligation guts') obligations+    (toStore, stats) <- (concat `bimap` M.unionsWith (+)) . unzip <$>+        mapM (checkObligation guts') obligations     let n = sum stats :: Int +    guts'' <- storeResults toStore  guts'+     let q :: Stat -> Int         q s = fromMaybe 0 $ M.lookup s stats @@ -190,16 +236,18 @@             vcat [ nest 2 (desc s) <> colon <+> ppr (q s)                  | s <- [minBound..maxBound], q s > 0 ] -    if q ExpSuccess + q ExpFailure == n-    then putMsg $ text "inspection testing successful" $$ summary_message-    else case upon_failure of-        AbortCompilation -> do-            errorMsg $ text "inspection testing unsuccessful" $$ summary_message-            liftIO $ exitFailure -- kill the compiler. Is there a nicer way?-        KeepGoing -> do-            warnMsg $ text "inspection testing unsuccessful" $$ summary_message+    -- Only print a message if there are some compile-time results to report+    unless (q StoredResult == n) $ do+        if q ExpSuccess + q ExpFailure + q StoredResult == n+        then putMsg $ text "inspection testing successful" $$ summary_message+        else case upon_failure of+            AbortCompilation -> do+                errorMsg $ text "inspection testing unsuccessful" $$ summary_message+                liftIO $ exitFailure -- kill the compiler. Is there a nicer way?+            KeepGoing -> do+                warnMsg $ text "inspection testing unsuccessful" $$ summary_message -    return guts'+    return guts''   desc :: Stat -> SDoc@@ -207,6 +255,7 @@ desc UnexpSuccess = text "unexpected successes" desc ExpFailure   = text "   expected failures" desc UnexpFailure = text " unexpected failures"+desc StoredResult = text "      results stored"  partitionMaybe :: (a -> Maybe b) -> [a] -> ([a], [b]) partitionMaybe f = partitionEithers . map (\x -> maybe (Left x) Right (f x))
+ examples/SimpleTest.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -O -fplugin=Test.Inspection.Plugin #-}+module Main where++import Test.Inspection+import Data.Maybe+import System.Exit++lhs, rhs, something_else :: (a -> b) -> Maybe a -> Bool++lhs f x = isNothing (fmap f x)++rhs _ Nothing = True+rhs _ (Just _) = False++something_else _ _ = False++printResult :: Result -> IO ()+printResult (Success s) = putStrLn s+printResult (Failure s) = putStrLn s++isSuccess :: Result -> Bool+isSuccess (Success _) = True+isSuccess (Failure _) = False++results :: [Result]+results =+    [ $(inspectTest $ 'lhs === 'rhs)+    , $(inspectTest $ 'lhs === 'something_else)+    ]++main :: IO ()+main = do+    mapM_ printResult results+    if map isSuccess results == [True, False]+    then exitSuccess+    else exitFailure
inspection-testing.cabal view
@@ -1,5 +1,5 @@ name:                inspection-testing-version:             0.1.2+version:             0.2 synopsis:            GHC plugin to do inspection testing description:         Some carefully crafted libraries make promises to their                      users beyond functionality and performance.@@ -72,6 +72,14 @@   build-depends:       base >=4.9 && <4.12   default-language:    Haskell2010   ghc-options:         -main-is Simple++test-suite simple-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      examples+  main-is:             SimpleTest.hs+  build-depends:       inspection-testing+  build-depends:       base >=4.9 && <4.12+  default-language:    Haskell2010  test-suite fusion   type:                exitcode-stdio-1.0