packages feed

skeletest 0.2.1 → 0.3.0

raw patch · 51 files changed

+2282/−1491 lines, 51 filesdep +exceptionsdep +terminal-sizedep ~ghcdep ~hedgehogPVP ok

version bump matches the API change (PVP)

Dependencies added: exceptions, terminal-size

Dependency ranges changed: ghc, hedgehog

API changes (from Hackage documentation)

- Skeletest.Internal.Error: instance GHC.Internal.Exception.Type.Exception Skeletest.Internal.Error.SkeletestError
- Skeletest.Internal.Error: instance GHC.Internal.Show.Show Skeletest.Internal.Error.SkeletestError
- Skeletest.Internal.Fixtures: instance GHC.Internal.Show.Show Skeletest.Internal.Fixtures.FixtureScope
- Skeletest.Internal.Fixtures: instance GHC.Internal.Show.Show Skeletest.Internal.Fixtures.FixtureScopeKey
- Skeletest.Internal.GHC: instance GHC.Internal.Base.Applicative Skeletest.Internal.GHC.CompilePs
- Skeletest.Internal.GHC: instance GHC.Internal.Base.Applicative Skeletest.Internal.GHC.CompileRn
- Skeletest.Internal.GHC: instance GHC.Internal.Base.Functor Skeletest.Internal.GHC.CompilePs
- Skeletest.Internal.GHC: instance GHC.Internal.Base.Functor Skeletest.Internal.GHC.CompileRn
- Skeletest.Internal.GHC: instance GHC.Internal.Base.Monad Skeletest.Internal.GHC.CompilePs
- Skeletest.Internal.GHC: instance GHC.Internal.Base.Monad Skeletest.Internal.GHC.CompileRn
- Skeletest.Internal.GHC: instance GHC.Internal.Show.Show (Skeletest.Internal.GHC.GhcIdP p)
- Skeletest.Internal.GHC: instance GHC.Internal.Show.Show (Skeletest.Internal.GHC.GhcLHsExpr p)
- Skeletest.Internal.GHC: instance GHC.Internal.Show.Show (Skeletest.Internal.GHC.HsExpr p)
- Skeletest.Internal.GHC: instance GHC.Internal.Show.Show (Skeletest.Internal.GHC.HsExprData p)
- Skeletest.Internal.GHC: instance GHC.Internal.Show.Show (Skeletest.Internal.GHC.HsName p)
- Skeletest.Internal.GHC: instance GHC.Internal.Show.Show (Skeletest.Internal.GHC.HsPat p)
- Skeletest.Internal.GHC: instance GHC.Internal.Show.Show Skeletest.Internal.GHC.GhcFixity
- Skeletest.Internal.Markers: instance GHC.Internal.Show.Show Skeletest.Internal.Markers.AnonMarker
- Skeletest.Internal.Markers: instance GHC.Internal.Show.Show Skeletest.Internal.Markers.SomeMarker
- Skeletest.Internal.Snapshot: [moduleName] :: SnapshotFile -> Text
- Skeletest.Internal.Snapshot: instance GHC.Internal.Show.Show Skeletest.Internal.Snapshot.SnapshotFile
- Skeletest.Internal.Snapshot: instance GHC.Internal.Show.Show Skeletest.Internal.Snapshot.SnapshotResult
- Skeletest.Internal.Snapshot: instance GHC.Internal.Show.Show Skeletest.Internal.Snapshot.SnapshotValue
- Skeletest.Internal.Spec: [specName] :: SpecInfo -> Text
- Skeletest.Internal.Spec: instance GHC.Internal.Base.Applicative Skeletest.Internal.Spec.Spec'
- Skeletest.Internal.Spec: instance GHC.Internal.Base.Functor Skeletest.Internal.Spec.Spec'
- Skeletest.Internal.Spec: instance GHC.Internal.Base.Monad Skeletest.Internal.Spec.Spec'
- Skeletest.Internal.Spec: instance GHC.Internal.Show.Show Skeletest.Internal.Spec.MarkerManual
- Skeletest.Internal.Spec: instance GHC.Internal.Show.Show Skeletest.Internal.Spec.MarkerSkip
- Skeletest.Internal.Spec: instance GHC.Internal.Show.Show Skeletest.Internal.Spec.MarkerXFail
- Skeletest.Internal.TestInfo: [testModule] :: TestInfo -> Text
- Skeletest.Internal.TestInfo: instance GHC.Internal.Show.Show Skeletest.Internal.TestInfo.TestInfo
- Skeletest.Internal.TestRunner: instance GHC.Internal.Exception.Type.Exception Skeletest.Internal.TestRunner.AssertionFail
- Skeletest.Internal.TestRunner: instance GHC.Internal.Show.Show Skeletest.Internal.TestRunner.AssertionFail
- Skeletest.Plugin: [testModule] :: TestInfo -> Text
- Skeletest.Plugin: instance GHC.Internal.Base.Monoid Skeletest.Plugin.Hooks
- Skeletest.Plugin: instance GHC.Internal.Base.Monoid Skeletest.Plugin.Plugin
- Skeletest.Plugin: instance GHC.Internal.Base.Semigroup Skeletest.Plugin.Hooks
- Skeletest.Plugin: instance GHC.Internal.Base.Semigroup Skeletest.Plugin.Plugin
- Skeletest.Prop.Internal: instance GHC.Internal.Base.Applicative Skeletest.Prop.Internal.PropertyM
- Skeletest.Prop.Internal: instance GHC.Internal.Base.Functor Skeletest.Prop.Internal.PropertyM
- Skeletest.Prop.Internal: instance GHC.Internal.Base.Monad Skeletest.Prop.Internal.PropertyM
+ Skeletest: FixtureCapturedOutput :: IO Text -> IO Text -> IO Text -> IO Text -> FixtureCapturedOutput
+ Skeletest: [getStderr] :: FixtureCapturedOutput -> IO Text
+ Skeletest: [getStdout] :: FixtureCapturedOutput -> IO Text
+ Skeletest: [readStderr] :: FixtureCapturedOutput -> IO Text
+ Skeletest: [readStdout] :: FixtureCapturedOutput -> IO Text
+ Skeletest: data FixtureCapturedOutput
+ Skeletest.Internal.Capture: FixtureCapturedOutput :: IO Text -> IO Text -> IO Text -> IO Text -> FixtureCapturedOutput
+ Skeletest.Internal.Capture: [getStderr] :: FixtureCapturedOutput -> IO Text
+ Skeletest.Internal.Capture: [getStdout] :: FixtureCapturedOutput -> IO Text
+ Skeletest.Internal.Capture: [readStderr] :: FixtureCapturedOutput -> IO Text
+ Skeletest.Internal.Capture: [readStdout] :: FixtureCapturedOutput -> IO Text
+ Skeletest.Internal.Capture: addCapturedOutput :: CapturedOutput -> TestResult -> TestResult
+ Skeletest.Internal.Capture: data CaptureOutputFlag
+ Skeletest.Internal.Capture: data FixtureCapturedOutput
+ Skeletest.Internal.Capture: instance Skeletest.Internal.CLI.IsFlag Skeletest.Internal.Capture.CaptureOutputFlag
+ Skeletest.Internal.Capture: instance Skeletest.Internal.Fixtures.Fixture Skeletest.Internal.Capture.FixtureCapturedOutput
+ Skeletest.Internal.Capture: instance Skeletest.Internal.Fixtures.Fixture Skeletest.Internal.Capture.FixtureCapturedOutputHandles
+ Skeletest.Internal.Capture: withCaptureOutput :: IO a -> IO (CapturedOutput, a)
+ Skeletest.Internal.Error: instance GHC.Exception.Type.Exception Skeletest.Internal.Error.SkeletestError
+ Skeletest.Internal.Error: instance GHC.Show.Show Skeletest.Internal.Error.SkeletestError
+ Skeletest.Internal.Fixtures: FixtureSkeletestTmpDir :: FilePath -> FixtureSkeletestTmpDir
+ Skeletest.Internal.Fixtures: instance GHC.Show.Show Skeletest.Internal.Fixtures.FixtureScope
+ Skeletest.Internal.Fixtures: instance GHC.Show.Show Skeletest.Internal.Fixtures.FixtureScopeKey
+ Skeletest.Internal.Fixtures: instance Skeletest.Internal.Fixtures.Fixture Skeletest.Internal.Fixtures.FixtureSkeletestTmpDir
+ Skeletest.Internal.Fixtures: newtype FixtureSkeletestTmpDir
+ Skeletest.Internal.GHC: getLoc :: HsExpr p -> Maybe SrcSpan
+ Skeletest.Internal.GHC: instance GHC.Base.Applicative Skeletest.Internal.GHC.CompilePs
+ Skeletest.Internal.GHC: instance GHC.Base.Applicative Skeletest.Internal.GHC.CompileRn
+ Skeletest.Internal.GHC: instance GHC.Base.Functor Skeletest.Internal.GHC.CompilePs
+ Skeletest.Internal.GHC: instance GHC.Base.Functor Skeletest.Internal.GHC.CompileRn
+ Skeletest.Internal.GHC: instance GHC.Base.Monad Skeletest.Internal.GHC.CompilePs
+ Skeletest.Internal.GHC: instance GHC.Base.Monad Skeletest.Internal.GHC.CompileRn
+ Skeletest.Internal.GHC: instance GHC.Show.Show (Skeletest.Internal.GHC.GhcIdP p)
+ Skeletest.Internal.GHC: instance GHC.Show.Show (Skeletest.Internal.GHC.GhcLHsExpr p)
+ Skeletest.Internal.GHC: instance GHC.Show.Show (Skeletest.Internal.GHC.HsExpr p)
+ Skeletest.Internal.GHC: instance GHC.Show.Show (Skeletest.Internal.GHC.HsExprData p)
+ Skeletest.Internal.GHC: instance GHC.Show.Show (Skeletest.Internal.GHC.HsName p)
+ Skeletest.Internal.GHC: instance GHC.Show.Show (Skeletest.Internal.GHC.HsPat p)
+ Skeletest.Internal.GHC: instance GHC.Show.Show Skeletest.Internal.GHC.GhcFixity
+ Skeletest.Internal.GHC.Compat: mkPrefixCon :: [arg] -> HsConDetails tyargs arg rec
+ Skeletest.Internal.GHC.Compat: noUserRdr :: Name -> Name
+ Skeletest.Internal.GHC.Compat: toGrhssGRHSs :: NonEmpty (LGRHS p body) -> [LGRHS p body]
+ Skeletest.Internal.GHC.Compat: type LIdOccP a = LIdP a
+ Skeletest.Internal.GHC.Compat: unLocWithUserRdr :: GenLocated l Name -> Name
+ Skeletest.Internal.Markers: instance GHC.Show.Show Skeletest.Internal.Markers.AnonMarker
+ Skeletest.Internal.Markers: instance GHC.Show.Show Skeletest.Internal.Markers.SomeMarker
+ Skeletest.Internal.Snapshot: [testFile] :: SnapshotFile -> Text
+ Skeletest.Internal.Snapshot: instance GHC.Show.Show Skeletest.Internal.Snapshot.SnapshotFile
+ Skeletest.Internal.Snapshot: instance GHC.Show.Show Skeletest.Internal.Snapshot.SnapshotResult
+ Skeletest.Internal.Snapshot: instance GHC.Show.Show Skeletest.Internal.Snapshot.SnapshotValue
+ Skeletest.Internal.Spec: instance GHC.Base.Applicative Skeletest.Internal.Spec.Spec'
+ Skeletest.Internal.Spec: instance GHC.Base.Functor Skeletest.Internal.Spec.Spec'
+ Skeletest.Internal.Spec: instance GHC.Base.Monad Skeletest.Internal.Spec.Spec'
+ Skeletest.Internal.Spec: instance GHC.Show.Show Skeletest.Internal.Spec.MarkerManual
+ Skeletest.Internal.Spec: instance GHC.Show.Show Skeletest.Internal.Spec.MarkerSkip
+ Skeletest.Internal.Spec: instance GHC.Show.Show Skeletest.Internal.Spec.MarkerXFail
+ Skeletest.Internal.TestInfo: instance GHC.Show.Show Skeletest.Internal.TestInfo.TestInfo
+ Skeletest.Internal.TestRunner: instance GHC.Exception.Type.Exception Skeletest.Internal.TestRunner.AssertionFail
+ Skeletest.Internal.TestRunner: instance GHC.Show.Show Skeletest.Internal.TestRunner.AssertionFail
+ Skeletest.Internal.Utils.BoxDrawing: BoxHeader :: Text -> BoxSpecContent
+ Skeletest.Internal.Utils.BoxDrawing: BoxText :: Text -> BoxSpecContent
+ Skeletest.Internal.Utils.BoxDrawing: data BoxSpecContent
+ Skeletest.Internal.Utils.BoxDrawing: drawBox :: BoxSpec -> IO Text
+ Skeletest.Internal.Utils.BoxDrawing: instance GHC.Classes.Eq Skeletest.Internal.Utils.BoxDrawing.BoxSpecContent
+ Skeletest.Internal.Utils.BoxDrawing: instance GHC.Show.Show Skeletest.Internal.Utils.BoxDrawing.BoxSpecContent
+ Skeletest.Internal.Utils.BoxDrawing: type BoxSpec = [BoxSpecContent]
+ Skeletest.Plugin: instance GHC.Base.Monoid Skeletest.Plugin.Hooks
+ Skeletest.Plugin: instance GHC.Base.Monoid Skeletest.Plugin.Plugin
+ Skeletest.Plugin: instance GHC.Base.Semigroup Skeletest.Plugin.Hooks
+ Skeletest.Plugin: instance GHC.Base.Semigroup Skeletest.Plugin.Plugin
+ Skeletest.Prop.Internal: instance GHC.Base.Applicative Skeletest.Prop.Internal.PropertyM
+ Skeletest.Prop.Internal: instance GHC.Base.Functor Skeletest.Prop.Internal.PropertyM
+ Skeletest.Prop.Internal: instance GHC.Base.Monad Skeletest.Prop.Internal.PropertyM
- Skeletest.Internal.Error: CompilationError :: Text -> SkeletestError
+ Skeletest.Internal.Error: CompilationError :: Maybe SrcSpan -> Text -> SkeletestError
- Skeletest.Internal.Error: skeletestPluginError :: String -> a
+ Skeletest.Internal.Error: skeletestPluginError :: Maybe SrcSpan -> String -> a
- Skeletest.Internal.GHC.Compat: genLoc :: NoAnn ann => e -> GenLocated ann e
+ Skeletest.Internal.GHC.Compat: genLoc :: e -> GenLocated (SrcAnn ann) e
- Skeletest.Internal.GHC.Compat: hsPar :: forall (p :: Pass). IsPass p => LHsExpr (GhcPass p) -> HsExpr (GhcPass p)
+ Skeletest.Internal.GHC.Compat: hsPar :: forall (p :: Pass). LHsExpr (GhcPass p) -> HsExpr (GhcPass p)
- Skeletest.Internal.GHC.Compat: mkPrefixFunRhs :: fn -> [ann] -> HsMatchContext fn
+ Skeletest.Internal.GHC.Compat: mkPrefixFunRhs :: LIdP GhcPs -> EpAnn () -> HsMatchContext GhcPs
- Skeletest.Internal.Predicate: anything :: forall (m :: Type -> Type) a. Monad m => Predicate m a
+ Skeletest.Internal.Predicate: anything :: forall a (m :: Type -> Type). Monad m => Predicate m a
- Skeletest.Internal.Spec: SpecInfo :: FilePath -> Text -> Spec -> SpecInfo
+ Skeletest.Internal.Spec: SpecInfo :: FilePath -> Spec -> SpecInfo
- Skeletest.Internal.TestInfo: TestInfo :: Text -> [Text] -> Text -> [SomeMarker] -> FilePath -> TestInfo
+ Skeletest.Internal.TestInfo: TestInfo :: [Text] -> Text -> [SomeMarker] -> FilePath -> TestInfo
- Skeletest.Internal.TestRunner: TestResultMessageSection :: Text -> TestResultMessage
+ Skeletest.Internal.TestRunner: TestResultMessageSection :: BoxSpec -> TestResultMessage
- Skeletest.Internal.TestRunner: testResultFromError :: SomeException -> TestResult
+ Skeletest.Internal.TestRunner: testResultFromError :: SomeException -> IO TestResult
- Skeletest.Main: runSkeletest :: [Plugin] -> [(FilePath, String, Spec)] -> IO ()
+ Skeletest.Main: runSkeletest :: [Plugin] -> [(FilePath, Spec)] -> IO ()
- Skeletest.Plugin: TestInfo :: Text -> [Text] -> Text -> [SomeMarker] -> FilePath -> TestInfo
+ Skeletest.Plugin: TestInfo :: [Text] -> Text -> [SomeMarker] -> FilePath -> TestInfo
- Skeletest.Plugin: TestResultMessageSection :: Text -> TestResultMessage
+ Skeletest.Plugin: TestResultMessageSection :: BoxSpec -> TestResultMessage
- Skeletest.Predicate: anything :: forall (m :: Type -> Type) a. Monad m => Predicate m a
+ Skeletest.Predicate: anything :: forall a (m :: Type -> Type). Monad m => Predicate m a

Files

CHANGELOG.md view
@@ -1,3 +1,20 @@+## v0.3.0++GHC support:+* Support GHC 9.14++New features:+* Automatically capture stdout/stderr ([#1](https://github.com/brandonchinn178/skeletest/issues/1))++API changes:+* Specify order of type variables for `P.anything` so that `P.anything @Int` works++Runtime changes:+* Add location to error messages+* Render test failures/errors in more visible box+* Display the path of the test file instead of guessing a module name ([#40](https://github.com/brandonchinn178/skeletest/issues/40))+* Flush stdout so test name is displayed while test is still running+ ## v0.2.1  * Add `P.list`
README.md view
@@ -286,7 +286,7 @@  ```haskell prop "reverse does not change the length" $ do-  xs <- forAll $ Gen.list (Gen.range 0 10) Gen.int+  xs <- forAll $ Gen.list (Range.linear 0 10) $ Gen.int (Range.linear 1 100)   length (reverse xs) `shouldBe` length xs ``` @@ -294,7 +294,7 @@  ```haskell prop "decodeUser . encodeUser === pure" $ do-  let genUser = User <$> Gen.text (Gen.range 0 10) Gen.unicode+  let genUser = User <$> Gen.text (Range.linear 0 10) Gen.unicode   (decodeUser . encodeUser) P.=== pure `shouldSatisfy` P.isoWith genUser ``` @@ -303,7 +303,7 @@ To ignore certain values, use `discard`:  ```haskell-x <- Gen.int (Gen.range (-10) 10)+x <- Gen.int (Range.linear (-10) 10) when (x == 0) discard ... ```@@ -401,6 +401,10 @@     ```      All tests in the given section will be marked with the given marker, which can be selected with `@my-marker`. You can see if a test has a marker with `findMarkers` (see the "Hooks" section).++### Output capturing++By default, Skeletest will capture all output to stdout/stderr that occurs during a test and will only display it if the test fails. To disable this, pass `--capture-output=off`.  ### Custom CLI flags 
skeletest.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0  name: skeletest-version: 0.2.1+version: 0.3.0 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@@ -36,6 +36,7 @@     Skeletest     Skeletest.Assertions     Skeletest.Internal.CLI+    Skeletest.Internal.Capture     Skeletest.Internal.Constants     Skeletest.Internal.Error     Skeletest.Internal.Fixtures@@ -50,6 +51,7 @@     Skeletest.Internal.TestInfo     Skeletest.Internal.TestRunner     Skeletest.Internal.TestTargets+    Skeletest.Internal.Utils.BoxDrawing     Skeletest.Internal.Utils.Color     Skeletest.Internal.Utils.Diff     Skeletest.Internal.Utils.HList@@ -70,6 +72,9 @@   if impl(ghc >= 9.12) && impl(ghc < 9.14)     other-modules:         Skeletest.Internal.GHC.Compat_9_12+  if impl(ghc >= 9.14) && impl(ghc < 9.16)+    other-modules:+        Skeletest.Internal.GHC.Compat_9_14   build-depends:       base < 5     , aeson@@ -79,15 +84,18 @@     , containers     , Diff >= 1.0     , directory+    , exceptions     , filepath-    , ghc ^>= 9.8 || ^>= 9.10 || ^>= 9.12-    , hedgehog+    , ghc ^>= 9.8 || ^>= 9.10 || ^>= 9.12 || ^>= 9.14+    , hedgehog >= 1.5     , megaparsec     , ordered-containers >= 0.2.4     , parser-combinators     , pretty+    , process     , recover-rtti     , template-haskell+    , terminal-size >= 0.2.0     , text     , transformers     , unliftio >= 0.2.17@@ -114,6 +122,7 @@     ExampleSpec     Skeletest.AssertionsSpec     Skeletest.Internal.CLISpec+    Skeletest.Internal.CaptureSpec     Skeletest.Internal.FixturesSpec     Skeletest.Internal.SnapshotSpec     Skeletest.Internal.SpecSpec
src/Skeletest.hs view
@@ -43,6 +43,7 @@    -- ** Built-in fixtures   FixtureTmpDir (..),+  FixtureCapturedOutput (..),    -- * CLI flags   Flag (..),@@ -52,9 +53,9 @@ ) where  import GHC.Stack (HasCallStack)- import Skeletest.Assertions import Skeletest.Internal.CLI+import Skeletest.Internal.Capture import Skeletest.Internal.Fixtures import Skeletest.Internal.Spec import Skeletest.Predicate
src/Skeletest/Assertions.hs view
@@ -20,10 +20,6 @@ import Data.Text qualified as Text import GHC.Stack (HasCallStack) import GHC.Stack qualified as GHC-import System.IO.Unsafe (unsafePerformIO)-import UnliftIO.Exception (bracket_, throwIO)-import UnliftIO.IORef (IORef, modifyIORef, newIORef, readIORef)- import Skeletest.Internal.Predicate (   Predicate,   PredicateResult (..),@@ -37,6 +33,9 @@   Testable (..),   testResultPass,  )+import System.IO.Unsafe (unsafePerformIO)+import UnliftIO.Exception (bracket_, throwIO)+import UnliftIO.IORef (IORef, modifyIORef, newIORef, readIORef)  instance Testable IO where   runTestable m = m >> pure testResultPass
src/Skeletest/Internal/CLI.hs view
@@ -35,15 +35,14 @@ import Data.Text qualified as Text import Data.Text.IO qualified as Text import Data.Typeable (TypeRep, Typeable, typeOf, typeRep)+import Skeletest.Internal.Error (SkeletestError (..), invariantViolation)+import Skeletest.Internal.TestTargets (TestTargets, parseTestTargets) import System.Environment (getArgs) import System.Exit (exitFailure, exitSuccess) import System.IO (stderr) import System.IO.Unsafe (unsafePerformIO) import UnliftIO.Exception (throwIO) -import Skeletest.Internal.Error (SkeletestError (..), invariantViolation)-import Skeletest.Internal.TestTargets (TestTargets, parseTestTargets)- -- | Register a CLI flag. -- -- Usage:@@ -122,8 +121,8 @@               , "Got: " <> show dyn               ]       Nothing -> throwIO $ CliFlagNotFound (Text.pack $ flagName @a)-  where-    rep = typeRep (Proxy @a)+ where+  rep = typeRep (Proxy @a)  {----- Load CLI arguments -----} @@ -146,84 +145,84 @@     CLIParseSuccess{testTargets, flagStore} -> do       setCliFlagStore flagStore       pure testTargets-  where-    helpText = getHelpText builtinFlags flags+ where+  helpText = getHelpText builtinFlags flags  getHelpText :: [Flag] -> [Flag] -> Text getHelpText builtinFlags customFlags =   Text.intercalate "\n\n" $     "Usage: skeletest [OPTIONS] [--] [TARGETS]" : map (uncurry renderSection) helpSections-  where-    helpSections =-      filter (not . Text.null . snd) $-        [ ("TEST SELECTION", testSelectionDocs)-        , ("BUILTIN OPTIONS", renderFlagList builtinFlagDocs)-        , ("CUSTOM OPTIONS", renderFlagList customFlagDocs)-        ]--    testSelectionDocs =-      Text.intercalate "\n" $-        [ "Test targets may be specified as plain positional arguments, with the following syntax:"-        , "    * Tests including substring:      '[myFooFunc]'"-        , "    * Tests tagged with marker:       '@fast'"-        , "    * Tests in file, relative to CWD: 'test/MyLib/FooSpec.hs'"-        , "    * Tests matching pattern in file: 'test/MyLib/FooSpec.hs[myFooFunc]'"-        , "        * Syntax sugar for '(test/MyLib/FooSpec.hs and [myFooFunc])'"-        , "    * Tests matching both targets:    '[func1] and [func2]'"-        , "    * Tests matching either target:   '[func1] or [func2]'"-        , "    * Tests not matching target:      'not [func1]'"-        , ""-        , "More examples:"-        , "    * 'test/MySpec.hs and ([myFooFunc] or [myBarFunc]) and @fast'"-        , "    * '[myFooFunc] or test/MySpec.hs[myBarFunc]'"-        , ""-        , "When multiple targets are specified, they are joined with 'or'."-        ]+ where+  helpSections =+    filter (not . Text.null . snd) $+      [ ("TEST SELECTION", testSelectionDocs)+      , ("BUILTIN OPTIONS", renderFlagList builtinFlagDocs)+      , ("CUSTOM OPTIONS", renderFlagList customFlagDocs)+      ] -    builtinFlagDocs = ("help", Just 'h', Nothing, "Display this help text") : fromFlags builtinFlags-    customFlagDocs = fromFlags customFlags-    fromFlags flags =-      [ (Text.pack (flagName @a), flagShort @a, mMetaVar, Text.pack (flagHelp @a))-      | Flag (Proxy :: Proxy a) <- flags-      , let mMetaVar =-              case flagSpec @a of-                SwitchFlag{} -> Nothing-                RequiredFlag{} -> Just $ Text.pack (flagMetaVar @a)-                OptionalFlag{} -> Just $ Text.pack (flagMetaVar @a)+  testSelectionDocs =+    Text.intercalate "\n" $+      [ "Test targets may be specified as plain positional arguments, with the following syntax:"+      , "    * Tests including substring:      '[myFooFunc]'"+      , "    * Tests tagged with marker:       '@fast'"+      , "    * Tests in file, relative to CWD: 'test/MyLib/FooSpec.hs'"+      , "    * Tests matching pattern in file: 'test/MyLib/FooSpec.hs[myFooFunc]'"+      , "        * Syntax sugar for '(test/MyLib/FooSpec.hs and [myFooFunc])'"+      , "    * Tests matching both targets:    '[func1] and [func2]'"+      , "    * Tests matching either target:   '[func1] or [func2]'"+      , "    * Tests not matching target:      'not [func1]'"+      , ""+      , "More examples:"+      , "    * 'test/MySpec.hs and ([myFooFunc] or [myBarFunc]) and @fast'"+      , "    * '[myFooFunc] or test/MySpec.hs[myBarFunc]'"+      , ""+      , "When multiple targets are specified, they are joined with 'or'."       ] -    renderSection title body =-      Text.intercalate "\n" $-        [ "===== " <> title-        , ""-        , body-        ]+  builtinFlagDocs = ("help", Just 'h', Nothing, "Display this help text") : fromFlags builtinFlags+  customFlagDocs = fromFlags customFlags+  fromFlags flags =+    [ (Text.pack (flagName @a), flagShort @a, mMetaVar, Text.pack (flagHelp @a))+    | Flag (Proxy :: Proxy a) <- flags+    , let mMetaVar =+            case flagSpec @a of+              SwitchFlag{} -> Nothing+              RequiredFlag{} -> Just $ Text.pack (flagMetaVar @a)+              OptionalFlag{} -> Just $ Text.pack (flagMetaVar @a)+    ] -    renderFlagList flagList =-      Text.intercalate "\n" . mkTabular $-        [ (shortName <> renderLongFlag longName <> metaVar, help)-        | (longName, mShortName, mMetaVar, help) <- flagList-        , let-            shortName =-              case mShortName of-                Just short -> renderShortFlag short <> ", "-                Nothing -> ""-            metaVar =-              case mMetaVar of-                Just meta -> " <" <> meta <> ">"-                Nothing -> ""-        ]+  renderSection title body =+    Text.intercalate "\n" $+      [ "===== " <> title+      , ""+      , body+      ] -    mkTabular rows0 =-      case NonEmpty.nonEmpty rows0 of-        Nothing -> []-        Just rows ->-          let fstColWidth = Foldable1.maximum $ NonEmpty.map (Text.length . fst) rows-              margin = 2 -- space between columns-           in [ a <> Text.replicate (fstColWidth - Text.length a + margin) " " <> b-              | (a, b) <- NonEmpty.toList rows-              ]+  renderFlagList flagList =+    Text.intercalate "\n" . mkTabular $+      [ (shortName <> renderLongFlag longName <> metaVar, help)+      | (longName, mShortName, mMetaVar, help) <- flagList+      , let+          shortName =+            case mShortName of+              Just short -> renderShortFlag short <> ", "+              Nothing -> ""+          metaVar =+            case mMetaVar of+              Just meta -> " <" <> meta <> ">"+              Nothing -> ""+      ] +  mkTabular rows0 =+    case NonEmpty.nonEmpty rows0 of+      Nothing -> []+      Just rows ->+        let fstColWidth = Foldable1.maximum $ NonEmpty.map (Text.length . fst) rows+            margin = 2 -- space between columns+         in [ a <> Text.replicate (fstColWidth - Text.length a + margin) " " <> b+            | (a, b) <- NonEmpty.toList rows+            ]+ {----- Parse args -----}  data CLIParseResult@@ -247,103 +246,103 @@   testTargets <- first CLIParseFailure $ parseTestTargets args'   flagStore' <- first CLIParseFailure $ resolveFlags flags flagStore   pure CLIParseSuccess{testTargets, flagStore = flagStore'}-  where-    extractLongFlags =-      toFlagMap renderLongFlag $-        [ (Text.pack $ flagName @a, f)-        | f@(Flag (Proxy :: Proxy a)) <- flags-        ]+ where+  extractLongFlags =+    toFlagMap renderLongFlag $+      [ (Text.pack $ flagName @a, f)+      | f@(Flag (Proxy :: Proxy a)) <- flags+      ] -    extractShortFlags =-      toFlagMap renderShortFlag $-        [ (shortFlag, f)-        | f@(Flag (Proxy :: Proxy a)) <- flags-        , Just shortFlag <- pure $ flagShort @a-        ]+  extractShortFlags =+    toFlagMap renderShortFlag $+      [ (shortFlag, f)+      | f@(Flag (Proxy :: Proxy a)) <- flags+      , Just shortFlag <- pure $ flagShort @a+      ] -    toFlagMap :: (Ord name) => (name -> Text) -> [(name, a)] -> Either CLIParseResult (Map name a)-    toFlagMap renderFlag vals =-      let go seen = \case-            [] -> Right $ Map.fromList vals-            (name, _) : xs-              | name `Set.member` seen -> Left . CLISetupFailure $ "Flag registered multiple times: " <> renderFlag name-              | otherwise -> go (Set.insert name seen) xs-       in go Set.empty vals+  toFlagMap :: (Ord name) => (name -> Text) -> [(name, a)] -> Either CLIParseResult (Map name a)+  toFlagMap renderFlag vals =+    let go seen = \case+          [] -> Right $ Map.fromList vals+          (name, _) : xs+            | name `Set.member` seen -> Left . CLISetupFailure $ "Flag registered multiple times: " <> renderFlag name+            | otherwise -> go (Set.insert name seen) xs+     in go Set.empty vals  type ArgParserM = Trans.StateT ([Text], CLIFlagStore) (Trans.Except Text)  parseCliArgsWith :: Map Text Flag -> Map Char Flag -> [String] -> Either Text ([Text], CLIFlagStore) parseCliArgsWith longFlags shortFlags = Trans.runExcept . flip Trans.execStateT ([], Map.empty) . parseArgs-  where-    parseArgs = \case-      [] -> pure ()-      "--" : rest -> addArgs rest-      curr : rest-        | Just longFlag <- Text.stripPrefix "--" (Text.pack curr) -> parseLongFlag longFlag rest-        | Just chars <- Text.stripPrefix "-" (Text.pack curr) ->-            case Text.unpack chars of-              [] -> argError "Invalid flag: -"-              [shortFlag] -> parseShortFlag shortFlag rest-              _ -> argError $ "Invalid flag: -" <> chars-        | otherwise -> addArgs [curr] >> parseArgs rest+ where+  parseArgs = \case+    [] -> pure ()+    "--" : rest -> addArgs rest+    curr : rest+      | Just longFlag <- Text.stripPrefix "--" (Text.pack curr) -> parseLongFlag longFlag rest+      | Just chars <- Text.stripPrefix "-" (Text.pack curr) ->+          case Text.unpack chars of+            [] -> argError "Invalid flag: -"+            [shortFlag] -> parseShortFlag shortFlag rest+            _ -> argError $ "Invalid flag: -" <> chars+      | otherwise -> addArgs [curr] >> parseArgs rest -    parseLongFlag name args =-      let (name', args') =-            case Text.breakOn "=" name of-              (_, "") -> (name, args)-              (n, post) -> (n, (drop 1 . Text.unpack) post : args)-       in parseFlag renderLongFlag longFlags name' args'-    parseShortFlag = parseFlag renderShortFlag shortFlags+  parseLongFlag name args =+    let (name', args') =+          case Text.breakOn "=" name of+            (_, "") -> (name, args)+            (n, post) -> (n, (drop 1 . Text.unpack) post : args)+     in parseFlag renderLongFlag longFlags name' args'+  parseShortFlag = parseFlag renderShortFlag shortFlags -    parseFlag :: (Ord name) => (name -> Text) -> Map name Flag -> name -> [String] -> ArgParserM ()-    parseFlag renderFlag flagMap name args = do-      Flag (Proxy :: Proxy a) <--        case Map.lookup name flagMap of-          Nothing -> argError $ "Unknown flag: " <> renderFlag name-          Just f -> pure f-      let parseFlagArg parseArg =-            case args of-              [] -> argError $ "Flag requires argument: " <> renderFlag name-              curr : rest -> parseArg curr >>= addFlagStore >> parseArgs rest-      case flagSpec @a of-        SwitchFlag{flagFromBool} -> addFlagStore (flagFromBool True) >> parseArgs args-        RequiredFlag{flagParse} -> parseFlagArg (Trans.lift . Trans.except . first Text.pack . flagParse)-        OptionalFlag{flagParse} -> parseFlagArg (Trans.lift . Trans.except . first Text.pack . flagParse)+  parseFlag :: (Ord name) => (name -> Text) -> Map name Flag -> name -> [String] -> ArgParserM ()+  parseFlag renderFlag flagMap name args = do+    Flag (Proxy :: Proxy a) <-+      case Map.lookup name flagMap of+        Nothing -> argError $ "Unknown flag: " <> renderFlag name+        Just f -> pure f+    let parseFlagArg parseArg =+          case args of+            [] -> argError $ "Flag requires argument: " <> renderFlag name+            curr : rest -> parseArg curr >>= addFlagStore >> parseArgs rest+    case flagSpec @a of+      SwitchFlag{flagFromBool} -> addFlagStore (flagFromBool True) >> parseArgs args+      RequiredFlag{flagParse} -> parseFlagArg (Trans.lift . Trans.except . first Text.pack . flagParse)+      OptionalFlag{flagParse} -> parseFlagArg (Trans.lift . Trans.except . first Text.pack . flagParse) -    argError = Trans.lift . Trans.throwE+  argError = Trans.lift . Trans.throwE -    addArgs :: [String] -> ArgParserM ()-    addArgs args = Trans.modify (first (<> map Text.pack args))+  addArgs :: [String] -> ArgParserM ()+  addArgs args = Trans.modify (first (<> map Text.pack args)) -    addFlagStore :: (Typeable a) => a -> ArgParserM ()-    addFlagStore x = Trans.modify (second (insertFlagStore x))+  addFlagStore :: (Typeable a) => a -> ArgParserM ()+  addFlagStore x = Trans.modify (second (insertFlagStore x))  resolveFlags :: [Flag] -> CLIFlagStore -> Either Text CLIFlagStore resolveFlags = flip (foldlM go)-  where-    go flagStore (Flag (Proxy :: Proxy a)) = do-      let rep = typeRep (Proxy @a)-      case flagSpec @a of-        SwitchFlag{flagFromBool} ->-          pure $-            if rep `Map.member` flagStore-              then flagStore-              else insertFlagStore (flagFromBool False) flagStore-        RequiredFlag{} ->+ where+  go flagStore (Flag (Proxy :: Proxy a)) = do+    let rep = typeRep (Proxy @a)+    case flagSpec @a of+      SwitchFlag{flagFromBool} ->+        pure $           if rep `Map.member` flagStore-            then pure flagStore-            else Left $ "Required flag not set: " <> renderLongFlag (Text.pack $ flagName @a)-        OptionalFlag{flagDefault} ->-          pure $-            if rep `Map.member` flagStore-              then flagStore-              else insertFlagStore flagDefault flagStore+            then flagStore+            else insertFlagStore (flagFromBool False) flagStore+      RequiredFlag{} ->+        if rep `Map.member` flagStore+          then pure flagStore+          else Left $ "Required flag not set: " <> renderLongFlag (Text.pack $ flagName @a)+      OptionalFlag{flagDefault} ->+        pure $+          if rep `Map.member` flagStore+            then flagStore+            else insertFlagStore flagDefault flagStore -    foldlM f z = \case-      [] -> pure z-      x : xs -> do-        z' <- f z x-        foldlM f z' xs+  foldlM f z = \case+    [] -> pure z+    x : xs -> do+      z' <- f z x+      foldlM f z' xs  renderLongFlag :: Text -> Text renderLongFlag = ("--" <>)
+ src/Skeletest/Internal/Capture.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoFieldSelectors #-}++module Skeletest.Internal.Capture (+  CaptureOutputFlag,+  withCaptureOutput,+  addCapturedOutput,+  FixtureCapturedOutput (..),+) where++import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.IO qualified as Text+import GHC.IO.Handle qualified as IO+import Skeletest.Internal.CLI (+  FlagSpec (..),+  IsFlag (..),+  getFlag,+ )+import Skeletest.Internal.Fixtures (+  Fixture (..),+  FixtureSkeletestTmpDir (..),+  getFixture,+  noCleanup,+  withCleanup,+ )+import Skeletest.Internal.TestRunner (+  TestResult (..),+  TestResultMessage (..),+ )+import Skeletest.Internal.Utils.BoxDrawing (BoxSpecContent (..))+import System.Directory (removePathForcibly)+import System.IO qualified as IO+import UnliftIO.Exception (finally)++newtype CaptureOutputFlag = CaptureOutputFlag Bool++instance IsFlag CaptureOutputFlag where+  flagName = "capture-output"+  flagHelp = "Whether to capture stdout/stderr: on (default), off"+  flagSpec =+    OptionalFlag+      { flagDefault = CaptureOutputFlag True+      , flagParse = \case+          "off" -> Right $ CaptureOutputFlag False+          "on" -> Right $ CaptureOutputFlag True+          s -> Left $ "invalid value: " <> s+      }++type CapturedOutput = Maybe (Text, Text)++withCaptureOutput :: IO a -> IO (CapturedOutput, a)+withCaptureOutput action = do+  CaptureOutputFlag output <- getFlag+  if output+    then do+      handles <- getFixture @FixtureCapturedOutputHandles+      (stdout, (stderr, a)) <- capture handles.stdout . capture handles.stderr $ action+      pure (Just (stdout, stderr), a)+    else (Nothing,) <$> action+ where+  capture handle m =+    withRestore handle.real $ do+      IO.hFlush handle.real+      IO.hDuplicateTo handle.log handle.real+      a <- m+      out <- getOutput handle+      pure (out, a)++  withRestore h m = do+    buf <- IO.hGetBuffering h+    orig <- IO.hDuplicate h+    m `finally` do+      IO.hDuplicateTo orig h+      IO.hSetBuffering h buf+      IO.hClose orig++addCapturedOutput :: CapturedOutput -> TestResult -> TestResult+addCapturedOutput = maybe id updateResult+ where+  updateResult output result =+    result+      { testResultMessage =+          TestResultMessageSection . concat $+            [ toBoxContents (testResultMessage result)+            , renderOutput output+            ]+      }+  toBoxContents = \case+    TestResultMessageNone -> []+    TestResultMessageInline msg -> [BoxText msg]+    TestResultMessageSection box -> box+  renderOutput (stdout, stderr) =+    concat+      [ if Text.null stdout then [] else [BoxHeader "Captured stdout", BoxText stdout]+      , if Text.null stderr then [] else [BoxHeader "Captured stderr", BoxText stderr]+      ]++data FixtureCapturedOutputHandles = FixtureCapturedOutputHandles+  { stdout :: LogHandle+  , stderr :: LogHandle+  }++data LogHandle = LogHandle+  { log :: IO.Handle+  , real :: IO.Handle+  }++initHandle ::+  IO.Handle ->+  FilePath ->+  FilePath ->+  IO (LogHandle, IO ())+initHandle real tmpdir file = do+  (fp, h) <- IO.openTempFile tmpdir file+  IO.hSetBuffering h IO.LineBuffering+  let handle = LogHandle{log = h, real = real}+      cleanup = IO.hClose h >> removePathForcibly fp+  pure (handle, cleanup)++instance Fixture FixtureCapturedOutputHandles where+  fixtureAction = do+    FixtureSkeletestTmpDir tmpdir <- getFixture+    (stdout, cleanupStdout) <- initHandle IO.stdout tmpdir "stdout"+    (stderr, cleanupStderr) <- initHandle IO.stderr tmpdir "stderr"+    pure . withCleanup FixtureCapturedOutputHandles{..} $ do+      cleanupStdout+      cleanupStderr++getOutput :: LogHandle -> IO Text+getOutput = readOutputFrom 0++readOutput :: LogHandle -> IO Text+readOutput handle = do+  pos <- IO.hTell handle.log+  readOutputFrom pos handle++readOutputFrom :: Integer -> LogHandle -> IO Text+readOutputFrom n handle = do+  -- Flush buffers+  IO.hFlush handle.real+  IO.hFlush handle.log++  -- Force handle to end of file, to refresh from real handle+  IO.hSeek handle.log IO.SeekFromEnd 0++  IO.hSeek handle.log IO.AbsoluteSeek n+  go ""+ where+  go acc = do+    out <- Text.hGetChunk handle.log+    if Text.null out+      then pure acc+      else go $! acc <> out++-- | Fixture for inspecting the captured output.+--+-- Intended to be used with @OverloadedRecordDot@:+--+-- @+-- output <- getFixture @FixtureCapturedOutput+--+-- -- Read all of stdout/stderr so far+-- stdout <- output.getStdout+-- stderr <- output.getStderr+--+-- -- Read everything in stdout/stderr since the last read+-- stdout_chunk <- output.readStdout+-- stderr_chunk <- output.readStderr+-- @+data FixtureCapturedOutput = FixtureCapturedOutput+  { getStdout :: IO Text+  , getStderr :: IO Text+  , readStdout :: IO Text+  , readStderr :: IO Text+  }++instance Fixture FixtureCapturedOutput where+  fixtureAction = do+    handles <- getFixture @FixtureCapturedOutputHandles+    pure . noCleanup $+      FixtureCapturedOutput+        { getStdout = getOutput handles.stdout+        , getStderr = getOutput handles.stderr+        , readStdout = readOutput handles.stdout+        , readStderr = readOutput handles.stderr+        }
src/Skeletest/Internal/Error.hs view
@@ -9,12 +9,12 @@  import Data.Text (Text) import Data.Text qualified as Text-import GHC.Utils.Panic (pgmError)+import GHC qualified import UnliftIO.Exception (Exception (..), impureThrow)  data SkeletestError   = -- | A user error during compilation, e.g. during the preprocessor or plugin phases.-    CompilationError Text+    CompilationError (Maybe GHC.SrcSpan) Text   | -- | An error in a situation that should never happen, and indicates a bug.     InvariantViolation Text   | TestInfoNotFound@@ -26,7 +26,7 @@ instance Exception SkeletestError where   displayException =     Text.unpack . \case-      CompilationError msg ->+      CompilationError _ msg ->         Text.unlines           [ ""           , "******************** skeletest failure ********************"@@ -46,10 +46,8 @@       SnapshotFileCorrupted fp ->         "Snapshot file was corrupted: " <> Text.pack fp -skeletestPluginError :: String -> a-skeletestPluginError = pgmError . stripEnd . displayException . CompilationError . Text.pack-  where-    stripEnd = Text.unpack . Text.stripEnd . Text.pack+skeletestPluginError :: Maybe GHC.SrcSpan -> String -> a+skeletestPluginError mloc = impureThrow . CompilationError mloc . Text.pack  invariantViolation :: String -> a invariantViolation = impureThrow . InvariantViolation . Text.pack
src/Skeletest/Internal/Fixtures.hs view
@@ -15,6 +15,7 @@   cleanupFixtures,    -- * Built-in fixtures+  FixtureSkeletestTmpDir (..),   FixtureTmpDir (..), ) where @@ -30,17 +31,22 @@ import Data.Proxy (Proxy (..)) import Data.Text qualified as Text import Data.Typeable (TypeRep, Typeable, eqT, typeOf, typeRep, (:~:) (Refl))-import System.Directory (createDirectory, getTemporaryDirectory, removePathForcibly)-import System.FilePath ((</>))-import System.IO.Unsafe (unsafePerformIO)-import UnliftIO.Exception (throwIO, tryAny)- import Skeletest.Internal.Error (SkeletestError (..), invariantViolation) import Skeletest.Internal.TestInfo (   TestInfo (testFile),   getTestInfo,  ) import Skeletest.Internal.Utils.Map qualified as Map.Utils+import System.Directory (+  createDirectory,+  createDirectoryIfMissing,+  getTemporaryDirectory,+  removePathForcibly,+ )+import System.FilePath ((</>))+import System.IO.Unsafe (unsafePerformIO)+import System.Process (getCurrentPid)+import UnliftIO.Exception (throwIO, tryAny)  class (Typeable a) => Fixture a where   -- | The scope of the fixture, defaults to per-test@@ -115,11 +121,11 @@       result@(fixture, _) <- fixtureAction @a       modifyFixtureRegistry $ \registry -> (insertFixture (FixtureLoaded result) registry, ())       pure fixture-  where-    rep = typeRep (Proxy @a)-    isInProgress = \case-      FixtureInProgress -> True-      _ -> False+ where+  rep = typeRep (Proxy @a)+  isInProgress = \case+    FixtureInProgress -> True+    _ -> False  -- | Clean up fixtures in the given scope. --@@ -144,11 +150,11 @@   case catMaybes errors of     e : _ -> throwIO e     [] -> pure ()-  where-    (getScopedFixtures, updateScopedFixtures) = getScopedAccessors scopeKey-    fromLeft = \case-      Left x -> Just x-      Right _ -> Nothing+ where+  (getScopedFixtures, updateScopedFixtures) = getScopedAccessors scopeKey+  fromLeft = \case+    Left x -> Just x+    Right _ -> Nothing  {----- Fixtures registry -----} @@ -167,23 +173,23 @@  fixtureRegistryRef :: IORef FixtureRegistry fixtureRegistryRef = unsafePerformIO $ newIORef emptyFixtureRegistry-  where-    emptyFixtureRegistry =-      FixtureRegistry-        { sessionFixtures = OMap.empty-        , fileFixtures = Map.empty-        , testFixtures = Map.empty-        }+ where+  emptyFixtureRegistry =+    FixtureRegistry+      { sessionFixtures = OMap.empty+      , fileFixtures = Map.empty+      , testFixtures = Map.empty+      } {-# NOINLINE fixtureRegistryRef #-}  modifyFixtureRegistry :: (FixtureRegistry -> (FixtureRegistry, a)) -> IO a modifyFixtureRegistry = atomicModifyIORef fixtureRegistryRef  getScopedAccessors ::-  FixtureScopeKey-  -> ( FixtureRegistry -> FixtureMap-     , (FixtureMap -> FixtureMap) -> FixtureRegistry -> FixtureRegistry-     )+  FixtureScopeKey ->+  ( FixtureRegistry -> FixtureMap+  , (FixtureMap -> FixtureMap) -> FixtureRegistry -> FixtureRegistry+  ) getScopedAccessors scopeKey =   case scopeKey of     PerTestFixtureKey tid ->@@ -201,13 +207,27 @@  {----- Built-in fixtures -----} +-- | A fixture that provides a global temporary directory for internal Skeletest use.+newtype FixtureSkeletestTmpDir = FixtureSkeletestTmpDir FilePath++instance Fixture FixtureSkeletestTmpDir where+  fixtureScope = PerSessionFixture+  fixtureAction = do+    tmpdir <- getTemporaryDirectory+    pid <- getCurrentPid+    let dir = tmpdir </> ("skeletest-tmp-dir." <> show pid)+    removePathForcibly dir+    createDirectoryIfMissing True dir+    pure . withCleanup (FixtureSkeletestTmpDir dir) $+      removePathForcibly dir+ -- | A fixture that provides a temporary directory that can be used in a test. newtype FixtureTmpDir = FixtureTmpDir FilePath  instance Fixture FixtureTmpDir where   fixtureAction = do-    tmpdir <- getTemporaryDirectory-    let dir = tmpdir </> "skeletest-tmp-dir"+    FixtureSkeletestTmpDir tmpdir <- getFixture+    let dir = tmpdir </> "test-tmp-dir"     removePathForcibly dir     createDirectory dir     pure . withCleanup (FixtureTmpDir dir) $
src/Skeletest/Internal/GHC.hs view
@@ -36,6 +36,7 @@   hsExprLam,   hsExprCase,   getExpr,+  getLoc,   renderHsExpr,    -- ** Types@@ -51,11 +52,13 @@   getHsName, ) where +import Control.Monad.Catch (handleJust) import Control.Monad.Trans.Class qualified as Trans import Control.Monad.Trans.State (StateT, evalStateT) import Control.Monad.Trans.State qualified as State import Data.Data (Data) import Data.Data qualified as Data+import Data.List.NonEmpty qualified as NonEmpty import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe (fromMaybe)@@ -70,13 +73,15 @@   IsPass,   unLoc,  )-import GHC qualified as GHC+import GHC qualified import GHC.Driver.Main qualified as GHC import GHC.Plugins qualified as GHC hiding (getHscEnv)+import GHC.Tc.Errors.Types qualified as GHC import GHC.Tc.Utils.Monad qualified as GHC import GHC.Types.Name qualified as GHC.Name import GHC.Types.Name.Cache qualified as GHC (NameCache) import GHC.Types.SourceText qualified as GHC.SourceText+import GHC.Utils.Error qualified as GHC import Language.Haskell.TH.Syntax qualified as TH import System.IO.Unsafe (unsafePerformIO) @@ -84,7 +89,10 @@ import Data.Foldable (foldl') #endif -import Skeletest.Internal.Error (invariantViolation, skeletestPluginError)+import Skeletest.Internal.Error (+  SkeletestError (CompilationError),+  invariantViolation,+ ) import Skeletest.Internal.GHC.Compat (genLoc) import Skeletest.Internal.GHC.Compat qualified as GHC.Compat @@ -132,12 +140,12 @@         group' <- runCompileRn $ modifyModuleExprs (onRename ctx moduleName) group         pure (gblEnv, group')     }-  where-    getModuleName GHC.Module{moduleName} = Text.pack $ GHC.moduleNameString moduleName+ where+  getModuleName GHC.Module{moduleName} = Text.pack $ GHC.moduleNameString moduleName -    modifyParsedResultModule f x = x{GHC.parsedResultModule = f $ GHC.parsedResultModule x}-    modifyHpmModule f x = x{GHC.hpm_module = f $ GHC.hpm_module x}-    modifyModDecls f x = x{GHC.hsmodDecls = f $ GHC.hsmodDecls x}+  modifyParsedResultModule f x = x{GHC.parsedResultModule = f $ GHC.parsedResultModule x}+  modifyHpmModule f x = x{GHC.hpm_module = f $ GHC.hpm_module x}+  modifyModDecls f x = x{GHC.hsmodDecls = f $ GHC.hsmodDecls x}  {----- ParsedModule -----} @@ -159,29 +167,29 @@         | Just funName <- map (getValName . unLoc) hsmodDecls         ]     }-  where-    getValName = \case-      GHC.ValD _ GHC.FunBind{fun_id} -> Just . hsGhcName . unLoc $ fun_id-      _ -> Nothing+ where+  getValName = \case+    GHC.ValD _ GHC.FunBind{fun_id} -> Just . hsGhcName . unLoc $ fun_id+    _ -> Nothing  {----- modifyModuleExprs -----}  modifyModuleExprs ::   forall m.   (MonadCompile m GhcRn) =>-  (HsExpr GhcRn -> HsExpr GhcRn)-  -> GHC.HsGroup GhcRn-  -> m (GHC.HsGroup GhcRn)+  (HsExpr GhcRn -> HsExpr GhcRn) ->+  GHC.HsGroup GhcRn ->+  m (GHC.HsGroup GhcRn) modifyModuleExprs f = go-  where-    go :: (Data a) => a -> m a-    go = Data.gmapM $ \x -> updateExpr x >>= go+ where+  go :: (Data a) => a -> m a+  go = Data.gmapM $ \x -> updateExpr x >>= go -    updateExpr :: (Data a) => a -> m a-    updateExpr (x :: a) =-      case Typeable.eqT @(GHC.LHsExpr GhcRn) @a of-        Just Typeable.Refl -> compileHsExpr . f . parseHsExpr $ x-        Nothing -> pure x+  updateExpr :: (Data a) => a -> m a+  updateExpr (x :: a) =+    case Typeable.eqT @(GHC.LHsExpr GhcRn) @a of+      Just Typeable.Refl -> compileHsExpr . f . parseHsExpr $ x+      Nothing -> pure x  {----- HsExpr -----} @@ -219,6 +227,14 @@ getExpr :: HsExpr p -> HsExprData p getExpr HsExprUnsafe{hsExpr} = hsExpr +getLoc :: HsExpr p -> Maybe GHC.SrcSpan+getLoc HsExprUnsafe{ghcExpr} = getLoc' <$> ghcExpr+ where+  getLoc' :: GhcLHsExpr p -> GHC.SrcSpan+  getLoc' = \case+    GhcLHsExprPs e -> GHC.getLocA e+    GhcLHsExprRn e -> GHC.getLocA e+ renderHsExpr :: HsExpr GhcRn -> Text renderHsExpr = \case   HsExprUnsafe{ghcExpr = Just e} -> Text.pack $ show e@@ -257,35 +273,35 @@  parseHsExpr :: GHC.LHsExpr GhcRn -> HsExpr GhcRn parseHsExpr = goExpr-  where-    goExpr e =-      HsExprUnsafe-        { ghcExpr = Just $ GhcLHsExprRn e-        , hsExpr = goData e-        }+ where+  goExpr e =+    HsExprUnsafe+      { ghcExpr = Just $ GhcLHsExprRn e+      , hsExpr = goData e+      } -    goData = \case-      L _ (GHC.HsVar _ (L _ name)) ->-        if (GHC.occNameSpace . GHC.occName) name == GHC.Name.dataName-          then HsExprCon (hsGhcName name)-          else HsExprVar (hsGhcName name)-      e@(L _ GHC.HsApp{}) ->-        let (f, xs) = collectApps e-         in HsExprApps (goExpr f) (map goExpr xs)-      L _ (GHC.OpApp _ lhs op rhs) ->-        HsExprOp (goExpr lhs) (goExpr op) (goExpr rhs)-      L _ (GHC.RecordCon _ conName GHC.HsRecFields{rec_flds}) ->-        HsExprRecordCon (hsGhcName $ unLoc conName) $ map (getRecField . unLoc) rec_flds-      L _ par@GHC.HsPar{} -> goData $ GHC.Compat.unHsPar par-      _ -> HsExprOther+  goData = \case+    L _ (GHC.HsVar _ name) ->+      if (GHC.occNameSpace . GHC.getOccName) (unLoc name) == GHC.Name.dataName+        then HsExprCon (hsGhcName . GHC.Compat.unLocWithUserRdr $ name)+        else HsExprVar (hsGhcName . GHC.Compat.unLocWithUserRdr $ name)+    e@(L _ GHC.HsApp{}) ->+      let (f, xs) = collectApps e+       in HsExprApps (goExpr f) (map goExpr xs)+    L _ (GHC.OpApp _ lhs op rhs) ->+      HsExprOp (goExpr lhs) (goExpr op) (goExpr rhs)+    L _ (GHC.RecordCon _ conName GHC.HsRecFields{rec_flds}) ->+      HsExprRecordCon (hsGhcName . GHC.Compat.unLocWithUserRdr $ conName) $ map (getRecField . unLoc) rec_flds+    L _ par@GHC.HsPar{} -> goData $ GHC.Compat.unHsPar par+    _ -> HsExprOther -    getRecField GHC.HsFieldBind{hfbLHS = field, hfbRHS = expr} =-      (hsGhcName . unLoc . GHC.Compat.foLabel . unLoc $ field, goExpr expr)+  getRecField GHC.HsFieldBind{hfbLHS = field, hfbRHS = expr} =+    (hsGhcName . unLoc . GHC.Compat.foLabel . unLoc $ field, goExpr expr) -    -- Collect an application of the form `((f a) b) c` and return `f [a, b, c]`-    collectApps = \case-      L _ (GHC.HsApp _ l r) -> let (f, xs) = collectApps l in (f, xs <> [r])-      e -> (e, [])+  -- Collect an application of the form `((f a) b) c` and return `f [a, b, c]`+  collectApps = \case+    L _ (GHC.HsApp _ l r) -> let (f, xs) = collectApps l in (f, xs <> [r])+    e -> (e, [])  {----- HsType -----} @@ -324,15 +340,15 @@ fromTHName nameCache name =   case unsafePerformIO $ GHC.thNameToGhcNameIO nameCache name of     Just n -> n-    Nothing -> skeletestPluginError $ "Could not get Name for `" <> show name <> "`"+    Nothing -> invariantViolation $ "Could not get Name for `" <> show name <> "`"  matchesNameImpl :: GHC.NameCache -> HsName GhcRn -> HsName GhcRn -> Bool matchesNameImpl nameCache n1 n2 = fromMaybe False $ (==) <$> go n1 <*> go n2-  where-    go = \case-      HsName name -> Just $ fromTHName nameCache name-      HsVarName _ -> Nothing -- new names will never match-      HsGhcName name -> Just $ unGhcIdP name+ where+  go = \case+    HsName name -> Just $ fromTHName nameCache name+    HsVarName _ -> Nothing -- new names will never match+    HsGhcName name -> Just $ unGhcIdP name  getHsName :: HsName p -> Text getHsName = \case@@ -365,8 +381,19 @@   deriving (Functor, Applicative, Monad)  runCompileRn :: CompileRn a -> GHC.TcM a-runCompileRn (CompileRn m) = evalStateT m Map.empty+runCompileRn (CompileRn m) = handleCompilationError $ evalStateT m Map.empty+ where+  handleCompilationError =+    handleJust+      ( \case+          CompilationError mloc msg -> Just $ do+            GHC.failAt (fromMaybe GHC.noSrcSpan mloc) $ mkTcError msg+          _ -> Nothing+      )+      id +  mkTcError = GHC.mkTcRnUnknownMessage . GHC.mkPlainError GHC.noHints . GHC.text . Text.unpack+ instance MonadHasNameCache CompileRn where   getNameCache = GHC.hsc_NC . GHC.env_top <$> (CompileRn . Trans.lift) GHC.getEnv instance MonadCompileName CompileRn GhcRn where@@ -383,8 +410,8 @@ compileHsName ::   forall p m.   (GHC.IsPass p, MonadCompile m (GhcPass p)) =>-  HsName (GhcPass p)-  -> m (GHC.IdP (GhcPass p))+  HsName (GhcPass p) ->+  m (GHC.IdP (GhcPass p)) compileHsName = \case   HsName name -> do     nameCache <- getNameCache@@ -410,163 +437,163 @@                 , m_grhss =                     GHC.GRHSs                       { grhssExt = GHC.emptyComments-                      , grhssGRHSs = [genLoc $ GHC.GRHS GHC.noAnn [] body]+                      , grhssGRHSs = GHC.Compat.toGrhssGRHSs $ genLoc (GHC.GRHS GHC.noAnn [] body) NonEmpty.:| []                       , grhssLocalBinds = GHC.EmptyLocalBinds GHC.noExtField                       }                 }           ]     ]-  where-    mkSigD name ty =-      genLoc-        . GHC.SigD GHC.noExtField-        . GHC.TypeSig GHC.noAnn [genLoc name]-        . GHC.HsWC GHC.noExtField-        . genLoc-        $ GHC.HsSig GHC.noExtField (GHC.HsOuterImplicit GHC.noExtField) ty+ where+  mkSigD name ty =+    genLoc+      . GHC.SigD GHC.noExtField+      . GHC.TypeSig GHC.noAnn [genLoc name]+      . GHC.HsWC GHC.noExtField+      . genLoc+      $ GHC.HsSig GHC.noExtField (GHC.HsOuterImplicit GHC.noExtField) ty  compileHsType :: (MonadCompile m GhcPs) => HsType GhcPs -> m (GHC.LHsType GhcPs) compileHsType = go-  where-    go = \case-      HsTypeCon name -> do-        genLoc . GHC.HsTyVar GHC.noAnn GHC.NotPromoted . genLoc <$> compileHsName name-      HsTypeApps ty0 tys -> do-        ty0' <- go ty0-        tys' <- mapM go tys-        pure $ foldl' (\l r -> genLoc $ GHC.HsAppTy GHC.noExtField l r) ty0' tys'-      HsTypeTuple tys -> do-        tys' <- mapM go tys-        pure . genLoc $ GHC.HsTupleTy GHC.noAnn GHC.HsBoxedOrConstraintTuple tys'+ where+  go = \case+    HsTypeCon name -> do+      genLoc . GHC.HsTyVar GHC.noAnn GHC.NotPromoted . genLoc <$> compileHsName name+    HsTypeApps ty0 tys -> do+      ty0' <- go ty0+      tys' <- mapM go tys+      pure $ foldl' (\l r -> genLoc $ GHC.HsAppTy GHC.noExtField l r) ty0' tys'+    HsTypeTuple tys -> do+      tys' <- mapM go tys+      pure . genLoc $ GHC.HsTupleTy GHC.noAnn GHC.HsBoxedOrConstraintTuple tys'  compileHsPat ::   forall p m.   (IsPass p, MonadCompile m (GhcPass p)) =>-  HsPat (GhcPass p)-  -> m (GHC.LPat (GhcPass p))+  HsPat (GhcPass p) ->+  m (GHC.LPat (GhcPass p)) compileHsPat = go-  where-    go = \case-      HsPatCon conName args -> do-        conName' <- fromConName conName-        con <- GHC.PrefixCon [] <$> mapM go args-        pure . genLoc $-          GHC.ConPat-            (onPsOrRn @p GHC.noAnn GHC.noExtField)-            conName'-            con-      HsPatVar name -> do-        name' <- onPsOrRn @p genLoc genLoc <$> compileHsName name-        pure . genLoc $ GHC.VarPat GHC.noExtField name'-      HsPatRecord conName fields -> do-        conName' <- fromConName conName-        con <- GHC.RecCon <$> compileRecFields go fields-        pure . genLoc $-          GHC.ConPat-            (onPsOrRn @p GHC.noAnn GHC.noExtField)-            conName'-            con-      HsPatWild -> do-        pure . genLoc $ GHC.WildPat $ onPsOrRn @p GHC.noExtField GHC.noExtField+ where+  go = \case+    HsPatCon conName args -> do+      conName' <- fromConName conName+      con <- GHC.Compat.mkPrefixCon <$> mapM go args+      pure . genLoc $+        GHC.ConPat+          (onPsOrRn @p GHC.noAnn GHC.noExtField)+          conName'+          con+    HsPatVar name -> do+      name' <- onPsOrRn @p genLoc genLoc <$> compileHsName name+      pure . genLoc $ GHC.VarPat GHC.noExtField name'+    HsPatRecord conName fields -> do+      conName' <- fromConName conName+      con <- GHC.RecCon <$> compileRecFields go fields+      pure . genLoc $+        GHC.ConPat+          (onPsOrRn @p GHC.noAnn GHC.noExtField)+          conName'+          con+    HsPatWild -> do+      pure . genLoc $ GHC.WildPat $ onPsOrRn @p GHC.noExtField GHC.noExtField -    fromConName = fmap (onPsOrRn @p genLoc genLoc) . compileHsName+  fromConName = fmap (genLocConLikeP @p) . compileHsName  compileHsExpr ::   forall p m.   (IsPass p, MonadCompile m (GhcPass p)) =>-  HsExpr (GhcPass p)-  -> m (GHC.LHsExpr (GhcPass p))+  HsExpr (GhcPass p) ->+  m (GHC.LHsExpr (GhcPass p)) compileHsExpr = goExpr-  where-    goExpr :: HsExpr (GhcPass p) -> m (GHC.LHsExpr (GhcPass p))-    goExpr = \case-      HsExprUnsafe{ghcExpr = Just e} -> pure $ unGhcLHsExpr e-      HsExprUnsafe{hsExpr = e} -> goData e+ where+  goExpr :: HsExpr (GhcPass p) -> m (GHC.LHsExpr (GhcPass p))+  goExpr = \case+    HsExprUnsafe{ghcExpr = Just e} -> pure $ unGhcLHsExpr e+    HsExprUnsafe{hsExpr = e} -> goData e -    goData :: HsExprData (GhcPass p) -> m (GHC.LHsExpr (GhcPass p))-    goData = \case-      HsExprCon name -> do-        genLoc . GHC.HsVar GHC.noExtField . genLocIdP @p <$> compileHsName name-      HsExprVar name -> do-        genLoc . GHC.HsVar GHC.noExtField . genLocIdP @p <$> compileHsName name-      HsExprApps f xs -> do-        f' <- goExpr f-        xs' <- mapM goExpr xs-        pure $ foldl' (\l r -> genLoc $ GHC.Compat.hsApp l r) (parens f') (map parens xs')-      HsExprOp _ _ _ ->-        invariantViolation "Compiling HsExprOp not yet supported"-      HsExprList exprs -> do-        exprs' <- mapM goExpr exprs-        pure . genLoc $-          GHC.ExplicitList-            (onPsOrRn @p GHC.noAnn GHC.noExtField)-            exprs'-      HsExprRecordCon con fields -> do-        con' <- genLocConLikeP @p <$> compileHsName con-        fields' <- compileRecFields goExpr fields-        pure . genLoc $-          GHC.RecordCon-            (onPsOrRn @p GHC.noAnn GHC.noExtField)-            con'-            fields'-      HsExprLitString s -> do-        pure . genLoc . GHC.Compat.hsLit $-          GHC.HsString GHC.SourceText.NoSourceText (fsText s)-      HsExprLam pats expr -> do-        pats' <- mapM compileHsPat pats-        expr' <- goExpr expr-        pure . genLoc . GHC.Compat.hsLamSingle $-          GHC.MG origin . genLoc $-            [ genLoc $+  goData :: HsExprData (GhcPass p) -> m (GHC.LHsExpr (GhcPass p))+  goData = \case+    HsExprCon name -> do+      genLoc . GHC.HsVar GHC.noExtField . genLocIdP @p <$> compileHsName name+    HsExprVar name -> do+      genLoc . GHC.HsVar GHC.noExtField . genLocIdP @p <$> compileHsName name+    HsExprApps f xs -> do+      f' <- goExpr f+      xs' <- mapM goExpr xs+      pure $ foldl' (\l r -> genLoc $ GHC.Compat.hsApp l r) (parens f') (map parens xs')+    HsExprOp _ _ _ ->+      invariantViolation "Compiling HsExprOp not yet supported"+    HsExprList exprs -> do+      exprs' <- mapM goExpr exprs+      pure . genLoc $+        GHC.ExplicitList+          (onPsOrRn @p GHC.noAnn GHC.noExtField)+          exprs'+    HsExprRecordCon con fields -> do+      con' <- genLocConLikeP @p <$> compileHsName con+      fields' <- compileRecFields goExpr fields+      pure . genLoc $+        GHC.RecordCon+          (onPsOrRn @p GHC.noAnn GHC.noExtField)+          con'+          fields'+    HsExprLitString s -> do+      pure . genLoc . GHC.Compat.hsLit $+        GHC.HsString GHC.SourceText.NoSourceText (fsText s)+    HsExprLam pats expr -> do+      pats' <- mapM compileHsPat pats+      expr' <- goExpr expr+      pure . genLoc . GHC.Compat.hsLamSingle $+        GHC.MG origin . genLoc $+          [ genLoc $+              GHC.Match+                { m_ext = GHC.Compat.xMatch+                , m_ctxt = GHC.Compat.lamAltSingle+                , m_pats = GHC.Compat.toMatchArgs pats'+                , m_grhss =+                    GHC.GRHSs+                      { grhssExt = GHC.emptyComments+                      , grhssGRHSs = GHC.Compat.toGrhssGRHSs $ genLoc (GHC.GRHS GHC.noAnn [] expr') NonEmpty.:| []+                      , grhssLocalBinds = GHC.EmptyLocalBinds GHC.noExtField+                      }+                }+          ]+    HsExprCase expr matches -> do+      expr' <- goExpr expr+      matches' <-+        sequence+          [ do+              pat' <- compileHsPat pat+              body' <- goExpr body+              pure . genLoc $                 GHC.Match                   { m_ext = GHC.Compat.xMatch-                  , m_ctxt = GHC.Compat.lamAltSingle-                  , m_pats = GHC.Compat.toMatchArgs pats'+                  , m_ctxt = GHC.CaseAlt+                  , m_pats = GHC.Compat.toMatchArgs [pat']                   , m_grhss =                       GHC.GRHSs                         { grhssExt = GHC.emptyComments-                        , grhssGRHSs = [genLoc $ GHC.GRHS GHC.noAnn [] expr']+                        , grhssGRHSs = GHC.Compat.toGrhssGRHSs $ genLoc (GHC.GRHS GHC.noAnn [] body') NonEmpty.:| []                         , grhssLocalBinds = GHC.EmptyLocalBinds GHC.noExtField                         }                   }-            ]-      HsExprCase expr matches -> do-        expr' <- goExpr expr-        matches' <--          sequence-            [ do-                pat' <- compileHsPat pat-                body' <- goExpr body-                pure . genLoc $-                  GHC.Match-                    { m_ext = GHC.Compat.xMatch-                    , m_ctxt = GHC.CaseAlt-                    , m_pats = GHC.Compat.toMatchArgs [pat']-                    , m_grhss =-                        GHC.GRHSs-                          { grhssExt = GHC.emptyComments-                          , grhssGRHSs = [genLoc $ GHC.GRHS GHC.noAnn [] body']-                          , grhssLocalBinds = GHC.EmptyLocalBinds GHC.noExtField-                          }-                    }-            | (pat, body) <- matches-            ]-        pure-          . genLoc-          . GHC.HsCase (onPsOrRn @p GHC.noAnn GHC.CaseAlt) expr'-          $ GHC.MG origin (genLoc matches')-      HsExprOther ->-        invariantViolation "Compiling HsExprOther not supported"+          | (pat, body) <- matches+          ]+      pure+        . genLoc+        . GHC.HsCase (onPsOrRn @p GHC.noAnn GHC.CaseAlt) expr'+        $ GHC.MG origin (genLoc matches')+    HsExprOther ->+      invariantViolation "Compiling HsExprOther not supported" -    origin = onPsOrRn @p GHC.FromSource GHC.FromSource+  origin = onPsOrRn @p GHC.FromSource GHC.FromSource -    parens :: (IsPass p) => GHC.LHsExpr (GhcPass p) -> GHC.LHsExpr (GhcPass p)-    parens = \case-      e@(L _ GHC.HsPar{}) -> e-      e@(L _ GHC.HsApp{}) -> genLoc $ GHC.Compat.hsPar e-      e@(L _ GHC.SectionL{}) -> genLoc $ GHC.Compat.hsPar e-      e@(L _ GHC.SectionR{}) -> genLoc $ GHC.Compat.hsPar e-      e -> e+  parens :: (IsPass p) => GHC.LHsExpr (GhcPass p) -> GHC.LHsExpr (GhcPass p)+  parens = \case+    e@(L _ GHC.HsPar{}) -> e+    e@(L _ GHC.HsApp{}) -> genLoc $ GHC.Compat.hsPar e+    e@(L _ GHC.SectionL{}) -> genLoc $ GHC.Compat.hsPar e+    e@(L _ GHC.SectionR{}) -> genLoc $ GHC.Compat.hsPar e+    e -> e  {----- FastString -----} @@ -625,9 +652,9 @@ compileRecFields ::   forall p m arg x.   (IsPass p, MonadCompile m (GhcPass p)) =>-  (x -> m arg)-  -> [(HsName (GhcPass p), x)]-  -> m (GHC.HsRecFields (GhcPass p) arg)+  (x -> m arg) ->+  [(HsName (GhcPass p), x)] ->+  m (GHC.HsRecFields (GhcPass p) arg) compileRecFields f fields = do   fields' <-     sequence@@ -644,27 +671,27 @@       | (field, x) <- fields       ]   pure $ GHC.Compat.mkHsRecFields fields'-  where-    compileFieldOcc field = do-      name <- compileHsName field-      pure $-        onPsOrRn @p-          GHC.FieldOcc-            { foExt = GHC.noExtField-            , foLabel = genLoc name-            }-          (GHC.Compat.fieldOccRn name)+ where+  compileFieldOcc field = do+    name <- compileHsName field+    pure $+      onPsOrRn @p+        GHC.FieldOcc+          { foExt = GHC.noExtField+          , foLabel = genLoc name+          }+        (GHC.Compat.fieldOccRn name)  genLocConLikeP ::   forall p.   (IsPass p) =>-  GHC.IdP (GhcPass p)-  -> GHC.XRec (GhcPass p) (GHC.ConLikeP (GhcPass p))-genLocConLikeP idp = onPsOrRn @p (genLoc idp) (genLoc idp)+  GHC.IdP (GhcPass p) ->+  GHC.XRec (GhcPass p) (GHC.ConLikeP (GhcPass p))+genLocConLikeP = onPsOrRn @p genLoc (genLoc . GHC.Compat.noUserRdr)  genLocIdP ::   forall p.   (IsPass p) =>-  GHC.IdP (GhcPass p)-  -> GHC.LIdP (GhcPass p)-genLocIdP idp = onPsOrRn @p (genLoc idp) (genLoc idp)+  GHC.IdP (GhcPass p) ->+  GHC.Compat.LIdOccP (GhcPass p)+genLocIdP = onPsOrRn @p genLoc (genLoc . GHC.Compat.noUserRdr)
src/Skeletest/Internal/GHC/Compat.hs view
@@ -8,4 +8,6 @@ import Skeletest.Internal.GHC.Compat_9_10 as X #elif __GLASGOW_HASKELL__ == 912 import Skeletest.Internal.GHC.Compat_9_12 as X+#elif __GLASGOW_HASKELL__ == 914+import Skeletest.Internal.GHC.Compat_9_14 as X #endif
src/Skeletest/Internal/GHC/Compat_9_10.hs view
@@ -6,10 +6,11 @@ ) where  import Data.Data (toConstr)+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NonEmpty import GHC hiding (FieldOcc (..), mkPrefixFunRhs) import GHC qualified import GHC.Types.Name.Reader (getRdrName)- import Skeletest.Internal.Error (invariantViolation)  hsLamSingle :: MatchGroup (GhcPass p) (LHsExpr (GhcPass p)) -> HsExpr (GhcPass p)@@ -57,6 +58,14 @@     , GHC.foLabel = genLoc $ getRdrName name     } +type LIdOccP a = GHC.LIdP a++noUserRdr :: Name -> Name+noUserRdr = id++unLocWithUserRdr :: GenLocated l Name -> Name+unLocWithUserRdr = unLoc+ hsApp :: LHsExpr (GhcPass p) -> LHsExpr (GhcPass p) -> HsExpr (GhcPass p) hsApp = HsApp noExtField @@ -68,3 +77,9 @@  toMatchArgs :: [LPat p] -> [LPat p] toMatchArgs = id++toGrhssGRHSs :: NonEmpty (LGRHS p body) -> [LGRHS p body]+toGrhssGRHSs = NonEmpty.toList++mkPrefixCon :: [arg] -> HsConDetails tyargs arg rec+mkPrefixCon = PrefixCon []
src/Skeletest/Internal/GHC/Compat_9_12.hs view
@@ -7,10 +7,11 @@ ) where  import Data.Data (toConstr)+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NonEmpty import GHC hiding (FieldOcc (..)) import GHC qualified import GHC.Types.Name.Reader (getRdrName)- import Skeletest.Internal.Error (invariantViolation)  hsLamSingle :: MatchGroup (GhcPass p) (LHsExpr (GhcPass p)) -> HsExpr (GhcPass p)@@ -44,8 +45,8 @@ mkHsRecFields ::   forall p arg.   (IsPass p) =>-  [LHsRecField (GhcPass p) arg]-  -> HsRecFields (GhcPass p) arg+  [LHsRecField (GhcPass p) arg] ->+  HsRecFields (GhcPass p) arg mkHsRecFields fields =   GHC.HsRecFields     { rec_ext =@@ -67,6 +68,14 @@     , GHC.foLabel = genLoc name     } +type LIdOccP a = GHC.LIdP a++noUserRdr :: Name -> Name+noUserRdr = id++unLocWithUserRdr :: GenLocated l Name -> Name+unLocWithUserRdr = unLoc+ hsApp :: LHsExpr (GhcPass p) -> LHsExpr (GhcPass p) -> HsExpr (GhcPass p) hsApp = HsApp noExtField @@ -75,3 +84,9 @@  toMatchArgs :: [LPat p] -> LocatedE [LPat p] toMatchArgs = genLoc++toGrhssGRHSs :: NonEmpty (LGRHS p body) -> [LGRHS p body]+toGrhssGRHSs = NonEmpty.toList++mkPrefixCon :: [arg] -> HsConDetails tyargs arg rec+mkPrefixCon = PrefixCon []
+ src/Skeletest/Internal/GHC/Compat_9_14.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}++module Skeletest.Internal.GHC.Compat_9_14 (+  module Skeletest.Internal.GHC.Compat_9_14,+  mkPrefixFunRhs,+) where++import Data.Data (toConstr)+import Data.List.NonEmpty (NonEmpty)+import GHC hiding (FieldOcc (..))+import GHC qualified+import GHC.Types.Name.Reader (getRdrName)+import GHC.Types.Name.Reader qualified as GHC++import Skeletest.Internal.Error (invariantViolation)++hsLamSingle :: MatchGroup (GhcPass p) (LHsExpr (GhcPass p)) -> HsExpr (GhcPass p)+hsLamSingle = HsLam noAnn LamSingle++lamAltSingle :: HsMatchContext fn+lamAltSingle = LamAlt LamSingle++hsLit :: HsLit (GhcPass p) -> HsExpr (GhcPass p)+hsLit = HsLit noExtField++hsPar :: forall p. (IsPass p) => LHsExpr (GhcPass p) -> HsExpr (GhcPass p)+hsPar =+  HsPar $+    case ghcPass @p of+      GhcPs -> noAnn+      GhcRn -> noExtField+      GhcTc -> invariantViolation "hsPar called in GhcTc"++unHsPar :: HsExpr GhcRn -> LHsExpr GhcRn+unHsPar = \case+  HsPar _ e -> e+  e -> invariantViolation $ "unHsPar called on " <> (show . toConstr) e++hsTupPresent :: LHsExpr (GhcPass p) -> HsTupArg (GhcPass p)+hsTupPresent = Present noExtField++xMatch :: XCMatch (GhcPass p) b+xMatch = noExtField++mkHsRecFields ::+  forall p arg.+  (IsPass p) =>+  [LHsRecField (GhcPass p) arg] ->+  HsRecFields (GhcPass p) arg+mkHsRecFields fields =+  GHC.HsRecFields+    { rec_ext =+        case ghcPass @p of+          GhcPs -> noExtField+          GhcRn -> noExtField+          GhcTc -> invariantViolation "mkHsRecFields called in GhcTc"+    , rec_flds = fields+    , rec_dotdot = Nothing+    }++foLabel :: GHC.FieldOcc GhcRn -> LIdP GhcRn+foLabel = GHC.foLabel++fieldOccRn :: Name -> GHC.FieldOcc GhcRn+fieldOccRn name =+  GHC.FieldOcc+    { GHC.foExt = getRdrName name+    , GHC.foLabel = genLoc name+    }++type LIdOccP a = GHC.LIdOccP a++noUserRdr :: Name -> GHC.WithUserRdr Name+noUserRdr = GHC.noUserRdr++unLocWithUserRdr :: GenLocated l (GHC.WithUserRdr a) -> a+unLocWithUserRdr = GHC.unLocWithUserRdr++hsApp :: LHsExpr (GhcPass p) -> LHsExpr (GhcPass p) -> HsExpr (GhcPass p)+hsApp = HsApp noExtField++genLoc :: (NoAnn ann) => e -> GenLocated ann e+genLoc = L noAnn++toMatchArgs :: [LPat p] -> LocatedE [LPat p]+toMatchArgs = genLoc++toGrhssGRHSs :: NonEmpty (LGRHS p body) -> NonEmpty (LGRHS p body)+toGrhssGRHSs = id++mkPrefixCon :: [arg] -> HsConDetails arg rec+mkPrefixCon = PrefixCon
src/Skeletest/Internal/GHC/Compat_9_8.hs view
@@ -5,11 +5,12 @@ ) where  import Data.Data (toConstr)+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NonEmpty import GHC hiding (FieldOcc (..), mkPrefixFunRhs) import GHC qualified import GHC.Types.Name.Reader (getRdrName) import GHC.Types.SrcLoc- import Skeletest.Internal.Error (invariantViolation)  hsLamSingle :: MatchGroup (GhcPass p) (LHsExpr (GhcPass p)) -> HsExpr (GhcPass p)@@ -52,6 +53,14 @@     , GHC.foLabel = genLoc $ getRdrName name     } +type LIdOccP a = GHC.LIdP a++noUserRdr :: Name -> Name+noUserRdr = id++unLocWithUserRdr :: GenLocated l Name -> Name+unLocWithUserRdr = unLoc+ hsApp :: LHsExpr (GhcPass p) -> LHsExpr (GhcPass p) -> HsExpr (GhcPass p) hsApp = HsApp noAnn @@ -63,3 +72,9 @@  toMatchArgs :: [LPat p] -> [LPat p] toMatchArgs = id++toGrhssGRHSs :: NonEmpty (LGRHS p body) -> [LGRHS p body]+toGrhssGRHSs = NonEmpty.toList++mkPrefixCon :: [arg] -> HsConDetails tyargs arg rec+mkPrefixCon = PrefixCon []
src/Skeletest/Internal/Plugin.hs view
@@ -11,11 +11,6 @@ import Data.Functor.Const (Const (..)) import Data.Maybe (fromMaybe, listToMaybe) import Data.Text qualified as Text--#if !MIN_VERSION_base(4, 20, 0)-import Data.Foldable (foldl')-#endif- import Skeletest.Internal.Constants (mainFileSpecsListIdentifier) import Skeletest.Internal.Error (skeletestPluginError) import Skeletest.Internal.GHC@@ -24,6 +19,10 @@ import Skeletest.Main qualified as Main import Skeletest.Plugin qualified as Plugin +#if !MIN_VERSION_base(4, 20, 0)+import Data.Foldable (foldl')+#endif+ -- | The plugin to convert a module in the tests directory. -- Injected by the preprocessor. plugin :: Plugin@@ -44,38 +43,38 @@ -- | Add 'main' function. transformMainModule :: ParsedModule -> ParsedModule transformMainModule modl = modl{moduleFuncs = (hsVarName "main", Just mainFun) : moduleFuncs modl}-  where-    findVar name =-      fmap hsExprVar . listToMaybe $-        [ funName-        | (funName, _) <- moduleFuncs modl-        , getHsName funName == name-        ]+ where+  findVar name =+    fmap hsExprVar . listToMaybe $+      [ funName+      | (funName, _) <- moduleFuncs modl+      , getHsName funName == name+      ] -    cliFlagsExpr = fromMaybe (hsExprList []) $ findVar "cliFlags"-    snapshotRenderersExpr = fromMaybe (hsExprList []) $ findVar "snapshotRenderers"-    hooksExpr = fromMaybe (hsExprVar $ hsName 'Plugin.defaultHooks) $ findVar "hooks"-    pluginsExpr = fromMaybe (hsExprList []) $ findVar "plugins"+  cliFlagsExpr = fromMaybe (hsExprList []) $ findVar "cliFlags"+  snapshotRenderersExpr = fromMaybe (hsExprList []) $ findVar "snapshotRenderers"+  hooksExpr = fromMaybe (hsExprVar $ hsName 'Plugin.defaultHooks) $ findVar "hooks"+  pluginsExpr = fromMaybe (hsExprList []) $ findVar "plugins" -    mainFun =-      FunDef-        { funType = HsTypeApps (HsTypeCon $ hsName ''IO) [HsTypeTuple []]-        , funPats = []-        , funBody =-            hsExprApps-              (hsExprVar $ hsName 'Main.runSkeletest)-              [ hsExprApps (hsExprVar (hsName '(:))) $-                  [ hsExprRecordCon-                      (hsName 'Plugin.Plugin)-                      [ (hsName 'Plugin.cliFlags, cliFlagsExpr)-                      , (hsName 'Plugin.snapshotRenderers, snapshotRenderersExpr)-                      , (hsName 'Plugin.hooks, hooksExpr)-                      ]-                  , pluginsExpr-                  ]-              , hsExprVar $ hsVarName mainFileSpecsListIdentifier-              ]-        }+  mainFun =+    FunDef+      { funType = HsTypeApps (HsTypeCon $ hsName ''IO) [HsTypeTuple []]+      , funPats = []+      , funBody =+          hsExprApps+            (hsExprVar $ hsName 'Main.runSkeletest)+            [ hsExprApps (hsExprVar (hsName '(:))) $+                [ hsExprRecordCon+                    (hsName 'Plugin.Plugin)+                    [ (hsName 'Plugin.cliFlags, cliFlagsExpr)+                    , (hsName 'Plugin.snapshotRenderers, snapshotRenderersExpr)+                    , (hsName 'Plugin.hooks, hooksExpr)+                    ]+                , pluginsExpr+                ]+            , hsExprVar $ hsVarName mainFileSpecsListIdentifier+            ]+      }  transformTestModule :: Ctx -> HsExpr GhcRn -> HsExpr GhcRn transformTestModule ctx =@@ -95,7 +94,7 @@ --       User x0 x1 -> Just (HCons (pure x0) $ HCons (pure x1) $ HNil) --       _ -> Nothing --   )---   (HCons (H.eq "user1") $ HCons (P.contains "@") $ HNil)+--   (HCons (P.eq "user1") $ HCons (P.contains "@") $ HNil) -- -- P.con User{name = P.eq "user1", email = P.contains "@"} -- ====>@@ -125,80 +124,80 @@     -- Check if P.con is by itself     HsExprVar name       | isCon name ->-          skeletestPluginError "P.con must be applied to a constructor"+          skeletestPluginError (getLoc e) "P.con must be applied to a constructor"     -- Check if P.con is being applied more than once     HsExprApps (getExpr -> HsExprVar name) (_ : _ : _)       | isCon name ->-          skeletestPluginError "P.con must be applied to exactly one argument"+          skeletestPluginError (getLoc e) "P.con must be applied to exactly one argument"     _ -> e-  where-    isCon = matchesName ctx (hsName 'P.con)+ where+  isCon = matchesName ctx (hsName 'P.con) -    convertCon con =-      case getExpr con of-        HsExprCon conName -> convertPrefixCon conName []-        HsExprApps (getExpr -> HsExprCon conName) preds -> convertPrefixCon conName preds-        HsExprRecordCon conName fields -> convertRecordCon conName fields-        _ -> skeletestPluginError "P.con must be applied to a constructor"-    convertPrefixCon conName preds =-      let-        exprNames = mkVarNames preds-       in-        hsExprApps (hsExprVar $ hsName 'P.conMatches) $-          [ hsExprLitString $ getHsName conName-          , hsExprCon $ hsName 'Nothing-          , mkDeconstruct (HsPatCon conName $ map HsPatVar exprNames) exprNames-          , mkPredList preds-          ]-    convertRecordCon conName fields =-      let-        (fieldNames, preds) = unzip fields-        fieldPats = [(field, HsPatVar field) | field <- fieldNames]-       in-        hsExprApps (hsExprVar $ hsName 'P.conMatches) $-          [ hsExprLitString $ getHsName conName-          , hsExprApps (hsExprCon $ hsName 'Just) [mkNamesList fieldNames]-          , mkDeconstruct (HsPatRecord conName fieldPats) fieldNames-          , mkPredList preds-          ]+  convertCon con =+    case getExpr con of+      HsExprCon conName -> convertPrefixCon conName []+      HsExprApps (getExpr -> HsExprCon conName) preds -> convertPrefixCon conName preds+      HsExprRecordCon conName fields -> convertRecordCon conName fields+      _ -> skeletestPluginError (getLoc e) "P.con must be applied to a constructor"+  convertPrefixCon conName preds =+    let+      exprNames = mkVarNames preds+     in+      hsExprApps (hsExprVar $ hsName 'P.conMatches) $+        [ hsExprLitString $ getHsName conName+        , hsExprCon $ hsName 'Nothing+        , mkDeconstruct (HsPatCon conName $ map HsPatVar exprNames) exprNames+        , mkPredList preds+        ]+  convertRecordCon conName fields =+    let+      (fieldNames, preds) = unzip fields+      fieldPats = [(field, HsPatVar field) | field <- fieldNames]+     in+      hsExprApps (hsExprVar $ hsName 'P.conMatches) $+        [ hsExprLitString $ getHsName conName+        , hsExprApps (hsExprCon $ hsName 'Just) [mkNamesList fieldNames]+        , mkDeconstruct (HsPatRecord conName fieldPats) fieldNames+        , mkPredList preds+        ] -    -- Generate variable names like x0, x1, ... for each element in the given list.-    mkVarNames =-      let mkVar i = "x" <> (Text.pack . show) i-       in zipWith (\i _ -> hsVarName (mkVar i)) [0 :: Int ..]+  -- Generate variable names like x0, x1, ... for each element in the given list.+  mkVarNames =+    let mkVar i = "x" <> (Text.pack . show) i+     in zipWith (\i _ -> hsVarName (mkVar i)) [0 :: Int ..] -    -- Create the deconstruction function:-    ---    -- \actual ->-    --   case actual of-    --     User{name} -> Just (HCons (pure name) HNil)-    --     _ -> Nothing-    ---    -- However, if 'User' is the only constructor, GHC complains about the wildcard-    -- being redundant. So we'll obfuscate it a bit with-    ---    -- \actual ->-    --   case pure actual of-    --     Just User{name} -> Just (HCons (pure name) HNil)-    --     _ -> Nothing-    mkDeconstruct pat argNames =-      hsExprLam [HsPatVar $ hsVarName "actual"] $-        hsExprCase (hsExprApps (hsExprVar $ hsName 'pure) [hsExprVar $ hsVarName "actual"]) $-          [ (HsPatCon (hsName 'Just) [pat], hsExprApps (hsExprCon $ hsName 'Just) [mkValsList argNames])-          , (HsPatWild, hsExprCon $ hsName 'Nothing)-          ]+  -- Create the deconstruction function:+  --+  -- \actual ->+  --   case actual of+  --     User{name} -> Just (HCons (pure name) HNil)+  --     _ -> Nothing+  --+  -- However, if 'User' is the only constructor, GHC complains about the wildcard+  -- being redundant. So we'll obfuscate it a bit with+  --+  -- \actual ->+  --   case pure actual of+  --     Just User{name} -> Just (HCons (pure name) HNil)+  --     _ -> Nothing+  mkDeconstruct pat argNames =+    hsExprLam [HsPatVar $ hsVarName "actual"] $+      hsExprCase (hsExprApps (hsExprVar $ hsName 'pure) [hsExprVar $ hsVarName "actual"]) $+        [ (HsPatCon (hsName 'Just) [pat], hsExprApps (hsExprCon $ hsName 'Just) [mkValsList argNames])+        , (HsPatWild, hsExprCon $ hsName 'Nothing)+        ] -    mkHList f = \case-      [] -> hsExprCon (hsName 'HNil)-      x : xs ->-        hsExprApps (hsExprCon $ hsName 'HCons) $-          [ f x-          , mkHList f xs-          ]+  mkHList f = \case+    [] -> hsExprCon (hsName 'HNil)+    x : xs ->+      hsExprApps (hsExprCon $ hsName 'HCons) $+        [ f x+        , mkHList f xs+        ] -    mkNamesList = mkHList $ \name -> hsExprApps (hsExprCon $ hsName 'Const) [hsExprLitString $ getHsName name]-    mkValsList = mkHList $ \val -> hsExprApps (hsExprVar $ hsName 'pure) [hsExprVar val]-    mkPredList = mkHList id+  mkNamesList = mkHList $ \name -> hsExprApps (hsExprCon $ hsName 'Const) [hsExprLitString $ getHsName name]+  mkValsList = mkHList $ \val -> hsExprApps (hsExprVar $ hsName 'pure) [hsExprVar val]+  mkPredList = mkHList id  -- | Replace all uses of P.=== with inlined IsoChecker value, with -- function name filled in.@@ -213,6 +212,6 @@       | matchesName ctx (hsName '(P.===)) eqeqeq ->           inlineIsoChecker l r     _ -> e-  where-    inlineIsoChecker l r = hsExprApps (hsExprCon $ hsName 'P.IsoChecker) [mkFun l, mkFun r]-    mkFun f = hsExprApps (hsExprCon $ hsName 'P.Fun) [hsExprLitString $ renderHsExpr f, f]+ where+  inlineIsoChecker l r = hsExprApps (hsExprCon $ hsName 'P.IsoChecker) [mkFun l, mkFun r]+  mkFun f = hsExprApps (hsExprCon $ hsName 'P.Fun) [hsExprLitString $ renderHsExpr f, f]
src/Skeletest/Internal/Predicate.hs view
@@ -88,11 +88,6 @@ import Debug.RecoverRTTI (anythingToString) import GHC.Generics ((:*:) (..)) import GHC.Stack qualified as GHC-import UnliftIO (MonadUnliftIO)-import UnliftIO.Exception (Exception, displayException, try)-import Prelude hiding (abs, all, and, any, elem, not, or, (&&), (||))-import Prelude qualified- import Skeletest.Internal.CLI (getFlag) import Skeletest.Internal.Error (invariantViolation) import Skeletest.Internal.Snapshot (@@ -110,6 +105,10 @@ import Skeletest.Internal.Utils.HList qualified as HList import Skeletest.Prop.Gen (Gen) import Skeletest.Prop.Internal (PropertyM, forAll)+import UnliftIO (MonadUnliftIO)+import UnliftIO.Exception (Exception, displayException, try)+import Prelude hiding (abs, all, and, any, elem, not, or, (&&), (||))+import Prelude qualified  data Predicate m a = Predicate   { predicateFunc :: a -> m PredicateFuncResult@@ -205,7 +204,7 @@ {----- General -----}  -- | A predicate that matches any value-anything :: (Monad m) => Predicate m a+anything :: forall a m. (Monad m) => Predicate m a anything =   Predicate     { predicateFunc = \_ ->@@ -258,48 +257,48 @@ -- >>> Just 1 `shouldSatisfy` P.just (P.gt 0) just :: (Monad m) => Predicate m a -> Predicate m (Maybe a) just p = conMatches "Just" fieldNames toFields preds-  where-    fieldNames = Nothing-    toFields = \case-      Just x -> Just . HCons (pure x) $ HNil-      _ -> Nothing-    preds = HCons p HNil+ where+  fieldNames = Nothing+  toFields = \case+    Just x -> Just . HCons (pure x) $ HNil+    _ -> Nothing+  preds = HCons p HNil  -- | A predicate checking if the input is Nothing -- -- >>> Nothing `shouldSatisfy` P.nothing nothing :: (Monad m) => Predicate m (Maybe a) nothing = conMatches "Nothing" fieldNames toFields preds-  where-    fieldNames = Nothing-    toFields = \case-      Nothing -> Just HNil-      _ -> Nothing-    preds = HNil+ where+  fieldNames = Nothing+  toFields = \case+    Nothing -> Just HNil+    _ -> Nothing+  preds = HNil  -- | A predicate checking if the input is Left, wrapping a value matching the given predicate. -- -- >>> Left 1 `shouldSatisfy` P.left (P.gt 0) left :: (Monad m) => Predicate m a -> Predicate m (Either a b) left p = conMatches "Left" fieldNames toFields preds-  where-    fieldNames = Nothing-    toFields = \case-      Left x -> Just . HCons (pure x) $ HNil-      _ -> Nothing-    preds = HCons p HNil+ where+  fieldNames = Nothing+  toFields = \case+    Left x -> Just . HCons (pure x) $ HNil+    _ -> Nothing+  preds = HCons p HNil  -- | A predicate checking if the input is Right, wrapping a value matching the given predicate. -- -- >>> Right 1 `shouldSatisfy` P.right (P.gt 0) right :: (Monad m) => Predicate m b -> Predicate m (Either a b) right p = conMatches "Right" fieldNames toFields preds-  where-    fieldNames = Nothing-    toFields = \case-      Right x -> Just . HCons (pure x) $ HNil-      _ -> Nothing-    preds = HCons p HNil+ where+  fieldNames = Nothing+  toFields = \case+    Right x -> Just . HCons (pure x) $ HNil+    _ -> Nothing+  preds = HCons p HNil  -- | A predicate checking if the input is a list matching exactly the given predicates. --@@ -324,10 +323,10 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    listify vals = "[" <> Text.intercalate ", " vals <> "]"-    disp = listify $ map predicateDisp predList-    dispNeg = "not " <> disp+ where+  listify vals = "[" <> Text.intercalate ", " vals <> "]"+  disp = listify $ map predicateDisp predList+  dispNeg = "not " <> disp  class IsTuple a where   type TupleArgs a :: [Type]@@ -379,11 +378,11 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    preds = toHListPred (Proxy @a) predTup-    tupify vals = "(" <> Text.intercalate ", " vals <> ")"-    disp = tupify $ HList.toListWith predicateDisp preds-    dispNeg = "not " <> disp+ where+  preds = toHListPred (Proxy @a) predTup+  tupify vals = "(" <> Text.intercalate ", " vals <> ")"+  disp = tupify $ HList.toListWith predicateDisp preds+  dispNeg = "not " <> disp  -- | A predicate checking if the input matches the given constructor. --@@ -395,6 +394,11 @@ -- -- Record fields that are omitted are not checked at all; i.e. -- @P.con Foo{}@ and @P.con Foo{a = P.anything}@ are equivalent.+--+-- Positional arguments work also, as well as dollar signs.+--+-- >>> let email = P.con $ Email P.anything (P.eq "example.com")+-- >>> user `shouldSatisfy` P.con User{email = email} con :: a -> Predicate m a con =   -- A placeholder that will be replaced with conMatches in the plugin.@@ -405,11 +409,11 @@ -- so it should not be written directly, only generated from `con`. conMatches ::   (Monad m) =>-  String-  -> Maybe (HList (Const String) fields)-  -> (a -> Maybe (HList Identity fields))-  -> HList (Predicate m) fields-  -> Predicate m a+  String ->+  Maybe (HList (Const String) fields) ->+  (a -> Maybe (HList Identity fields)) ->+  HList (Predicate m) fields ->+  Predicate m a conMatches conNameS mFieldNames deconstruct preds =   Predicate     { predicateFunc = \actual ->@@ -425,20 +429,20 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    conName = Text.pack conNameS-    disp = "matches " <> predsDisp-    dispNeg = "does not match " <> predsDisp-    predsDisp = consify $ HList.toListWith predicateDisp preds+ where+  conName = Text.pack conNameS+  disp = "matches " <> predsDisp+  dispNeg = "does not match " <> predsDisp+  predsDisp = consify $ HList.toListWith predicateDisp preds -    -- consify ["= 1", "anything"] => User{id = (= 1), name = anything}-    -- consify ["= 1", "anything"] => Foo (= 1) anything-    consify vals =-      case HList.uncheck <$> mFieldNames of-        Nothing -> Text.unwords $ conName : map parens vals-        Just fieldNames ->-          let fields = zipWith (\field v -> Text.pack field <> " = " <> parens v) fieldNames vals-           in conName <> "{" <> Text.intercalate ", " fields <> "}"+  -- consify ["= 1", "anything"] => User{id = (= 1), name = anything}+  -- consify ["= 1", "anything"] => Foo (= 1) anything+  consify vals =+    case HList.uncheck <$> mFieldNames of+      Nothing -> Text.unwords $ conName : map parens vals+      Just fieldNames ->+        let fields = zipWith (\field v -> Text.pack field <> " = " <> parens v) fieldNames vals+         in conName <> "{" <> Text.intercalate ", " fields <> "}"  {----- Numeric -----} @@ -457,17 +461,17 @@ approx Tolerance{..} =   mkPredicateOp "≈" "≉" $ \actual expected ->     Prelude.abs (actual - expected) <= getTolerance expected-  where-    mRelTol = fromTol <$> rel-    absTol = fromTol abs-    getTolerance expected =-      case mRelTol of-        Just relTol -> max (relTol * Prelude.abs expected) absTol-        Nothing -> absTol+ where+  mRelTol = fromTol <$> rel+  absTol = fromTol abs+  getTolerance expected =+    case mRelTol of+      Just relTol -> max (relTol * Prelude.abs expected) absTol+      Nothing -> absTol -    fromTol x-      | x < 0 = error $ "tolerance can't be negative: " <> show x-      | otherwise = fromRational x+  fromTol x+    | x < 0 = error $ "tolerance can't be negative: " <> show x+    | otherwise = fromRational x  -- | The tolerance to use in 'approx'. --@@ -541,9 +545,9 @@     , predicateDisp = andify predList     , predicateDispNeg = "At least one failure:\n" <> andify predList     }-  where-    andify = Text.intercalate "\nand "-    predList = map (parens . predicateDisp) preds+ where+  andify = Text.intercalate "\nand "+  predList = map (parens . predicateDisp) preds  -- | A predicate checking if the input matches any of the given predicates --@@ -556,9 +560,9 @@     , predicateDisp = orify predList     , predicateDispNeg = "All failures:\n" <> orify predList     }-  where-    orify = Text.intercalate "\nor "-    predList = map (parens . predicateDisp) preds+ where+  orify = Text.intercalate "\nor "+  predList = map (parens . predicateDisp) preds  {----- Containers -----} @@ -629,9 +633,9 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    disp = "has prefix " <> render prefix-    dispNeg = "does not have prefix " <> render prefix+ where+  disp = "has prefix " <> render prefix+  dispNeg = "does not have prefix " <> render prefix  -- | A predicate checking if the input contains the given subsequence --@@ -654,9 +658,9 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    disp = "has infix " <> render elems-    dispNeg = "does not have infix " <> render elems+ where+  disp = "has infix " <> render elems+  dispNeg = "does not have infix " <> render elems  -- | A predicate checking if the input has the given suffix --@@ -679,9 +683,9 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    disp = "has suffix " <> render suffix-    dispNeg = "does not have suffix " <> render suffix+ where+  disp = "has suffix " <> render suffix+  dispNeg = "does not have suffix " <> render suffix  {----- IO -----} @@ -753,9 +757,9 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    disp = "throws (" <> predicateDisp <> ")"-    dispNeg = "does not throw (" <> predicateDisp <> ")"+ where+  disp = "throws (" <> predicateDisp <> ")"+  dispNeg = "does not throw (" <> predicateDisp <> ")"  {----- Functions -----} @@ -766,7 +770,7 @@ -- -- @ -- prop "reverse . reverse === id" $ do---   let genList = Gen.list (Gen.linear 0 10) $ Gen.int (Gen.linear 0 1000)+--   let genList = Gen.list (Range.linear 0 10) $ Gen.int (Range.linear 0 1000) --   (reverse . reverse) P.=== id \`shouldSatisfy\` P.isoWith genList -- @ (===) :: (a -> b) -> (a -> b) -> IsoChecker a b@@ -803,9 +807,9 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    disp = "isomorphic"-    dispNeg = "not isomorphic"+ where+  disp = "isomorphic"+  dispNeg = "not isomorphic"  {----- Snapshot -----} @@ -855,15 +859,15 @@  mkPredicateOp ::   (Monad m) =>-  Text-  -- ^ operator-  -> Text-  -- ^ negative operator-  -> (a -> a -> Bool)-  -- ^ actual -> expected -> success-  -> a-  -- ^ expected-  -> Predicate m a+  -- | operator+  Text ->+  -- | negative operator+  Text ->+  -- | actual -> expected -> success+  (a -> a -> Bool) ->+  -- | expected+  a ->+  Predicate m a mkPredicateOp op negOp f expected =   Predicate     { predicateFunc = \actual -> do@@ -880,15 +884,15 @@     , predicateDisp = disp     , predicateDispNeg = dispNeg     }-  where-    disp = op <> " " <> render expected-    dispNeg = negOp <> " " <> render expected+ where+  disp = op <> " " <> render expected+  dispNeg = negOp <> " " <> render expected  runPredicates :: (Monad m) => HList (Predicate m) xs -> HList Identity xs -> m [PredicateFuncResult] runPredicates preds = HList.toListWithM run . HList.hzip preds-  where-    run :: (Predicate m :*: Identity) a -> m PredicateFuncResult-    run (p :*: Identity x) = predicateFunc p x+ where+  run :: (Predicate m :*: Identity) a -> m PredicateFuncResult+  run (p :*: Identity x) = predicateFunc p x  verifyAll :: ([Text] -> Text) -> [PredicateFuncResult] -> PredicateFuncResult verifyAll mergeMessages results =@@ -900,8 +904,8 @@           Nothing -> mergeMessages $ map predicateExplain results     , predicateShowFailCtx = showMergedCtxs results     }-  where-    firstFailure = listToMaybe $ filter (Prelude.not . predicateSuccess) results+ where+  firstFailure = listToMaybe $ filter (Prelude.not . predicateSuccess) results  verifyAny :: ([Text] -> Text) -> [PredicateFuncResult] -> PredicateFuncResult verifyAny mergeMessages results =@@ -913,8 +917,8 @@           Nothing -> mergeMessages $ map predicateExplain results     , predicateShowFailCtx = showMergedCtxs results     }-  where-    firstSuccess = listToMaybe $ filter predicateSuccess results+ where+  firstSuccess = listToMaybe $ filter predicateSuccess results  render :: a -> Text render = Text.pack . anythingToString
src/Skeletest/Internal/Preprocessor.hs view
@@ -10,13 +10,12 @@ import Data.Maybe (mapMaybe) import Data.Text (Text) import Data.Text qualified as Text+import Skeletest.Internal.Constants (mainFileSpecsListIdentifier)+import Skeletest.Internal.Error (SkeletestError (..)) import System.Directory (doesDirectoryExist, listDirectory) import System.FilePath (makeRelative, splitExtensions, takeDirectory, (</>)) import UnliftIO.Exception (throwIO) -import Skeletest.Internal.Constants (mainFileSpecsListIdentifier)-import Skeletest.Internal.Error (SkeletestError (..))- -- | Preprocess the given Haskell file. See Main.hs processFile :: FilePath -> Text -> IO Text processFile path file = do@@ -25,14 +24,14 @@     . addLine pluginPragma     . addLine linePragma     $ file'-  where-    addLine line f = line <> "\n" <> f-    quoted s = "\"" <> s <> "\""+ where+  addLine line f = line <> "\n" <> f+  quoted s = "\"" <> s <> "\"" -    pluginPragma = "{-# OPTIONS_GHC -fplugin=Skeletest.Internal.Plugin #-}"-    linePragma =-      -- this is needed to tell GHC to use original path in error messages-      "{-# LINE 1 " <> quoted (Text.pack path) <> " #-}"+  pluginPragma = "{-# OPTIONS_GHC -fplugin=Skeletest.Internal.Plugin #-}"+  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 =@@ -43,11 +42,11 @@     [] -> True     -- something else? just silently ignore it     _ -> False-  where-    getModuleName s =-      case Text.words s of-        "module" : name : _ -> Just name-        _ -> Nothing+ where+  getModuleName s =+    case Text.words s of+      "module" : name : _ -> Just name+      _ -> Nothing  updateMainFile :: FilePath -> Text -> IO Text updateMainFile path file = do@@ -63,67 +62,67 @@ -- ["My.Module.Test1", "My.Module.Test2", ...] findTestModules :: FilePath -> IO [(FilePath, Text)] findTestModules path = mapMaybe toTestModule <$> listDirectoryRecursive testDir-  where-    testDir = takeDirectory path+ where+  testDir = takeDirectory path -    toTestModule fp = do-      guard (fp /= path)-      (fpNoExt, ".hs") <- pure $ splitExtensions fp-      guard ("Spec" `Text.isSuffixOf` Text.pack fpNoExt)-      name <- moduleNameFromPath $ Text.pack $ makeRelative testDir fpNoExt-      pure (fp, name)+  toTestModule fp = do+    guard (fp /= path)+    (fpNoExt, ".hs") <- pure $ splitExtensions fp+    guard ("Spec" `Text.isSuffixOf` Text.pack fpNoExt)+    name <- moduleNameFromPath $ Text.pack $ makeRelative testDir fpNoExt+    pure (fp, name) -    moduleNameFromPath = fmap (Text.intercalate ".") . mapM validateModuleName . Text.splitOn "/"+  moduleNameFromPath = fmap (Text.intercalate ".") . mapM validateModuleName . Text.splitOn "/" -    -- https://www.haskell.org/onlinereport/syntax-iso.html-    -- large { small | large | digit | ' }-    validateModuleName name = do-      (first, rest) <- Text.uncons name-      guard $ isUpper first-      guard $ Text.all (\c -> isUpper c || isLower c || isDigit c || c == '\'') rest-      pure name+  -- https://www.haskell.org/onlinereport/syntax-iso.html+  -- large { small | large | digit | ' }+  validateModuleName name = do+    (first, rest) <- Text.uncons name+    guard $ isUpper first+    guard $ Text.all (\c -> isUpper c || isLower c || isDigit c || c == '\'') rest+    pure name  addSpecsList :: [(FilePath, Text)] -> Text -> Text addSpecsList testModules file =   Text.unlines     [ file-    , mainFileSpecsListIdentifier <> " :: [(FilePath, String, Spec)]"+    , mainFileSpecsListIdentifier <> " :: [(FilePath, Spec)]"     , mainFileSpecsListIdentifier <> " = " <> renderSpecList specsList     ]-  where-    specsList =-      [ (quote $ Text.pack fp, quote modName, modName <> ".spec")-      | (fp, modName) <- testModules-      ]-    quote s = "\"" <> s <> "\""-    renderSpecList xs = "[" <> (Text.intercalate ", " . map renderSpecInfo) xs <> "]"-    renderSpecInfo (fp, name, spec) = "(" <> fp <> ", " <> name <> ", " <> spec <> ")"+ where+  specsList =+    [ (quote $ Text.pack fp, modName <> ".spec")+    | (fp, modName) <- testModules+    ]+  quote s = "\"" <> s <> "\""+  renderSpecList xs = "[" <> (Text.intercalate ", " . map renderSpecInfo) xs <> "]"+  renderSpecInfo (fp, spec) = "(" <> fp <> ", " <> spec <> ")"  -- | Add imports after the Skeletest.Main import, which should always be present in the Main module. insertImports :: [(FilePath, Text)] -> Text -> Either SkeletestError Text insertImports testModules file =   let (pre, post) = break isSkeletestImport $ Text.lines file    in if null post-        then Left $ CompilationError "Could not find Skeletest.Main import in Main module"+        then Left $ CompilationError Nothing "Could not find Skeletest.Main import in Main module"         else pure . Text.unlines $ pre <> importTests <> post-  where-    isSkeletestImport line =-      case Text.words line of-        "import" : "Skeletest.Main" : _ -> True-        _ -> False+ where+  isSkeletestImport line =+    case Text.words line of+      "import" : "Skeletest.Main" : _ -> True+      _ -> False -    importTests =-      [ "import qualified " <> name-      | (_, name) <- testModules-      ]+  importTests =+    [ "import qualified " <> name+    | (_, name) <- testModules+    ]  {----- Helpers -----}  listDirectoryRecursive :: FilePath -> IO [FilePath] listDirectoryRecursive fp = fmap (sort . concat) . mapM (go . (fp </>)) =<< listDirectory fp-  where-    go child = do-      isDir <- doesDirectoryExist child-      if isDir-        then listDirectoryRecursive child-        else pure [child]+ where+  go child = do+    isDir <- doesDirectoryExist child+    if isDir+      then listDirectoryRecursive child+      else pure [child]
src/Skeletest/Internal/Snapshot.hs view
@@ -49,6 +49,17 @@ import Data.Typeable qualified as Typeable import Data.Void (absurd) import Debug.RecoverRTTI (anythingToString)+import Skeletest.Internal.CLI (FlagSpec (..), IsFlag (..))+import Skeletest.Internal.Error (SkeletestError (..), invariantViolation)+import Skeletest.Internal.Fixtures (+  Fixture (..),+  FixtureScope (..),+  getFixture,+  noCleanup,+  withCleanup,+ )+import Skeletest.Internal.TestInfo (TestInfo (..), getTestInfo)+import Skeletest.Internal.Utils.Map qualified as Map.Utils import System.Directory (createDirectoryIfMissing) import System.FilePath (replaceExtension, splitFileName, takeDirectory, (</>)) import System.IO.Error (isDoesNotExistError)@@ -63,18 +74,6 @@   writeIORef,  ) -import Skeletest.Internal.CLI (FlagSpec (..), IsFlag (..))-import Skeletest.Internal.Error (SkeletestError (..), invariantViolation)-import Skeletest.Internal.Fixtures (-  Fixture (..),-  FixtureScope (..),-  getFixture,-  noCleanup,-  withCleanup,- )-import Skeletest.Internal.TestInfo (TestInfo (..), getTestInfo)-import Skeletest.Internal.Utils.Map qualified as Map.Utils- {----- Infrastructure -----}  data SnapshotTestFixture = SnapshotTestFixture@@ -141,38 +140,38 @@ updateSnapshot snapshotContext testResult = do   SnapshotFileFixture{snapshotFileRef} <- getFixture   modifyIORef' snapshotFileRef (Just . setSnapshot . fromMaybe emptySnapshotFile)-  where-    SnapshotContext-      { snapshotRenderers = renderers-      , snapshotTestInfo = testInfo@TestInfo{testModule}-      , snapshotIndex-      } = snapshotContext+ where+  SnapshotContext+    { snapshotRenderers = renderers+    , snapshotTestInfo = testInfo@TestInfo{testFile}+    , snapshotIndex+    } = snapshotContext -    emptySnapshotFile =-      SnapshotFile-        { moduleName = testModule-        , snapshots = Map.empty-        }+  emptySnapshotFile =+    SnapshotFile+      { testFile = Text.pack testFile+      , snapshots = Map.empty+      } -    testIdentifier = toTestIdentifier testInfo-    renderedTestResult = renderVal renderers testResult-    setSnapshot snapshotFile@SnapshotFile{snapshots} =-      let setForTest = Map.Utils.adjustNested (setAt snapshotIndex renderedTestResult) testIdentifier-       in snapshotFile{snapshots = setForTest snapshots}+  testIdentifier = toTestIdentifier testInfo+  renderedTestResult = renderVal renderers testResult+  setSnapshot snapshotFile@SnapshotFile{snapshots} =+    let setForTest = Map.Utils.adjustNested (setAt snapshotIndex renderedTestResult) testIdentifier+     in snapshotFile{snapshots = setForTest snapshots} -    -- Set the given snapshot at the given index. If the index is too large,-    -- fill in with empty snapshots.-    ---    -- >>> setAt 3 "x" ["a"] == ["a", "", "", "x"]-    setAt i0 v =-      let go = \cases-            i [] -> replicate i emptySnapshotVal <> [v]-            0 (_ : xs) -> v : xs-            i (x : xs) -> x : go (i - 1) xs-       in if i0 < 0-            then invariantViolation $ "Got negative snapshot index: " <> show i0-            else go i0-    emptySnapshotVal = SnapshotValue{snapshotContent = "", snapshotLang = Nothing}+  -- Set the given snapshot at the given index. If the index is too large,+  -- fill in with empty snapshots.+  --+  -- >>> setAt 3 "x" ["a"] == ["a", "", "", "x"]+  setAt i0 v =+    let go = \cases+          i [] -> replicate i emptySnapshotVal <> [v]+          0 (_ : xs) -> v : xs+          i (x : xs) -> x : go (i - 1) xs+     in if i0 < 0+          then invariantViolation $ "Got negative snapshot index: " <> show i0+          else go i0+  emptySnapshotVal = SnapshotValue{snapshotContent = "", snapshotLang = Nothing}  data SnapshotResult   = SnapshotMissing@@ -200,27 +199,27 @@       if snapshotContent == renderedTestResult         then SnapshotMatches         else SnapshotDiff{snapshotContent, renderedTestResult}-  where-    SnapshotContext-      { snapshotRenderers = renderers-      , snapshotTestInfo = testInfo-      , snapshotIndex-      } = snapshotContext+ where+  SnapshotContext+    { snapshotRenderers = renderers+    , snapshotTestInfo = testInfo+    , snapshotIndex+    } = snapshotContext -    returnE = throwE-    renderedTestResultVal = renderVal renderers testResult+  returnE = throwE+  renderedTestResultVal = renderVal renderers testResult -    safeIndex xs0 i0 =-      let go = \cases-            _ [] -> Nothing-            0 (x : _) -> Just x-            i (_ : xs) -> go (i - 1) xs-       in if i0 < 0 then Nothing else go i0 xs0+  safeIndex xs0 i0 =+    let go = \cases+          _ [] -> Nothing+          0 (x : _) -> Just x+          i (_ : xs) -> go (i - 1) xs+     in if i0 < 0 then Nothing else go i0 xs0  {----- Snapshot file -----}  data SnapshotFile = SnapshotFile-  { moduleName :: Text+  { testFile :: Text   , snapshots :: Map TestIdentifier [SnapshotValue]   -- ^ full test identifier => snapshots   -- e.g. ["group1", "group2", "returns val1 and val2"] => ["val1", "val2"]@@ -240,99 +239,99 @@  getSnapshotPath :: FilePath -> FilePath getSnapshotPath testFile = testDir </> "__snapshots__" </> snapshotFileName-  where-    (testDir, testFileName) = splitFileName testFile-    snapshotFileName = replaceExtension testFileName ".snap.md"+ where+  (testDir, testFileName) = splitFileName testFile+  snapshotFileName = replaceExtension testFileName ".snap.md"  toTestIdentifier :: TestInfo -> TestIdentifier toTestIdentifier TestInfo{testContexts, testName} = testContexts <> [testName]  decodeSnapshotFile :: Text -> Maybe SnapshotFile decodeSnapshotFile = parseFile . Text.lines-  where-    parseFile = \case-      line : rest-        | Just moduleName <- Text.stripPrefix "# " line -> do-            let snapshotFile =-                  SnapshotFile-                    { moduleName = Text.strip moduleName-                    , snapshots = Map.empty-                    }-            parseSections snapshotFile Nothing rest-      _ -> Nothing--    parseSections ::-      SnapshotFile-      -- \^ The parsed snapshot file so far-      -> Maybe [Text]-      -- \^ The current test identifier, if one is set-      -> [Text]-      -- \^ The rest of the lines to process-      -> Maybe SnapshotFile-    parseSections snapshotFile@SnapshotFile{snapshots} mTest = \case-      [] -> pure snapshotFile-      line : rest-        -- ignore empty lines-        | "" <- Text.strip line -> parseSections snapshotFile mTest rest-        -- found a test section-        | Just sectionName <- Text.stripPrefix "## " line -> do-            let testIdentifier = map Text.strip $ Text.splitOn " / " sectionName-            let snapshotFile' = snapshotFile{snapshots = Map.insert testIdentifier [] snapshots}-            parseSections snapshotFile' (Just testIdentifier) rest-        -- found the beginning of a snapshot-        | Just lang <- (Text.stripPrefix "```" . Text.strip) line -> do-            testIdentifier <- mTest-            (snapshot, rest') <- parseSnapshot [] rest-            let-              snapshotVal =-                SnapshotValue-                  { snapshotContent = snapshot-                  , snapshotLang = if Text.null lang then Nothing else Just lang+ where+  parseFile = \case+    line : rest+      | Just testFile <- Text.stripPrefix "# " line -> do+          let snapshotFile =+                SnapshotFile+                  { testFile = Text.strip testFile+                  , snapshots = Map.empty                   }-              snapshotFile' = snapshotFile{snapshots = Map.adjust (<> [snapshotVal]) testIdentifier snapshots}-            parseSections snapshotFile' mTest rest'-        -- anything else is invalid-        | otherwise -> Nothing+          parseSections snapshotFile Nothing rest+    _ -> Nothing -    parseSnapshot snapshot = \case-      [] -> Nothing-      line : rest-        | "```" <- Text.strip line -> pure (Text.unlines snapshot, rest)-        | otherwise -> parseSnapshot (snapshot <> [line]) rest+  parseSections ::+    SnapshotFile ->+    -- \^ The parsed snapshot file so far+    Maybe [Text] ->+    -- \^ The current test identifier, if one is set+    [Text] ->+    -- \^ The rest of the lines to process+    Maybe SnapshotFile+  parseSections snapshotFile@SnapshotFile{snapshots} mTest = \case+    [] -> pure snapshotFile+    line : rest+      -- ignore empty lines+      | "" <- Text.strip line -> parseSections snapshotFile mTest rest+      -- found a test section+      | Just sectionName <- Text.stripPrefix "## " line -> do+          let testIdentifier = map Text.strip $ Text.splitOn " / " sectionName+          let snapshotFile' = snapshotFile{snapshots = Map.insert testIdentifier [] snapshots}+          parseSections snapshotFile' (Just testIdentifier) rest+      -- found the beginning of a snapshot+      | Just lang <- (Text.stripPrefix "```" . Text.strip) line -> do+          testIdentifier <- mTest+          (snapshot, rest') <- parseSnapshot [] rest+          let+            snapshotVal =+              SnapshotValue+                { snapshotContent = snapshot+                , snapshotLang = if Text.null lang then Nothing else Just lang+                }+            snapshotFile' = snapshotFile{snapshots = Map.adjust (<> [snapshotVal]) testIdentifier snapshots}+          parseSections snapshotFile' mTest rest'+      -- anything else is invalid+      | otherwise -> Nothing +  parseSnapshot snapshot = \case+    [] -> Nothing+    line : rest+      | "```" <- Text.strip line -> pure (Text.unlines snapshot, rest)+      | otherwise -> parseSnapshot (snapshot <> [line]) rest+ encodeSnapshotFile :: SnapshotFile -> Text encodeSnapshotFile SnapshotFile{..} =   Text.intercalate "\n" $-    h1 moduleName : concatMap toSection (Map.toList snapshots)-  where-    toSection (testIdentifier, snaps) =-      h2 (Text.intercalate " / " testIdentifier) : map codeBlock snaps+    h1 testFile : concatMap toSection (Map.toList snapshots)+ where+  toSection (testIdentifier, snaps) =+    h2 (Text.intercalate " / " testIdentifier) : map codeBlock snaps -    h1 s = "# " <> s <> "\n"-    h2 s = "## " <> s <> "\n"-    codeBlock SnapshotValue{..} =-      Text.concat-        [ "```" <> fromMaybe "" snapshotLang <> "\n"-        , snapshotContent-        , "```\n"-        ]+  h1 s = "# " <> s <> "\n"+  h2 s = "## " <> s <> "\n"+  codeBlock SnapshotValue{..} =+    Text.concat+      [ "```" <> fromMaybe "" snapshotLang <> "\n"+      , snapshotContent+      , "```\n"+      ]  normalizeSnapshotFile :: SnapshotFile -> SnapshotFile normalizeSnapshotFile file@SnapshotFile{snapshots} =   file     { snapshots = Map.fromList . map normalize . Map.toList $ snapshots     }-  where-    normalize (testIdentifier, vals) =-      ( map (sanitizeNonPrint . sanitizeSlashes . Text.strip) testIdentifier-      , map normalizeSnapshotVal vals-      )+ where+  normalize (testIdentifier, vals) =+    ( map (sanitizeNonPrint . sanitizeSlashes . Text.strip) testIdentifier+    , map normalizeSnapshotVal vals+    ) -    sanitizeSlashes = Text.replace " /" " \\/"+  sanitizeSlashes = Text.replace " /" " \\/" -    sanitizeNonPrint = Text.concatMap $ \case-      c | (not . isPrint) c -> Text.drop 1 . Text.dropEnd 1 . Text.pack . show $ c-      c -> Text.singleton c+  sanitizeNonPrint = Text.concatMap $ \case+    c | (not . isPrint) c -> Text.drop 1 . Text.dropEnd 1 . Text.pack . show $ c+    c -> Text.singleton c  {----- Renderers -----} @@ -360,12 +359,12 @@   , plainRenderer @Text id   , jsonRenderer   ]-  where-    jsonRenderer =-      SnapshotRenderer-        { render = TextL.toStrict . TextL.decodeUtf8 . Aeson.encodePretty @Aeson.Value-        , snapshotLang = Just "json"-        }+ where+  jsonRenderer =+    SnapshotRenderer+      { render = TextL.toStrict . TextL.decodeUtf8 . Aeson.encodePretty @Aeson.Value+      , snapshotLang = Just "json"+      }  renderVal :: (Typeable a) => [SnapshotRenderer] -> a -> SnapshotValue renderVal renderers a =@@ -377,10 +376,10 @@           , snapshotLang = Nothing           }       rendered : _ -> rendered-  where-    tryRender SnapshotRenderer{..} =-      let toValue v = SnapshotValue{snapshotContent = render v, snapshotLang}-       in toValue <$> Typeable.cast a+ where+  tryRender SnapshotRenderer{..} =+    let toValue v = SnapshotValue{snapshotContent = render v, snapshotLang}+     in toValue <$> Typeable.cast a  normalizeSnapshotVal :: SnapshotValue -> SnapshotValue normalizeSnapshotVal SnapshotValue{..} =@@ -388,13 +387,13 @@     { snapshotContent = normalizeTrailingNewlines snapshotContent     , snapshotLang = collapse $ Text.filter isAlpha <$> snapshotLang     }-  where-    collapse = \case-      Just "" -> Nothing-      m -> m+ where+  collapse = \case+    Just "" -> Nothing+    m -> m -    -- Ensure there's exactly one trailing newline.-    normalizeTrailingNewlines s = Text.dropWhileEnd (== '\n') s <> "\n"+  -- Ensure there's exactly one trailing newline.+  normalizeTrailingNewlines s = Text.dropWhileEnd (== '\n') s <> "\n"  snapshotRenderersRef :: IORef [SnapshotRenderer] snapshotRenderersRef = unsafePerformIO $ newIORef []
src/Skeletest/Internal/Spec.hs view
@@ -41,13 +41,8 @@ import Data.Text (Text) import Data.Text qualified as Text import Data.Text.IO qualified as Text-import UnliftIO.Exception (-  finally,-  fromException,-  try,- )- import Skeletest.Assertions (Testable, runTestable)+import Skeletest.Internal.Capture (addCapturedOutput, withCaptureOutput) import Skeletest.Internal.Fixtures (FixtureScopeKey (..), cleanupFixtures) import Skeletest.Internal.Markers (   AnonMarker (..),@@ -65,9 +60,16 @@  ) import Skeletest.Internal.TestTargets (TestTarget, TestTargets, matchesTest) import Skeletest.Internal.TestTargets qualified as TestTargets+import Skeletest.Internal.Utils.BoxDrawing (drawBox) import Skeletest.Internal.Utils.Color qualified as Color import Skeletest.Plugin (Hooks (..), defaultHooks) import Skeletest.Prop.Internal (Property)+import System.IO qualified as IO+import UnliftIO.Exception (+  finally,+  fromException,+  try,+ )  type Spec = Spec' () @@ -106,22 +108,22 @@ traverseSpecTrees ::   forall m.   (Monad m) =>-  ( (SpecTree -> m SpecTree)-    -> [SpecTree]-    -> m [SpecTree]-  )-  -> Spec-  -> m Spec+  ( (SpecTree -> m SpecTree) ->+    [SpecTree] ->+    m [SpecTree]+  ) ->+  Spec ->+  m Spec traverseSpecTrees f = withSpecTrees go-  where-    go :: [SpecTree] -> m [SpecTree]-    go = f recurseGroups+ where+  go :: [SpecTree] -> m [SpecTree]+  go = f recurseGroups -    recurseGroups = \case-      group@SpecGroup{} -> do-        trees' <- go $ groupTrees group-        pure group{groupTrees = trees'}-      stest@SpecTest{} -> pure stest+  recurseGroups = \case+    group@SpecGroup{} -> do+      trees' <- go $ groupTrees group+      pure group{groupTrees = trees'}+    stest@SpecTest{} -> pure stest  -- | Map the tree with the given processing function. --@@ -129,12 +131,12 @@ -- -- >>> mapSpecTrees (\go -> post . map go . pre) spec mapSpecTrees ::-  ( (SpecTree -> SpecTree)-    -> [SpecTree]-    -> [SpecTree]-  )-  -> Spec-  -> Spec+  ( (SpecTree -> SpecTree) ->+    [SpecTree] ->+    [SpecTree]+  ) ->+  Spec ->+  Spec mapSpecTrees f = runIdentity . traverseSpecTrees (\go -> pure . f (runIdentity . go))  {----- Execute spec -----}@@ -147,58 +149,58 @@       (`finally` cleanupFixtures (PerFileFixtureKey specPath)) $ do         let emptyTestInfo =               TestInfo-                { testModule = specName-                , testContexts = []+                { testContexts = []                 , testName = ""                 , testMarkers = []                 , testFile = specPath                 }-        Text.putStrLn specName+        Text.putStrLn $ Text.pack specPath         runTrees emptyTestInfo $ getSpecTrees specSpec-  where-    Hooks{..} = builtinHooks <> hooks0-    builtinHooks = xfailHook <> skipHook--    runTrees baseTestInfo = fmap and . mapM (runTree baseTestInfo)-    runTree baseTestInfo = \case-      SpecGroup{..} -> do-        let lvl = getIndentLevel baseTestInfo-        Text.putStrLn $ indent lvl groupLabel-        runTrees baseTestInfo{TestInfo.testContexts = TestInfo.testContexts baseTestInfo <> [groupLabel]} groupTrees-      SpecTest{..} -> do-        let lvl = getIndentLevel baseTestInfo-        Text.putStr $ indent lvl (testName <> ": ")+ where+  Hooks{..} = builtinHooks <> hooks0+  builtinHooks = xfailHook <> skipHook -        let testInfo =-              baseTestInfo-                { TestInfo.testName = testName-                , TestInfo.testMarkers = testMarkers-                }-        TestResult{..} <--          withTestInfo testInfo $ do-            tid <- myThreadId-            runTest testInfo testAction `finally` cleanupFixtures (PerTestFixtureKey tid)+  runTrees baseTestInfo = fmap and . mapM (runTree baseTestInfo)+  runTree baseTestInfo = \case+    SpecGroup{..} -> do+      let lvl = getIndentLevel baseTestInfo+      Text.putStrLn $ indent lvl groupLabel+      runTrees baseTestInfo{TestInfo.testContexts = TestInfo.testContexts baseTestInfo <> [groupLabel]} groupTrees+    SpecTest{..} -> do+      let lvl = getIndentLevel baseTestInfo+      Text.putStr $ indent lvl (testName <> ": ")+      IO.hFlush IO.stdout -        Text.putStrLn testResultLabel-        case testResultMessage of-          TestResultMessageNone -> pure ()-          TestResultMessageInline msg -> Text.putStrLn $ indent (lvl + 1) msg-          TestResultMessageSection msg -> Text.putStrLn $ withBorder msg-        pure testResultSuccess+      let testInfo =+            baseTestInfo+              { TestInfo.testName = testName+              , TestInfo.testMarkers = testMarkers+              }+      TestResult{..} <-+        withTestInfo testInfo $ do+          tid <- myThreadId+          runTest testInfo testAction `finally` cleanupFixtures (PerTestFixtureKey tid) -    runTest info action =-      hookRunTest info $ do-        try action >>= \case-          Right result -> pure result-          Left e-            | Just e' <- fromException e -> testResultFromAssertionFail e'-            | otherwise -> pure $ testResultFromError e+      Text.putStrLn testResultLabel+      case testResultMessage of+        TestResultMessageNone -> pure ()+        TestResultMessageInline msg -> Text.putStrLn $ indent (lvl + 1) msg+        TestResultMessageSection box -> drawBox box >>= Text.putStrLn+      pure testResultSuccess -    getIndentLevel testInfo = length (TestInfo.testContexts testInfo) + 1 -- +1 to include the module name-    indent lvl = Text.intercalate "\n" . map (Text.replicate (lvl * 4) " " <>) . Text.splitOn "\n"+  runTest info action =+    hookRunTest info $ do+      (mCapture, resultOrError) <- withCaptureOutput (try action)+      case resultOrError of+        Right result -> pure result+        Left e ->+          fmap (addCapturedOutput mCapture) $+            case fromException e of+              Just e' -> testResultFromAssertionFail e'+              Nothing -> testResultFromError e -    border = Text.replicate 80 "-"-    withBorder msg = Text.intercalate "\n" [border, msg, border]+  getIndentLevel testInfo = length (TestInfo.testContexts testInfo) + 1 -- +1 to include the module name+  indent lvl = Text.intercalate "\n" . map (Text.replicate (lvl * 4) " " <>) . Text.splitOn "\n"  {----- Entrypoint -----} @@ -206,7 +208,6 @@  data SpecInfo = SpecInfo   { specPath :: FilePath-  , specName :: Text   , specSpec :: Spec   } @@ -215,10 +216,10 @@   let spec = mapSpecTrees (\go -> filter (not . isEmptySpec) . map go) (specSpec info)   guard $ (not . null . getSpecTrees) spec   pure info{specSpec = spec}-  where-    isEmptySpec = \case-      SpecGroup _ [] -> True-      _ -> False+ where+  isEmptySpec = \case+    SpecGroup _ [] -> True+    _ -> False  -- TODO: make hookable? implement manual tests with hook? applyTestSelections :: TestTargets -> SpecRegistry -> SpecRegistry@@ -226,55 +227,55 @@   Just selections -> map (applyTestSelections' selections)   -- if no selections are specified, hide manual tests   Nothing -> map (\info -> info{specSpec = hideManualTests $ specSpec info})-  where-    hideManualTests = mapSpecTrees (\go -> filter (not . isManualTest) . map go)-    isManualTest = \case-      SpecGroup{} -> False-      SpecTest{testMarkers} -> isJust $ findMarker @MarkerManual testMarkers+ where+  hideManualTests = mapSpecTrees (\go -> filter (not . isManualTest) . map go)+  isManualTest = \case+    SpecGroup{} -> False+    SpecTest{testMarkers} -> isJust $ findMarker @MarkerManual testMarkers  applyTestSelections' :: TestTarget -> SpecInfo -> SpecInfo applyTestSelections' selections info = info{specSpec = applySelections $ specSpec info}-  where-    applySelections = (`Trans.runReader` []) . traverseSpecTrees apply+ where+  applySelections = (`Trans.runReader` []) . traverseSpecTrees apply -    apply go = mapMaybeM $ \case-      group@SpecGroup{groupLabel} -> Just <$> Trans.local (<> [groupLabel]) (go group)-      stest@SpecTest{testName, testMarkers} -> do-        groups <- Trans.ask-        let attrs =-              TestTargets.TestAttrs-                { testPath = specPath info-                , testIdentifier = groups <> [testName]-                , testMarkers = [Text.pack $ getMarkerName m | SomeMarker m <- testMarkers]-                }-        pure $-          if matchesTest selections attrs-            then Just stest-            else Nothing+  apply go = mapMaybeM $ \case+    group@SpecGroup{groupLabel} -> Just <$> Trans.local (<> [groupLabel]) (go group)+    stest@SpecTest{testName, testMarkers} -> do+      groups <- Trans.ask+      let attrs =+            TestTargets.TestAttrs+              { testPath = specPath info+              , testIdentifier = groups <> [testName]+              , testMarkers = [Text.pack $ getMarkerName m | SomeMarker m <- testMarkers]+              }+      pure $+        if matchesTest selections attrs+          then Just stest+          else Nothing -    mapMaybeM f = fmap catMaybes . mapM f+  mapMaybeM f = fmap catMaybes . mapM f  {----- Defining a Spec -----}  -- | The entity or concept being tested. describe :: String -> Spec -> Spec describe name = runIdentity . withSpecTrees (pure . (: []) . mkGroup)-  where-    mkGroup trees =-      SpecGroup-        { groupLabel = Text.pack name-        , groupTrees = trees-        }+ where+  mkGroup trees =+    SpecGroup+      { groupLabel = Text.pack name+      , groupTrees = trees+      }  test :: (Testable m) => String -> m () -> Spec test name t = Spec $ tell [mkTest]-  where-    mkTest =-      SpecTest-        { testName = Text.pack name-        , testMarkers = []-        , testAction = runTestable t-        }+ where+  mkTest =+    SpecTest+      { testName = Text.pack name+      , testMarkers = []+      , testAction = runTestable t+      }  -- | Define an IO-based test. --@@ -317,21 +318,21 @@           Just (MarkerXFail reason) -> modify reason <$> runTest           Nothing -> runTest     }-  where-    modify reason TestResult{..} =-      if testResultSuccess-        then-          TestResult-            { testResultSuccess = False-            , testResultLabel = Color.red "XPASS"-            , testResultMessage = TestResultMessageInline reason-            }-        else-          TestResult-            { testResultSuccess = True-            , testResultLabel = Color.yellow "XFAIL"-            , testResultMessage = TestResultMessageInline reason-            }+ where+  modify reason TestResult{..} =+    if testResultSuccess+      then+        TestResult+          { testResultSuccess = False+          , testResultLabel = Color.red "XPASS"+          , testResultMessage = TestResultMessageInline reason+          }+      else+        TestResult+          { testResultSuccess = True+          , testResultLabel = Color.yellow "XFAIL"+          , testResultMessage = TestResultMessageInline reason+          }  -- | Skip all tests in the given spec. --@@ -383,11 +384,11 @@ -- Useful for selecting tests from the command line or identifying tests in hooks withMarker :: (IsMarker a) => a -> Spec -> Spec withMarker m = mapSpecTrees (\go -> map (addMarker . go))-  where-    marker = SomeMarker m-    addMarker = \case-      group@SpecGroup{} -> group-      tree@SpecTest{} -> tree{testMarkers = marker : testMarkers tree}+ where+  marker = SomeMarker m+  addMarker = \case+    group@SpecGroup{} -> group+    tree@SpecTest{} -> tree{testMarkers = marker : testMarkers tree}  -- | Adds the given names as plain markers to all tests in the given spec. --
src/Skeletest/Internal/TestInfo.hs view
@@ -11,18 +11,16 @@ import Data.Map (Map) import Data.Map qualified as Map import Data.Text (Text)+import Skeletest.Internal.Error (invariantViolation)+import Skeletest.Internal.Markers (SomeMarker) import System.IO.Unsafe (unsafePerformIO) import UnliftIO (MonadUnliftIO) import UnliftIO.Concurrent (ThreadId, myThreadId) import UnliftIO.Exception (bracket_) import UnliftIO.IORef (IORef, modifyIORef, newIORef, readIORef) -import Skeletest.Internal.Error (invariantViolation)-import Skeletest.Internal.Markers (SomeMarker)- data TestInfo = TestInfo-  { testModule :: Text-  , testContexts :: [Text]+  { testContexts :: [Text]   , testName :: Text   , testMarkers :: [SomeMarker]   , testFile :: FilePath@@ -40,9 +38,9 @@ withTestInfo info m = do   tid <- myThreadId   bracket_ (set tid) (unset tid) m-  where-    set tid = modifyIORef testInfoMapRef $ Map.insert tid info-    unset tid = modifyIORef testInfoMapRef $ Map.delete tid+ where+  set tid = modifyIORef testInfoMapRef $ Map.insert tid info+  unset tid = modifyIORef testInfoMapRef $ Map.delete tid  lookupTestInfo :: (MonadIO m) => m (Maybe TestInfo) lookupTestInfo = do
src/Skeletest/Internal/TestRunner.hs view
@@ -18,25 +18,29 @@   FailContext, ) where +import Control.Monad (guard) import Control.Monad.IO.Class (MonadIO) import Data.Maybe (listToMaybe) import Data.Text (Text) import Data.Text qualified as Text import Data.Text.IO qualified as Text+import Data.Typeable (typeOf)+import GHC.IO.Exception qualified as GHC import GHC.Stack (CallStack) import GHC.Stack qualified as GHC+import Skeletest.Internal.Error (SkeletestError)+import Skeletest.Internal.TestInfo (TestInfo)+import Skeletest.Internal.Utils.BoxDrawing (BoxSpec, BoxSpecContent (..))+import Skeletest.Internal.Utils.Color qualified as Color+import Text.Read (readMaybe) import UnliftIO.Exception (   Exception,-  SomeException,+  SomeException (..),   displayException,   fromException,   try,  ) -import Skeletest.Internal.Error (SkeletestError)-import Skeletest.Internal.TestInfo (TestInfo)-import Skeletest.Internal.Utils.Color qualified as Color- {----- Testable -----}  class (MonadIO m) => Testable m where@@ -61,7 +65,7 @@ data TestResultMessage   = TestResultMessageNone   | TestResultMessageInline Text-  | TestResultMessageSection Text+  | TestResultMessageSection BoxSpec  testResultPass :: TestResult testResultPass =@@ -78,24 +82,75 @@     TestResult       { testResultSuccess = False       , testResultLabel = Color.red "FAIL"-      , testResultMessage = TestResultMessageSection msg+      , testResultMessage = TestResultMessageSection [BoxText msg]       } -testResultFromError :: SomeException -> TestResult-testResultFromError e =-  TestResult-    { testResultSuccess = False-    , testResultLabel = Color.red "ERROR"-    , testResultMessage = TestResultMessageInline $ Text.pack msg-    }-  where-    msg =-      case fromException e of-        -- In GHC 9.10+, SomeException shows the callstack, which we don't-        -- want to see for known Skeletest errors-        Just (err :: SkeletestError) -> displayException err-        Nothing -> displayException e+testResultFromError :: SomeException -> IO TestResult+testResultFromError e = do+  msg <- renderMsg+  pure+    TestResult+      { testResultSuccess = False+      , testResultLabel = Color.red "ERROR"+      , testResultMessage = TestResultMessageSection [BoxText msg]+      }+ where+  renderMsg+    -- In GHC 9.10+, SomeException shows the callstack, which we don't+    -- want to see for known Skeletest errors+    | Just (err :: SkeletestError) <- fromException e = do+        pure $ Text.pack $ displayException err+    -- Handle pattern match fail in a do-block+    | Just err <- parseDoBlockFail e = do+        renderDoBlockFail err+    | SomeException err <- e = do+        pure . Text.strip . Text.pack . unlines $+          [ "Got exception of type `" <> (show . typeOf) err <> "`:"+          , displayException e+          ] +{----- DoBlockFail -----}++data DoBlockFail = DoBlockFail+  { doBlockFailMessage :: Text+  , doBlockFailFile :: FilePath+  , doBlockFailLine :: Int+  , doBlockFailStartCol :: Int+  , doBlockFailEndCol :: Int+  }++-- | See if the exception is from a pattern match fail in a do-block.+parseDoBlockFail :: SomeException -> Maybe DoBlockFail+parseDoBlockFail e = do+  GHC.IOError _ GHC.UserError _ msgStr _ _ <- fromException e+  let msg = Text.pack msgStr+  guard $ "Pattern match failure " `Text.isPrefixOf` msg+  [msgWithoutLoc, locInfo] <- pure $ Text.splitOn " at " msg+  [file, lineStr, colSpan] <- pure $ Text.splitOn ":" locInfo+  line <- readT lineStr+  [startCol, endCol] <- mapM readT $ Text.splitOn "-" colSpan+  pure+    DoBlockFail+      { doBlockFailMessage = msgWithoutLoc+      , doBlockFailFile = Text.unpack file+      , doBlockFailLine = line+      , doBlockFailStartCol = startCol+      , -- seems like srcLocEndCol is exclusive, while the columns in the fail message are inclusive+        doBlockFailEndCol = endCol + 1+      }+ where+  readT = readMaybe . Text.unpack++renderDoBlockFail :: DoBlockFail -> IO Text+renderDoBlockFail DoBlockFail{..} =+  renderPrettyFailure+    doBlockFailMessage+    doBlockFailContext+    [ (doBlockFailFile, doBlockFailLine, doBlockFailStartCol, doBlockFailEndCol)+    ]+ where+  doBlockFailContext = []+ {----- AssertionFail -----}  data AssertionFail = AssertionFail@@ -122,43 +177,62 @@ -- Right 1 ≠ Left 1 -- @ renderAssertionFail :: AssertionFail -> IO Text-renderAssertionFail AssertionFail{..} = do-  prettyStackTrace <- mapM renderCallLine . reverse $ GHC.getCallStack callStack+renderAssertionFail AssertionFail{..} =+  renderPrettyFailure+    testFailMessage+    testFailContext+    [ (srcLocFile, srcLocStartLine, srcLocStartCol, srcLocEndCol)+    | (_, GHC.SrcLoc{..}) <- GHC.getCallStack callStack+    ]++-- | Render a test failure like:+--+-- @+-- At test/Skeletest/Internal/TestTargetsSpec.hs:19:+-- |+-- |           parseTestTargets input `shouldBe` Right (Just expected)+-- |                                   ^^^^^^^^+--+-- Right 1 ≠ Left 1+-- @+renderPrettyFailure ::+  -- | Message+  Text ->+  FailContext ->+  -- | Call stack (file, line, startCol, endCol)+  [(FilePath, Int, Int, Int)] ->+  IO Text+renderPrettyFailure msg ctx callstack = do+  prettyStackTrace <- mapM renderCallLine . reverse $ callstack   pure . Text.intercalate "\n\n" . concat $     [ prettyStackTrace-    , if null testFailContext+    , if null ctx         then []-        else [Text.intercalate "\n" $ reverse testFailContext]-    , [testFailMessage]+        else [Text.intercalate "\n" $ reverse ctx]+    , [msg]     ]-  where-    renderCallLine (_, loc) = do-      let-        path = GHC.srcLocFile loc-        lineNum = GHC.srcLocStartLine loc-        startCol = GHC.srcLocStartCol loc-        endCol = GHC.srcLocEndCol loc--      mLine <--        try (Text.readFile path) >>= \case-          Right srcFile -> pure $ getLineNum lineNum srcFile-          Left (_ :: SomeException) -> pure Nothing-      let (srcLine, pointerLine) =-            case mLine of-              Just line ->-                ( line-                , Text.replicate (startCol - 1) " " <> Text.replicate (endCol - startCol) "^"-                )-              Nothing ->-                ( "<unknown line>"-                , ""-                )+ where+  renderCallLine (path, lineNum, startCol, endCol) = do+    mLine <-+      try (Text.readFile path) >>= \case+        Right srcFile -> pure $ getLineNum lineNum srcFile+        Left (_ :: SomeException) -> pure Nothing+    let (srcLine, pointerLine) =+          case mLine of+            Just line ->+              ( line+              , Text.replicate (startCol - 1) " " <> Text.replicate (endCol - startCol) "^"+              )+            Nothing ->+              ( "<unknown line>"+              , ""+              ) -      pure . Text.intercalate "\n" $-        [ Text.pack path <> ":" <> (Text.pack . show) lineNum <> ":"-        , "|"-        , "| " <> srcLine-        , "| " <> pointerLine-        ]+    pure . Text.intercalate "\n" $+      [ Text.pack path <> ":" <> (Text.pack . show) lineNum <> ":"+      , "|"+      , "| " <> srcLine+      , "| " <> pointerLine+      ] -    getLineNum n = listToMaybe . take 1 . drop (n - 1) . Text.lines+  getLineNum n = listToMaybe . take 1 . drop (n - 1) . Text.lines
src/Skeletest/Internal/TestTargets.hs view
@@ -44,15 +44,15 @@  matchesTest :: TestTarget -> TestAttrs -> Bool matchesTest selection TestAttrs{..} = go selection-  where-    go = \case-      TestTargetEverything -> True-      TestTargetFile path -> testPath == path-      TestTargetName s -> s `Text.isInfixOf` Text.unwords testIdentifier-      TestTargetMarker marker -> marker `elem` testMarkers-      TestTargetNot e -> not $ go e-      TestTargetAnd l r -> go l && go r-      TestTargetOr l r -> go l || go r+ where+  go = \case+    TestTargetEverything -> True+    TestTargetFile path -> testPath == path+    TestTargetName s -> s `Text.isInfixOf` Text.unwords testIdentifier+    TestTargetMarker marker -> marker `elem` testMarkers+    TestTargetNot e -> not $ go e+    TestTargetAnd l r -> go l && go r+    TestTargetOr l r -> go l || go r  {----- Parsing -----} @@ -61,8 +61,8 @@   case NonEmpty.nonEmpty args of     Nothing -> pure Nothing     Just args' -> Just . Foldable1.foldr1 TestTargetOr <$> mapM parseTestTarget args'-  where-    parseTestTarget = first showTestTargetParseError . Parser.parse (testTargetParser <* Parser.eof) ""+ where+  parseTestTarget = first showTestTargetParseError . Parser.parse (testTargetParser <* Parser.eof) ""  type Parser = Parsec Void Text type ParseErrorBundle = Parser.ParseErrorBundle Text Void@@ -85,32 +85,32 @@     [ [prefix "not" TestTargetNot]     , [binary "and" TestTargetAnd, binary "or" TestTargetOr]     ]-  where-    prefix name f = Parser.Prefix (f <$ symbol name)-    binary name f = Parser.InfixL (f <$ symbol name)+ where+  prefix name f = Parser.Prefix (f <$ symbol name)+  binary name f = Parser.InfixL (f <$ symbol name) -    symbol = Parser.L.symbol Parser.space-    parens = Parser.between (symbol "(") (symbol ")")+  symbol = Parser.L.symbol Parser.space+  parens = Parser.between (symbol "(") (symbol ")") -    everythingParser = TestTargetEverything <$ symbol "*"+  everythingParser = TestTargetEverything <$ symbol "*" -    nameParser =-      Parser.label "test name" $-        fmap TestTargetName . Parser.between (symbol "[") (symbol "]") $-          Parser.takeWhile1P Nothing (/= ']')+  nameParser =+    Parser.label "test name" $+      fmap TestTargetName . Parser.between (symbol "[") (symbol "]") $+        Parser.takeWhile1P Nothing (/= ']') -    markerParser =-      Parser.label "marker" . ignoreSpacesAfter $ do-        _ <- symbol "@"-        fmap TestTargetMarker . Parser.takeWhile1P Nothing $-          (||) <$> isAlphaNum <*> (`elem` ("-_." :: [Char]))+  markerParser =+    Parser.label "marker" . ignoreSpacesAfter $ do+      _ <- symbol "@"+      fmap TestTargetMarker . Parser.takeWhile1P Nothing $+        (||) <$> isAlphaNum <*> (`elem` ("-_." :: [Char])) -    fileParser =-      Parser.label "test file" . ignoreSpacesAfter $-        fmap (TestTargetFile . Text.unpack) . Parser.takeWhile1P Nothing $-          (||) <$> isAlphaNum <*> (`elem` ("-_./" :: [Char]))+  fileParser =+    Parser.label "test file" . ignoreSpacesAfter $+      fmap (TestTargetFile . Text.unpack) . Parser.takeWhile1P Nothing $+        (||) <$> isAlphaNum <*> (`elem` ("-_./" :: [Char])) -    ignoreSpacesAfter m = m <* Parser.space+  ignoreSpacesAfter m = m <* Parser.space  showTestTargetParseError :: ParseErrorBundle -> Text showTestTargetParseError bundle =
+ src/Skeletest/Internal/Utils/BoxDrawing.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Skeletest.Internal.Utils.BoxDrawing (+  BoxSpec,+  BoxSpecContent (..),+  drawBox,+) where++import Data.Text (Text)+import Data.Text qualified as Text+import System.Console.Terminal.Size qualified as Term++type BoxSpec = [BoxSpecContent]++data BoxSpecContent+  = BoxText Text+  | BoxHeader Text+  deriving (Show, Eq)++drawBox :: BoxSpec -> IO Text+drawBox box = do+  width <- maybe 80 (max 40 . Term.width) <$> Term.size+  pure $ drawBox' width box++drawBox' :: Int -> BoxSpec -> Text+drawBox' width boxContents = Text.intercalate "\n" $ [header] <> concatMap draw boxContents <> [footer]+ where+  header = "╔" <> Text.replicate (width - 2) "═" <> "╗"+  footer = "╚" <> Text.replicate (width - 2) "═" <> "╝"++  draw = \case+    BoxHeader s ->+      [ drawLine ""+      , "╟─" <> cpad (width - 4) "─" ("⟨ " <> s <> " ⟩") <> "─╢"+      ]+    BoxText s ->+      [ drawLine line+      | rawLine <- Text.lines s+      , line <- if Text.null rawLine then [""] else Text.chunksOf (width - 4) rawLine+      ]+  drawLine s = "║ " <> rpad (width - 4) " " s <> " ║"++  rpad n fill s = s <> Text.replicate (n - Text.length s) fill+  cpad n fill s =+    let total = n - Text.length s+        left = total `div` 2+        right = total - left+     in Text.replicate left fill <> s <> Text.replicate right fill
src/Skeletest/Internal/Utils/Diff.hs view
@@ -13,5 +13,5 @@   Text.pack . PP.render $     prettyContextDiff (ppText fromName) (ppText toName) (ppText . Diff.unnumber) $       getContextDiff (Just 5) (Text.lines fromContent) (Text.lines toContent)-  where-    ppText = PP.text . Text.unpack+ where+  ppText = PP.text . Text.unpack
src/Skeletest/Internal/Utils/HList.hs view
@@ -37,10 +37,10 @@  hzipWithM ::   (Monad m) =>-  (forall x. f x -> g x -> m (h x))-  -> HList f xs-  -> HList g xs-  -> m (HList h xs)+  (forall x. f x -> g x -> m (h x)) ->+  HList f xs ->+  HList g xs ->+  m (HList h xs) hzipWithM k = \cases   HNil HNil -> pure HNil   (HCons f fs) (HCons g gs) -> HCons <$> k f g <*> hzipWithM k fs gs
src/Skeletest/Main.hs view
@@ -21,11 +21,8 @@ ) where  import Control.Monad (unless)-import Data.Maybe (fromMaybe)-import Data.Text qualified as Text-import System.Exit (exitFailure)- import Skeletest.Internal.CLI (Flag, flag, loadCliArgs)+import Skeletest.Internal.Capture (CaptureOutputFlag) import Skeletest.Internal.Snapshot (   SnapshotRenderer (..),   SnapshotUpdateFlag,@@ -42,11 +39,12 @@  ) import Skeletest.Plugin (Plugin (..)) import Skeletest.Prop.Internal (PropLimitFlag, PropSeedFlag)+import System.Exit (exitFailure) -runSkeletest :: [Plugin] -> [(FilePath, String, Spec)] -> IO ()+runSkeletest :: [Plugin] -> [(FilePath, Spec)] -> IO () runSkeletest = runSkeletest' . mconcat -runSkeletest' :: Plugin -> [(FilePath, String, Spec)] -> IO ()+runSkeletest' :: Plugin -> [(FilePath, Spec)] -> IO () runSkeletest' Plugin{..} testModules = do   selections <- loadCliArgs builtinFlags cliFlags   setSnapshotRenderers (snapshotRenderers <> defaultSnapshotRenderers)@@ -54,19 +52,16 @@   let initialSpecs = map mkSpec testModules   success <- runSpecs hooks . pruneSpec . applyTestSelections selections $ initialSpecs   unless success exitFailure-  where-    builtinFlags =-      [ flag @SnapshotUpdateFlag-      , flag @PropSeedFlag-      , flag @PropLimitFlag-      ]--    mkSpec (specPath, name, specSpec) =-      SpecInfo-        { specPath-        , specName = stripSuffix "Spec" $ Text.pack name-        , specSpec-        }+ where+  builtinFlags =+    [ flag @SnapshotUpdateFlag+    , flag @PropSeedFlag+    , flag @PropLimitFlag+    , flag @CaptureOutputFlag+    ] -    -- same as Text.stripSuffix, except return original string if not match-    stripSuffix suf s = fromMaybe s $ Text.stripSuffix suf s+  mkSpec (specPath, specSpec) =+    SpecInfo+      { specPath+      , specSpec+      }
src/Skeletest/Prop/Internal.hs view
@@ -45,14 +45,6 @@ import Hedgehog.Internal.Runner qualified as Hedgehog import Hedgehog.Internal.Seed qualified as Hedgehog.Seed import Hedgehog.Internal.Source qualified as Hedgehog-import Text.Read (readEither, readMaybe)-import UnliftIO.Exception (throwIO)-import UnliftIO.IORef (IORef, newIORef, readIORef, writeIORef)--#if !MIN_VERSION_base(4, 20, 0)-import Data.Foldable (foldl')-#endif- import Skeletest.Internal.CLI (FlagSpec (..), IsFlag (..), getFlag) import Skeletest.Internal.TestInfo (getTestInfo) import Skeletest.Internal.TestRunner (@@ -63,7 +55,14 @@   testResultPass,  ) import Skeletest.Internal.Utils.Color qualified as Color+import Text.Read (readEither, readMaybe)+import UnliftIO.Exception (throwIO)+import UnliftIO.IORef (IORef, newIORef, readIORef, writeIORef) +#if !MIN_VERSION_base(4, 20, 0)+import Data.Foldable (foldl')+#endif+ -- | A property to run, with optional configuration settings specified up front. -- -- Settings should be specified before any other 'Property' calls; any settings@@ -121,44 +120,44 @@  resolveConfig :: [PropertyConfig] -> Hedgehog.PropertyConfig resolveConfig = foldl' go defaultConfig-  where-    defaultConfig =-      Hedgehog.PropertyConfig-        { propertyDiscardLimit = 100-        , propertyShrinkLimit = 1000-        , propertyShrinkRetries = 0-        , propertyTerminationCriteria = Hedgehog.NoConfidenceTermination 100-        , propertySkip = Nothing-        }+ where+  defaultConfig =+    Hedgehog.PropertyConfig+      { propertyDiscardLimit = 100+      , propertyShrinkLimit = 1000+      , propertyShrinkRetries = 0+      , propertyTerminationCriteria = Hedgehog.NoConfidenceTermination 100+      , propertySkip = Nothing+      } -    go cfg = \case-      DiscardLimit x -> cfg{Hedgehog.propertyDiscardLimit = Hedgehog.DiscardLimit x}-      ShrinkLimit x -> cfg{Hedgehog.propertyShrinkLimit = Hedgehog.ShrinkLimit x}-      ShrinkRetries x -> cfg{Hedgehog.propertyShrinkRetries = Hedgehog.ShrinkRetries x}-      SetConfidence x ->-        cfg-          { Hedgehog.propertyTerminationCriteria =-              case Hedgehog.propertyTerminationCriteria cfg of-                Hedgehog.NoEarlyTermination _ tests -> Hedgehog.NoEarlyTermination (Hedgehog.Confidence $ fromIntegral x) tests-                Hedgehog.NoConfidenceTermination tests -> Hedgehog.NoEarlyTermination (Hedgehog.Confidence $ fromIntegral x) tests-                Hedgehog.EarlyTermination _ tests -> Hedgehog.EarlyTermination (Hedgehog.Confidence $ fromIntegral x) tests-          }-      SetVerifiedTermination ->-        cfg-          { Hedgehog.propertyTerminationCriteria =-              case Hedgehog.propertyTerminationCriteria cfg of-                Hedgehog.NoEarlyTermination c tests -> Hedgehog.EarlyTermination c tests-                Hedgehog.NoConfidenceTermination tests -> Hedgehog.EarlyTermination Hedgehog.defaultConfidence tests-                Hedgehog.EarlyTermination c tests -> Hedgehog.EarlyTermination c tests-          }-      SetTestLimit x ->-        cfg-          { Hedgehog.propertyTerminationCriteria =-              case Hedgehog.propertyTerminationCriteria cfg of-                Hedgehog.NoEarlyTermination c _ -> Hedgehog.NoEarlyTermination c (Hedgehog.TestLimit x)-                Hedgehog.NoConfidenceTermination _ -> Hedgehog.NoConfidenceTermination (Hedgehog.TestLimit x)-                Hedgehog.EarlyTermination c _ -> Hedgehog.EarlyTermination c (Hedgehog.TestLimit x)-          }+  go cfg = \case+    DiscardLimit x -> cfg{Hedgehog.propertyDiscardLimit = Hedgehog.DiscardLimit x}+    ShrinkLimit x -> cfg{Hedgehog.propertyShrinkLimit = Hedgehog.ShrinkLimit x}+    ShrinkRetries x -> cfg{Hedgehog.propertyShrinkRetries = Hedgehog.ShrinkRetries x}+    SetConfidence x ->+      cfg+        { Hedgehog.propertyTerminationCriteria =+            case Hedgehog.propertyTerminationCriteria cfg of+              Hedgehog.NoEarlyTermination _ tests -> Hedgehog.NoEarlyTermination (Hedgehog.Confidence $ fromIntegral x) tests+              Hedgehog.NoConfidenceTermination tests -> Hedgehog.NoEarlyTermination (Hedgehog.Confidence $ fromIntegral x) tests+              Hedgehog.EarlyTermination _ tests -> Hedgehog.EarlyTermination (Hedgehog.Confidence $ fromIntegral x) tests+        }+    SetVerifiedTermination ->+      cfg+        { Hedgehog.propertyTerminationCriteria =+            case Hedgehog.propertyTerminationCriteria cfg of+              Hedgehog.NoEarlyTermination c tests -> Hedgehog.EarlyTermination c tests+              Hedgehog.NoConfidenceTermination tests -> Hedgehog.EarlyTermination Hedgehog.defaultConfidence tests+              Hedgehog.EarlyTermination c tests -> Hedgehog.EarlyTermination c tests+        }+    SetTestLimit x ->+      cfg+        { Hedgehog.propertyTerminationCriteria =+            case Hedgehog.propertyTerminationCriteria cfg of+              Hedgehog.NoEarlyTermination c _ -> Hedgehog.NoEarlyTermination c (Hedgehog.TestLimit x)+              Hedgehog.NoConfidenceTermination _ -> Hedgehog.NoConfidenceTermination (Hedgehog.TestLimit x)+              Hedgehog.EarlyTermination c _ -> Hedgehog.EarlyTermination c (Hedgehog.TestLimit x)+        }  runProperty :: Property -> IO TestResult runProperty = \case@@ -242,63 +241,63 @@                     -- N.B. testFailContext is reversed!                     testFailContext failure <> reverse info                 }-  where-    reportProgress _ = pure ()-    renderSeed report =-      let Hedgehog.Seed value gamma = Hedgehog.reportSeed report-       in show value <> ":" <> show gamma-    renderCoverage coverage testCount =-      let columns =-            [ (name, percentStr, percentBar)-            | Hedgehog.MkLabel{..} <- List.sortOn Hedgehog.labelLocation $ Map.elems coverage-            , let-                Hedgehog.LabelName name = labelName-                Hedgehog.CoverCount count = labelAnnotation-                percent = round $ fromIntegral count / fromIntegral testCount * (100 :: Double)-                percentStr = show percent <> "%"-                percentBar = renderPercentBar percent-            ]-          (maxNameLen, maxPercentLen) =-            foldr-              ( \(name, percent, _) (nameAcc, percentAcc) ->-                  (max (length name) nameAcc, max (length percent) percentAcc)-              )-              (0, 0)-              columns-       in [ rjust maxNameLen name <> " " <> rjust maxPercentLen percentStr <> " " <> percentBar-          | (name, percentStr, percentBar) <- columns+ where+  reportProgress _ = pure ()+  renderSeed report =+    let Hedgehog.Seed value gamma = Hedgehog.reportSeed report+     in show value <> ":" <> show gamma+  renderCoverage coverage testCount =+    let columns =+          [ (name, percentStr, percentBar)+          | Hedgehog.MkLabel{..} <- List.sortOn Hedgehog.labelLocation $ Map.elems coverage+          , let+              Hedgehog.LabelName name = labelName+              Hedgehog.CoverCount count = labelAnnotation+              percent = round $ fromIntegral count / fromIntegral testCount * (100 :: Double)+              percentStr = show percent <> "%"+              percentBar = renderPercentBar percent           ]-    rjust n s = replicate (n - length s) ' ' <> s-    renderPercentBar percent =-      -- render percentage bar 20 characters wide-      let (n, r) = percent `divMod` 5-       in concat-            [ replicate n '█'-            , case r of-                0 -> ""-                1 -> "▏"-                2 -> "▍"-                3 -> "▌"-                4 -> "▊"-                _ -> "" -- unreachable-            , replicate (20 - n - (if r == 0 then 0 else 1)) '·'-            ]-    toCallStack mSpan =-      GHC.fromCallSiteList $-        case mSpan of-          Nothing -> []-          Just Hedgehog.Span{..} ->-            let loc =-                  GHC.SrcLoc-                    { srcLocPackage = ""-                    , srcLocModule = ""-                    , srcLocFile = spanFile-                    , srcLocStartLine = Hedgehog.unLineNo spanStartLine-                    , srcLocStartCol = Hedgehog.unColumnNo spanStartColumn-                    , srcLocEndLine = Hedgehog.unLineNo spanEndLine-                    , srcLocEndCol = Hedgehog.unColumnNo spanEndColumn-                    }-             in [("<unknown>", loc)]+        (maxNameLen, maxPercentLen) =+          foldr+            ( \(name, percent, _) (nameAcc, percentAcc) ->+                (max (length name) nameAcc, max (length percent) percentAcc)+            )+            (0, 0)+            columns+     in [ rjust maxNameLen name <> " " <> rjust maxPercentLen percentStr <> " " <> percentBar+        | (name, percentStr, percentBar) <- columns+        ]+  rjust n s = replicate (n - length s) ' ' <> s+  renderPercentBar percent =+    -- render percentage bar 20 characters wide+    let (n, r) = percent `divMod` 5+     in concat+          [ replicate n '█'+          , case r of+              0 -> ""+              1 -> "▏"+              2 -> "▍"+              3 -> "▌"+              4 -> "▊"+              _ -> "" -- unreachable+          , replicate (20 - n - (if r == 0 then 0 else 1)) '·'+          ]+  toCallStack mSpan =+    GHC.fromCallSiteList $+      case mSpan of+        Nothing -> []+        Just Hedgehog.Span{..} ->+          let loc =+                GHC.SrcLoc+                  { srcLocPackage = ""+                  , srcLocModule = ""+                  , srcLocFile = spanFile+                  , srcLocStartLine = Hedgehog.unLineNo spanStartLine+                  , srcLocStartCol = Hedgehog.unColumnNo spanStartColumn+                  , srcLocEndLine = Hedgehog.unLineNo spanEndLine+                  , srcLocEndCol = Hedgehog.unColumnNo spanEndColumn+                  }+           in [("<unknown>", loc)]  loadPropFlags :: IO (Hedgehog.Seed, [PropertyConfig]) loadPropFlags = do@@ -389,12 +388,12 @@       { flagDefault = PropSeedFlag Nothing       , flagParse = parse       }-    where-      parse s = maybe (Left $ "Invalid seed: " <> s) Right $ do-        (valS, ':' : gammaS) <- pure $ break (== ':') s-        val <- readMaybe valS-        gamma <- readMaybe gammaS-        pure . PropSeedFlag . Just $ Hedgehog.Seed val gamma+   where+    parse s = maybe (Left $ "Invalid seed: " <> s) Right $ do+      (valS, ':' : gammaS) <- pure $ break (== ':') s+      val <- readMaybe valS+      gamma <- readMaybe gammaS+      pure . PropSeedFlag . Just $ Hedgehog.Seed val gamma  newtype PropLimitFlag = PropLimitFlag (Maybe Int) 
src/bin/skeletest-preprocessor.hs view
@@ -45,7 +45,7 @@ handleErrors = handle $ \(e :: SkeletestError) -> do   hPutStrLn stderr $ normalizeLines $ displayException e   exitFailure-  where-    normalizeLines-      | __GLASGOW_HASKELL__ == (908 :: Int) = dropWhileEnd (== '\n')-      | otherwise = id+ where+  normalizeLines+    | __GLASGOW_HASKELL__ == (908 :: Int) = dropWhileEnd (== '\n')+    | otherwise = id
test/ExampleSpec.hs view
@@ -4,7 +4,6 @@  import Data.IORef (IORef, modifyIORef', newIORef, readIORef) import Data.Text qualified as Text- import Skeletest import Skeletest.Predicate qualified as P import Skeletest.Prop qualified as Prop
test/Skeletest/AssertionsSpec.hs view
@@ -1,10 +1,15 @@+{-# LANGUAGE CPP #-}+ module Skeletest.AssertionsSpec (spec) where  import Skeletest import Skeletest.Predicate qualified as P- import Skeletest.TestUtils.Integration +#if __GLASGOW_HASKELL__ == 910+import Data.Text qualified as Text+#endif+ spec :: Spec spec = do   describe "shouldBe" $ do@@ -140,3 +145,51 @@     code `shouldBe` ExitFailure 1     stderr `shouldBe` ""     stdout `shouldSatisfy` P.matchesSnapshot++  integration . it "shows helpful error on pattern match fail" $ do+    runner <- getFixture+    addTestFile runner "ExampleSpec.hs" $+      [ "module ExampleSpec (spec) where"+      , ""+      , "import Skeletest"+      , "import qualified Skeletest.Predicate as P"+      , ""+      , "spec = it \"should fail\" $ do"+      , "  Just x <- pure Nothing"+      , "  x `shouldBe` True"+      ]++    (code, stdout, stderr) <- runTests runner []+    code `shouldBe` ExitFailure 1+    stderr `shouldBe` ""+    stdout `shouldSatisfy` P.matchesSnapshot++  integration . it "shows unrecognized exceptions" $ do+    runner <- getFixture+    addTestFile runner "ExampleSpec.hs" $+      [ "module ExampleSpec (spec) where"+      , ""+      , "import Skeletest"+      , "import qualified Skeletest.Predicate as P"+      , ""+      , "spec = it \"should fail\" $ do"+      , "  _ <- readFile \"unknown-file.txt\""+      , "  pure ()"+      ]++    (code, stdout, stderr) <- runTests runner []+    code `shouldBe` ExitFailure 1+    stderr `shouldBe` ""+    sanitizeTraceback stdout `shouldSatisfy` P.matchesSnapshot++-- GHC 9.10 specifically added a backtrace to SomeException, which was reverted in 9.12+-- https://github.com/haskell/core-libraries-committee/issues/285+sanitizeTraceback :: String -> String+#if __GLASGOW_HASKELL__ == 910+sanitizeTraceback s =+  let (pre, post) = break (Text.pack "HasCallStack backtrace:" `Text.isInfixOf`) $ Text.lines $ Text.pack s+      (_, post2) = break (Text.pack "╚" `Text.isPrefixOf`) $ drop 1 post+   in Text.unpack . Text.unlines $ pre ++ post2+#else+sanitizeTraceback = id+#endif
test/Skeletest/Internal/CLISpec.hs view
@@ -5,14 +5,13 @@ import Data.Map qualified as Map import Data.Typeable (Typeable, typeOf) import Skeletest-import Skeletest.Predicate qualified as P- import Skeletest.Internal.CLI (   CLIFlagStore,   CLIParseResult (..),   flag,   parseCliArgs,  )+import Skeletest.Predicate qualified as P import Skeletest.TestUtils.Integration  newtype FooFlag = FooFlag String
+ test/Skeletest/Internal/CaptureSpec.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE OverloadedRecordDot #-}++module Skeletest.Internal.CaptureSpec (spec) where++import Data.List qualified as List+import Skeletest+import Skeletest.Predicate qualified as P+import Skeletest.TestUtils.Integration++spec :: Spec+spec = do+  mapM_ runtimeSpec ["stdout", "stderr"]++  describe "FixtureCapturedOutput" $ do+    mapM_ fixtureGetSpec $+      [ ("stdout", "getStdout")+      , ("stderr", "getStderr")+      ]+    mapM_ fixtureReadSpec $+      [ ("stdout", "readStdout")+      , ("stderr", "readStderr")+      ]++runtimeSpec :: String -> Spec+runtimeSpec handle = do+  describe handle $ do+    integration . it "is hidden on test success" $ do+      runner <- getFixture+      addTestFile runner "ExampleSpec.hs" $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "module ExampleSpec (spec) where"+        , ""+        , "import Skeletest"+        , "import System.IO qualified as IO"+        , ""+        , "spec = do"+        , "  it \"before\" $ do"+        , "    " <> render_hPutStrLn handle "before"+        , "  it \"test\" $ do"+        , "    " <> render_hPutStrLn handle "line1"+        , "    " <> render_hPutStrLn handle "line2"+        ]+      (code, stdout, stderr) <- runTests runner []+      stderr `shouldBe` ""+      stdout `shouldSatisfy` P.matchesSnapshot+      code `shouldBe` ExitSuccess++    integration . it "is rendered on test failure" $ do+      runner <- getFixture+      addTestFile runner "ExampleSpec.hs" $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "module ExampleSpec (spec) where"+        , ""+        , "import Skeletest"+        , "import System.IO qualified as IO"+        , ""+        , "spec = do"+        , "  it \"before\" $ do"+        , "    " <> render_hPutStrLn handle "before"+        , "  it \"test\" $ do"+        , "    " <> render_hPutStrLn handle "line1"+        , "    " <> render_hPutStrLn handle "line2"+        , "    1 `shouldBe` 2"+        ]+      (code, stdout, stderr) <- runTests runner []+      stderr `shouldBe` ""+      stdout `shouldSatisfy` P.matchesSnapshot+      code `shouldBe` ExitFailure 1++    integration . it "is rendered on test error" $ do+      runner <- getFixture+      addTestFile runner "ExampleSpec.hs" $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "module ExampleSpec (spec) where"+        , ""+        , "import Skeletest"+        , "import System.IO qualified as IO"+        , ""+        , "spec = do"+        , "  it \"before\" $ do"+        , "    " <> render_hPutStrLn handle "before"+        , "  it \"test\" $ do"+        , "    " <> render_hPutStrLn handle "line1"+        , "    " <> render_hPutStrLn handle "line2"+        , "    Just _ <- pure Nothing"+        , "    pure ()"+        ]+      (code, stdout, stderr) <- runTests runner []+      stderr `shouldBe` ""+      stdout `shouldSatisfy` P.matchesSnapshot+      code `shouldBe` ExitFailure 1++    integration . it "is not captured with --capture-output=off" $ do+      runner <- getFixture+      addTestFile runner "ExampleSpec.hs" $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "module ExampleSpec (spec) where"+        , ""+        , "import Skeletest"+        , "import System.IO qualified as IO"+        , ""+        , "spec = do"+        , "  it \"before\" $ do"+        , "    " <> render_hPutStrLn handle "before"+        , "  it \"test\" $ do"+        , "    " <> render_hPutStrLn handle "line1"+        , "    " <> render_hPutStrLn handle "line2"+        ]+      (code, stdout, stderr) <- runTests runner ["--capture-output=off"]+      List.intercalate "\n\n" [">>> stdout", stdout, ">>> stderr", stderr] `shouldSatisfy` P.matchesSnapshot+      code `shouldBe` ExitSuccess++fixtureGetSpec :: (String, String) -> Spec+fixtureGetSpec (handle, func) =+  describe func $ do+    integration . it "returns captured output from current test" $ do+      runner <- getFixture+      addTestFile runner "ExampleSpec.hs" $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "module ExampleSpec (spec) where"+        , ""+        , "import Skeletest"+        , "import System.IO qualified as IO"+        , ""+        , "spec = do"+        , "  it \"before\" $ do"+        , "    " <> render_hPutStrLn handle "before"+        , "  it \"test\" $ do"+        , "    output <- getFixture @FixtureCapturedOutput"+        , "    s <- output." <> func+        , "    s `shouldBe` " <> show ""+        , "    " <> render_hPutStrLn handle "test1"+        , "    s <- output." <> func+        , "    s `shouldBe` " <> show "test1\n"+        , "    " <> render_hPutStrLn handle "test2"+        , "    s <- output." <> func+        , "    s `shouldBe` " <> show "test1\ntest2\n"+        ]+      (code, stdout, stderr) <- runTests runner []+      stderr `shouldBe` ""+      context stdout $+        code `shouldBe` ExitSuccess++fixtureReadSpec :: (String, String) -> Spec+fixtureReadSpec (handle, func) =+  describe func $ do+    integration . it "returns captured output from current test" $ do+      runner <- getFixture+      addTestFile runner "ExampleSpec.hs" $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "module ExampleSpec (spec) where"+        , ""+        , "import Skeletest"+        , "import System.IO qualified as IO"+        , ""+        , "spec = do"+        , "  it \"before\" $ do"+        , "    " <> render_hPutStrLn handle "before"+        , "  it \"test\" $ do"+        , "    output <- getFixture @FixtureCapturedOutput"+        , "    s <- output." <> func+        , "    s `shouldBe` " <> show ""+        , "    " <> render_hPutStrLn handle "test1"+        , "    s <- output." <> func+        , "    s `shouldBe` " <> show "test1\n"+        , "    " <> render_hPutStrLn handle "test2"+        , "    s <- output." <> func+        , "    s `shouldBe` " <> show "test2\n"+        ]+      (code, stdout, stderr) <- runTests runner []+      stderr `shouldBe` ""+      context stdout $+        code `shouldBe` ExitSuccess++render_hPutStrLn :: String -> String -> String+render_hPutStrLn handle s = "IO.hPutStrLn IO." <> handle <> " " <> show s
test/Skeletest/Internal/FixturesSpec.hs view
@@ -2,7 +2,6 @@  import Skeletest import Skeletest.Predicate qualified as P- import Skeletest.TestUtils.Integration  spec :: Spec
test/Skeletest/Internal/SnapshotSpec.hs view
@@ -1,14 +1,12 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}  module Skeletest.Internal.SnapshotSpec (spec) where  import Data.Aeson qualified as Aeson import Data.String (fromString)+import Data.Text qualified as Text import Skeletest-import Skeletest.Predicate qualified as P-import Skeletest.Prop.Gen qualified as Gen-import Skeletest.Prop.Range qualified as Range- import Skeletest.Internal.Snapshot (   SnapshotFile (..),   SnapshotValue (..),@@ -16,6 +14,9 @@   encodeSnapshotFile,   normalizeSnapshotFile,  )+import Skeletest.Predicate qualified as P+import Skeletest.Prop.Gen qualified as Gen+import Skeletest.Prop.Range qualified as Range import Skeletest.TestUtils.Integration  spec :: Spec@@ -111,25 +112,29 @@  genSnapshotFileRaw :: Gen SnapshotFile genSnapshotFileRaw = do-  moduleName <- Gen.text (Range.linear 0 100) genHsModuleChar+  testFile <- genHsModule   snapshots <- Gen.map rangeNumTests genSnapshot   pure SnapshotFile{..}-  where-    rangeNumTests = Range.linear 0 10-    rangeSnapshotsPerTest = Range.linear 0 5-    rangeSnapshotSize = Range.linear 0 1000+ where+  rangeNumTests = Range.linear 0 10+  rangeSnapshotsPerTest = Range.linear 0 5+  rangeSnapshotSize = Range.linear 0 1000 -    genHsModuleChar = Gen.choice [Gen.alphaNum, pure '\'']+  genHsModule = do+    dirs <- Gen.list (Range.linear 0 10) genHsModuleName+    file <- genHsModuleName+    pure $ Text.intercalate "/" dirs <> file <> ".hs"+  genHsModuleName = Gen.text (Range.linear 0 50) $ Gen.choice [Gen.alphaNum, pure '\''] -    genSnapshot = do-      ident <- Gen.list (Range.linear 1 10) (Gen.text (Range.linear 1 100) Gen.unicode)-      vals <- Gen.list rangeSnapshotsPerTest genSnapshotVal-      pure (ident, vals)+  genSnapshot = do+    ident <- Gen.list (Range.linear 1 10) (Gen.text (Range.linear 1 100) Gen.unicode)+    vals <- Gen.list rangeSnapshotsPerTest genSnapshotVal+    pure (ident, vals) -    genSnapshotVal = do-      snapshotContent <- Gen.text rangeSnapshotSize Gen.unicode-      snapshotLang <- Gen.maybe $ Gen.text (Range.linear 1 5) Gen.unicode-      pure SnapshotValue{..}+  genSnapshotVal = do+    snapshotContent <- Gen.text rangeSnapshotSize Gen.unicode+    snapshotLang <- Gen.maybe $ Gen.text (Range.linear 1 5) Gen.unicode+    pure SnapshotValue{..}  genSnapshotFile :: Gen SnapshotFile genSnapshotFile = normalizeSnapshotFile <$> genSnapshotFileRaw
test/Skeletest/Internal/SpecSpec.hs view
@@ -2,7 +2,6 @@  import Skeletest import Skeletest.Predicate qualified as P- import Skeletest.TestUtils.Integration  spec :: Spec
test/Skeletest/Internal/TestTargetsSpec.hs view
@@ -3,9 +3,8 @@ module Skeletest.Internal.TestTargetsSpec (spec) where  import Skeletest-import Skeletest.Predicate qualified as P- import Skeletest.Internal.TestTargets+import Skeletest.Predicate qualified as P  spec :: Spec spec = do
test/Skeletest/Internal/__snapshots__/CLISpec.snap.md view
@@ -1,9 +1,12 @@-# Skeletest.Internal.CLI+# test/Skeletest/Internal/CLISpec.hs  ## getFlag / errors if flag is not registered  ```-Example+./ExampleSpec.hs     should error: ERROR-        CLI flag 'my-flag' was not registered. Did you add it to cliFlags in Main.hs?+╔══════════════════════════════════════════════════════════════════════════════╗+║ CLI flag 'my-flag' was not registered. Did you add it to cliFlags in Main.hs ║+║ ?                                                                            ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```
test/Skeletest/Internal/__snapshots__/FixturesSpec.snap.md view
@@ -1,9 +1,12 @@-# Skeletest.Internal.Fixtures+# test/Skeletest/Internal/FixturesSpec.hs  ## getFixture / detects circular dependencies  ```-Example+./ExampleSpec.hs     should error: ERROR-        Found circular dependency when resolving fixtures: FixtureA -> FixtureB -> FixtureD -> FixtureA+╔══════════════════════════════════════════════════════════════════════════════╗+║ Found circular dependency when resolving fixtures: FixtureA -> FixtureB -> F ║+║ ixtureD -> FixtureA                                                          ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```
test/Skeletest/Internal/__snapshots__/SnapshotSpec.snap.md view
@@ -1,11 +1,13 @@-# Skeletest.Internal.Snapshot+# test/Skeletest/Internal/SnapshotSpec.hs  ## detects corrupted snapshot files  ```-Example+./ExampleSpec.hs     should error: ERROR-        Snapshot file was corrupted: ./__snapshots__/ExampleSpec.snap.md+╔══════════════════════════════════════════════════════════════════════════════╗+║ Snapshot file was corrupted: ./__snapshots__/ExampleSpec.snap.md             ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## renders JSON values@@ -22,24 +24,24 @@ ## shows helpful failure messages  ```-Example+./ExampleSpec.hs     fails: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:7:-|-|   unlines ["new1", "same1", "same2", "new2"] `shouldSatisfy` P.matchesSnapshot-|                                              ^^^^^^^^^^^^^^^--Result differed from snapshot. Update snapshot with --update.---- expected-+++ actual-@@ -1,4 +1,4 @@-+new1- same1--old1- same2--old2-+new2----------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:7:                                                          ║+║ |                                                                            ║+║ |   unlines ["new1", "same1", "same2", "new2"] `shouldSatisfy` P.matchesSnap ║+║ shot                                                                         ║+║ |                                              ^^^^^^^^^^^^^^^               ║+║                                                                              ║+║ Result differed from snapshot. Update snapshot with --update.                ║+║ --- expected                                                                 ║+║ +++ actual                                                                   ║+║ @@ -1,4 +1,4 @@                                                              ║+║ +new1                                                                        ║+║  same1                                                                       ║+║ -old1                                                                        ║+║  same2                                                                       ║+║ -old2                                                                        ║+║ +new2                                                                        ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```
test/Skeletest/Internal/__snapshots__/SpecSpec.snap.md view
@@ -1,9 +1,9 @@-# Skeletest.Internal.Spec+# test/Skeletest/Internal/SpecSpec.hs  ## skip / skips tests completely  ```-Example+./ExampleSpec.hs     should not run: SKIP         broken tests     should not run either: SKIP@@ -13,7 +13,7 @@ ## xfail / checks for expected failures  ```-Example+./ExampleSpec.hs     should fail: XFAIL         broken tests     should fail too: XFAIL@@ -23,7 +23,7 @@ ## xfail / errors on unexpected passes  ```-Example+./ExampleSpec.hs     should fail: XPASS         broken tests     should fail too: XPASS
test/Skeletest/Internal/__snapshots__/TestTargetsSpec.snap.md view
@@ -1,4 +1,4 @@-# Skeletest.Internal.TestTargets+# test/Skeletest/Internal/TestTargetsSpec.hs  ## parseTestTargets / fails with a helpful error message 
test/Skeletest/MainSpec.hs view
@@ -5,7 +5,6 @@ import Data.Text qualified as Text import Skeletest import Skeletest.Predicate qualified as P- import Skeletest.TestUtils.Integration  spec :: Spec@@ -59,16 +58,16 @@  normalizePluginError :: String -> String normalizePluginError = Text.unpack . go . Text.pack-  where-    replace old new = Text.replace (Text.pack old) (Text.pack new)-    go-      | __GLASGOW_HASKELL__ == (908 :: Int) = replace "*** Exception: ExitFailure 1" "\n*** Exception: ExitFailure 1"-      | otherwise = id+ where+  replace old new = Text.replace (Text.pack old) (Text.pack new)+  go+    | __GLASGOW_HASKELL__ == (908 :: Int) = replace "*** Exception: ExitFailure 1" "\n*** Exception: ExitFailure 1"+    | otherwise = id  normalizeGhc29916 :: String -> String normalizeGhc29916 = Text.unpack . go . Text.pack-  where-    replace old new = Text.replace (Text.pack old) (Text.pack new)-    go-      | __GLASGOW_HASKELL__ == (908 :: Int) = replace "<generated>" "<no location info>"-      | otherwise = id+ where+  replace old new = Text.replace (Text.pack old) (Text.pack new)+  go+    | __GLASGOW_HASKELL__ == (908 :: Int) = replace "<generated>" "<no location info>"+    | otherwise = id
test/Skeletest/PredicateSpec.hs view
@@ -7,11 +7,10 @@ import Data.IORef (newIORef, readIORef, writeIORef) import Data.Text qualified as Text import Skeletest-import Skeletest.Predicate qualified as P-import UnliftIO.Exception (Exception, throwIO)- import Skeletest.Internal.Predicate (PredicateResult (..), runPredicate)+import Skeletest.Predicate qualified as P import Skeletest.TestUtils.Integration+import UnliftIO.Exception (Exception, throwIO)  data User = User   { name :: String@@ -398,67 +397,67 @@  normalizeVars :: String -> String normalizeVars = go-  where-    go = \case-      [] -> []-      'x' : '0' : '_' : cs -> "x0" <> go (drop 4 cs)-      'a' : 'c' : 't' : 'u' : 'a' : 'l' : '_' : cs -> "actual" <> go (drop 4 cs)-      c : cs -> c : go cs+ where+  go = \case+    [] -> []+    'x' : '0' : '_' : cs -> "x0" <> go (drop 4 cs)+    'a' : 'c' : 't' : 'u' : 'a' : 'l' : '_' : cs -> "actual" <> go (drop 4 cs)+    c : cs -> c : go cs  normalizeConFailure :: String -> String normalizeConFailure = Text.unpack . go . Text.pack-  where-    go-      | __GLASGOW_HASKELL__ == (908 :: Int) =-          let old =-                Text.pack . unlines $-                  [ "    • In a stmt of a 'do' block:"-                  , "        User \"alice\" (Just 1)"-                  , "          `shouldSatisfy`"-                  , "            Skeletest.Internal.Predicate.conMatches"-                  , "              \"User\" Nothing"-                  , "              \\ actual"-                  , "                -> case pure actual of"-                  , "                     Just (User x0)"-                  , "                       -> Just"-                  , "                            (Skeletest.Internal.Utils.HList.HCons"-                  , "                               (pure x0) Skeletest.Internal.Utils.HList.HNil)"-                  , "                     _ -> Nothing"-                  , "              (Skeletest.Internal.Utils.HList.HCons"-                  , "                 (P.eq \"\") Skeletest.Internal.Utils.HList.HNil)"-                  , "      In the second argument of ‘($)’, namely"-                  , "        ‘do User \"alice\" (Just 1)"-                  , "              `shouldSatisfy`"-                  , "                Skeletest.Internal.Predicate.conMatches"-                  , "                  \"User\" Nothing"-                  , "                  \\ actual"-                  , "                    -> case pure actual of"-                  , "                         Just (User x0) -> ..."-                  , "                         _ -> ..."-                  , "                  (Skeletest.Internal.Utils.HList.HCons"-                  , "                     (P.eq \"\") Skeletest.Internal.Utils.HList.HNil)’"-                  , "      In the expression:"-                  , "        it \"should error\""-                  , "          $ do User \"alice\" (Just 1)"-                  , "                 `shouldSatisfy`"-                  , "                   Skeletest.Internal.Predicate.conMatches"-                  , "                     \"User\" Nothing"-                  , "                     \\ actual"-                  , "                       -> case pure actual of"-                  , "                            Just (User x0) -> ..."-                  , "                            _ -> ..."-                  , "                     (Skeletest.Internal.Utils.HList.HCons"-                  , "                        (P.eq \"\") Skeletest.Internal.Utils.HList.HNil)"-                  ]-              new =-                Text.pack . unlines $-                  [ "    • In the pattern: User x0"-                  , "      In the pattern: Just (User x0)"-                  , "      In a case alternative:"-                  , "          Just (User x0)"-                  , "            -> Just"-                  , "                 (Skeletest.Internal.Utils.HList.HCons"-                  , "                    (pure x0) Skeletest.Internal.Utils.HList.HNil)"-                  ]-           in Text.replace old new-      | otherwise = id+ where+  go+    | __GLASGOW_HASKELL__ == (908 :: Int) =+        let old =+              Text.pack . unlines $+                [ "    • In a stmt of a 'do' block:"+                , "        User \"alice\" (Just 1)"+                , "          `shouldSatisfy`"+                , "            Skeletest.Internal.Predicate.conMatches"+                , "              \"User\" Nothing"+                , "              \\ actual"+                , "                -> case pure actual of"+                , "                     Just (User x0)"+                , "                       -> Just"+                , "                            (Skeletest.Internal.Utils.HList.HCons"+                , "                               (pure x0) Skeletest.Internal.Utils.HList.HNil)"+                , "                     _ -> Nothing"+                , "              (Skeletest.Internal.Utils.HList.HCons"+                , "                 (P.eq \"\") Skeletest.Internal.Utils.HList.HNil)"+                , "      In the second argument of ‘($)’, namely"+                , "        ‘do User \"alice\" (Just 1)"+                , "              `shouldSatisfy`"+                , "                Skeletest.Internal.Predicate.conMatches"+                , "                  \"User\" Nothing"+                , "                  \\ actual"+                , "                    -> case pure actual of"+                , "                         Just (User x0) -> ..."+                , "                         _ -> ..."+                , "                  (Skeletest.Internal.Utils.HList.HCons"+                , "                     (P.eq \"\") Skeletest.Internal.Utils.HList.HNil)’"+                , "      In the expression:"+                , "        it \"should error\""+                , "          $ do User \"alice\" (Just 1)"+                , "                 `shouldSatisfy`"+                , "                   Skeletest.Internal.Predicate.conMatches"+                , "                     \"User\" Nothing"+                , "                     \\ actual"+                , "                       -> case pure actual of"+                , "                            Just (User x0) -> ..."+                , "                            _ -> ..."+                , "                     (Skeletest.Internal.Utils.HList.HCons"+                , "                        (P.eq \"\") Skeletest.Internal.Utils.HList.HNil)"+                ]+            new =+              Text.pack . unlines $+                [ "    • In the pattern: User x0"+                , "      In the pattern: Just (User x0)"+                , "      In a case alternative:"+                , "          Just (User x0)"+                , "            -> Just"+                , "                 (Skeletest.Internal.Utils.HList.HCons"+                , "                    (pure x0) Skeletest.Internal.Utils.HList.HNil)"+                ]+         in Text.replace old new+    | otherwise = id
test/Skeletest/PropSpec.hs view
@@ -4,7 +4,6 @@ import Skeletest.Predicate qualified as P import Skeletest.Prop.Gen qualified as Gen import Skeletest.Prop.Range qualified as Range- import Skeletest.TestUtils.Integration  spec :: Spec
test/Skeletest/TestUtils/Integration.hs view
@@ -60,12 +60,12 @@         { testRunnerDir = tmpdir         , testRunnerSettingsRef = settingsRef         }-    where-      defaultSettings =-        TestRunnerSettings-          { mainFile = ["import Skeletest.Main"]-          , testFiles = []-          }+   where+    defaultSettings =+      TestRunnerSettings+        { mainFile = ["import Skeletest.Main"]+        , testFiles = []+        }  setMainFile :: FixtureTestRunner -> FileContents -> IO () setMainFile FixtureTestRunner{testRunnerSettingsRef} contents =@@ -93,25 +93,25 @@         ]    pure (code, sanitize stdout, sanitize stderr)-  where-    addFile fp contents = do-      let path = testRunnerDir </> fp-      createDirectoryIfMissing True (takeDirectory path)-      writeFile path (unlines contents)+ where+  addFile fp contents = do+    let path = testRunnerDir </> fp+    createDirectoryIfMissing True (takeDirectory path)+    writeFile path (unlines contents) -    ghcArgs =-      concat-        [ ["-hide-all-packages"]-        , ["-F", "-pgmF=skeletest-preprocessor"]-        , ["-package skeletest"]-        ]-    setCWD dir p = p{cwd = Just dir}+  ghcArgs =+    concat+      [ ["-hide-all-packages"]+      , ["-F", "-pgmF=skeletest-preprocessor"]+      , ["-package skeletest"]+      ]+  setCWD dir p = p{cwd = Just dir} -    sanitize = Text.unpack . stripControlChars . Text.strip . Text.pack-    stripControlChars s =-      case Text.breakOn "\x1b" s of-        (_, "") -> s-        (pre, post) -> pre <> stripControlChars (Text.drop 1 . Text.dropWhile (/= 'm') $ post)+  sanitize = Text.unpack . stripControlChars . Text.strip . Text.pack+  stripControlChars s =+    case Text.breakOn "\x1b" s of+      (_, "") -> s+      (pre, post) -> pre <> stripControlChars (Text.drop 1 . Text.dropWhile (/= 'm') $ post)  expectSuccess :: (HasCallStack) => IO (ExitCode, String, String) -> IO (String, String) expectSuccess m = do
test/Skeletest/__snapshots__/AssertionsSpec.snap.md view
@@ -1,131 +1,157 @@-# Skeletest.Assertions+# test/Skeletest/AssertionsSpec.hs  ## context / should show failure context  ```-Example+./ExampleSpec.hs     should fail: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:7:-|-|     1 `shouldBe` (2 :: Int)-|       ^^^^^^^^^^--hello-world--1 ≠ 2---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:7:                                                          ║+║ |                                                                            ║+║ |     1 `shouldBe` (2 :: Int)                                                ║+║ |       ^^^^^^^^^^                                                           ║+║                                                                              ║+║ hello                                                                        ║+║ world                                                                        ║+║                                                                              ║+║ 1 ≠ 2                                                                        ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## failTest / should show failure  ```-Example+./ExampleSpec.hs     should fail: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:5:-|-| spec = it "should fail" $ failTest "error message"-|                           ^^^^^^^^--error message---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:5:                                                          ║+║ |                                                                            ║+║ | spec = it "should fail" $ failTest "error message"                         ║+║ |                           ^^^^^^^^                                         ║+║                                                                              ║+║ error message                                                                ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## shouldBe / should show helpful failure  ```-Example+./ExampleSpec.hs     should fail: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:5:-|-| spec = it "should fail" $ 1 `shouldBe` (2 :: Int)-|                             ^^^^^^^^^^--1 ≠ 2---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:5:                                                          ║+║ |                                                                            ║+║ | spec = it "should fail" $ 1 `shouldBe` (2 :: Int)                          ║+║ |                             ^^^^^^^^^^                                     ║+║                                                                              ║+║ 1 ≠ 2                                                                        ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## shouldNotBe / should show helpful failure  ```-Example+./ExampleSpec.hs     should fail: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:5:-|-| spec = it "should fail" $ 1 `shouldNotBe` (1 :: Int)-|                             ^^^^^^^^^^^^^--1 = 1--Expected:-  ≠ 1--Got:-  1---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:5:                                                          ║+║ |                                                                            ║+║ | spec = it "should fail" $ 1 `shouldNotBe` (1 :: Int)                       ║+║ |                             ^^^^^^^^^^^^^                                  ║+║                                                                              ║+║ 1 = 1                                                                        ║+║                                                                              ║+║ Expected:                                                                    ║+║   ≠ 1                                                                        ║+║                                                                              ║+║ Got:                                                                         ║+║   1                                                                          ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## shouldNotSatisfy / should show helpful failure  ```-Example+./ExampleSpec.hs     should fail: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:6:-|-| spec = it "should fail" $ 1 `shouldNotSatisfy` P.gt (0 :: Int)-|                             ^^^^^^^^^^^^^^^^^^--1 > 0--Expected:-  ≯ 0--Got:-  1---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:6:                                                          ║+║ |                                                                            ║+║ | spec = it "should fail" $ 1 `shouldNotSatisfy` P.gt (0 :: Int)             ║+║ |                             ^^^^^^^^^^^^^^^^^^                             ║+║                                                                              ║+║ 1 > 0                                                                        ║+║                                                                              ║+║ Expected:                                                                    ║+║   ≯ 0                                                                        ║+║                                                                              ║+║ Got:                                                                         ║+║   1                                                                          ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## shouldSatisfy / should show helpful failure  ```-Example+./ExampleSpec.hs     should fail: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:6:-|-| spec = it "should fail" $ (-1) `shouldSatisfy` P.gt (0 :: Int)-|                                ^^^^^^^^^^^^^^^---1 ≯ 0---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:6:                                                          ║+║ |                                                                            ║+║ | spec = it "should fail" $ (-1) `shouldSatisfy` P.gt (0 :: Int)             ║+║ |                                ^^^^^^^^^^^^^^^                             ║+║                                                                              ║+║ -1 ≯ 0                                                                       ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## shows backtrace of failed assertions  ```-Example+./ExampleSpec.hs     should fail: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:6:-|-| spec = it "should fail" $ expectPositive (-1)-|                           ^^^^^^^^^^^^^^+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:6:                                                          ║+║ |                                                                            ║+║ | spec = it "should fail" $ expectPositive (-1)                              ║+║ |                           ^^^^^^^^^^^^^^                                   ║+║                                                                              ║+║ ./ExampleSpec.hs:9:                                                          ║+║ |                                                                            ║+║ | expectPositive = expectGT 0                                                ║+║ |                  ^^^^^^^^                                                  ║+║                                                                              ║+║ ./ExampleSpec.hs:12:                                                         ║+║ |                                                                            ║+║ | expectGT x actual = actual `shouldSatisfy` P.gt x                          ║+║ |                            ^^^^^^^^^^^^^^^                                 ║+║                                                                              ║+║ -1 ≯ 0                                                                       ║+╚══════════════════════════════════════════════════════════════════════════════╝+``` -./ExampleSpec.hs:9:-|-| expectPositive = expectGT 0-|                  ^^^^^^^^+## shows helpful error on pattern match fail -./ExampleSpec.hs:12:-|-| expectGT x actual = actual `shouldSatisfy` P.gt x-|                            ^^^^^^^^^^^^^^^+```+./ExampleSpec.hs+    should fail: ERROR+╔══════════════════════════════════════════════════════════════════════════════╗+║ ExampleSpec.hs:7:                                                            ║+║ |                                                                            ║+║ |   Just x <- pure Nothing                                                   ║+║ |   ^^^^^^                                                                   ║+║                                                                              ║+║ Pattern match failure in 'do' block                                          ║+╚══════════════════════════════════════════════════════════════════════════════╝+``` --1 ≯ 0---------------------------------------------------------------------------------+## shows unrecognized exceptions++```+./ExampleSpec.hs+    should fail: ERROR+╔══════════════════════════════════════════════════════════════════════════════╗+║ Got exception of type `IOException`:                                         ║+║ unknown-file.txt: openFile: does not exist (No such file or directory)       ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```
test/Skeletest/__snapshots__/MainSpec.snap.md view
@@ -1,4 +1,4 @@-# Skeletest.Main+# test/Skeletest/MainSpec.hs  ## errors if Skeletest.Main not imported 
test/Skeletest/__snapshots__/PredicateSpec.snap.md view
@@ -1,4 +1,4 @@-# Skeletest.Internal.Predicate+# test/Skeletest/PredicateSpec.hs  ## Combinators / && / shows helpful failure messages @@ -197,28 +197,29 @@ ## Data types / con / fails to compile when applied to multiple arguments  ```-<no location info>: error:-    -******************** skeletest failure ********************-P.con must be applied to exactly one argument+ExampleSpec.hs:7:22: error:+    P.con must be applied to exactly one argument+  |+7 |   "" `shouldSatisfy` P.con 1 2+  |                      ^^^^^^^^^ ```  ## Data types / con / fails to compile when not applied to anything  ```-<no location info>: error:-    -******************** skeletest failure ********************-P.con must be applied to a constructor+ExampleSpec.hs:7:22: error: P.con must be applied to a constructor+  |+7 |   "" `shouldSatisfy` P.con+  |                      ^^^^^ ```  ## Data types / con / fails to compile with non-constructor  ```-<no location info>: error:-    -******************** skeletest failure ********************-P.con must be applied to a constructor+ExampleSpec.hs:7:22: error: P.con must be applied to a constructor+  |+7 |   "" `shouldSatisfy` P.con ""+  |                      ^^^^^^^^ ```  ## Data types / con / fails to compile with omitted positional fields@@ -250,22 +251,22 @@ ## Data types / con / shows a helpful failure message  ```-Example+./ExampleSpec.hs     should error: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:9:-|-|   User "alice" `shouldSatisfy` P.con User{name = P.eq ""}-|                ^^^^^^^^^^^^^^^--"alice" ≠ []--Expected:-  matches User{name = (= [])}--Got:-  User "alice"---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:9:                                                          ║+║ |                                                                            ║+║ |   User "alice" `shouldSatisfy` P.con User{name = P.eq ""}                  ║+║ |                ^^^^^^^^^^^^^^^                                             ║+║                                                                              ║+║ "alice" ≠ []                                                                 ║+║                                                                              ║+║ Expected:                                                                    ║+║   matches User{name = (= [])}                                                ║+║                                                                              ║+║ Got:                                                                         ║+║   User "alice"                                                               ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## Data types / list / shows helpful failure messages
test/Skeletest/__snapshots__/PropSpec.snap.md view
@@ -1,52 +1,54 @@-# Skeletest.Prop+# test/Skeletest/PropSpec.hs  ## === / shows a helpful failure message  ```-Example+./ExampleSpec.hs     is isomorphic: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:10:-|-|     (read . show) P.=== (+ 1) `shouldSatisfy` P.isoWith (Gen.int $ Range.linear 0 10)-|                               ^^^^^^^^^^^^^^^--Failed after 1 tests.-Rerun with --seed=0:0 to reproduce.--./ExampleSpec.hs:10:47 ==> 0--0 ≠ 1-where-  0 = (read . show) 0-  1 = (+ 1) 0---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:10:                                                         ║+║ |                                                                            ║+║ |     (read . show) P.=== (+ 1) `shouldSatisfy` P.isoWith (Gen.int $ Range.l ║+║ inear 0 10)                                                                  ║+║ |                               ^^^^^^^^^^^^^^^                              ║+║                                                                              ║+║ Failed after 1 tests.                                                        ║+║ Rerun with --seed=0:0 to reproduce.                                          ║+║                                                                              ║+║ ./ExampleSpec.hs:10:47 ==> 0                                                 ║+║                                                                              ║+║ 0 ≠ 1                                                                        ║+║ where                                                                        ║+║   0 = (read . show) 0                                                        ║+║   1 = (+ 1) 0                                                                ║+╚══════════════════════════════════════════════════════════════════════════════╝     is not isomorphic: FAIL----------------------------------------------------------------------------------./ExampleSpec.hs:12:-|-|     (read . show) P.=== id `shouldNotSatisfy` P.isoWith (Gen.int $ Range.linear 0 10)-|                            ^^^^^^^^^^^^^^^^^^--Failed after 1 tests.-Rerun with --seed=0:0 to reproduce.--./ExampleSpec.hs:12:47 ==> 0--0 = 0-where-  0 = (read . show) 0-  0 = id 0---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ ./ExampleSpec.hs:12:                                                         ║+║ |                                                                            ║+║ |     (read . show) P.=== id `shouldNotSatisfy` P.isoWith (Gen.int $ Range.l ║+║ inear 0 10)                                                                  ║+║ |                            ^^^^^^^^^^^^^^^^^^                              ║+║                                                                              ║+║ Failed after 1 tests.                                                        ║+║ Rerun with --seed=0:0 to reproduce.                                          ║+║                                                                              ║+║ ./ExampleSpec.hs:12:47 ==> 0                                                 ║+║                                                                              ║+║ 0 = 0                                                                        ║+║ where                                                                        ║+║   0 = (read . show) 0                                                        ║+║   0 = id 0                                                                   ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```  ## setDiscardLimit / sets discard limit  ```-Example+./ExampleSpec.hs     discards: FAIL----------------------------------------------------------------------------------Gave up after 10 discards.-Passed 0 tests.---------------------------------------------------------------------------------+╔══════════════════════════════════════════════════════════════════════════════╗+║ Gave up after 10 discards.                                                   ║+║ Passed 0 tests.                                                              ║+╚══════════════════════════════════════════════════════════════════════════════╝ ```
test/__snapshots__/ExampleSpec.snap.md view
@@ -1,4 +1,4 @@-# Example+# test/ExampleSpec.hs  ## predicates / matches snapshots