packages feed

hspec-golden-aeson 0.1.0.0 → 0.2.0.0

raw patch · 9 files changed

+331/−109 lines, 9 filesdep ~quickcheck-arbitrary-adtPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: quickcheck-arbitrary-adt

API changes (from Hackage documentation)

+ Test.Aeson.GenericSpecs: CustomDirectoryName :: String -> GoldenDirectoryOption
+ Test.Aeson.GenericSpecs: GoldenDirectory :: GoldenDirectoryOption
+ Test.Aeson.GenericSpecs: Settings :: GoldenDirectoryOption -> Bool -> Int -> Settings
+ Test.Aeson.GenericSpecs: [goldenDirectoryOption] :: Settings -> GoldenDirectoryOption
+ Test.Aeson.GenericSpecs: [sampleSize] :: Settings -> Int
+ Test.Aeson.GenericSpecs: [useModuleNameAsSubDirectory] :: Settings -> Bool
+ Test.Aeson.GenericSpecs: data GoldenDirectoryOption
+ Test.Aeson.GenericSpecs: data Settings
+ Test.Aeson.GenericSpecs: defaultSettings :: Settings
+ Test.Aeson.GenericSpecs: roundtripAndGoldenADTSpecsWithSettings :: forall a. (Arbitrary a, ToADTArbitrary a, Eq a, Show a, ToJSON a, FromJSON a) => Settings -> Proxy a -> Spec
+ Test.Aeson.Internal.Utils: CustomDirectoryName :: String -> GoldenDirectoryOption
+ Test.Aeson.Internal.Utils: GoldenDirectory :: GoldenDirectoryOption
+ Test.Aeson.Internal.Utils: Settings :: GoldenDirectoryOption -> Bool -> Int -> Settings
+ Test.Aeson.Internal.Utils: [goldenDirectoryOption] :: Settings -> GoldenDirectoryOption
+ Test.Aeson.Internal.Utils: [sampleSize] :: Settings -> Int
+ Test.Aeson.Internal.Utils: [useModuleNameAsSubDirectory] :: Settings -> Bool
+ Test.Aeson.Internal.Utils: data GoldenDirectoryOption
+ Test.Aeson.Internal.Utils: data Settings
+ Test.Aeson.Internal.Utils: defaultSettings :: Settings
- Test.Aeson.GenericSpecs: goldenADTSpecs :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) => Int -> Proxy a -> Spec
+ Test.Aeson.GenericSpecs: goldenADTSpecs :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) => Settings -> Proxy a -> Spec
- Test.Aeson.GenericSpecs: goldenSpecs :: (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => Int -> Proxy a -> Spec
+ Test.Aeson.GenericSpecs: goldenSpecs :: (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => Settings -> Proxy a -> Spec
- Test.Aeson.GenericSpecs: roundtripAndGoldenADTSpecs :: forall a. (Arbitrary a, ToADTArbitrary a, Eq a, Show a, ToJSON a, FromJSON a) => Int -> Proxy a -> Spec
+ Test.Aeson.GenericSpecs: roundtripAndGoldenADTSpecs :: forall a. (Arbitrary a, ToADTArbitrary a, Eq a, Show a, ToJSON a, FromJSON a) => Proxy a -> Spec
- Test.Aeson.GenericSpecs: roundtripAndGoldenSpecs :: forall a. (Arbitrary a, Eq a, Show a, ToJSON a, FromJSON a, Typeable a) => Int -> Proxy a -> Spec
+ Test.Aeson.GenericSpecs: roundtripAndGoldenSpecs :: forall a. (Arbitrary a, Eq a, Show a, ToJSON a, FromJSON a, Typeable a) => Proxy a -> Spec
- Test.Aeson.Internal.ADT.GoldenSpecs: compareWithGolden :: forall a. (Show a, Eq a, FromJSON a, ToJSON a, ToADTArbitrary a) => String -> ConstructorArbitraryPair a -> FilePath -> IO ()
+ Test.Aeson.Internal.ADT.GoldenSpecs: compareWithGolden :: forall a. (Show a, Eq a, FromJSON a, ToJSON a, ToADTArbitrary a) => String -> Maybe String -> String -> ConstructorArbitraryPair a -> FilePath -> IO ()
- Test.Aeson.Internal.ADT.GoldenSpecs: goldenADTSpecs :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) => Int -> Proxy a -> Spec
+ Test.Aeson.Internal.ADT.GoldenSpecs: goldenADTSpecs :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) => Settings -> Proxy a -> Spec
- Test.Aeson.Internal.ADT.GoldenSpecs: goldenADTSpecsWithNote :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) => Int -> Proxy a -> Maybe String -> Spec
+ Test.Aeson.Internal.ADT.GoldenSpecs: goldenADTSpecsWithNote :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) => Settings -> Proxy a -> Maybe String -> Spec
- Test.Aeson.Internal.ADT.GoldenSpecs: mkFaultyFilePath :: forall a. String -> ConstructorArbitraryPair a -> FilePath
+ Test.Aeson.Internal.ADT.GoldenSpecs: mkFaultyFilePath :: forall a. FilePath -> Maybe FilePath -> FilePath -> ConstructorArbitraryPair a -> FilePath
- Test.Aeson.Internal.ADT.GoldenSpecs: mkGoldenFilePath :: forall a. String -> ConstructorArbitraryPair a -> FilePath
+ Test.Aeson.Internal.ADT.GoldenSpecs: mkGoldenFilePath :: forall a. FilePath -> Maybe FilePath -> FilePath -> ConstructorArbitraryPair a -> FilePath
- Test.Aeson.Internal.ADT.GoldenSpecs: testConstructor :: forall a. (Eq a, Show a, FromJSON a, ToJSON a, ToADTArbitrary a) => Int -> String -> ConstructorArbitraryPair a -> SpecWith (Arg (IO ()))
+ Test.Aeson.Internal.ADT.GoldenSpecs: testConstructor :: forall a. (Eq a, Show a, FromJSON a, ToJSON a, ToADTArbitrary a) => Settings -> String -> String -> ConstructorArbitraryPair a -> SpecWith (Arg (IO ()))
- Test.Aeson.Internal.GoldenSpecs: compareWithGolden :: forall a. (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => Proxy a -> FilePath -> IO ()
+ Test.Aeson.Internal.GoldenSpecs: compareWithGolden :: forall a. (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => FilePath -> Maybe FilePath -> Proxy a -> FilePath -> IO ()
- Test.Aeson.Internal.GoldenSpecs: createGoldenfile :: forall a. (Show a, Arbitrary a, ToJSON a) => Int -> Proxy a -> FilePath -> IO ()
+ Test.Aeson.Internal.GoldenSpecs: createGoldenfile :: forall a. (Show a, Arbitrary a, ToJSON a) => Settings -> Proxy a -> FilePath -> IO ()
- Test.Aeson.Internal.GoldenSpecs: goldenSpecs :: (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => Int -> Proxy a -> Spec
+ Test.Aeson.Internal.GoldenSpecs: goldenSpecs :: (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => Settings -> Proxy a -> Spec
- Test.Aeson.Internal.GoldenSpecs: goldenSpecsWithNote :: (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => Int -> Proxy a -> Maybe String -> Spec
+ Test.Aeson.Internal.GoldenSpecs: goldenSpecsWithNote :: forall a. (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) => Settings -> Proxy a -> Maybe String -> Spec
- Test.Aeson.Internal.GoldenSpecs: mkFaultyFile :: Typeable a => Proxy a -> FilePath
+ Test.Aeson.Internal.GoldenSpecs: mkFaultyFile :: Typeable a => FilePath -> Maybe FilePath -> Proxy a -> FilePath
- Test.Aeson.Internal.GoldenSpecs: mkGoldenFile :: Typeable a => Proxy a -> FilePath
+ Test.Aeson.Internal.GoldenSpecs: mkGoldenFile :: Typeable a => FilePath -> Maybe FilePath -> Proxy a -> FilePath

Files

hspec-golden-aeson.cabal view
@@ -1,5 +1,5 @@ name:                hspec-golden-aeson-version:             0.1.0.0+version:             0.2.0.0 synopsis:            Use tests to monitor changes in Aeson serialization description:         Use tests to monitor changes in Aeson serialization homepage:            https://github.com/plow-technologies/hspec-golden-aeson#readme@@ -30,7 +30,7 @@                      , filepath                      , hspec                      , random-                     , quickcheck-arbitrary-adt+                     , quickcheck-arbitrary-adt >= 0.2.0.0                      , QuickCheck                      , transformers   default-language:    Haskell2010@@ -55,7 +55,7 @@                      , quickcheck-arbitrary-adt                      , QuickCheck                      , transformers-  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  ghc-options:         -Wall -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fno-warn-name-shadowing   default-language:    Haskell2010  source-repository head
src/Test/Aeson/GenericSpecs.hs view
@@ -1,3 +1,19 @@+{-|+Module      : Test.Aeson.GenericSpecs+Description : Export all necessary functions+Copyright   : (c) Plow Technologies, 2016+License     : BSD3+Maintainer  : mchaver@gmail.com+Stability   : Beta++This package provides tools for testing Aeson serialization.++- Test that 'ToJSON' and 'FromJSON' instances are isomorphic.+- Alert you when unexpected changes in Aeson serialization occur.+- Record JSON formatting of Haskell types.++-}+ {-# LANGUAGE ScopedTypeVariables #-}  module Test.Aeson.GenericSpecs (@@ -10,8 +26,13 @@ , goldenADTSpecs , roundtripADTSpecs , roundtripAndGoldenADTSpecs+, roundtripAndGoldenADTSpecsWithSettings +  -- * Util , shouldBeIdentity+, GoldenDirectoryOption(..)+, Settings(..)+, defaultSettings    -- * re-exports , Proxy(..)@@ -35,21 +56,31 @@ -- compared, the sampleSize is derived from the file. roundtripAndGoldenSpecs :: forall a.   (Arbitrary a, Eq a, Show a, ToJSON a, FromJSON a, Typeable a)-  => Int-  -> Proxy a-  -> Spec-roundtripAndGoldenSpecs sampleSize proxy = do+  => Proxy a -> Spec+roundtripAndGoldenSpecs proxy =+  roundtripAndGoldenSpecsWithSettings defaultSettings proxy++-- | 'roundtripAndGoldenSpecs' with custom settings.+roundtripAndGoldenSpecsWithSettings :: forall a.+  (Arbitrary a, Eq a, Show a, ToJSON a, FromJSON a, Typeable a)+  => Settings -> Proxy a -> Spec+roundtripAndGoldenSpecsWithSettings settings proxy = do   roundtripSpecs proxy-  goldenSpecs sampleSize proxy+  goldenSpecs settings proxy  -- | run roundtrip and golden tests for all constructors of a type. -- sampleSize is used only when creating the golden files. When they are -- compared, the sampleSize is derived from the file. roundtripAndGoldenADTSpecs :: forall a.   (Arbitrary a, ToADTArbitrary a, Eq a, Show a, ToJSON a, FromJSON a)-  => Int-  -> Proxy a-  -> Spec-roundtripAndGoldenADTSpecs sampleSize proxy = do+  => Proxy a -> Spec+roundtripAndGoldenADTSpecs proxy =+  roundtripAndGoldenADTSpecsWithSettings defaultSettings proxy++-- | 'roundtripAndGoldenADTSpecs' with custom settings.+roundtripAndGoldenADTSpecsWithSettings :: forall a.+  (Arbitrary a, ToADTArbitrary a, Eq a, Show a, ToJSON a, FromJSON a)+  => Settings -> Proxy a -> Spec+roundtripAndGoldenADTSpecsWithSettings settings proxy = do   roundtripADTSpecs proxy-  goldenADTSpecs sampleSize proxy+  goldenADTSpecs settings proxy
src/Test/Aeson/Internal/ADT/GoldenSpecs.hs view
@@ -1,6 +1,18 @@+{-|+Module      : Test.Aeson.Internal.ADT.GoldenSpecs+Description : Golden tests for ToADTArbitrary+Copyright   : (c) Plow Technologies, 2016+License     : BSD3+Maintainer  : mchaver@gmail.com+Stability   : Beta++Internal module, use at your own risk.+-}+ {-# LANGUAGE FlexibleContexts     #-} {-# LANGUAGE FlexibleInstances    #-} {-# LANGUAGE OverloadedStrings    #-}+{-# LANGUAGE RecordWildCards      #-} {-# LANGUAGE ScopedTypeVariables  #-} {-# LANGUAGE TypeOperators        #-} {-# LANGUAGE UndecidableInstances #-}@@ -24,42 +36,65 @@ import           System.Random  import           Test.Aeson.Internal.RandomSamples+import           Test.Aeson.Internal.Utils import           Test.Hspec import           Test.QuickCheck import           Test.QuickCheck.Arbitrary.ADT --- | for a type a, create a set of golden files if they do not exist, compare--- with golden file if it exists. Golden file encodes json format of a type+-- | Tests to ensure that JSON encoding has not unintentionally changed. This+-- could be caused by the following:+--+-- - A type's instances of `ToJSON` or 'FromJSON' have changed.+-- - Selectors have been edited, added or deleted.+-- - You have changed version of Aeson the way Aeson serialization has changed+--   works.+--+-- If you run this function and the golden files do not+-- exist, it will create them for each constructor. It they do exist, it will+-- compare with golden file if it exists. Golden file encodes json format of a+-- type. It is recommended that you put the golden files under revision control+-- to help monitor changes. goldenADTSpecs :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) =>-  Int -> Proxy a -> Spec-goldenADTSpecs sampleSize proxy = goldenADTSpecsWithNote sampleSize proxy Nothing+  Settings -> Proxy a -> Spec+goldenADTSpecs settings proxy = goldenADTSpecsWithNote settings proxy Nothing +-- | same as 'goldenADTSpecs' but has the option of passing a note to the+-- 'describe' function. goldenADTSpecsWithNote :: forall a. (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) =>-  Int -> Proxy a -> Maybe String -> Spec-goldenADTSpecsWithNote sampleSize Proxy mNote = do-  (typeName,constructors) <- runIO $ fmap (_adtTypeName &&& _adtCAPs) <$> generate $ toADTArbitrary (Proxy :: Proxy a)+  Settings -> Proxy a -> Maybe String -> Spec+goldenADTSpecsWithNote settings Proxy mNote = do+  (moduleName,(typeName,constructors)) <- runIO $ fmap (adtModuleName &&& adtTypeName &&& adtCAPs) <$> generate $ toADTArbitrary (Proxy :: Proxy a)   describe ("JSON encoding of " ++ typeName ++ note) $-    mapM_ (testConstructor sampleSize typeName) constructors+    mapM_ (testConstructor settings moduleName typeName) constructors   where     note = maybe "" (" " ++) mNote +-- | test a single set of values from a constructor for a given type. testConstructor :: forall a. (Eq a, Show a, FromJSON a, ToJSON a, ToADTArbitrary a) =>-  Int -> String -> ConstructorArbitraryPair a -> SpecWith ( Arg (IO ()))-testConstructor sampleSize typeName cap =+  Settings -> String -> String -> ConstructorArbitraryPair a -> SpecWith ( Arg (IO ()))+testConstructor Settings{..} moduleName typeName cap = do   it ("produces the same JSON as is found in " ++ goldenFile) $ do     exists <- doesFileExist goldenFile     if exists-      then compareWithGolden typeName cap goldenFile+      then compareWithGolden topDir mModuleName typeName cap goldenFile       else createGoldenFile sampleSize cap goldenFile   where-    goldenFile = mkGoldenFilePath typeName cap+    goldenFile = mkGoldenFilePath topDir mModuleName typeName cap+    topDir = case goldenDirectoryOption of+      GoldenDirectory -> "golden"+      CustomDirectoryName d -> d+    mModuleName = case useModuleNameAsSubDirectory of+      True  -> Just moduleName+      False -> Nothing +-- | The golden files already exist. Serialize values with the same seed from+-- the golden files of each constructor and compare. compareWithGolden :: forall a. (Show a, Eq a, FromJSON a, ToJSON a, ToADTArbitrary a) =>-  String -> ConstructorArbitraryPair a -> FilePath -> IO ()-compareWithGolden typeName cap goldenFile = do+  String -> Maybe String -> String -> ConstructorArbitraryPair a -> FilePath -> IO ()+compareWithGolden topDir mModuleName typeName cap goldenFile = do   goldenSeed <- readSeed =<< readFile goldenFile   sampleSize <- readSampleSize =<< readFile goldenFile-  newSamples <- mkRandomADTSamplesForConstructor sampleSize (Proxy :: Proxy a) (_capConstructor cap) goldenSeed+  newSamples <- mkRandomADTSamplesForConstructor sampleSize (Proxy :: Proxy a) (capConstructor cap) goldenSeed   whenFails (writeComparisonFile newSamples) $ do     goldenSamples :: RandomSamples a <-       either (throwIO . ErrorCall) return =<<@@ -69,19 +104,22 @@   where     whenFails :: forall b c. IO c -> IO b -> IO b     whenFails = flip onException-    faultyFile = mkFaultyFilePath typeName cap++    faultyFile = mkFaultyFilePath topDir mModuleName typeName cap+     writeComparisonFile newSamples = do       writeFile faultyFile (encodePretty newSamples)       putStrLn $         "\n" ++         "INFO: Written the current encodings into " ++ faultyFile ++ "." +-- | The golden files do not exist. Create them for each constructor. createGoldenFile :: forall a. (ToJSON a, ToADTArbitrary a) =>   Int -> ConstructorArbitraryPair a -> FilePath -> IO () createGoldenFile sampleSize cap goldenFile = do   createDirectoryIfMissing True (takeDirectory goldenFile)   rSeed <- randomIO :: IO Int-  rSamples <- mkRandomADTSamplesForConstructor sampleSize (Proxy :: Proxy a) (_capConstructor cap) rSeed+  rSamples <- mkRandomADTSamplesForConstructor sampleSize (Proxy :: Proxy a) (capConstructor cap) rSeed   writeFile goldenFile $ encodePretty rSamples    putStrLn $@@ -91,20 +129,34 @@     "  will compare JSON encodings with this from now on.\n" ++     "  Please, consider putting " ++ goldenFile ++ " under version control." -mkGoldenFilePath :: forall a. String -> ConstructorArbitraryPair a -> FilePath-mkGoldenFilePath typeName cap = "golden" </> typeName </> _capConstructor cap <.> "json"+-- | Create the file path for the golden file. Optionally use the module name to+-- help avoid name collissions. Different modules can have types of the same+-- name.+mkGoldenFilePath :: forall a. FilePath -> Maybe FilePath -> FilePath -> ConstructorArbitraryPair a -> FilePath+mkGoldenFilePath topDir mModuleName typeName cap =+  case mModuleName of+    Nothing -> topDir </> typeName </> capConstructor cap <.> "json"+    Just moduleName -> topDir </> moduleName </> typeName </> capConstructor cap <.> "json" -mkFaultyFilePath :: forall a. String -> ConstructorArbitraryPair a -> FilePath-mkFaultyFilePath typeName cap = "golden" </> typeName </> _capConstructor cap <.> "faulty" <.> "json"+-- | Create the file path to save results from a failed golden test. Optionally+-- use the module name to help avoid name collisions.  Different modules can+-- have types of the same name.+mkFaultyFilePath :: forall a. FilePath -> Maybe FilePath -> FilePath -> ConstructorArbitraryPair a -> FilePath+mkFaultyFilePath topDir mModuleName typeName cap =+  case mModuleName of+    Nothing -> topDir </> typeName </> capConstructor cap <.> "faulty" <.> "json"+    Just moduleName -> topDir </> moduleName </> typeName </> capConstructor cap <.> "faulty" <.> "json" +-- | Create a number of arbitrary instances of a particular constructor given+-- a sample size and a random seed. mkRandomADTSamplesForConstructor :: forall a. (ToADTArbitrary a) =>   Int -> Proxy a -> String -> Int -> IO (RandomSamples a) mkRandomADTSamplesForConstructor sampleSize Proxy conName rSeed = do   generatedADTs <- generate gen-  let caps         = concat $ _adtCAPs <$> generatedADTs-      filteredCAPs = filter (\x -> _capConstructor x == conName) caps-      arbs         = _capArbitrary <$> filteredCAPs+  let caps         = concat $ adtCAPs <$> generatedADTs+      filteredCAPs = filter (\x -> capConstructor x == conName) caps+      arbs         = capArbitrary <$> filteredCAPs   return $ RandomSamples rSeed arbs   where     correctedSampleSize = if sampleSize <= 0 then 1 else sampleSize-    gen = setSeed rSeed $ replicateM sampleSize (toADTArbitrary (Proxy :: Proxy a))+    gen = setSeed rSeed $ replicateM correctedSampleSize (toADTArbitrary (Proxy :: Proxy a))
src/Test/Aeson/Internal/ADT/RoundtripSpecs.hs view
@@ -1,6 +1,16 @@+{-|+Module      : Test.Aeson.Internal.ADT.RoundtripSpecs+Description : Roundtrip tests for ToADTArbitrary+Copyright   : (c) Plow Technologies, 2016+License     : BSD3+Maintainer  : mchaver@gmail.com+Stability   : Beta++Internal module, use at your own risk.+-}+ {-# LANGUAGE ScopedTypeVariables #-} --- | Internal module, use at your own risk. module Test.Aeson.Internal.ADT.RoundtripSpecs where  import           Control.Arrow@@ -16,12 +26,12 @@  import Control.Monad --- | A roundtrip test to check whether values of the given type--- can be successfully converted to JSON and back to a Haskell value.+-- | A roundtrip test to check whether values of all of constructors of the+-- given type can be successfully converted to JSON and back to a Haskell value. ----- 'roundtripSpecs' will+-- 'roundtripADTSpecs' will ----- - create random values using 'Arbitrary',+-- - create random values for each constructor using 'ToADTArbitrary', -- - convert them into JSON using 'ToJSON', -- - read them back into Haskell using 'FromJSON' and -- - make sure that the result is the same as the value it started with@@ -32,6 +42,8 @@   -> Spec roundtripADTSpecs proxy = genericAesonRoundtripADTWithNote proxy Nothing +-- | Same as 'roundtripADTSpecs' but has the option of passing a note to the+-- 'describe' function. genericAesonRoundtripADTWithNote :: forall a.   (ToADTArbitrary a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a)   => Proxy a@@ -39,9 +51,9 @@   -> Spec genericAesonRoundtripADTWithNote _ mNote = do   adt <- runIO $ generate (toADTArbitrary (Proxy :: Proxy a))-  describe ("JSON encoding of " ++ addBrackets (_adtTypeName adt) ++ note) $+  describe ("JSON encoding of " ++ addBrackets (adtTypeName adt) ++ note) $     it "allows to encode values with aeson and read them back" $-      forM_ (_adtCAPs adt) $ \cap ->-        (Aeson.encode >>> aesonDecodeIO) (_capArbitrary cap) `shouldReturn` _capArbitrary cap+      forM_ (adtCAPs adt) $ \cap ->+        (Aeson.encode >>> aesonDecodeIO) (capArbitrary cap) `shouldReturn` capArbitrary cap   where     note = maybe "" (" " ++) mNote
src/Test/Aeson/Internal/GoldenSpecs.hs view
@@ -1,4 +1,16 @@-{-# LANGUAGE OverloadedStrings #-}+{-|+Module      : Test.Aeson.Internal.GoldenSpecs+Description : Golden tests for Arbitrary+Copyright   : (c) Plow Technologies, 2016+License     : BSD3+Maintainer  : mchaver@gmail.com+Stability   : Beta++Internal module, use at your own risk.+-}++{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE RecordWildCards     #-} {-# LANGUAGE ScopedTypeVariables #-}  module Test.Aeson.Internal.GoldenSpecs where@@ -23,60 +35,55 @@ import           Test.Hspec import           Test.QuickCheck --- | Allows to obtain tests that will try to ensure that the JSON encoding--- didn't change unintentionally. To this end 'goldenSpecs' will+-- | Tests to ensure that JSON encoding has not unintentionally changed. This+-- could be caused by the following: ----- - write a file @golden.json/TYPENAME.json@ in the current directory---   containing a number of JSON-encoded sample values,--- - during subsequent tests it will encode the same sample values again and---   compare them with the saved golden encodings,--- - on failure it will create a file @golden.json/TYPENAME.faulty.json@ for---   easy manual inspection.+-- - A type's instances of `ToJSON` or 'FromJSON' have changed.+-- - Selectors have been edited, added or deleted.+-- - You have changed version of Aeson the way Aeson serialization has changed+--   works. ----- You can consider putting the golden files under revision control. That way--- it'll be obvious when JSON encodings change.+-- If you run this function and the golden files do not+-- exist, it will create them for each constructor. It they do exist, it will+-- compare with golden file if it exists. Golden file encodes json format of a+-- type. It is recommended that you put the golden files under revision control+-- to help monitor changes. goldenSpecs :: (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) =>-  Int -> Proxy a -> Spec-goldenSpecs sampleSize proxy = goldenSpecsWithNote sampleSize proxy Nothing+  Settings -> Proxy a -> Spec+goldenSpecs settings proxy = goldenSpecsWithNote settings proxy Nothing -goldenSpecsWithNote :: (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) =>-  Int -> Proxy a -> Maybe String -> Spec-goldenSpecsWithNote sampleSize proxy mNote = do-  let goldenFile = mkGoldenFile proxy+-- | same as 'goldenSpecs' but has the option of passing a note to the+-- 'describe' function.+goldenSpecsWithNote :: forall a. (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) =>+  Settings -> Proxy a -> Maybe String -> Spec+goldenSpecsWithNote settings@Settings{..} proxy mNote = do+  mModuleName <-+    if useModuleNameAsSubDirectory+      then return Nothing+      else do+        arbA <- runIO $ generate (arbitrary :: Gen a)+        return $ Just $ show . tyConModule . typeRepTyCon . typeOf $ arbA++  let goldenFile = mkGoldenFile topDir mModuleName proxy       note = maybe "" (" " ++) mNote+   describe ("JSON encoding of " ++ addBrackets (show (typeRep proxy)) ++ note) $     it ("produces the same JSON as is found in " ++ goldenFile) $ do       exists <- doesFileExist goldenFile       if exists-        then compareWithGolden proxy goldenFile-        else createGoldenfile sampleSize proxy goldenFile--mkGoldenFile :: Typeable a => Proxy a -> FilePath-mkGoldenFile proxy =-  "golden.json" </> show (typeRep proxy) <.> "json"--mkFaultyFile :: Typeable a => Proxy a -> FilePath-mkFaultyFile proxy =-  "golden.json" </> show (typeRep proxy) <.> "faulty" <.> "json"--createGoldenfile :: forall a . (Show a, Arbitrary a, ToJSON a) =>-  Int -> Proxy a -> FilePath -> IO ()-createGoldenfile sampleSize proxy goldenFile = do-  createDirectoryIfMissing True (takeDirectory goldenFile)-  rSeed <- randomIO-  rSamples <- mkRandomSamples sampleSize proxy rSeed-  writeFile goldenFile (encodePretty rSamples)-  putStrLn $-    "\n" ++-    "WARNING: Running for the first time, not testing anything.\n" ++-    "  Created " ++ goldenFile ++ " containing random samples,\n" ++-    "  will compare JSON encodings with this from now on.\n" ++-    "  Please, consider putting " ++ goldenFile ++ " under version control."+        then compareWithGolden topDir mModuleName proxy goldenFile+        else createGoldenfile settings proxy goldenFile+  where+    topDir = case goldenDirectoryOption of+      GoldenDirectory -> "golden"+      CustomDirectoryName d -> d +-- | The golden files already exist. Serialize values with the same seed from+-- the golden file and compare the with the JSON in the golden file. compareWithGolden :: forall a .   (Eq a, Show a, Typeable a, Arbitrary a, ToJSON a, FromJSON a) =>-  Proxy a -> FilePath -> IO ()-compareWithGolden proxy goldenFile = do+  FilePath -> Maybe FilePath -> Proxy a -> FilePath -> IO ()+compareWithGolden topDir mModuleName proxy goldenFile = do   goldenSeed <- readSeed =<< readFile goldenFile   sampleSize <- readSampleSize =<< readFile goldenFile   newSamples <- mkRandomSamples sampleSize proxy goldenSeed@@ -90,12 +97,50 @@     whenFails :: forall b c . IO c -> IO b -> IO b     whenFails = flip onException +    faultyFile = mkFaultyFile topDir mModuleName proxy+     writeComparisonFile newSamples = do-      writeFile (mkFaultyFile proxy) (encodePretty newSamples)+      writeFile faultyFile (encodePretty newSamples)       putStrLn $         "\n" ++-        "INFO: Written the current encodings into " ++ mkFaultyFile proxy ++ "."+        "INFO: Written the current encodings into " ++ faultyFile ++ "." +-- | The golden files do not exist. Create it.+createGoldenfile :: forall a . (Show a, Arbitrary a, ToJSON a) =>+  Settings -> Proxy a -> FilePath -> IO ()+createGoldenfile Settings{..} proxy goldenFile = do+  createDirectoryIfMissing True (takeDirectory goldenFile)+  rSeed <- randomIO+  rSamples <- mkRandomSamples sampleSize proxy rSeed+  writeFile goldenFile (encodePretty rSamples)++  putStrLn $+    "\n" +++    "WARNING: Running for the first time, not testing anything.\n" +++    "  Created " ++ goldenFile ++ " containing random samples,\n" +++    "  will compare JSON encodings with this from now on.\n" +++    "  Please, consider putting " ++ goldenFile ++ " under version control."++-- | Create the file path for the golden file. Optionally use the module name to+-- help avoid name collissions. Different modules can have types of the same+-- name.+mkGoldenFile :: Typeable a => FilePath -> Maybe FilePath -> Proxy a -> FilePath+mkGoldenFile topDir mModuleName proxy =+  case mModuleName of+    Nothing         -> topDir </> show (typeRep proxy) <.> "json"+    Just moduleName -> topDir </> moduleName </> show (typeRep proxy) <.> "json"++-- | Create the file path to save results from a failed golden test. Optionally+-- use the module name to help avoid name collisions.  Different modules can+-- have types of the same name.+mkFaultyFile :: Typeable a => FilePath -> Maybe FilePath -> Proxy a -> FilePath+mkFaultyFile topDir mModuleName proxy =+  case mModuleName of+    Nothing         -> topDir </> show (typeRep proxy) <.> "faulty" <.> "json"+    Just moduleName -> topDir </> moduleName </> show (typeRep proxy) <.> "faulty" <.> "json"++-- | Create a number of arbitrary instances of a type+-- a sample size and a random seed. mkRandomSamples :: forall a . Arbitrary a =>   Int -> Proxy a -> Int -> IO (RandomSamples a) mkRandomSamples sampleSize Proxy rSeed = RandomSamples rSeed <$> generate gen
src/Test/Aeson/Internal/RandomSamples.hs view
@@ -1,3 +1,14 @@+{-|+Module      : Test.Aeson.Internal.RandomSamples+Description : Types and functions to faciliate sampling+Copyright   : (c) Plow Technologies, 2016+License     : BSD3+Maintainer  : mchaver@gmail.com+Stability   : Beta++Internal module, use at your own risk.+-}+ {-# LANGUAGE DeriveGeneric        #-} {-# LANGUAGE ScopedTypeVariables  #-} @@ -17,7 +28,7 @@  -- | RandomSamples, using a seed allows you to replicate an arbitrary. By -- storing the seed and the samples (previously produced arbitraries), we can--- try to reproduce the same samples by generating the arbitraries with a seed+-- try to reproduce the same samples by generating the arbitraries with a seed.  data RandomSamples a = RandomSamples {   seed    :: Int@@ -27,16 +38,17 @@ instance FromJSON a => FromJSON (RandomSamples a) instance ToJSON   a => ToJSON   (RandomSamples a) +-- | Apply the seed. setSeed :: Int -> Gen a -> Gen a setSeed rSeed (MkGen g) = MkGen $ \ _randomSeed size -> g (mkQCGen rSeed) size --- | reads the seed without looking at the samples+-- | Reads the seed without looking at the samples. readSeed :: ByteString -> IO Int readSeed s = case eitherDecode s :: Either String (RandomSamples Value) of   Right rSamples -> return $ seed rSamples   Left err -> throwIO $ ErrorCall err --- | read the sample size+-- | Read the sample size. readSampleSize :: ByteString -> IO Int readSampleSize s = case eitherDecode s :: Either String (RandomSamples Value) of   Right rSamples -> return . length . samples $ rSamples
src/Test/Aeson/Internal/RoundtripSpecs.hs view
@@ -1,6 +1,16 @@+{-|+Module      : Test.Aeson.Internal.RoundtripSpecs+Description : Roundtrip tests for Arbitrary+Copyright   : (c) Plow Technologies, 2016+License     : BSD3+Maintainer  : mchaver@gmail.com+Stability   : Beta++Internal module, use at your own risk.+-}+ {-# LANGUAGE ScopedTypeVariables #-} --- | Internal module, use at your own risk. module Test.Aeson.Internal.RoundtripSpecs where  import           Control.Arrow@@ -12,8 +22,8 @@ import           Test.Hspec import           Test.QuickCheck --- | Allows to obtain a roundtrip test to check whether values of the given type--- can be successfully converted to JSON and back.+-- | A roundtrip test to check whether values of the given type+-- can be successfully converted to JSON and back to a Haskell value. -- -- 'roundtripSpecs' will --@@ -27,7 +37,8 @@   Proxy a -> Spec roundtripSpecs proxy = genericAesonRoundtripWithNote proxy Nothing --- | use this directly if you want to add your own notes.+-- | Same as 'roundtripSpecs', but optionally add notes to the 'describe'+-- function. genericAesonRoundtripWithNote :: forall a .   (Typeable a, Eq a, Show a, Arbitrary a, ToJSON a, FromJSON a) =>   Proxy a -> Maybe String -> Spec
src/Test/Aeson/Internal/Utils.hs view
@@ -1,3 +1,13 @@+{-|+Module      : Test.Aeson.Internal.Utils+Description : Utility types, functions and values+Copyright   : (c) Plow Technologies, 2016+License     : BSD3+Maintainer  : mchaver@gmail.com+Stability   : Beta+-}++ {-# LANGUAGE ScopedTypeVariables #-}  module Test.Aeson.Internal.Utils where@@ -12,6 +22,22 @@  import           Test.Hspec import           Test.QuickCheck+++data Settings = Settings {+  goldenDirectoryOption       :: GoldenDirectoryOption -- ^ use a custom directory name or use the generic "golden" directory.++, useModuleNameAsSubDirectory :: Bool -- ^ If true, use the module name in the file path, otherwise ignore it.++, sampleSize                  :: Int -- ^ How many instances of each type you want. If you use ADT versions than it will use the sample size for each constructor.+}++-- | A custom directory name or a preselected directory name.+data GoldenDirectoryOption = CustomDirectoryName String | GoldenDirectory++-- | The default settings for general use cases.+defaultSettings :: Settings+defaultSettings = Settings GoldenDirectory False 5  -- | put brackets around a String. addBrackets :: String -> String
test/Test/Aeson/GenericSpecsSpec.hs view
@@ -23,17 +23,22 @@ import qualified Test.Types                     as T import qualified Test.Types.AlteredSelector     as TAS import qualified Test.Types.BrokenSerialization as TBS+import qualified Test.Types.MismatchedToAndFromSerialization as MTFS import qualified Test.Types.NewSelector         as TNS   -- summaryFailures spec :: Spec spec = do-  describe "Test.Aeson.GenericSpecs: roundTrip" $-    it "" $ do+  describe "Test.Aeson.GenericSpecs: roundTrip" $ do+    it "should pass when ToJSON and FromJSON are defined appropriately" $ do       (s1,_) <- hspecSilently $ roundtripADTSpecs (Proxy :: Proxy T.Person)       summaryFailures s1 `shouldBe` 0 +    it "should fail when ToJSON and FromJSON definitions do not match" $ do+      (s1,_) <- hspecSilently $ roundtripADTSpecs (Proxy :: Proxy MTFS.Person)+      summaryFailures s1 `shouldBe` 1+   describe "Test.Aeson.GenericSpecs: goldenADTSpecs" $ do     it "create golden test files" $ do       -- clean up previously existing golden folder@@ -44,32 +49,60 @@        -- files for Person and SumType do not exist       -- create them by running goldenADTSpecs-      _ <- hspecSilently $ goldenADTSpecs 5 (Proxy :: Proxy T.Person)-      _ <- hspecSilently $ goldenADTSpecs 5 (Proxy :: Proxy T.SumType)+      _ <- hspecSilently $ goldenADTSpecs defaultSettings (Proxy :: Proxy T.Person)+      _ <- hspecSilently $ goldenADTSpecs defaultSettings (Proxy :: Proxy T.SumType)        doesFileExist "golden/Person/Person.json"    `shouldReturn` True       doesFileExist "golden/SumType/SumType1.json" `shouldReturn` True       doesFileExist "golden/SumType/SumType2.json" `shouldReturn` True       doesFileExist "golden/SumType/SumType3.json" `shouldReturn` True ++    it "create golden test files in a sub directory using the module name" $ do+      -- files for Person and SumType do not exist+      -- create them by running goldenADTSpecs+      _ <- hspecSilently $ goldenADTSpecs defaultSettings {useModuleNameAsSubDirectory = True } (Proxy :: Proxy T.Person)+      _ <- hspecSilently $ goldenADTSpecs defaultSettings {useModuleNameAsSubDirectory = True } (Proxy :: Proxy T.SumType)++      doesFileExist "golden/Test.Types/Person/Person.json"    `shouldReturn` True+      doesFileExist "golden/Test.Types/SumType/SumType1.json" `shouldReturn` True+      doesFileExist "golden/Test.Types/SumType/SumType2.json" `shouldReturn` True+      doesFileExist "golden/Test.Types/SumType/SumType3.json" `shouldReturn` True++    it "create golden test files in a user defined directory" $ do+      let topDir = "json-tests"+      -- clean up previously existing user defined folder+      bg <- doesDirectoryExist topDir+      if bg+        then removeDirectoryRecursive topDir+        else return ()++      -- files for Person and SumType do not exist+      -- create them by running goldenADTSpecs+      _ <- hspecSilently $ goldenADTSpecs (defaultSettings {goldenDirectoryOption = CustomDirectoryName topDir}) (Proxy :: Proxy T.Person)+      _ <- hspecSilently $ goldenADTSpecs (defaultSettings {goldenDirectoryOption = CustomDirectoryName topDir}) (Proxy :: Proxy T.SumType)++      doesFileExist "json-tests/Person/Person.json"    `shouldReturn` True+      doesFileExist "json-tests/SumType/SumType1.json" `shouldReturn` True+      doesFileExist "json-tests/SumType/SumType2.json" `shouldReturn` True+      doesFileExist "json-tests/SumType/SumType3.json" `shouldReturn` True+     it "goldenADTSpecs should pass for existing golden files in which model types and serialization have not changed" $ do-      (s1,_) <- hspecSilently $ goldenADTSpecs 5 (Proxy :: Proxy T.Person)-      (s2,_) <- hspecSilently $ goldenADTSpecs 5 (Proxy :: Proxy T.SumType)+      (s1,_) <- hspecSilently $ goldenADTSpecs defaultSettings (Proxy :: Proxy T.Person)+      (s2,_) <- hspecSilently $ goldenADTSpecs defaultSettings (Proxy :: Proxy T.SumType)       (summaryFailures s1 + summaryFailures s2) `shouldBe` 0      it "goldenADTSpecs for types which have changed the values of ToJSON or FromJSON keys should fail to match the goldenFiles" $ do-      (s1,_) <- hspecSilently $ goldenADTSpecs 5 (Proxy :: Proxy TBS.Person)+      (s1,_) <- hspecSilently $ goldenADTSpecs defaultSettings (Proxy :: Proxy TBS.Person)       summaryFailures s1 `shouldBe` 1      it "goldenADTSpecs for types which have changed the values of ToJSON or FromJSON keys should fail to match the goldenFiles" $ do-      (s1,_) <- hspecSilently $ goldenADTSpecs 5 (Proxy :: Proxy TNS.Person)+      (s1,_) <- hspecSilently $ goldenADTSpecs defaultSettings (Proxy :: Proxy TNS.Person)       summaryFailures s1 `shouldBe` 1      it "goldenADTSpecs for types which have altered the name of the selector and using generic implementation of ToJSON and FromJSON should fail to match the goldenFiles" $ do-      (s1,_) <- hspecSilently $ goldenADTSpecs 5 (Proxy :: Proxy TAS.Person)+      (s1,_) <- hspecSilently $ goldenADTSpecs defaultSettings (Proxy :: Proxy TAS.Person)       summaryFailures s1 `shouldBe` 1--  main :: IO () main = hspec spec