packages feed

keiki-codec-json-test 0.1.0.0 → 0.2.0.0

raw patch · 8 files changed

+233/−25 lines, 8 filesdep ~keikidep ~keiki-codec-jsondep ~keiki-codec-json-testPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: keiki, keiki-codec-json, keiki-codec-json-test

API changes (from Hackage documentation)

+ Keiki.Codec.JSON.Test: class EqRegFile (rs :: [Slot])
+ Keiki.Codec.JSON.Test: eqRegFile :: EqRegFile rs => RegFile rs -> RegFile rs -> Bool
+ Keiki.Codec.JSON.Test: instance (GHC.Classes.Eq t, Keiki.Codec.JSON.Test.EqRegFile rs) => Keiki.Codec.JSON.Test.EqRegFile ('(s, t) : rs)
+ Keiki.Codec.JSON.Test: instance Keiki.Codec.JSON.Test.EqRegFile '[]
+ Keiki.Codec.JSON.Test: regFileCodecPropsEq :: forall (rs :: [Slot]). (RegFileToJSON rs, ArbitraryRegFile rs, EqRegFile rs) => Spec
+ Keiki.Codec.JSON.Test.GoldenFile: regFileGoldenFileSpec :: forall (rs :: [Slot]). (RegFileToJSON rs, EqRegFile rs) => String -> FilePath -> RegFile rs -> Spec

Files

CHANGELOG.md view
@@ -9,6 +9,18 @@ ## [Unreleased]  +## [0.2.0.0] — 2026-07-13++### Added++- `EqRegFile` and `regFileCodecPropsEq` provide value-level heterogeneous+  round-trip checks. The original byte-comparing `regFileCodecProps` remains for+  compatibility, with its nested-`Maybe` blind spot now documented.+- `Keiki.Codec.JSON.Test.GoldenFile.regFileGoldenFileSpec` pins a whole+  register file against checked-in JSON in both directions, with a deliberately+  failing `KEIKI_UPDATE_GOLDENS` regeneration workflow.++ ## [0.1.0.0] — 2026-06-07  Initial Hackage release. Co-released with `keiki-0.1.0.0` and
README.md view
@@ -33,7 +33,7 @@ import Test.Hspec (describe, hspec)  import Keiki.Codec.JSON.Test-  ( regFileCodecProps+  ( regFileCodecPropsEq   , regFileShapeSensitivitySpec   , someKnownShape   )@@ -41,6 +41,9 @@   ( SlotGolden (..)   , slotGoldenSpec   )+import Keiki.Codec.JSON.Test.GoldenFile+  ( regFileGoldenFileSpec+  )  -- Your slot type and slot lists: -- data Email = Email Text deriving (...)@@ -56,8 +59,15 @@     , sgBytes = "\"alice@example.com\""     }) +  -- Whole-snapshot backward compatibility. `myFixture` is a fixed+  -- `RegFile MySlots`; add the JSON file to extra-source-files.+  regFileGoldenFileSpec+    "MySlots"+    "test/golden/my-slots.value.json"+    myFixture+   -- Round-trip + determinism over the snapshot's slot list.-  describe "props: MySlots" (regFileCodecProps @MySlots)+  describe "props: MySlots" (regFileCodecPropsEq @MySlots)    -- Sensitivity: every named mutation must flip the shape hash.   describe "sensitivity: MySlots" $@@ -73,6 +83,21 @@ lacks `Arbitrary`, write one — it is typically a one-liner via `quickcheck-instances` for the standard library types. +`regFileCodecPropsEq` is the preferred round-trip helper: its inductive+`EqRegFile` comparison checks decoded slot values. The older+`regFileCodecProps` remains available for slot types without `Eq`, but it compares+re-encoded bytes and therefore cannot detect lossy decodes that re-encode+identically (notably `Just Nothing -> null -> Nothing`).++When an intentional wire change requires regenerating whole-snapshot files, use+the guarded two-run workflow:++```sh+KEIKI_UPDATE_GOLDENS=1 cabal test   # rewrites files, then fails deliberately+cabal test                          # must pass with the variable unset+git diff test/golden/               # review every changed byte+```+ ## When you don't need this  `keiki-codec-json` alone is sufficient for production use. This@@ -88,4 +113,4 @@  The self-test exercises every public helper against a small toy fixture (`Email`, `DemoSlots`, `DemoSlotsRenamed`). Expected-output: 7 examples, 0 failures.+output: 11 examples, 0 failures.
keiki-codec-json-test.cabal view
@@ -1,6 +1,6 @@-cabal-version:   3.0-name:            keiki-codec-json-test-version:         0.1.0.0+cabal-version:      3.0+name:               keiki-codec-json-test+version:            0.2.0.0 synopsis:   Property-test toolkit for keiki-codec-json downstream consumers. @@ -22,17 +22,24 @@   for production do not transitively pick up @QuickCheck@ /   @hspec@ / @quickcheck-instances@. -license:         BSD-3-Clause-author:          Nadeem Bitar-maintainer:      nadeem@gmail.com-copyright:       2026 Nadeem Bitar-category:        Codec, Testing-build-type:      Simple-tested-with:     GHC >=9.12 && <9.13+license:            BSD-3-Clause+author:             Nadeem Bitar+maintainer:         nadeem@gmail.com+copyright:          2026 Nadeem Bitar+category:           Codec, Testing+build-type:         Simple+tested-with:        GHC >=9.12 && <9.13 extra-doc-files:   CHANGELOG.md   README.md +extra-source-files: test/golden/*.json++source-repository head+  type:     git+  location: https://github.com/shinzui/keiki.git+  subdir:   keiki-codec-json-test+ common warnings   ghc-options:     -Wall -Wcompat -Widentities -Wincomplete-record-updates@@ -54,6 +61,7 @@   exposed-modules:     Keiki.Codec.JSON.Test     Keiki.Codec.JSON.Test.Golden+    Keiki.Codec.JSON.Test.GoldenFile    hs-source-dirs:  src   build-depends:@@ -61,8 +69,8 @@     , base                  ^>=4.21     , bytestring            ^>=0.12     , hspec                 ^>=2.11-    , keiki                 ^>=0.1-    , keiki-codec-json      ^>=0.1+    , keiki                 ^>=0.2+    , keiki-codec-json      ^>=0.2     , QuickCheck            ^>=2.15     , quickcheck-instances  ^>=0.3     , text                  ^>=2.1@@ -78,9 +86,9 @@     , base                   ^>=4.21     , bytestring             ^>=0.12     , hspec                  ^>=2.11-    , keiki                  ^>=0.1-    , keiki-codec-json       ^>=0.1-    , keiki-codec-json-test  ^>=0.1+    , keiki                  ^>=0.2+    , keiki-codec-json       ^>=0.2+    , keiki-codec-json-test  ^>=0.2     , QuickCheck             ^>=2.15     , quickcheck-instances   ^>=0.3     , text                   ^>=2.1
src/Keiki/Codec/JSON/Test.hs view
@@ -5,7 +5,7 @@ -- -- Wires three helpers into a consumer's @hspec@ test suite: ----- * 'regFileCodecProps' — four QuickCheck properties (Value-path and+-- * 'regFileCodecProps' / 'regFileCodecPropsEq' — four QuickCheck properties (Value-path and --   Encoding-path round-trip, within-path determinism on both --   paths) against the consumer's own slot list. Mirror of the --   EP-36 M3 in-tree property suite at@@ -34,8 +34,12 @@   ( -- * Arbitrary generator for slot lists     ArbitraryRegFile (..), +    -- * Value equality for slot lists+    EqRegFile (..),+     -- * Round-trip + determinism properties     regFileCodecProps,+    regFileCodecPropsEq,      -- * Sensitivity helper     SomeKnownRegFileShape (..),@@ -92,6 +96,19 @@   where   arbRegFile = RCons (Proxy @s) <$> arbitrary <*> arbRegFile +-- * Value equality for slot lists -------------------------------------------++-- | Inductive equality for a heterogeneous 'RegFile'. Consumers receive an+-- instance automatically when every slot value has 'Eq'.+class EqRegFile (rs :: [Slot]) where+  eqRegFile :: RegFile rs -> RegFile rs -> Bool++instance EqRegFile '[] where+  eqRegFile _ _ = True++instance (Eq t, EqRegFile rs) => EqRegFile ('(s, t) ': rs) where+  eqRegFile (RCons _ x xs) (RCons _ y ys) = x == y && eqRegFile xs ys+ -- * Round-trip + determinism properties -------------------------------------  -- | Run the EP-36 M3 codec property suite against an arbitrary slot@@ -107,12 +124,12 @@ --   'RegFile' yields byte-equal output. -- * /Encoding path within-path determinism (R9):/ same. ----- Implementation note: 'RegFile' has no 'Eq' or 'Show' instance (the--- slot list is heterogeneous), so the round-trip assertion uses the--- re-encoded bytes as the comparison point. If the parsed--- 'RegFile' re-encodes to the same bytes as the original, the--- round-trip is structurally exact. This is the same trick the--- in-tree EP-36 M3 spec uses.+-- Implementation note: 'RegFile' has no general 'Eq' or 'Show' instance (the+-- slot list is heterogeneous), so this compatibility helper compares re-encoded+-- bytes. That cannot detect a lossy decode when the changed value re-encodes to+-- the same bytes; aeson's @Just Nothing -> null -> Nothing@ collapse is the known+-- example. Consumers whose slot values have 'Eq' should prefer+-- 'regFileCodecPropsEq', which compares decoded values through 'EqRegFile'. -- -- Type-application invocation form: --@@ -174,6 +191,70 @@               Right rf' ->                 AesonEnc.encodingToLazyByteString (regFileToEncoding rf')                   === bytes++    valueDeterministic :: RegFile rs -> Property+    valueDeterministic rf =+      Aeson.encode (regFileToJSON rf)+        === Aeson.encode (regFileToJSON rf)++    encodingDeterministic :: RegFile rs -> Property+    encodingDeterministic rf =+      AesonEnc.encodingToLazyByteString (regFileToEncoding rf)+        === AesonEnc.encodingToLazyByteString (regFileToEncoding rf)++-- | Value-comparing form of 'regFileCodecProps'. It runs the same four+-- properties but compares decoded slot values through 'EqRegFile', closing the+-- byte-idempotence blind spot of the compatibility helper.+--+-- @+-- regFileCodecPropsEq \@MyAppSnapshotSlots+-- @+regFileCodecPropsEq ::+  forall rs.+  ( RegFileToJSON rs,+    ArbitraryRegFile rs,+    EqRegFile rs+  ) =>+  Spec+regFileCodecPropsEq = do+  describe "Roundtrip" $ do+    it "Value path round-trips by value" $+      forAllShow (arbRegFile @rs) showRf valueRoundTrip+    it "Encoding path round-trips by value" $+      forAllShow (arbRegFile @rs) showRf encodingRoundTrip++  describe "Determinism (R9 within-path)" $ do+    it "Value path is deterministic" $+      forAllShow (arbRegFile @rs) showRf valueDeterministic+    it "Encoding path is deterministic" $+      forAllShow (arbRegFile @rs) showRf encodingDeterministic+  where+    showRf :: RegFile rs -> String+    showRf = show . regFileToJSON++    valueRoundTrip :: RegFile rs -> Property+    valueRoundTrip rf =+      let bytes = Aeson.encode (regFileToJSON rf)+       in case Aeson.decode bytes of+            Nothing ->+              False+                === error+                  "Aeson.decode failed on our own Value-path encoder output"+            Just value -> case regFileFromJSON @rs value of+              Left message ->+                False === error ("regFileFromJSON failed: " <> message)+              Right decoded -> eqRegFile decoded rf === True++    encodingRoundTrip :: RegFile rs -> Property+    encodingRoundTrip rf =+      let bytes = AesonEnc.encodingToLazyByteString (regFileToEncoding rf)+       in case Aeson.decode bytes of+            Nothing ->+              False === error "Aeson.decode failed on streaming-encoder output"+            Just value -> case regFileFromJSON @rs value of+              Left message ->+                False === error ("regFileFromJSON failed: " <> message)+              Right decoded -> eqRegFile decoded rf === True      valueDeterministic :: RegFile rs -> Property     valueDeterministic rf =
+ src/Keiki/Codec/JSON/Test/GoldenFile.hs view
@@ -0,0 +1,59 @@+-- |+-- Module      : Keiki.Codec.JSON.Test.GoldenFile+-- Description : Checked-in whole-RegFile JSON golden-file discipline.+--+-- 'regFileGoldenFileSpec' checks both persistence directions: the current+-- decoder must read bytes checked into source control as the fixed expected+-- value, and the current Value-path encoder must reproduce those bytes exactly.+--+-- To accept an intentional wire-format change, run the consumer test suite once+-- with @KEIKI_UPDATE_GOLDENS=1@. The helper rewrites the file and deliberately+-- fails; unset the variable, review the diff, and rerun before committing.+module Keiki.Codec.JSON.Test.GoldenFile+  ( regFileGoldenFileSpec,+  )+where++import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy qualified as LBS+import Keiki.Codec.JSON (RegFileToJSON, regFileFromJSON, regFileToJSON)+import Keiki.Codec.JSON.Test (EqRegFile (eqRegFile))+import Keiki.Core (RegFile)+import System.Environment (lookupEnv)+import Test.Hspec (Spec, describe, expectationFailure, it, runIO, shouldBe)++-- | Pin a fixed register file against a checked-in Value-path JSON file.+--+-- The file path is resolved relative to the consumer package's test working+-- directory. Add it to the package's @extra-source-files@ so source+-- distributions retain the compatibility evidence.+regFileGoldenFileSpec ::+  forall rs.+  (RegFileToJSON rs, EqRegFile rs) =>+  String ->+  FilePath ->+  RegFile rs ->+  Spec+regFileGoldenFileSpec label path expected = describe label $ do+  update <- runIO (lookupEnv "KEIKI_UPDATE_GOLDENS")+  case update of+    Just _ -> do+      runIO (LBS.writeFile path currentBytes)+      it "regenerates and requires a clean review run" $+        expectationFailure regenerationMessage+    Nothing -> do+      historicalBytes <- runIO (LBS.readFile path)+      it "decodes checked-in bytes to the fixed value" $+        case Aeson.decode historicalBytes of+          Nothing -> expectationFailure "golden file is not valid JSON"+          Just value -> case regFileFromJSON @rs value of+            Left message -> expectationFailure ("golden decode failed: " <> message)+            Right actual -> eqRegFile actual expected `shouldBe` True+      it "freezes current Value-path bytes" $+        currentBytes `shouldBe` historicalBytes+  where+    currentBytes = Aeson.encode (regFileToJSON expected)++regenerationMessage :: String+regenerationMessage =+  "golden regenerated; unset KEIKI_UPDATE_GOLDENS, re-run, and review git diff"
test/Keiki/Codec/JSON/Test/Demo.hs view
@@ -9,12 +9,16 @@   ( Email (..),     DemoSlots,     DemoSlotsRenamed,+    demoRegFile,   ) where  import Data.Aeson qualified as Aeson+import Data.Proxy (Proxy (..)) import Data.Text (Text)+import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Core (RegFile (..)) import Keiki.Shape (CanonicalTypeName) import Test.QuickCheck (Arbitrary (..)) import Test.QuickCheck.Instances ()@@ -43,3 +47,8 @@   '[ '("emailAddress", Email),      '("count", Int)    ]++demoRegFile :: RegFile DemoSlots+demoRegFile =+  RCons (Proxy @"email") (Email (T.pack "a@b.c")) $+    RCons (Proxy @"count") (7 :: Int) RNil
test/Spec.hs view
@@ -13,6 +13,7 @@ import Data.Text qualified as T import Keiki.Codec.JSON.Test   ( regFileCodecProps,+    regFileCodecPropsEq,     regFileShapeSensitivitySpec,     someKnownShape,   )@@ -20,11 +21,13 @@   ( DemoSlots,     DemoSlotsRenamed,     Email (..),+    demoRegFile,   ) import Keiki.Codec.JSON.Test.Golden   ( SlotGolden (..),     slotGoldenSpec,   )+import Keiki.Codec.JSON.Test.GoldenFile (regFileGoldenFileSpec) import Test.Hspec (describe, hspec)  main :: IO ()@@ -41,6 +44,16 @@   describe     "Keiki.Codec.JSON.Test.regFileCodecProps @DemoSlots"     (regFileCodecProps @DemoSlots)++  describe+    "Keiki.Codec.JSON.Test.regFileCodecPropsEq @DemoSlots"+    (regFileCodecPropsEq @DemoSlots)++  describe "Keiki.Codec.JSON.Test.GoldenFile.regFileGoldenFileSpec" $+    regFileGoldenFileSpec+      "DemoSlots"+      "test/golden/demo-regfile.value.json"+      demoRegFile    describe "Keiki.Codec.JSON.Test.regFileShapeSensitivitySpec" $     regFileShapeSensitivitySpec
+ test/golden/demo-regfile.value.json view
@@ -0,0 +1,1 @@+{"count":7,"email":"a@b.c"}