packages feed

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 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