hspec-expectations-json (empty) → 1.0.0.0
raw patch · 9 files changed
+778/−0 lines, 9 filesdep +Diffdep +HUnitdep +aeson
Dependencies added: Diff, HUnit, aeson, aeson-pretty, aeson-qq, base, hspec, hspec-expectations-json, scientific, text, unordered-containers, vector
Files
- CHANGELOG.md +1/−0
- LICENSE +19/−0
- README.md +156/−0
- hspec-expectations-json.cabal +83/−0
- library/Test/Hspec/Expectations/Json.hs +165/−0
- library/Test/Hspec/Expectations/Json/Internal.hs +139/−0
- tests/Spec.hs +2/−0
- tests/Test/Hspec/Expectations/Json/InternalSpec.hs +100/−0
- tests/Test/Hspec/Expectations/JsonSpec.hs +113/−0
+ CHANGELOG.md view
@@ -0,0 +1,1 @@+TODO
+ LICENSE view
@@ -0,0 +1,19 @@+Copyright (c) 2020 Freckle Engineering <engineering@freckle.com>++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ README.md view
@@ -0,0 +1,156 @@+# Hspec Expectations for JSON Values++Comparing JSON `Value`s in Haskell tests comes with some challenges:++- In API responses, additive changes are typically safe and an important way to+ evolve responses without breaking clients. Therefore, assertions against such+ responses often want to ignore any unexpected keys in `Object`s (at any+ depth), as any clients would.++- Order often doesn't matter in API responses either, so it should be possible+ to assert equality regardless of `Array` ordering (again, at any depth).++- When an assertion fails, showing the difference clearly needs to take the+ above into account (i.e. it can't show keys you've ignored, or ordering+ differences you didn't care about), and it has to display things clearly, e.g.+ as a diff.++This library handles all these things.++## Usage++**NOTE**: this is effectively a distillation of the [Haddocks](#TODO), please+view them directly for your installed version, to ensure accurate information.++Four expectations exist with the following behaviors:++| Assertion that **fails** on: | extra `Object` keys | wrong `Array` order |+| ---------------------------- | ------------------- | ------------------- |+| `shouldBeJson` | Yes | Yes |+| `shouldBeUnorderedJson` | Yes | No |+| `shouldMatchJson` | No | No |+| `shouldMatchOrderedJson` | No | Yes |++Each of these, when they fail, print a difference between the objects, where the+expected-on object has been normalized to avoid indicating any of the+differences your expectation is ignoring.++### `shouldBeJson`++Passing:++```hs+catchFailure $+ [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`+ [aesonQQ| { "a": true, "b": false } |]+```++Failing:++```hs+catchFailure $+ [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`+ [aesonQQ| { "a": true, "b": true } |]+```++```diff+ {+ "a": true,+--- "b": true++++ "b": false+ }+```++### `shouldBeUnorderedJson`++Passing:++```hs+catchFailure $+ [aesonQQ| { "a": [true, false], "b": false } |] `shouldBeUnorderedJson`+ [aesonQQ| { "a": [false, true], "b": false } |]+```++Failing:++```hs+catchFailure $+ [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldBeUnorderedJson`+ [aesonQQ| { "a": [false, true], "b": true } |]+```++```diff+ {+ "a": [+ false,+ true+ ],+--- "b": true++++ "b": false,++++ "c": true+ }+```++### `shouldMatchJson`++Passing:++```hs+catchFailure $+ [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`+ [aesonQQ| { "a": [false, true], "b": false } |]+```++Failing:++```hs+catchFailure $+ [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`+ [aesonQQ| { "a": [false, true], "b": true } |]+```++```diff+ {+ "a": [+ false,+ true+ ],+--- "b": true++++ "b": false+ }+```++### `shouldMatchOrderedJson`++Passing:++```hs+catchFailure $+ [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`+ [aesonQQ| { "a": [true, false], "b": false } |]+```++Failing:++```hs+catchFailure $+ [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`+ [aesonQQ| { "a": [false, true], "b": true } |]+```++```diff+ {+ "a": [+--- false,+--- true++++ true,++++ false+ ],+--- "b": true++++ "b": false+ }+```++---++[LICENSE](./LICENSE) | [CHANGELOG](./CHANGELOG.md)
+ hspec-expectations-json.cabal view
@@ -0,0 +1,83 @@+cabal-version: 1.18++-- This file has been generated from package.yaml by hpack version 0.31.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: d230360984f4f18d1f942af26155cfa6065ecbc543e330f3f60c37c72ddd6336++name: hspec-expectations-json+version: 1.0.0.0+synopsis: Hspec expectations for JSON Values+description: Hspec expectations for JSON Values+ .+ Comparing JSON `Value`s in Haskell tests comes with some challenges:+ .+ - In API responses, additive changes are typically safe and an important way+ to evolve responses without breaking clients. Therefore, assertions against+ such responses often want to ignore any unexpected keys in `Object`s (at any+ depth), as any clients would.+ .+ - Order often doesn't matter in API responses either, so it should be possible+ to assert equality regardless of `Array` ordering (again, at any depth).+ .+ - When an assertion fails, showing the difference clearly needs to take the+ above into account (i.e. it can't show keys you've ignored, or ordering+ differences you didn't care about), and it has to display things clearly,+ e.g. as a diff.+ .+ This library handles all these things.+category: Test+homepage: https://github.com/freckle/hspec-expectations-json#readme+bug-reports: https://github.com/freckle/hspec-expectations-json/issues+author: Freckle Engineering+maintainer: engineering@freckle.com+copyright: 2020 Freckle Education+license: MIT+license-file: LICENSE+build-type: Simple+extra-doc-files:+ CHANGELOG.md+ README.md++source-repository head+ type: git+ location: https://github.com/freckle/hspec-expectations-json++library+ exposed-modules:+ Test.Hspec.Expectations.Json+ Test.Hspec.Expectations.Json.Internal+ other-modules:+ Paths_hspec_expectations_json+ hs-source-dirs:+ library+ default-extensions: BangPatterns DataKinds DeriveAnyClass DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TypeApplications TypeFamilies+ build-depends:+ Diff >=0.4.0 && <0.5+ , HUnit >=1.6.0.0 && <1.7+ , aeson >=1.4.7.1 && <1.5+ , aeson-pretty >=0.8.8 && <0.9+ , base >=4.11 && <4.14+ , scientific >=0.3.6.2 && <0.4+ , text >=1.2.4.0 && <1.3+ , unordered-containers >=0.2.10.0 && <0.3+ , vector >=0.12.1.2 && <0.13+ default-language: Haskell2010++test-suite spec+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Test.Hspec.Expectations.Json.InternalSpec+ Test.Hspec.Expectations.JsonSpec+ Paths_hspec_expectations_json+ hs-source-dirs:+ tests+ default-extensions: BangPatterns DataKinds DeriveAnyClass DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TypeApplications TypeFamilies+ build-depends:+ aeson-qq+ , base >=4.11 && <4.14+ , hspec+ , hspec-expectations-json+ default-language: Haskell2010
+ library/Test/Hspec/Expectations/Json.hs view
@@ -0,0 +1,165 @@+-- | Expectations on JSON 'Value's+--+-- Semantics:+--+-- +--------------------------+-------------------+-------------------++-- | Assertion that fails on: | extra Object keys | wrong Array order ++-- +==========================+===================+===================++-- | 'shouldBeJson' | Yes | Yes |+-- +--------------------------+-------------------+-------------------++-- | 'shouldBeUnorderedJson' | Yes | No |+-- +--------------------------+-------------------+-------------------++-- | 'shouldMatchJson' | No | No |+-- +--------------------------+-------------------+-------------------++-- | 'shouldMatchOrderedJson' | No | Yes |+-- +--------------------------+-------------------+-------------------++--+module Test.Hspec.Expectations.Json+ ( shouldBeJson+ , shouldBeUnorderedJson+ , shouldMatchJson+ , shouldMatchOrderedJson++ -- * As predicates+ -- | These are only created when a specific need arises+ , matchesJson+ )+where++import Prelude++import Data.Aeson+import Data.Aeson.Encode.Pretty (encodePretty)+import Data.Text.Lazy (toStrict)+import Data.Text.Lazy.Encoding (decodeUtf8)+import GHC.Stack+import Test.Hspec.Expectations.Json.Internal++-- $setup+-- >>> :set -XQuasiQuotes+-- >>> import Data.Aeson.QQ (aesonQQ)+-- >>> import Test.HUnit.Lang (HUnitFailure(..), formatFailureReason)+-- >>> import Control.Exception (handle)+-- >>> let printFailure (HUnitFailure _ r) = putStr $ formatFailureReason r+-- >>> let catchFailure f = handle printFailure $ f >> putStrLn "<passed>"++-- | Compare two JSON values, with a useful diff+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`+-- [aesonQQ| { "a": true, "b": false } |]+-- :}+-- <passed>+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`+-- [aesonQQ| { "a": true, "b": true } |]+-- :}+-- {+-- "a": true,+-- --- "b": true+-- +++ "b": false+-- }+--+shouldBeJson :: HasCallStack => Value -> Value -> IO ()+shouldBeJson a b = assertBoolWithDiff (a == b) (toText b) (toText a)+ where toText = toStrict . decodeUtf8 . encodePretty . normalizeScientific++infix 1 `shouldBeJson`++-- | 'shouldBeJson', ignoring Array ordering+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": [true, false], "b": false } |] `shouldBeUnorderedJson`+-- [aesonQQ| { "a": [false, true], "b": false } |]+-- :}+-- <passed>+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldBeUnorderedJson`+-- [aesonQQ| { "a": [false, true], "b": true } |]+-- :}+-- {+-- "a": [+-- false,+-- true+-- ],+-- --- "b": true+-- +++ "b": false,+-- +++ "c": true+-- }+--+shouldBeUnorderedJson :: HasCallStack => Value -> Value -> IO ()+shouldBeUnorderedJson a b = sortJsonArrays a `shouldBeJson` sortJsonArrays b++infix 1 `shouldBeUnorderedJson`++-- | 'shouldBeJson', ignoring extra Object keys or Array ordering+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`+-- [aesonQQ| { "a": [false, true], "b": false } |]+-- :}+-- <passed>+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`+-- [aesonQQ| { "a": [false, true], "b": true } |]+-- :}+-- {+-- "a": [+-- false,+-- true+-- ],+-- --- "b": true+-- +++ "b": false+-- }+--+shouldMatchJson :: HasCallStack => Value -> Value -> IO ()+shouldMatchJson sup sub =+ sortJsonArrays (pruneJson (Superset sup) (Subset sub))+ `shouldBeJson` sortJsonArrays sub++infix 1 `shouldMatchJson`++-- | Compare JSON values with the same semantics as 'shouldMatchJson'+matchesJson :: Value -> Value -> Bool+matchesJson sup sub =+ sortJsonArrays (pruneJson (Superset sup) (Subset sub)) == sortJsonArrays sub++-- | 'shouldBeJson', ignoring extra Object keys+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`+-- [aesonQQ| { "a": [true, false], "b": false } |]+-- :}+-- <passed>+--+-- >>> :{+-- catchFailure $+-- [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`+-- [aesonQQ| { "a": [false, true], "b": true } |]+-- :}+-- {+-- "a": [+-- --- false,+-- --- true+-- +++ true,+-- +++ false+-- ],+-- --- "b": true+-- +++ "b": false+-- }+--+shouldMatchOrderedJson :: HasCallStack => Value -> Value -> IO ()+shouldMatchOrderedJson sup sub =+ pruneJson (Superset sup) (Subset sub) `shouldBeJson` sub++infix 1 `shouldMatchOrderedJson`
+ library/Test/Hspec/Expectations/Json/Internal.hs view
@@ -0,0 +1,139 @@+-- | Internal building-blocks for JSON 'Value' expectations+module Test.Hspec.Expectations.Json.Internal+ (+ -- * Pretty diff+ assertBoolWithDiff++ -- * Pruning 'Object's+ , Superset(..)+ , Subset(..)+ , pruneJson++ -- * Sorting 'Array's+ , Sortable(..)+ , sortJsonArrays+ , vectorSortOn++ -- * Dealing with 'Scientific'+ , normalizeScientific+ )+where++import Prelude++import Data.Aeson+import Data.Algorithm.Diff (PolyDiff(..), getDiff)+import qualified Data.HashMap.Strict as HashMap+import Data.List (sortOn)+import qualified Data.Scientific as Scientific+import Data.Text (Text)+import qualified Data.Text as T+import Data.Vector (Vector)+import qualified Data.Vector as V+import GHC.Stack (HasCallStack)+import qualified Test.HUnit as HUnit++assertBoolWithDiff :: HasCallStack => Bool -> Text -> Text -> IO ()+assertBoolWithDiff asserting expected got =+ flip HUnit.assertBool asserting . unlines . map addSign $ getDiff+ (lines (T.unpack expected))+ (lines (T.unpack got))+ where+ addSign = \case+ Both _ s -> " " ++ s+ First s -> "---" ++ s+ Second s -> "+++" ++ s++newtype Superset = Superset Value++newtype Subset = Subset Value++-- | Recursively remove items in the 'Superset' value not present in 'Subset'+pruneJson :: Superset -> Subset -> Value+pruneJson (Superset sup) (Subset sub) = case (sup, sub) of+ (Object a, Object b) -> Object+ $ HashMap.intersectionWith (\x y -> pruneJson (Superset x) (Subset y)) a b++ -- Pruning elements in Arrays is *extremely* tricky in that it interacts with+ -- both sorting and matching in what should be a function independent of those+ -- concerns. There are no good options here, so we make some concessions:+ --+ -- 1. It's expected you don't subset differently in different elements of the+ -- same list. If you have an assertion that needs this behavior, do it+ -- manually, please+ --+ -- 2. It's expected that sorting will be done after pruning, if you intend to+ -- match irrespective of extra keys or ordering (shouldMatchJson does this)+ --+ -- Therefore, we grab the first element from the Subset Array (if present) and+ -- prune all elements of the Superset Array using it. This ensures that+ -- different sorts or length in the Superset side are preserved, but we+ -- are still able to prune *before* the sorting required for matching, which+ -- is important.+ --+ -- Other options such as sort-before-prune, or pair-wise pruning (with align+ -- or zip) all correctly handle some cases but not all. And most importantly,+ -- the cases those options don't handle come out as confusing assertion+ -- failures.+ --+ (Array a, Array b) -> Array $ case b V.!? 0 of+ Nothing -> a+ Just y -> (\x -> pruneJson (Superset x) (Subset y)) <$> a++ (x, _) -> x++newtype Sortable = Sortable Value+ deriving newtype Eq++instance Ord Sortable where+ Sortable a `compare` Sortable b = case (a, b) of+ (String x, String y) -> x `compare` y+ (Number x, Number y) -> x `compare` y+ (Bool x, Bool y) -> x `compare` y+ (Null, Null) -> EQ -- forgive me+ (Array x, Array y) -> V.map Sortable x `compare` V.map Sortable y+ (Object x, Object y) ->+ HashMap.map Sortable x `compare` HashMap.map Sortable y+ (x, y) -> arbitraryRank x `compare` arbitraryRank y+ where+ arbitraryRank :: Value -> Int+ arbitraryRank = \case+ Object{} -> 5+ Array{} -> 4+ String{} -> 3+ Number{} -> 2+ Bool{} -> 1+ Null -> 0++sortJsonArrays :: Value -> Value+sortJsonArrays = \case+ Array v -> Array $ vectorSortOn Sortable $ sortJsonArrays <$> v+ Object hm -> Object $ HashMap.map sortJsonArrays hm+ x@String{} -> x+ x@Number{} -> x+ x@Bool{} -> x+ x@Null{} -> x++vectorSortOn :: Ord b => (a -> b) -> Vector a -> Vector a+vectorSortOn f v = v V.// zip [0 ..] sorted+ where sorted = sortOn f $ V.toList v++-- | Normalize all 'Number' values to 'Double' precision+--+-- Internally, @1@ and @1.0@ are represented as different values of the+-- 'Scientific' data type. These will compare equally, but if there is some+-- /other/ difference that fails the assertion, they will render as a difference+-- in the message, confusing the reader.+--+-- This sends them through an 'id' function as 'Double', which will make either+-- print as @1.0@ consistently.+--+normalizeScientific :: Value -> Value+normalizeScientific = \case+ Object hm -> Object $ HashMap.map normalizeScientific hm+ Array vs -> Array $ normalizeScientific <$> vs+ x@String{} -> x+ Number sci ->+ Number $ Scientific.fromFloatDigits @Double $ Scientific.toRealFloat sci+ x@Bool{} -> x+ x@Null -> x
+ tests/Spec.hs view
@@ -0,0 +1,2 @@+{-# OPTIONS_GHC -fno-warn-missing-export-lists #-}+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ tests/Test/Hspec/Expectations/Json/InternalSpec.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE QuasiQuotes #-}++module Test.Hspec.Expectations.Json.InternalSpec+ ( spec+ )+where++import Prelude++import Data.Aeson.QQ+import Test.Hspec+import Test.Hspec.Expectations.Json (shouldBeJson)+import Test.Hspec.Expectations.Json.Internal++spec :: Spec+spec = do+ describe "pruneJson" $ do+ it "prunes object keys from Superset not present in Subset" $ do+ let+ sup = Superset [aesonQQ|{ "foo": "foo", "baz": "baz" }|]+ sub = Subset [aesonQQ|{ "foo": "bar" } |]++ pruneJson sup sub `shouldBeJson` [aesonQQ|{ "foo": "foo" }|]++ it "prunes object keys recursively Superset not present in Subset" $ do+ let+ sup = Superset [aesonQQ|+ { "foo": { "bar": "bar" , "baz": "baz" }+ , "bat": "bat"+ }+ |]++ sub = Subset [aesonQQ|+ { "foo": { "bar": "baz" }+ }+ |]++ pruneJson sup sub `shouldBeJson` [aesonQQ|{ "foo": { "bar": "bar" } }|]++ it "prunes objects within Arrays" $ do+ let+ sup = Superset [aesonQQ|+ [ { "foo": "bar", "quix": "cats" }+ , { "foo": "bats", "quix": "baz" }+ ]+ |]++ sub = Subset [aesonQQ|+ [ { "foo": "zap" }+ , { "foo": "zop" }+ ]+ |]++ pruneJson sup sub `shouldBeJson` [aesonQQ|+ [ { "foo": "bar" }+ , { "foo": "bats" }+ ]+ |]++ it "handles mismatching types" $ do+ let+ sup = Superset [aesonQQ|+ { "foo": { "bar": 1, "baz": "baz" }+ , "bat": { "bat": 1 }+ }+ |]++ sub = Subset [aesonQQ|+ { "foo": { "bar": "baz" }+ , "bat": "bat"+ }+ |]++ pruneJson sup sub `shouldBeJson` [aesonQQ|+ { "foo": { "bar": 1 }+ , "bat": { "bat": 1 }+ }+ |]++ describe "sortJsonArrays" $ do+ it "sorts arrays" $ do+ let+ unsorted = [aesonQQ|["number_facts", "number_basics"]|]+ sorted = [aesonQQ|["number_basics", "number_facts"]|]++ sortJsonArrays unsorted `shouldBeJson` sorted++ it "sorts arrays in object keys" $ do+ let+ unsorted = [aesonQQ|{ "x": ["number_facts", "number_basics"] }|]+ sorted = [aesonQQ|{ "x": ["number_basics", "number_facts"] }|]++ sortJsonArrays unsorted `shouldBeJson` sorted++ it "works on arrays of nested arrays" $ do+ let+ unsorted = [aesonQQ|[{ "x": ["number_facts", "number_basics"] }]|]+ sorted = [aesonQQ|[{ "x": ["number_basics", "number_facts"] }]|]++ sortJsonArrays unsorted `shouldBeJson` sorted
+ tests/Test/Hspec/Expectations/JsonSpec.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE QuasiQuotes #-}++module Test.Hspec.Expectations.JsonSpec+ ( spec+ )+where++import Prelude++import Data.Aeson.QQ+import Test.Hspec+import Test.Hspec.Expectations.Json++spec :: Spec+spec = do+ describe "shouldMatchJson" $ do+ it "passes regardless of array order" $ do+ let+ a = [aesonQQ|[{ "foo": 1 }, { "foo": 0 }]|]+ b = [aesonQQ|[{ "foo": 0 }, { "foo": 1 }]|]++ a `shouldMatchJson` b++ it "passes regardless of array order at depth" $ do+ let+ a = [aesonQQ|{ "a": [{ "foo": 1 }, { "foo": 0 }] }|]+ b = [aesonQQ|{ "a": [{ "foo": 0 }, { "foo": 1 }] }|]++ a `shouldMatchJson` b++ it "passes regardless of extra keys" $ do+ let+ a = [aesonQQ|{ "foo": "bar", "baz": "bat" }|]+ b = [aesonQQ|{ "foo": "bar" }|]++ a `shouldMatchJson` b++ it "passes regardless of extra keys at depth" $ do+ let+ a = [aesonQQ|{ "a": { "foo": "bar", "baz": "bat" } }|]+ b = [aesonQQ|{ "a": { "foo": "bar" } }|]++ a `shouldMatchJson` b++ it "matches pruned and unsorted array elements" $ do+ let+ sup1 = [aesonQQ|{ "studentId": 1, "x": true }|]+ sub1 = [aesonQQ|{ "studentId": 1 }|]+ sup2 = [aesonQQ|{ "studentId": 2, "x": true }|]+ sub2 = [aesonQQ|{ "studentId": 2 }|]+ sup3 = [aesonQQ|{ "studentId": 3, "x": true }|]+ sub3 = [aesonQQ|{ "studentId": 3 }|]+ sup4 = [aesonQQ|{ "studentId": 4, "x": true }|]+ sub4 = [aesonQQ|{ "studentId": 4 }|]++ a = [aesonQQ|+ [ { "stats": [#{sup3}] }+ , { "stats": [#{sup3}, #{sup1}] }+ , { "stats": [#{sup4}, #{sup2}] }+ , { "stats": [#{sup2}] }+ , { "stats": [#{sup4}] }+ , { "stats": [#{sup4}] }+ ]+ |]++ b = [aesonQQ|+ [ { "stats": [#{sub1}, #{sub3}] }+ , { "stats": [#{sub3}] }+ , { "stats": [#{sub2}, #{sub4}] }+ , { "stats": [#{sub2}] }+ , { "stats": [#{sub4}] }+ , { "stats": [#{sub4}] }+ ]+ |]++ a `shouldMatchJson` b++ it "handles cases where sorting differs after pruning" $ do+ let+ a = [aesonQQ|+ [ { "shortName": "B"+ , "subSkills":+ [ { "shortName": "1"+ , "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e"+ }+ ]+ }+ , { "shortName": "A"+ , "subSkills":+ [ { "shortName": "a"+ , "uspId": "71839723561ba1a49cf2a789dbe50302"+ }+ , { "shortName": "b"+ , "uspId": "4096d2cfebcab73438971ae2304544ee"+ }+ ]+ }+ ]+ |]+ b = [aesonQQ|+ [ { "subSkills":+ [ { "uspId": "71839723561ba1a49cf2a789dbe50302" }+ , { "uspId": "4096d2cfebcab73438971ae2304544ee" }+ ]+ }+ , { "subSkills":+ [ { "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e" }+ ]+ }+ ]+ |]++ a `shouldMatchJson` b