skeletest 0.3.2 → 0.3.3
raw patch · 13 files changed
+175/−32 lines, 13 filesdep −colour
Dependencies removed: colour
Files
- CHANGELOG.md +11/−0
- README.md +9/−0
- skeletest.cabal +1/−2
- src/Skeletest.hs +1/−0
- src/Skeletest/Assertions.hs +8/−1
- src/Skeletest/Internal/GHC.hs +6/−6
- src/Skeletest/Internal/Plugin.hs +18/−7
- src/Skeletest/Internal/Preprocessor.hs +57/−12
- src/Skeletest/Internal/Utils/Color.hs +1/−2
- src/Skeletest/Plugin.hs +1/−0
- src/bin/skeletest-preprocessor.hs +30/−2
- test/Skeletest/AssertionsSpec.hs +18/−0
- test/Skeletest/__snapshots__/AssertionsSpec.snap.md +14/−0
CHANGELOG.md view
@@ -1,3 +1,14 @@+## v0.3.3++Configuration:+* Add `-optF=main:_` option for configuring the name of the main module/function ([#64](https://github.com/brandonchinn178/skeletest/issues/64))++API changes:+* Added `shouldReturn`++Runtime changes:+* Fix gray text output, using standard ANSI codes instead of a hardcoded RGB value+ ## v0.3.2 API changes:
README.md view
@@ -88,6 +88,8 @@ build-tool-depends: skeletest:skeletest-preprocessor ``` + Options may be specified as `-optF=<name>:<value>`. See [Options](#options).+ 1. Add `Main.hs`: ```haskell@@ -190,6 +192,7 @@ * `shouldNotSatisfy` - equivalent to `shouldSatisfy` with `P.not` * `shouldBe` - equivalent to `shouldSatisfy` with `P.eq` * `shouldNotBe` - equivalent to `shouldNotSatisfy` with `P.eq`+* `shouldReturn` - equivalent to `shouldSatisfy` with `P.returns . P.eq` `shouldSatisfy` is the most general function, but the others are provided for convenience. `shouldSatisfy` takes in the value being tested on the left, and a predicate on the right. Predicates should be imported from `Skeletest.Predicate`, qualified as `P`. @@ -427,6 +430,12 @@ ```haskell MyFlag flagVal <- getFlag ```++### Options++Skeletest accepts the following options, which may be added as `-optF=<name>:<value>`:++* `main`: Specify the main module/function, as either `OtherMain.otherMainFunc`, `OtherMain` (equivalent to `OtherMain.main`), or `otherMainFunc` (equivalent to `Main.otherMainFunc`). Generally only useful with the `-main-is` GHC option. ### Hooks
skeletest.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: skeletest-version: 0.3.2+version: 0.3.3 synopsis: Batteries-included, opinionated test framework description: Batteries-included, opinionated test framework. See README.md for more details. homepage: https://github.com/brandonchinn178/skeletest#readme@@ -81,7 +81,6 @@ , aeson , aeson-pretty , ansi-terminal >= 0.7.0- , colour , containers , Diff >= 1.0 , directory
src/Skeletest.hs view
@@ -20,6 +20,7 @@ shouldNotBe, shouldSatisfy, shouldNotSatisfy,+ shouldReturn, context, failTest, HasCallStack,
src/Skeletest/Assertions.hs view
@@ -7,6 +7,7 @@ shouldNotBe, shouldSatisfy, shouldNotSatisfy,+ shouldReturn, context, failTest, AssertionFail (..),@@ -42,7 +43,7 @@ context = contextIO throwFailure = throwIO -infix 1 `shouldBe`, `shouldNotBe`, `shouldSatisfy`, `shouldNotSatisfy`+infix 1 `shouldBe`, `shouldNotBe`, `shouldSatisfy`, `shouldNotSatisfy`, `shouldReturn` -- | Assert that the given input should match the given value. -- Equivalent to @actual `shouldSatisfy` P.eq expected@@@ -65,6 +66,12 @@ -- | Assert that the given input should not satisfy the given predicate. shouldNotSatisfy :: (HasCallStack, Testable m) => a -> Predicate m a -> m () actual `shouldNotSatisfy` p = GHC.withFrozenCallStack $ actual `shouldSatisfy` P.not p++-- | Assert that the given input should return the given value.+-- Equivalent to @actual `shouldSatisfy` (P.returns . P.eq) expected@+-- @since 0.3.3+shouldReturn :: (HasCallStack, Testable m, Eq a) => m a -> a -> m ()+actual `shouldReturn` expected = GHC.withFrozenCallStack $ actual `shouldSatisfy` (P.returns . P.eq) expected contextIO :: String -> IO a -> IO a contextIO msg =
src/Skeletest/Internal/GHC.hs view
@@ -102,8 +102,8 @@ -- | Our pure definition of PluginDef, agnostic of GHC version. data PluginDef = PluginDef { isPure :: Bool- , modifyParsed :: ModuleName -> ParsedModule -> ParsedModule- , onRename :: Ctx -> ModuleName -> HsExpr GhcRn -> HsExpr GhcRn+ , modifyParsed :: [GHC.CommandLineOption] -> ModuleName -> ParsedModule -> ParsedModule+ , onRename :: [GHC.CommandLineOption] -> Ctx -> ModuleName -> HsExpr GhcRn -> HsExpr GhcRn } data Ctx = Ctx@@ -116,11 +116,11 @@ mkPlugin PluginDef{..} = GHC.defaultPlugin { GHC.pluginRecompile = if isPure then GHC.purePlugin else GHC.impurePlugin- , GHC.parsedResultAction = \_ modInfo result -> do+ , GHC.parsedResultAction = \opts modInfo result -> do let moduleName = getModuleName $ GHC.ms_mod modInfo parsedModule = initParsedModule . GHC.hpm_module . GHC.parsedResultModule $ result- ParsedModule{moduleFuncs} = modifyParsed moduleName parsedModule+ ParsedModule{moduleFuncs} = modifyParsed opts moduleName parsedModule newDecls <- runCompilePs . fmap concat . sequence $ [ compileFunDef funName funDef@@ -129,7 +129,7 @@ pure . (modifyParsedResultModule . modifyHpmModule . fmap . modifyModDecls) (newDecls <>) $ result- , GHC.renamedResultAction = \_ gblEnv group -> do+ , GHC.renamedResultAction = \opts gblEnv group -> do nameCache <- GHC.hsc_NC . GHC.env_top <$> GHC.getEnv let moduleName = getModuleName $ GHC.tcg_mod gblEnv@@ -137,7 +137,7 @@ Ctx { matchesName = matchesNameImpl nameCache }- group' <- runCompileRn $ modifyModuleExprs (onRename ctx moduleName) group+ group' <- runCompileRn $ modifyModuleExprs (onRename opts ctx moduleName) group pure (gblEnv, group') } where
src/Skeletest/Internal/Plugin.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskellQuotes #-} {-# LANGUAGE ViewPatterns #-}@@ -15,6 +16,7 @@ import Skeletest.Internal.Error (skeletestPluginError) import Skeletest.Internal.GHC import Skeletest.Internal.Predicate qualified as P+import Skeletest.Internal.Preprocessor qualified as Preprocessor import Skeletest.Internal.Utils.HList (HList (..)) import Skeletest.Main qualified as Main import Skeletest.Plugin qualified as Plugin@@ -30,19 +32,28 @@ mkPlugin PluginDef { isPure = True- , modifyParsed = \modName modl ->- if modName == "Main"- then transformMainModule modl- else modl- , onRename = \ctx modName expr ->+ , modifyParsed = \opts modName modl ->+ let options = decodeOptions opts+ in if modName == options.mainModuleName+ then transformMainModule options modl+ else modl+ , onRename = \_ ctx modName expr -> if "Spec" `Text.isSuffixOf` modName then transformTestModule ctx expr else expr }+ where+ decodeOptions =+ either (error . Text.unpack) id . \case+ [opts] -> Preprocessor.decodeOptions (Text.pack opts)+ _ -> Left "" -- | Add 'main' function.-transformMainModule :: ParsedModule -> ParsedModule-transformMainModule modl = modl{moduleFuncs = (hsVarName "main", Just mainFun) : moduleFuncs modl}+transformMainModule :: Preprocessor.Options -> ParsedModule -> ParsedModule+transformMainModule options modl =+ modl+ { moduleFuncs = (hsVarName options.mainFuncName, Just mainFun) : moduleFuncs modl+ } where findVar name = fmap hsExprVar . listToMaybe $
src/Skeletest/Internal/Preprocessor.hs view
@@ -1,7 +1,12 @@+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NoFieldSelectors #-} module Skeletest.Internal.Preprocessor ( processFile,+ Options (..),+ defaultOptions,+ decodeOptions, ) where import Control.Monad (guard)@@ -14,12 +19,44 @@ import Skeletest.Internal.Error (SkeletestError (..)) import System.Directory (doesDirectoryExist, listDirectory) import System.FilePath (makeRelative, splitExtensions, takeDirectory, (</>))+import Text.Read (readMaybe) import UnliftIO.Exception (throwIO) +data Options = Options+ { mainModuleName :: Text+ , mainFuncName :: Text+ }+ deriving (Show, Read)++defaultOptions :: Options+defaultOptions =+ Options+ { mainModuleName = "Main"+ , mainFuncName = "main"+ }++encodeOptions :: Options -> Text+encodeOptions = Text.pack . show++decodeOptions :: Text -> Either Text Options+decodeOptions =+ maybe (Left "Could not decode skeletest-preprocessor options") Right+ . readMaybe+ . Text.unpack+ . unquote+ where+ unquote s =+ case Text.stripPrefix "\"" s >>= Text.stripSuffix "\"" of+ Just s' -> Text.replace "\\\"" "\"" s'+ Nothing -> s+ -- | Preprocess the given Haskell file. See Main.hs-processFile :: FilePath -> Text -> IO Text-processFile path file = do- file' <- if isMain file then updateMainFile path file else pure file+processFile :: Options -> FilePath -> Text -> IO Text+processFile options path file = do+ file' <-+ if getModuleName file == options.mainModuleName+ then updateMainFile path file+ else pure file pure . addLine pluginPragma . addLine linePragma@@ -28,22 +65,30 @@ addLine line f = line <> "\n" <> f quoted s = "\"" <> s <> "\"" - pluginPragma = "{-# OPTIONS_GHC -fplugin=Skeletest.Internal.Plugin #-}"+ pluginMod = "Skeletest.Internal.Plugin"+ quote s = "\"" <> Text.replace "\"" "\\\"" s <> "\""+ pluginPragma =+ Text.unwords+ [ "{-# OPTIONS_GHC"+ , "-fplugin=" <> pluginMod+ , "-fplugin-opt=" <> pluginMod <> ":" <> (quote . encodeOptions) options+ , "#-}"+ ] linePragma = -- this is needed to tell GHC to use original path in error messages "{-# LINE 1 " <> quoted (Text.pack path) <> " #-}" -isMain :: Text -> Bool-isMain file =- case mapMaybe getModuleName $ Text.lines file of+getModuleName :: Text -> Text+getModuleName file =+ case mapMaybe parseModuleLine $ Text.lines file of -- there was a module line- [name] -> name == "Main"- -- there were no module lines, it's the main module- [] -> True+ [name] -> name+ -- there were no module lines, it's the Main module+ [] -> "Main" -- something else? just silently ignore it- _ -> False+ _ -> "" where- getModuleName s =+ parseModuleLine s = case Text.words s of "module" : name : _ -> Just name _ -> Nothing
src/Skeletest/Internal/Utils/Color.hs view
@@ -5,7 +5,6 @@ gray, ) where -import Data.Colour.Names qualified as Color import Data.Text (Text) import Data.Text qualified as Text import System.Console.ANSI qualified as ANSI@@ -23,4 +22,4 @@ yellow = withANSI [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Yellow] gray :: Text -> Text-gray = withANSI [ANSI.SetRGBColor ANSI.Foreground Color.gray]+gray = withANSI [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Black]
src/Skeletest/Plugin.hs view
@@ -68,6 +68,7 @@ data Hooks = Hooks { hookModifyFileSpecs :: [SpecTree] -> IO [SpecTree] -- ^ Modify the specs in a file+ -- @since 0.3.2 , hookRunTest :: TestInfo -> IO TestResult -> IO TestResult -- ^ Modify how a test is executed }
src/bin/skeletest-preprocessor.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-} {-| A preprocessor that registers skeletest in a test suite. @@ -19,6 +20,8 @@ -} module Main where +import Data.Char (isUpper)+import Data.Foldable (foldlM) import Data.List (dropWhileEnd) import Data.Text.IO qualified as Text import GHC.IO.Encoding (setLocaleEncoding, utf8)@@ -27,8 +30,10 @@ import System.IO (hPutStrLn, stderr) import UnliftIO.Exception (displayException, handle) +import Data.Text qualified as Text import Skeletest.Internal.Error (SkeletestError) import Skeletest.Internal.Preprocessor (processFile)+import Skeletest.Internal.Preprocessor qualified as Preprocessor main :: IO () main = handleErrors $ do@@ -37,8 +42,10 @@ getArgs >>= \case -- https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/phases.html#options-affecting-a-haskell-pre-processor- [fp, input, output] -> Text.readFile input >>= processFile fp >>= Text.writeFile output- _ -> error "The skeletest preprocessor does not accept any additional arguments."+ fp : input : output : args -> do+ options <- either error pure $ foldlM parseOpts Preprocessor.defaultOptions args+ Text.readFile input >>= processFile options fp >>= Text.writeFile output+ _ -> error "The skeletest preprocessor expects at least three arguments." -- | Output SkeletestError handleErrors :: IO a -> IO a@@ -49,3 +56,24 @@ normalizeLines | __GLASGOW_HASKELL__ == (908 :: Int) = dropWhileEnd (== '\n') | otherwise = id++parseOpts :: Preprocessor.Options -> String -> Either String Preprocessor.Options+parseOpts opts argStr+ | Just (mMainMod, mMainFunc) <- Text.stripPrefix "main:" arg >>= parseMain =+ Right+ . maybe id (\s o -> o{Preprocessor.mainModuleName = s}) mMainMod+ . maybe id (\s o -> o{Preprocessor.mainFuncName = s}) mMainFunc+ $ opts+ | otherwise = Left $ "Unknown option: " <> argStr+ where+ arg = Text.pack argStr+ parseMain s =+ case Text.splitOn "." s of+ [modName, modFunc] -> Just (Just modName, Just modFunc)+ [_] -> do+ (c, _) <- Text.uncons s+ Just $+ if isUpper c+ then (Just s, Nothing)+ else (Nothing, Just s)+ _ -> Nothing
test/Skeletest/AssertionsSpec.hs view
@@ -86,6 +86,24 @@ stderr `shouldBe` "" stdout `shouldSatisfy` P.matchesSnapshot + describe "shouldReturn" $ do+ it "should pass" $+ pure 1 `shouldReturn` (1 :: Int)++ integration . it "should show helpful failure" $ do+ runner <- getFixture+ addTestFile runner "ExampleSpec.hs" $+ [ "module ExampleSpec (spec) where"+ , ""+ , "import Skeletest"+ , ""+ , "spec = it \"should fail\" $ pure 1 `shouldReturn` (2 :: Int)"+ ]++ (stdout, stderr) <- expectFailure $ runTests runner []+ stderr `shouldBe` ""+ stdout `shouldSatisfy` P.matchesSnapshot+ describe "context" $ do integration . it "should show failure context" $ do runner <- getFixture
test/Skeletest/__snapshots__/AssertionsSpec.snap.md view
@@ -85,6 +85,20 @@ ╰─────────────────────────────────────────────────────────────────────────────── ``` +## shouldReturn / should show helpful failure++```+./ExampleSpec.hs+╭── should fail: FAIL+│ ./ExampleSpec.hs:5:+│ │+│ │ spec = it "should fail" $ pure 1 `shouldReturn` (2 :: Int)+│ │ ^^^^^^^^^^^^^^+│ +│ 1 ≠ 2+╰───────────────────────────────────────────────────────────────────────────────+```+ ## shouldSatisfy / should show helpful failure ```