packages feed

cuddle 1.2.0.0 → 1.3.0.0

raw patch · 26 files changed

+937/−257 lines, 26 filesdep ~antigenPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: antigen

API changes (from Hackage documentation)

- Codec.CBOR.Cuddle.CBOR.Gen: instance GHC.Generics.Generic Codec.CBOR.Cuddle.CBOR.Gen.GenEnv
- Codec.CBOR.Cuddle.CBOR.Validator.Trace: [ListValidationUnappliedRules] :: NonEmpty (CTree ValidatorStageSimple) -> ListValidationTrace 'IsInvalid
- Codec.CBOR.Cuddle.CDDL.CBORGenerator: CBORGenerator :: (CTreeRoot GenPhase -> AntiGen WrappedTerm) -> CBORGenerator
- Codec.CBOR.Cuddle.CDDL.CBORGenerator: newtype CBORGenerator
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: Progress :: !Int -> !Int -> Progress
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: [ListValidationUnappliedRule] :: CTree ValidatorStageSimple -> ListValidationTrace 'IsInvalid
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: [refProgress] :: Progress -> !Int
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: [termProgress] :: Progress -> !Int
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: class IsValidationTrace (t :: Validity -> Type)
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: data Progress
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: instance GHC.Base.Monoid Codec.CBOR.Cuddle.CBOR.Validator.Trace.Progress
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: instance GHC.Base.Semigroup Codec.CBOR.Cuddle.CBOR.Validator.Trace.Progress
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: instance GHC.Classes.Eq Codec.CBOR.Cuddle.CBOR.Validator.Trace.Progress
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: instance GHC.Classes.Ord Codec.CBOR.Cuddle.CBOR.Validator.Trace.Progress
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: measureProgress :: forall (v :: Validity). IsValidationTrace t => t v -> Progress
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: CBORGen :: ReaderT GenEnv AntiGen a -> CBORGen a
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: GenEnv :: CTreeRoot GenPhase -> Bool -> GenEnv
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: [geRoot] :: GenEnv -> CTreeRoot GenPhase
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: [geTwiddle] :: GenEnv -> Bool
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: data GenEnv
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: instance Control.Monad.Reader.Class.MonadReader Codec.CBOR.Cuddle.CDDL.CBORGenerator.GenEnv Codec.CBOR.Cuddle.CDDL.CBORGenerator.CBORGen
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: instance GHC.Base.Applicative Codec.CBOR.Cuddle.CDDL.CBORGenerator.CBORGen
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: instance GHC.Base.Functor Codec.CBOR.Cuddle.CDDL.CBORGenerator.CBORGen
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: instance GHC.Base.Monad Codec.CBOR.Cuddle.CDDL.CBORGenerator.CBORGen
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: instance GHC.Classes.Ord Codec.CBOR.Cuddle.CDDL.CBORGenerator.WrappedTerm
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: instance GHC.Generics.Generic Codec.CBOR.Cuddle.CDDL.CBORGenerator.GenEnv
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: instance Test.QuickCheck.GenT.MonadGen Codec.CBOR.Cuddle.CDDL.CBORGenerator.CBORGen
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: liftAntiGen :: AntiGen a -> CBORGen a
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: lookupCddl :: Name -> CBORGen (Maybe (CTree GenPhase))
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: newtype CBORGen a
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: runCBORGen :: GenEnv -> CBORGen a -> AntiGen a
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: withAntiGen :: (AntiGen a -> AntiGen b) -> CBORGen a -> CBORGen b
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: withTwiddle :: Bool -> CBORGen a -> CBORGen a
+ Codec.CBOR.Cuddle.CDDL.CTree: instance GHC.Enum.Bounded Codec.CBOR.Cuddle.CDDL.CTree.PTerm
+ Codec.CBOR.Cuddle.CDDL.CTree: instance GHC.Enum.Enum Codec.CBOR.Cuddle.CDDL.CTree.PTerm
+ Codec.CBOR.Cuddle.CDDL.CTree: instance Test.QuickCheck.Arbitrary.Arbitrary Codec.CBOR.Cuddle.CDDL.CTree.PTerm
+ Codec.CBOR.Cuddle.CDDL.CTree: nintMin :: Integer
+ Codec.CBOR.Cuddle.CDDL.CTree: uintMax :: Integer
+ Codec.CBOR.Cuddle.Huddle: withCBORGen :: HasGenerator a => CBORGen WrappedTerm -> a -> a
- Codec.CBOR.Cuddle.CBOR.Validator.Trace: [ListValidationLeftoverTerms] :: NonEmpty Term -> ListValidationTrace 'IsInvalid
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: [ListValidationLeftoverTerms] :: NonEmpty Term -> Maybe (CTree ValidatorStageSimple, ValidationTrace 'IsInvalid) -> ListValidationTrace 'IsInvalid
- Codec.CBOR.Cuddle.CBOR.Validator.Trace: [MapValidationLeftoverKVs] :: [(Term, Term)] -> MapValidationTrace 'IsInvalid
+ Codec.CBOR.Cuddle.CBOR.Validator.Trace: [MapValidationLeftoverKVs] :: (Term, Term) -> Maybe (CTree ValidatorStageSimple, MapValidationTrace 'IsInvalid) -> MapValidationTrace 'IsInvalid
- Codec.CBOR.Cuddle.CDDL.CBORGenerator: generatorL :: HasGenerator a => Lens' a (Maybe CBORGenerator)
+ Codec.CBOR.Cuddle.CDDL.CBORGenerator: generatorL :: HasGenerator a => Lens' a (Maybe (CBORGen WrappedTerm))

Files

CHANGELOG.md view
@@ -1,5 +1,15 @@ # Changelog for `cuddle` +## 1.3.0.0++* Replace `Int` progress measure with lexicographic `Progress` type for more accurate trace comparison+* Rewrite map generation to produce unique keys+* Add `withTwiddle`+* `generateFromName` now enables twiddling by default (`geTwiddle = True`), which can change the shape of generated terms (e.g. `TBytes` vs `TBytesI`, `TList` vs `TListI`)+* Replace `CBORGenerator` with `CBORGen`+* Add `withCBORGen`+* Deprecate `withGenerator` and `withAntiGen`+ ## 1.2.0.0  * Add `Pretty` instance for `PTerm` 
bin/Main.hs view
@@ -41,7 +41,9 @@ import Data.Text qualified as T import Data.Text.Encoding (encodeUtf8) import Data.Text.IO qualified as T+import Data.Version (showVersion) import Options.Applicative+import Paths_cuddle (version) import Prettyprinter (   LayoutOptions (..),   PageWidth (..),@@ -295,7 +297,7 @@   options <-     execParser $       info-        (pCommand <**> helper)+        (pCommand <**> helper <**> simpleVersioner (showVersion version))         ( fullDesc             <> progDesc "Manipulate CDDL files"             <> header "cuddle"
cuddle.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: cuddle-version: 1.2.0.0+version: 1.3.0.0 synopsis: CDDL Generator and test utilities description:   Cuddle is a library for generating and manipulating [CDDL](https://datatracker.ietf.org/doc/html/rfc8610).@@ -63,7 +63,7 @@    build-depends:     QuickCheck >=2.14.3 && <2.18,-    antigen >=0.3.1 && <0.4,+    antigen ^>=0.4,     base >=4.18 && <5,     base16-bytestring >=1.0.2,     boxes >=0.1.5,@@ -74,6 +74,7 @@     data-default-class >=0.2,     foldable1-classes-compat >=0.1.1,     generic-optics >=2.2.1,+    generic-random,     half,     hashable >=1.4,     megaparsec >=9.5,@@ -140,6 +141,12 @@     random,     text, +  autogen-modules:+    Paths_cuddle++  other-modules:+    Paths_cuddle+ test-suite cuddle-test   import: warnings   default-language: GHC2021@@ -181,6 +188,7 @@     pretty-simple,     prettyprinter,     prettyprinter-ansi-terminal,+    quickcheck-transformer,     random,     string-qq >=0.0.6,     text,
+ golden/cborControlBad.txt view
@@ -0,0 +1,2 @@+[0;91munsatisfied control:+  .cbor simpleRule[0m
+ golden/deeplyNestedRefVsRealProgress.txt view
@@ -0,0 +1,10 @@+array+└ app: [0;92mint[0m+  [0;91mleftover elements, closest matching rule:[0m+    [0;92m? list_rule[0m+    └ ref: [0;94mlist_rule[0m+      └ array+        └ app: [0;92mint[0m+          [0;91mfailed to apply required rule:[0m+          └ [0;91mfailed to apply: +              int[0m
golden/huddleRangeArrayTwoStrings.txt view
@@ -3,4 +3,5 @@   app: [0;92mint[0m   app: [0;92mtext[0m   app: [0;92mtext[0m-  [0;91mnot all required rules have been applied[0m+  [0;91mrequired rule not satisfied:[0m+    1* text
+ golden/listSkippedRule.txt view
@@ -0,0 +1,7 @@+array+└ app: [0;92mint[0m+  [0;91mleftover elements, closest matching rule:[0m+    [0;92m? bar[0m+    └ ref: [0;94mbar[0m+      └ [0;91mfailed to apply: +          text[0m
+ golden/listSkippedRuleNested.txt view
@@ -0,0 +1,10 @@+array+└ app: [0;92mint[0m+  [0;91mleftover elements, closest matching rule:[0m+    [0;92m? baz[0m+    └ ref: [0;94mbaz[0m+      └ array+        └ app: [0;92mint[0m+          [0;91mfailed to apply required rule:[0m+          └ [0;91mfailed to apply: +              text[0m
+ golden/listTooShort.txt view
@@ -0,0 +1,4 @@+array+└ app: [0;92mint[0m+  [0;91mrequired rule not satisfied:[0m+    text
+ golden/listZeroOrMore.txt view
@@ -0,0 +1,5 @@+array+└ app: [0;92mint[0m+  app: [0;92mint[0m+  app: [0;92mtext[0m+  [0;91mleftover elements not matched by any rule[0m
+ golden/mapLeftoverKV.txt view
@@ -0,0 +1,8 @@+map+└ [0;91mleftover key-value pairs, closest matching rule:[0m+    [0;92m? 2 ==> int[0m+    └ key:+      └ app: [0;92m2[0m+      value: [0;91m(fail)[0m+      └ [0;91mfailed to apply: +          int[0m
+ golden/mapNestedValue.txt view
@@ -0,0 +1,12 @@+map+└ [0;91mleftover key-value pairs, closest matching rule:[0m+    [0;92m? 1 ==> bar[0m+    └ key:+      └ app: [0;92m1[0m+      value: [0;91m(fail)[0m+      └ ref: [0;94mbar[0m+        └ array+          └ app: [0;92mint[0m+            [0;91mfailed to apply required rule:[0m+            └ [0;91mfailed to apply: +                text[0m
+ golden/mapNoMatchingKey.txt view
@@ -0,0 +1,2 @@+map+└ [0;91mleftover key-value pairs not matched by any rule[0m
golden/refTermTooLong.txt view
@@ -6,4 +6,4 @@       └ app: [0;92m1[0m         app: [0;92m2[0m         app: [0;92m3[0m-        [0;91mleftover elements after all rules have been applied[0m+        [0;91mleftover elements not matched by any rule[0m
src/Codec/CBOR/Cuddle/CBOR/Gen.hs view
@@ -31,12 +31,25 @@   ValueVariant (..),  ) import Codec.CBOR.Cuddle.CDDL.CBORGenerator (-  CBORGenerator (..),+  CBORGen (..),+  GenEnv (..),   GenPhase,   WrappedTerm (..),   XXCTree (..),+  liftAntiGen,+  lookupCddl,+  runCBORGen,+  withAntiGen,+  withTwiddle,  )-import Codec.CBOR.Cuddle.CDDL.CTree (CTree (..), CTreeRoot (..), PTerm (..), foldCTree)+import Codec.CBOR.Cuddle.CDDL.CTree (+  CTree (..),+  CTreeRoot (..),+  PTerm (..),+  foldCTree,+  nintMin,+  uintMax,+ ) import Codec.CBOR.Cuddle.CDDL.CTree qualified as CTree import Codec.CBOR.Cuddle.CDDL.CtlOp qualified as CtlOp import Codec.CBOR.Cuddle.CDDL.Resolve (XXCTree (..))@@ -44,15 +57,17 @@ import Codec.CBOR.Term (Term (..)) import Codec.CBOR.Term qualified as CBOR import Codec.CBOR.Write qualified as CBOR-import Control.Monad ((<=<))+import Control.Monad (zipWithM, (<=<))+import Control.Monad.Reader (asks) import Data.ByteString (ByteString)-import Data.ByteString.Lazy qualified as BSL+import Data.ByteString.Lazy qualified as LBS import Data.Char (chr)-import Data.Containers.ListUtils (nubOrdOn) import Data.Functor ((<&>))+import Data.List (sortOn) import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as Map import Data.Maybe (fromMaybe)+import Data.Ord (Down (..)) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Internal.Encoding.Utf8 (utf8Length)@@ -61,17 +76,18 @@ import Data.Text.Lazy.Builder (toLazyText) import Data.Text.Lazy.Builder qualified as LB import Data.Word (Word64, Word8)-import GHC.Generics (Generic) import GHC.Stack (HasCallStack) import Numeric.Half (Half (..), fromHalf)+import Prettyprinter (Pretty (..), layoutCompact)+import Prettyprinter.Render.Text (renderStrict) import System.Random.Stateful (Random, StatefulGen (..), runStateGen_, uniformByteStringM) import Test.AntiGen (   AntiGen,   antiChoose,-  antiNonNegative,-  antiNonPositive,   faultyNum,+  reweigh,   runAntiGen,+  withAnnotation,   (|!),  ) import Test.QuickCheck (@@ -80,7 +96,7 @@  ) import Test.QuickCheck qualified as QC import Test.QuickCheck.Gen (Gen (..), getSize)-import Test.QuickCheck.GenT (MonadGen (..), elements, listOf, oneof, suchThat, vectorOf)+import Test.QuickCheck.GenT (MonadGen (..), elements, frequency, listOf, oneof, suchThat, vectorOf)  -- TODO remove this once QuickCheck gets QC data QC = QC@@ -125,12 +141,6 @@ -- Generator infrastructure -------------------------------------------------------------------------------- --- | Generator context, parametrised over the type of the random seed-newtype GenEnv = GenEnv-  { cddl :: CTreeRoot GenPhase-  }-  deriving (Generic)- -------------------------------------------------------------------------------- -- Postlude --------------------------------------------------------------------------------@@ -145,11 +155,39 @@ genHalf :: MonadGen m => m Float genHalf = do   half <- Half <$> arbitrary-  if isInfinite half || isDenormalized half || isNaN half+  if isDenormalized half     then genHalf     else pure $ fromHalf half -genTerm :: AntiGen Term+twiddleString :: Text -> CBORGen Term+twiddleString t = do+  twiddle <- asks geTwiddle+  if twiddle+    then ($ t) <$> elements [TString, TStringI . TL.fromStrict]+    else pure $ TString t++twiddleList :: [Term] -> CBORGen Term+twiddleList t = do+  twiddle <- asks geTwiddle+  if twiddle+    then ($ t) <$> elements [TList, TListI]+    else pure $ TList t++twiddleBytes :: ByteString -> CBORGen Term+twiddleBytes t = do+  twiddle <- asks geTwiddle+  if twiddle+    then ($ t) <$> elements [TBytes, TBytesI . LBS.fromStrict]+    else pure $ TBytes t++twiddleMap :: [(Term, Term)] -> CBORGen Term+twiddleMap t = do+  twiddle <- asks geTwiddle+  if twiddle+    then ($ t) <$> elements [TMap, TMapI]+    else pure $ TMap t++genTerm :: CBORGen Term genTerm =   oneof     [ TInt <$> choose (minBound, maxBound)@@ -158,14 +196,10 @@           [ choose (toInteger (maxBound :: Int) + 1, toInteger (maxBound :: Word64))           , choose (negate (toInteger (maxBound :: Word64)), toInteger (minBound :: Int) - 1)           ]-    , TBytes <$> (genNBytes . getNonNegative =<< arbitrary)-    , TBytesI <$> (genNLazyBytes . getNonNegative =<< arbitrary)-    , TString . T.pack <$> arbitrary-    , TStringI . TL.pack <$> arbitrary-    , TList <$> listOf smallerTerm-    , TListI <$> listOf smallerTerm-    , TMap <$> listOf ((,) <$> smallerTerm <*> smallerTerm)-    , TMapI <$> listOf ((,) <$> smallerTerm <*> smallerTerm)+    , twiddleBytes =<< genNBytes . getNonNegative =<< arbitrary+    , twiddleString . T.pack =<< arbitrary+    , twiddleList =<< listOf smallerTerm+    , twiddleMap =<< listOf ((,) <$> smallerTerm <*> smallerTerm)     , TTagged <$> choose (6, maxBound :: Word64) <*> smallerTerm     , TBool <$> arbitrary     , pure TNull@@ -175,7 +209,7 @@     , TDouble <$> arbitrary     ]   where-    smallerTerm :: AntiGen Term+    smallerTerm :: CBORGen Term     smallerTerm = scale (`div` 5) genTerm  genNBytes :: MonadGen m => Int -> m ByteString@@ -186,9 +220,6 @@   numElems <- choose (0, sz)   genNBytes numElems -genNLazyBytes :: MonadGen m => Int -> m BSL.ByteString-genNLazyBytes n = BSL.fromStrict <$> genNBytes n- genCharAtMostBytes :: MonadGen m => Int -> m Char genCharAtMostBytes 1 = chr <$> choose (0x00, 0x7F) genCharAtMostBytes 2 = chr <$> choose (0x00, 0x7FF)@@ -212,20 +243,41 @@ genText = sized $ \sz -> genNBytesText =<< choose (0, sz)  -- | Primitive types defined by the CDDL specification, with their generators-genPostlude :: PTerm -> AntiGen Term-genPostlude pt = case pt of-  PTBool -> TBool <$> arbitrary-  PTUInt -> TInteger <$> antiNonNegative-  PTNInt -> TInteger <$> antiNonPositive-  PTInt -> TInteger . fromIntegral <$> choose (minBound :: Int, maxBound)-  PTHalf -> THalf <$> choose (-65504, 65504)-  PTFloat -> TFloat <$> arbitrary-  PTDouble -> TDouble <$> arbitrary-  PTBytes -> TBytes <$> genBytes-  PTText -> TString <$> genText-  PTAny -> genTerm-  PTNil -> pure TNull-  PTUndefined -> pure $ TSimple 23+genPostlude :: PTerm -> CBORGen Term+genPostlude pt = genPTerm =<< liftAntiGen (faultyPTerm pt)+  where+    genExcluding ls =+      elements (filter (`notElem` ls) [minBound .. maxBound])+    nonPInteger p =+      pure p |! genExcluding [PTUInt, PTNInt, PTInt, PTAny]+    -- \| Introduces a decision point that can change the type of the CBOR term that is generated+    faultyPTerm t = reweigh 0.1 $+      case t of+        PTAny -> pure PTAny+        p@PTUInt -> nonPInteger p+        p@PTNInt -> nonPInteger p+        p@PTInt -> nonPInteger p+        p -> pure p |! genExcluding [PTAny, p]+    genUInt = choose (0, uintMax)+    genNInt = choose (nintMin, -1)+    genAboveUInt = choose (uintMax + 1, 2 * uintMax)+    genBelowNInt = choose (2 * nintMin, nintMin - 1)+    -- \| Given a CDDL postlude type, generates a value of that type+    genPTerm = \case+      PTBool -> TBool <$> arbitrary+      -- For integers, introduce decision points that sometimes generate+      -- out-of-bounds values+      PTUInt -> TInteger <$> liftAntiGen (genUInt |! oneof [genNInt, genBelowNInt, genAboveUInt])+      PTNInt -> TInteger <$> liftAntiGen (genNInt |! oneof [genUInt, genBelowNInt, genAboveUInt])+      PTInt -> TInteger <$> choose (nintMin, uintMax)+      PTHalf -> THalf <$> genHalf+      PTFloat -> TFloat <$> arbitrary+      PTDouble -> TDouble <$> arbitrary+      PTBytes -> twiddleBytes =<< genBytes+      PTText -> twiddleString =<< genText+      PTAny -> genTerm+      PTNil -> pure TNull+      PTUndefined -> pure $ TSimple 23  -------------------------------------------------------------------------------- -- Kinds of terms@@ -247,13 +299,6 @@ singleTermList (P _ y : xs) = (y :) <$> singleTermList xs singleTermList _ = Nothing --- | Convert a list of wrapped terms to a list of pairs of terms, or fail if any--- 'SingleTerm's are present.-pairTermList :: [WrappedTerm] -> Maybe [(Term, Term)]-pairTermList [] = Just []-pairTermList (P x y : xs) = ((x, y) :) <$> pairTermList xs-pairTermList _ = Nothing- showSimple :: CTree GenPhase -> String showSimple = show . mapIndex @_ @_ @GenSimple @@ -265,55 +310,161 @@ -- Generator functions -------------------------------------------------------------------------------- -genSized :: HasCallStack => Word64 -> CTree i -> AntiGen WrappedTerm-genSized s target = do-  case target of-    CTree.Postlude PTText -> S . TString <$> genNBytesText (fromIntegral s)-    CTree.Postlude PTBytes -> S . TBytes <$> genNBytes (fromIntegral s)-    CTree.Postlude PTUInt -> S . TInteger <$> choose (0, 256 ^ s - 1)-    _ -> error "Cannot apply size operator to target "+genSized :: HasCallStack => Word64 -> CTree GenPhase -> CBORGen WrappedTerm+genSized s target = annotateTerm target $ case target of+  CTree.Postlude PTText -> fmap S . twiddleString =<< genNBytesText (fromIntegral s)+  CTree.Postlude PTBytes -> fmap S . twiddleBytes =<< genNBytes (fromIntegral s)+  CTree.Postlude PTUInt -> S . TInteger <$> choose (0, 256 ^ s - 1)+  _ -> error "Cannot apply size operator to target "  range :: Enum a => RangeBound -> a -> a -> (a, a) range ClOpen x y = (x, pred y) range Closed x y = (x, y) -genBetween :: (Integral a, Random a) => (a, a) -> AntiGen a-genBetween rng = do+genBetween :: (Integral a, Random a) => (a, a) -> CBORGen a+genBetween rng = liftAntiGen $ do   size <- liftGen getSize   let     sizeBounds :: Integral a => (a, a)     sizeBounds = (0, fromIntegral size)   antiChoose rng sizeBounds -genForCTree ::-  HasCallStack => CTreeRoot GenPhase -> CTree GenPhase -> AntiGen WrappedTerm-genForCTree _ (CTree.Literal v) = pure . S $ valueToTerm v-genForCTree _ (CTree.Postlude pt) = S <$> genPostlude pt-genForCTree cddl (CTree.Map nodes) = do-  items <- pairTermList . flattenWrappedList <$> traverse (genForCTree cddl) nodes-  case items of-    Just ts ->+annotate :: Text -> CBORGen b -> CBORGen b+annotate ann = withAntiGen $ withAnnotation ann++-- | Annotate a generator with the label derived from a CTree node.+-- Control nodes are transparent — the annotation comes from the target inside.+annotateTerm :: CTree GenPhase -> CBORGen a -> CBORGen a+annotateTerm = \case+  CTree.Literal _ -> annotate "literal"+  CTree.Postlude pt -> annotate $ renderStrict . layoutCompact $ pretty pt+  CTree.Map _ -> annotate "map"+  CTree.Array _ -> annotate "array"+  CTree.Choice _ -> id+  CTree.Group _ -> annotate "group"+  CTree.KV {} -> annotate "kv"+  CTree.Occur {} -> id+  CTree.Range {} -> annotate "range"+  CTree.Control {} -> id+  CTree.Enum _ -> annotate "enum"+  CTree.Unwrap _ -> annotate "unwrap"+  CTree.Tag {} -> annotate "tag"+  CTree.CTreeE (GenRef n) -> annotate (unName n)+  CTree.CTreeE (GenGenerator _ _) -> annotate "custom_generator"++genForCTree :: HasCallStack => CTree GenPhase -> CBORGen WrappedTerm+genForCTree node = annotateTerm node $ case node of+  CTree.Literal v -> S <$> valueToTerm v+  CTree.Postlude pt -> S <$> genPostlude pt+  CTree.Map nodes -> genMap nodes+  CTree.Array nodes -> genArray nodes+  CTree.Choice (NE.toList -> nodes) -> do+    ix <- choose (0, length nodes - 1)+    genForCTree $ nodes !! ix+  CTree.Group nodes -> G <$> traverse genForCTree nodes+  CTree.KV key value _cut -> genKV key value+  CTree.Occur item occurs ->+    applyOccurenceIndicator occurs (genForCTree item)+  CTree.Range from to bounds -> genRange from to bounds+  CTree.Control op target controller -> genControl op target controller+  CTree.Enum tree -> genEnum tree+  CTree.Unwrap n -> genForCTree n+  CTree.Tag t n -> genTag t n+  CTree.CTreeE (GenRef n) -> genForNode n+  CTree.CTreeE (GenGenerator gen _) -> gen++-- | Generate a map from a list of nodes+genMap ::+  HasCallStack => [CTree GenPhase] -> CBORGen WrappedTerm+genMap nodes = do+  let+    elemsNeeded KV {} = 1+    elemsNeeded (Occur _ OIOneOrMore) = 1+    elemsNeeded (Occur _ (OIBounded (Just lo) _)) = lo+    elemsNeeded _ = 0++    tryGenKV ::+      Int -> Map.Map Term a -> CTree GenPhase -> CTree GenPhase -> CBORGen (Maybe (Term, Term))+    tryGenKV nTries m kNode vNode = go nTries+      where+        unS (S x) = x+        unS x = error $ "Expected single, got " <> show x+        go !n+          | n > 0 = do+              k <- unS <$> scale (`div` 2) (withAntiGen (withAnnotation "key") $ genForCTree kNode)+              if Map.notMember k m+                then do+                  v <- unS <$> scale (`div` 2) (withAntiGen (withAnnotation "value") $ genForCTree vNode)+                  pure $ Just (k, v)+                else go (n - 1)+          | otherwise = pure Nothing++    genNodes :: Int -> Map.Map Term Term -> [CTree GenPhase] -> CBORGen (Maybe (Map.Map Term Term))+    genNodes _ m [] = pure $ Just m+    genNodes !i !m (n : ns) =       let-        -- De-duplicate keys in the map.-        -- Per RFC7049:-        -- >> A map that has duplicate keys may be well-formed, but it is not-        -- >> valid, and thus it causes indeterminate decoding-        tsNodup = nubOrdOn fst ts+        ann = withAntiGen (withAnnotation (T.pack $ show i))+        cont x y = scale (\s -> max 0 (s - 1)) $ genNodes (i + 1) x y+        optGenKV kNode vNode = sized $ \sz ->+          frequency [(100, pure Nothing), (max 0 sz, ann $ tryGenKV 10 m kNode vNode)]        in-        pure . S $ TMap tsNodup-    Nothing -> error "Single terms in map context"-genForCTree cddl (CTree.Array nodes) = do-  items <- singleTermList . flattenWrappedList <$> traverse (genForCTree cddl) nodes+        case n of+          KV kNode vNode _ -> do+            mKV <- ann $ tryGenKV 100 m kNode vNode+            case mKV of+              Just (k, v) -> cont (Map.insert k v m) ns+              Nothing -> pure Nothing+          Occur kv@(KV kNode vNode _) oi -> case oi of+            OIOptional -> do+              mt <- optGenKV kNode vNode+              case mt of+                Just (k, v) -> cont (Map.insert k v m) ns+                Nothing -> cont m ns+            OIZeroOrMore -> do+              mt <- optGenKV kNode vNode+              case mt of+                Just (k, v) -> cont (Map.insert k v m) (n : ns)+                Nothing -> cont m ns+            OIOneOrMore -> genNodes i m (kv : Occur kv OIZeroOrMore : ns)+            OIBounded mlb mub -> do+              let+                clampedPred 0 = 0+                clampedPred x = x - 1+                newLow = clampedPred <$> mlb+                newHigh = clampedPred <$> mub+                res+                  | maybe False (> 0) mlb = genNodes i m (kv : Occur kv (OIBounded newLow newHigh) : ns)+                  | maybe False (< 1) mub = genNodes i m ns+                  | otherwise = do+                      mt <- optGenKV kNode vNode+                      case mt of+                        Just (k, v) -> cont (Map.insert k v m) (Occur kv (OIBounded newLow newHigh) : ns)+                        Nothing -> genNodes i m ns+              res+          node -> error $ "Unexpected node: " <> showSimple node+  mItems <- genNodes 0 Map.empty $ sortOn (Down . elemsNeeded) nodes+  case mItems of+    Just items -> S <$> twiddleMap (Map.toList items)+    Nothing -> error "Failed to generate unique keys for map after max retries"++-- | Generate an array from a list of nodes+genArray :: HasCallStack => [CTree GenPhase] -> CBORGen WrappedTerm+genArray nodes = do+  items <-+    singleTermList . flattenWrappedList+      <$> zipWithM+        (\i node -> withAntiGen (withAnnotation (T.pack $ show i)) $ genForCTree node)+        [0 :: Int ..]+        nodes   case items of-    Just ts -> pure . S $ TList ts+    Just ts -> S <$> twiddleList ts     Nothing -> error "Something weird happened which shouldn't be possible"-genForCTree cddl (CTree.Choice (NE.toList -> nodes)) = do-  ix <- choose (0, length nodes - 1)-  genForCTree cddl $ nodes !! ix-genForCTree cddl (CTree.Group nodes) = G <$> traverse (genForCTree cddl) nodes-genForCTree cddl (CTree.KV key value _cut) = do-  kg <- genForCTree cddl key-  vg <- genForCTree cddl value++-- | Generate a key-value pair+genKV :: HasCallStack => CTree GenPhase -> CTree GenPhase -> CBORGen WrappedTerm+genKV key value = do+  kg <- withAntiGen (withAnnotation "key") $ genForCTree key+  vg <- withAntiGen (withAnnotation "value") $ genForCTree value   case (kg, vg) of     (S k, S v) -> pure $ P k v     _ ->@@ -322,11 +473,17 @@           <> showSimple key           <> " => "           <> showSimple value-genForCTree cddl (CTree.Occur item occurs) =-  applyOccurenceIndicator occurs (genForCTree cddl item)-genForCTree cddl (CTree.Range from to bounds) = do-  term1 <- dropNegativeGen $ genForCTree cddl from-  term2 <- dropNegativeGen $ genForCTree cddl to++-- | Generate a value from a range+genRange ::+  HasCallStack =>+  CTree GenPhase ->+  CTree GenPhase ->+  RangeBound ->+  CBORGen WrappedTerm+genRange from to bounds = do+  term1 <- withAntiGen dropNegativeGen $ genForCTree from+  term2 <- withAntiGen dropNegativeGen $ genForCTree to   case (term1, term2) of     (S (TInt a), S (TInt b))       | a <= b -> genBetween (range bounds a b) <&> S . TInt@@ -342,9 +499,17 @@     (S (TDouble a), S (TDouble b))       | a <= b -> choose (range bounds a b) <&> S . TDouble     (a, b) -> error $ "invalid range (a = " <> show a <> ", b = " <> show b <> ")"-genForCTree cddl (CTree.Control op target controller) = do++-- | Generate a value with a control operator applied+genControl ::+  HasCallStack =>+  CtlOp.CtlOp ->+  CTree GenPhase ->+  CTree GenPhase ->+  CBORGen WrappedTerm+genControl op target controller = annotateTerm target $ do   resolvedController <- case controller of-    CTreeE (GenRef n) -> dropNegativeGen $ resolveRef cddl n+    CTreeE (GenRef n) -> withAntiGen dropNegativeGen $ resolveRef n     x -> pure x   case (op, resolvedController) of     (CtlOp.Le, CTree.Literal (Value (VUInt n) _)) -> case target of@@ -356,12 +521,12 @@       _ -> error "Cannot apply lt operator to target"     (CtlOp.Lt, _) -> error $ "Invalid controller for .lt operator: " <> showSimple controller     (CtlOp.Size, CTree.Literal (Value (VUInt s) _)) -> do-      s' <- pure s |! sized (\sz -> choose (0, fromIntegral sz) `suchThat` (/= s))+      s' <- liftAntiGen $ pure s |! sized (\sz -> choose (0, fromIntegral sz) `suchThat` (/= s))       genSized s' target-    (CtlOp.Size, CTree.Range {CTree.from, CTree.to}) -> do+    (CtlOp.Size, CTree.Range {CTree.from, CTree.to}) ->       case (from, to) of         (CTree.Literal (Value (VUInt f) _), CTree.Literal (Value (VUInt t) _)) -> do-          s <- sized $ \sz ->+          s <- liftAntiGen $ sized $ \sz ->             antiChoose               (fromIntegral f, fromIntegral t)               (0, max (succ t) $ fromIntegral sz)@@ -375,37 +540,45 @@         "Invalid controller for .size operator: "           <> showSimple controller     (CtlOp.Cbor, _) -> do-      enc <- genForCTree cddl controller+      enc <- genForCTree controller       case enc of-        S x -> pure . S . TBytes . CBOR.toStrictByteString $ CBOR.encodeTerm x+        S x -> fmap S . twiddleBytes $ CBOR.toStrictByteString (CBOR.encodeTerm x)         _ -> error "Controller does not correspond to a single term"     (c, _) -> error $ "Controller not yet implemented: " <> show c-genForCTree cddl (CTree.Enum tree) = do-  case tree of-    CTree.Group trees -> do-      ix <- choose (0, length trees - 1)-      genForCTree cddl $ trees !! ix-    _ -> error "Attempt to form an enum from something other than a group"-genForCTree cddl (CTree.Unwrap node) = genForCTree cddl node-genForCTree cddl (CTree.Tag t node) = do-  tag <- faultyNum t-  enc <- genForCTree cddl node++-- | Generate a value from an enum+genEnum :: HasCallStack => CTree GenPhase -> CBORGen WrappedTerm+genEnum tree = case tree of+  CTree.Group trees -> do+    ix <- choose (0, length trees - 1)+    genForCTree $ trees !! ix+  _ -> error "Attempt to form an enum from something other than a group"++-- | Generate a tagged value+genTag :: HasCallStack => Word64 -> CTree GenPhase -> CBORGen WrappedTerm+genTag t node = do+  tag <- liftAntiGen $ faultyNum t+  enc <- case tag of+    n+      | n == 2 || n == 3 ->+          -- TODO remove this once `cborg` can decode indefinite bytes in bignums+          withTwiddle False $ genForCTree node+    _ -> genForCTree node   case enc of     S x -> pure $ S $ TTagged tag x     _ -> error "Tag controller does not correspond to a single term"-genForCTree cddl (CTree.CTreeE (GenRef n)) = genForNode cddl n-genForCTree cddl (CTree.CTreeE (GenGenerator (CBORGenerator gen) _)) = gen cddl -genForNode :: HasCallStack => CTreeRoot GenPhase -> Name -> AntiGen WrappedTerm-genForNode cddl = genForCTree cddl <=< resolveRef cddl+genForNode :: HasCallStack => Name -> CBORGen WrappedTerm+genForNode = genForCTree <=< resolveRef  -- | Take a reference and resolve it to the relevant Tree, following multiple -- links if necessary.-resolveRef :: MonadGen m => CTreeRoot GenPhase -> Name -> m (CTree GenPhase)-resolveRef (CTreeRoot cddl) n = do+resolveRef :: Name -> CBORGen (CTree GenPhase)+resolveRef n = do+  mRule <- lookupCddl n   -- Since we follow a reference, we decrease the 'size' of the Gen monad.   scale (\x -> max 0 $ x - 1) $-    case Map.lookup n cddl of+    case mRule of       Nothing -> error $ "Unbound reference: " <> show n       Just val -> pure val @@ -418,11 +591,12 @@ -- single CBOR term (e.g. if the name resolves to a group, which cannot be -- generated outside a context). generateFromName :: HasCallStack => CTreeRoot GenPhase -> Name -> AntiGen Term-generateFromName root@(CTreeRoot cddl) n = do-  case Map.lookup n cddl of+generateFromName root@(CTreeRoot cddlMap) n = do+  let env = GenEnv {geRoot = root, geTwiddle = True}+  case Map.lookup n cddlMap of     Nothing -> error $ "Unbound reference: " <> show n     Just val ->-      genForCTree root val >>= \case+      runCBORGen env (genForCTree val) >>= \case         S x -> pure x         _ ->           error $@@ -446,8 +620,8 @@ -- | Apply an occurence indicator to a group entry applyOccurenceIndicator ::   OccurrenceIndicator ->-  AntiGen WrappedTerm ->-  AntiGen WrappedTerm+  CBORGen WrappedTerm ->+  CBORGen WrappedTerm applyOccurenceIndicator OIOptional oldGen =   sizeBiasedBool >>= \case     False -> pure $ G mempty@@ -455,7 +629,7 @@ applyOccurenceIndicator OIZeroOrMore oldGen =   G <$> listOfScaled oldGen applyOccurenceIndicator OIOneOrMore oldGen =-  G <$> listOfScaled1 oldGen+  withAntiGen (\g -> G <$> listOfScaled1 g) oldGen applyOccurenceIndicator (OIBounded mlb mub) oldGen =   sized $ \sz -> do     let@@ -464,20 +638,20 @@     i <- fromIntegral <$> genBetween (lb, ub)     G <$> vectorOf i (scale (`div` (i + 1)) oldGen) -valueToTerm :: Value -> Term+valueToTerm :: Value -> CBORGen Term valueToTerm (Value x _) = valueVariantToTerm x -valueVariantToTerm :: ValueVariant -> Term+valueVariantToTerm :: ValueVariant -> CBORGen Term valueVariantToTerm (VUInt i)-  | toInteger i <= toInteger (maxBound :: Int) = TInt (fromIntegral i)-  | otherwise = TInteger (fromIntegral i)+  | toInteger i <= toInteger (maxBound :: Int) = pure $ TInt (fromIntegral i)+  | otherwise = pure $ TInteger (fromIntegral i) valueVariantToTerm (VNInt i)-  | -toInteger i >= toInteger (minBound :: Int) = TInt (-fromIntegral i)-  | otherwise = TInteger (-fromIntegral i)-valueVariantToTerm (VBignum i) = TInteger i-valueVariantToTerm (VFloat16 i) = THalf i-valueVariantToTerm (VFloat32 i) = TFloat i-valueVariantToTerm (VFloat64 i) = TDouble i-valueVariantToTerm (VText t) = TString t-valueVariantToTerm (VBytes b) = TBytes b-valueVariantToTerm (VBool b) = TBool b+  | -toInteger i >= toInteger (minBound :: Int) = pure $ TInt (-fromIntegral i)+  | otherwise = pure $ TInteger (-fromIntegral i)+valueVariantToTerm (VBignum i) = pure $ TInteger i+valueVariantToTerm (VFloat16 i) = pure $ THalf i+valueVariantToTerm (VFloat32 i) = pure $ TFloat i+valueVariantToTerm (VFloat64 i) = pure $ TDouble i+valueVariantToTerm (VText t) = twiddleString t+valueVariantToTerm (VBytes b) = twiddleBytes b+valueVariantToTerm (VBool b) = pure $ TBool b
src/Codec/CBOR/Cuddle/CBOR/Validator.hs view
@@ -11,6 +11,7 @@ import Codec.CBOR.Cuddle.CBOR.Validator.Trace (   ControlInfo (..),   Evidenced (..),+  IsValidationTrace (..),   ListValidationTrace (..),   MapValidationTrace (..),   SValidity (..),@@ -34,8 +35,9 @@ import Data.Bits hiding (And) import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as BSL-import Data.Function ((&))+import Data.Function (on, (&)) import Data.IntSet qualified as IS+import Data.List (maximumBy) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as Map@@ -145,12 +147,14 @@     -- a = any     Postlude PTAny -> terminal rule     -- a = int-    Postlude PTInt -> terminal rule+    Postlude PTInt+      | i >= nintMin && i <= uintMax -> terminal rule     -- a = uint     Postlude PTUInt-      | i >= 0 -> terminal rule+      | i >= 0 && i <= uintMax -> terminal rule     -- a = nint-    Postlude PTNInt | i <= 0 -> terminal rule+    Postlude PTNInt+      | i >= nintMin && i <= -1 -> terminal rule     -- a = x     Literal (Value (VUInt i') _) | i == fromIntegral i' -> terminal rule     -- a = -x@@ -624,7 +628,7 @@ controlBytes cddl bs Cbor ctrl =   case deserialiseFromBytes decodeTerm (BSL.fromStrict bs) of     Right (BSL.null -> True, term) -> isValid $ validateTerm cddl term ctrl-    _ -> error "Not yet implemented"+    _ -> False controlBytes cddl bs Cborseq ctrl =   case deserialiseFromBytes decodeTerm (BSL.fromStrict (BS.snoc (BS.cons 0x9f bs) 0xff)) of     Right (BSL.null -> True, TListI terms) -> isValid $ validateTerm cddl (TList terms) (Array [Occur ctrl OIZeroOrMore])@@ -756,29 +760,43 @@ validateList cddl terms rule =   case rule of     Postlude PTAny -> terminal rule-    Array rules -> mapTrace ListTrace . fst $ validate finalize terms rules+    Array rules -> mapTrace ListTrace . fst $ validate finalize [] terms rules       where-        finalize [] = evidence ListValidationDone-        finalize (x : xs) = evidence . ListValidationLeftoverTerms $ x :| xs+        finalize _ [] = evidence ListValidationDone+        finalize skipped (x : xs) =+          let leftovers = x :| xs+              unwrapOccur (Occur ct _) = ct+              unwrapOccur ct = ct+              attempts =+                [ (mapIndex r, trc)+                | r <- skipped+                , Evidenced SInvalid trc <- [validateTerm cddl x (unwrapOccur r)]+                , measureProgress trc > mempty+                ]+              bestAttempt = case attempts of+                [] -> Nothing+                _ -> Just $ maximumBy (compare `on` (measureProgress . snd)) attempts+           in evidence $ ListValidationLeftoverTerms leftovers bestAttempt     Choice opts -> validateChoice (validateList cddl terms) opts     _ -> unapplicable rule   where     validate ::-      ([Term] -> Evidenced ListValidationTrace) ->+      ([CTree ValidatorStage] -> [Term] -> Evidenced ListValidationTrace) ->+      [CTree ValidatorStage] ->       [Term] ->       [CTree ValidatorStage] ->       (Evidenced ListValidationTrace, [Term])-    validate f tss [] = (f tss, tss)-    validate f [] (r : rs)-      | isOptional r = validate f [] rs-      | otherwise = (evidence $ ListValidationUnappliedRules (mapIndex <$> r :| rs), [])-    validate f tss@(t : ts) (r : rs) =+    validate f skipped tss [] = (f skipped tss, tss)+    validate f skipped [] (r : rs)+      | isOptional r = validate f skipped [] rs+      | otherwise = (evidence . ListValidationUnappliedRule $ mapIndex r, [])+    validate f skipped tss@(t : ts) (r : rs) =       let         consumeTerm ct g = case validateTerm cddl t ct of           Evidenced SValid trc -> first (mapTrace $ ListValidationConsume (mapIndex r) trc) $ g ts           Evidenced SInvalid trc -> (evidence $ ListValidationMissingRequired (mapIndex ct) trc, tss) -        consumeGroup ns gp g = case validate (const $ evidence ListValidationDone) tss gp of+        consumeGroup ns gp g = case validate (\_ _ -> evidence ListValidationDone) [] tss gp of           (Evidenced SValid trc, leftover) -> first (mapTrace $ ListValidationConsumeGroup (reverse ns) trc) $ g leftover           (Evidenced SInvalid trc, leftover) -> (evidence $ ListValidationBadGroup (reverse ns) trc, leftover) @@ -792,9 +810,9 @@         consume (Group gp) g = consumeGroup [] gp g         consume (KV _ v _) g = consume v g         consume ct g = consumeTerm ct g-        continue l = validate f l rs-        skipRule = validate f tss rs-        rewriteRule newRule l = validate f l (newRule : rs)+        continue l = validate f skipped l rs+        skipRule = validate f (r : skipped) tss rs+        rewriteRule newRule l = validate f skipped l (newRule : rs)         validateRule =           \case             Occur ct oi -> case oi of@@ -833,8 +851,24 @@   where     validate ::       [CTree ValidatorStage] -> [(Term, Term)] -> [CTree ValidatorStage] -> Evidenced MapValidationTrace-    validate [] [] [] = evidence MapValidationDone-    validate _ kvs [] = evidence $ MapValidationLeftoverKVs kvs+    validate _ [] [] = evidence MapValidationDone+    validate exhausted (kv : _) [] =+      let+        unwrapOccur (Occur ct _) = ct+        unwrapOccur ct = ct+        attempts =+          [ (mapIndex r, MapValidationInvalidValue (mapIndex r) kTrc vTrc)+          | r <- exhausted+          , KV k v _ <- [unwrapOccur r]+          , Evidenced SValid kTrc <- [validateTerm cddl (fst kv) k]+          , Evidenced SInvalid vTrc <- [validateTerm cddl (snd kv) v]+          , measureProgress (MapValidationInvalidValue (mapIndex r) kTrc vTrc) > mempty+          ]+        bestAttempt = case attempts of+          [] -> Nothing+          _ -> Just $ maximumBy (compare `on` (measureProgress . snd)) attempts+       in+        evidence $ MapValidationLeftoverKVs kv bestAttempt     validate [] [] rs =       case NE.nonEmpty $ filter (not . isOptional) rs of         Nothing -> evidence MapValidationDone
src/Codec/CBOR/Cuddle/CBOR/Validator/Trace.hs view
@@ -19,11 +19,12 @@   ListValidationTrace (..),   MapValidationTrace (..),   Evidenced (..),+  IsValidationTrace (..),   ControlInfo (..),   TraceOptions (..),+  Progress (..),   defaultTraceOptions,   showSimple,-  traceValidity,   isValid,   prettyValidationTrace,   showValidationTrace,@@ -184,16 +185,22 @@  data ListValidationTrace (v :: Validity) where   ListValidationDone :: ListValidationTrace IsValid-  ListValidationLeftoverTerms :: NonEmpty Term -> ListValidationTrace IsInvalid-  ListValidationUnappliedRules ::-    NonEmpty (CTree ValidatorStageSimple) -> ListValidationTrace IsInvalid+  ListValidationLeftoverTerms ::+    NonEmpty Term ->+    Maybe (CTree ValidatorStageSimple, ValidationTrace IsInvalid) ->+    ListValidationTrace IsInvalid+  ListValidationUnappliedRule ::+    CTree ValidatorStageSimple ->+    ListValidationTrace IsInvalid   ListValidationConsume ::     CTree ValidatorStageSimple ->     ValidationTrace IsValid ->     ListValidationTrace v ->     ListValidationTrace v   ListValidationMissingRequired ::-    CTree ValidatorStageSimple -> ValidationTrace IsInvalid -> ListValidationTrace IsInvalid+    CTree ValidatorStageSimple ->+    ValidationTrace IsInvalid ->+    ListValidationTrace IsInvalid   ListValidationConsumeGroup ::     [Name] ->     ListValidationTrace IsValid ->@@ -208,7 +215,10 @@  data MapValidationTrace (v :: Validity) where   MapValidationDone :: MapValidationTrace IsValid-  MapValidationLeftoverKVs :: [(Term, Term)] -> MapValidationTrace IsInvalid+  MapValidationLeftoverKVs ::+    (Term, Term) ->+    Maybe (CTree ValidatorStageSimple, MapValidationTrace IsInvalid) ->+    MapValidationTrace IsInvalid   MapValidationUnappliedRules :: NonEmpty (CTree ValidatorStageSimple) -> MapValidationTrace IsInvalid   MapValidationInvalidValue ::     CTree ValidatorStageSimple ->@@ -250,9 +260,24 @@       GT -> x       _ -> y +data Progress = Progress {termProgress :: !Int, refProgress :: !Int}+  deriving (Eq, Ord)++instance Semigroup Progress where+  Progress t1 r1 <> Progress t2 r2 = Progress (t1 + t2) (r1 + r2)++instance Monoid Progress where+  mempty = Progress 0 0++increaseTermProgress :: Progress -> Progress+increaseTermProgress p = p <> Progress 1 0++increaseRefProgress :: Progress -> Progress+increaseRefProgress p = p <> Progress 0 1+ class IsValidationTrace (t :: Validity -> Type) where   traceValidity :: t v -> SValidity v-  measureProgress :: t v -> Int+  measureProgress :: t v -> Progress  instance IsValidationTrace ValidationTrace where   traceValidity = \case@@ -269,51 +294,55 @@     TagTrace _ x -> traceValidity x    measureProgress = \case-    TerminalRule {} -> 1-    CustomSuccess -> 1-    ChoiceBranch {} -> 1-    UnapplicableRule {} -> 0-    CustomFailure {} -> 0-    UnsatisfiedControl {} -> 0-    InvalidTag {} -> 0-    ReferenceRule _ x -> succ $ measureProgress x+    TerminalRule {} -> Progress 1 0+    CustomSuccess -> Progress 1 0+    ChoiceBranch _ x -> measureProgress x+    UnapplicableRule {} -> mempty+    CustomFailure {} -> mempty+    UnsatisfiedControl {} -> mempty+    InvalidTag {} -> mempty+    ReferenceRule _ x -> increaseRefProgress $ measureProgress x     ListTrace x -> measureProgress x     MapTrace x -> measureProgress x-    TagTrace _ x -> succ $ measureProgress x+    TagTrace _ x -> increaseTermProgress $ measureProgress x  instance IsValidationTrace ListValidationTrace where   traceValidity = \case     ListValidationDone -> SValid     ListValidationLeftoverTerms {} -> SInvalid-    ListValidationUnappliedRules {} -> SInvalid+    ListValidationUnappliedRule {} -> SInvalid     ListValidationMissingRequired {} -> SInvalid     ListValidationBadGroup _ _ -> SInvalid     ListValidationConsume _ _ x -> traceValidity x     ListValidationConsumeGroup _ _ x -> traceValidity x    measureProgress = \case-    ListValidationDone -> 10-    ListValidationConsume _ _ x -> succ $ measureProgress x-    ListValidationLeftoverTerms _ -> 0-    ListValidationMissingRequired _ _ -> 0-    ListValidationUnappliedRules _ -> 0-    ListValidationConsumeGroup _ g x -> measureProgress g + measureProgress x-    ListValidationBadGroup _ x -> measureProgress x+    ListValidationDone -> Progress 1 0+    ListValidationConsume _ _ x -> increaseTermProgress $ measureProgress x+    ListValidationLeftoverTerms _ Nothing -> mempty+    ListValidationLeftoverTerms _ (Just (_, trc)) -> measureProgress trc+    ListValidationMissingRequired _ _ -> mempty+    ListValidationUnappliedRule _ -> mempty+    ListValidationConsumeGroup rs g x -> Progress 0 (length rs) <> measureProgress g <> measureProgress x+    ListValidationBadGroup rs x -> Progress 0 (length rs) <> measureProgress x  instance IsValidationTrace MapValidationTrace where   traceValidity = \case     MapValidationDone -> SValid-    MapValidationLeftoverKVs _ -> SInvalid+    MapValidationLeftoverKVs _ _ -> SInvalid     MapValidationUnappliedRules _ -> SInvalid     MapValidationInvalidValue {} -> SInvalid     MapValidationConsume _ _ _ x -> traceValidity x    measureProgress = \case-    MapValidationDone -> 10-    MapValidationLeftoverKVs _ -> 0-    MapValidationUnappliedRules _ -> 0-    MapValidationConsume _ _ _ x -> 3 + measureProgress x-    MapValidationInvalidValue {} -> 2+    MapValidationDone -> Progress 1 0+    MapValidationLeftoverKVs _ Nothing -> mempty+    MapValidationLeftoverKVs _ (Just (_, trc)) -> measureProgress trc+    MapValidationUnappliedRules _ -> mempty+    MapValidationConsume _ kTrc vTrc x ->+      measureProgress kTrc <> measureProgress vTrc <> measureProgress x+    MapValidationInvalidValue _ kTrc vTrc ->+      measureProgress kTrc <> measureProgress vTrc  evidence :: (Show (t v), IsValidationTrace t) => t v -> Evidenced t evidence x = Evidenced (traceValidity x) x@@ -345,8 +374,21 @@ prettyListValidationResult :: TraceOptions -> ListValidationTrace v -> Doc AnsiStyle prettyListValidationResult opts@TraceOptions {..} = \case   ListValidationDone -> mempty-  ListValidationLeftoverTerms _ -> annotate (color Red) "leftover elements after all rules have been applied"-  ListValidationUnappliedRules _ -> annotate (color Red) "not all required rules have been applied"+  ListValidationLeftoverTerms _ Nothing ->+    annotate (color Red) "leftover elements not matched by any rule"+  ListValidationLeftoverTerms _ (Just (rule, trc)) ->+    hang 2 $+      vsep+        [ annotate (color Red) "leftover elements, closest matching rule:"+        , annotate (color Green) $ pretty rule+        , nestContainer $ prettyValidationTrace opts trc+        ]+  ListValidationUnappliedRule r ->+    hang 2 $+      vsep+        [ annotate (color Red) "required rule not satisfied:"+        , pretty r+        ]   ListValidationConsume _ t c     | toFoldValid -> foldValid 1 c     | otherwise -> vsep $ continue c [prettyValidationTrace opts t]@@ -383,7 +425,15 @@ prettyMapValidationResult :: TraceOptions -> MapValidationTrace v -> Doc AnsiStyle prettyMapValidationResult opts@TraceOptions {..} = \case   MapValidationDone -> mempty-  MapValidationLeftoverKVs _ -> annotate (color Red) "no more rules left to apply"+  MapValidationLeftoverKVs _ Nothing ->+    annotate (color Red) "leftover key-value pairs not matched by any rule"+  MapValidationLeftoverKVs _ (Just (rule, trc)) ->+    hang 2 $+      vsep+        [ annotate (color Red) "leftover key-value pairs, closest matching rule:"+        , annotate (color Green) $ pretty rule+        , nestContainer $ prettyMapValidationResult opts trc+        ]   MapValidationUnappliedRules rs ->     hang 2 $       vsep
src/Codec/CBOR/Cuddle/CDDL/CBORGenerator.hs view
@@ -2,7 +2,8 @@ {-# LANGUAGE TypeFamilies #-}  module Codec.CBOR.Cuddle.CDDL.CBORGenerator (-  CBORGenerator (..),+  CBORGen (..),+  GenEnv (..),   HasGenerator (..),   WrappedTerm (..),   CBORValidator (..),@@ -10,21 +11,63 @@   GenPhase,   XXCTree (..),   CustomValidatorResult (..),+  liftAntiGen,+  runCBORGen,+  lookupCddl,+  withAntiGen,+  withTwiddle, ) where  import Codec.CBOR.Cuddle.CDDL (Name)-import Codec.CBOR.Cuddle.CDDL.CTree (CTree, CTreeRoot, XXCTree)+import Codec.CBOR.Cuddle.CDDL.CTree (CTree, CTreeRoot (..), XXCTree) import Codec.CBOR.Term (Term)+import Control.Monad.Reader (MonadReader (..), ReaderT (..), asks, mapReaderT)+import Data.Map.Strict qualified as Map import Data.Text (Text) import GHC.Generics (Generic) import Optics.Core (Lens') import Test.AntiGen (AntiGen)+import Test.QuickCheck.GenT (MonadGen (..))  type data GenPhase +data GenEnv = GenEnv+  { geRoot :: CTreeRoot GenPhase+  , geTwiddle :: Bool+  }+  deriving (Generic)++newtype CBORGen a = CBORGen (ReaderT GenEnv AntiGen a)+  deriving (Functor, Applicative, Monad, MonadReader GenEnv)++instance MonadGen CBORGen where+  liftGen g = CBORGen $ ReaderT $ \_ -> liftGen g+  variant n (CBORGen m) = CBORGen $ mapReaderT (variant n) m+  sized f = CBORGen . ReaderT $ \env -> sized $ \n ->+    let CBORGen m = f n in runReaderT m env+  resize n (CBORGen m) = CBORGen $ mapReaderT (resize n) m+  choose rng = CBORGen . ReaderT $ \_ -> choose rng++liftAntiGen :: AntiGen a -> CBORGen a+liftAntiGen m = CBORGen . ReaderT $ const m++runCBORGen :: GenEnv -> CBORGen a -> AntiGen a+runCBORGen env (CBORGen m) = runReaderT m env++lookupCddl :: Name -> CBORGen (Maybe (CTree GenPhase))+lookupCddl n = do+  CTreeRoot root <- asks geRoot+  pure $ Map.lookup n root++withAntiGen :: (AntiGen a -> AntiGen b) -> CBORGen a -> CBORGen b+withAntiGen f (CBORGen m) = CBORGen $ ReaderT $ \env -> f (runReaderT m env)++withTwiddle :: Bool -> CBORGen a -> CBORGen a+withTwiddle t = local (\x -> x {geTwiddle = t})+ data instance XXCTree GenPhase   = GenRef Name-  | GenGenerator CBORGenerator (CTree GenPhase)+  | GenGenerator (CBORGen WrappedTerm) (CTree GenPhase)  data WrappedTerm   = -- | Single term@@ -33,12 +76,10 @@     P Term Term   | -- | Group term     G [WrappedTerm]-  deriving (Eq, Show)--newtype CBORGenerator = CBORGenerator (CTreeRoot GenPhase -> AntiGen WrappedTerm)+  deriving (Eq, Ord, Show)  class HasGenerator a where-  generatorL :: Lens' a (Maybe CBORGenerator)+  generatorL :: Lens' a (Maybe (CBORGen WrappedTerm))  class HasValidator a where   validatorL :: Lens' a (Maybe CBORValidator)
src/Codec/CBOR/Cuddle/CDDL/CTree.hs view
@@ -13,6 +13,8 @@ import Data.Map.Strict qualified as Map import Data.Word (Word64) import GHC.Generics (Generic)+import Generic.Random (genericArbitraryU)+import Test.QuickCheck (Arbitrary (..))  -------------------------------------------------------------------------------- @@ -137,6 +139,17 @@   | PTAny   | PTNil   | PTUndefined-  deriving (Eq, Generic, Ord, Show)+  deriving (Eq, Generic, Ord, Show, Enum, Bounded) +instance Arbitrary PTerm where+  arbitrary = genericArbitraryU+ instance Hashable PTerm++-- Bounds++uintMax :: Integer+uintMax = 2 ^ (64 :: Int) - 1++nintMin :: Integer+nintMin = -(2 ^ (64 :: Int))
src/Codec/CBOR/Cuddle/CDDL/CTreePhase.hs view
@@ -12,7 +12,7 @@ ) where  import Codec.CBOR.Cuddle.CDDL (XCddl, XRule, XTerm, XXTopLevel, XXType2)-import Codec.CBOR.Cuddle.CDDL.CBORGenerator (CBORGenerator, CBORValidator)+import Codec.CBOR.Cuddle.CDDL.CBORGenerator (CBORGen, CBORValidator, WrappedTerm) import Data.Default.Class (Default) import Data.Hashable (Hashable) import GHC.Generics (Generic)@@ -29,7 +29,7 @@ data instance XCddl CTreePhase = CTreeXCddl   deriving (Generic, Show, Eq, Ord) -data instance XRule CTreePhase = CTreeXRule (Maybe CBORGenerator) (Maybe CBORValidator)+data instance XRule CTreePhase = CTreeXRule (Maybe (CBORGen WrappedTerm)) (Maybe CBORValidator)   deriving (Generic)  data instance XXType2 CTreePhase
src/Codec/CBOR/Cuddle/CDDL/Resolve.hs view
@@ -66,9 +66,10 @@ import Data.List (foldl') #endif import Codec.CBOR.Cuddle.CDDL.CBORGenerator (-  CBORGenerator,+  CBORGen,   CBORValidator,   GenPhase,+  WrappedTerm,   XXCTree (..),  ) import Codec.CBOR.Cuddle.CDDL.CTreePhase (CTreePhase, XRule (..))@@ -96,7 +97,7 @@  data CDDLMapEntry = CDDLMapEntry   { cmeProvidedParameters :: ProvidedParameters (TypeOrGroup CTreePhase)-  , cmeCustomGenerator :: Maybe CBORGenerator+  , cmeCustomGenerator :: Maybe (CBORGen WrappedTerm)   , cmeCustomValidator :: Maybe CBORValidator   }   deriving (Generic)@@ -158,7 +159,7 @@ data instance XXCTree OrReferenced   = -- | Reference to another node with possible generic arguments supplied     OrRef Name [CTree OrReferenced]-  | OGenerator CBORGenerator (CTree OrReferenced)+  | OGenerator (CBORGen WrappedTerm) (CTree OrReferenced)   | OValidator CBORValidator (CTree OrReferenced)  type data OrReferencedSimple@@ -393,7 +394,7 @@  data instance XXCTree DistReferenced   = DRef (DistRef DistReferenced)-  | DGenerator CBORGenerator (CTree DistReferenced)+  | DGenerator (CBORGen WrappedTerm) (CTree DistReferenced)   | DValidator CBORValidator (CTree DistReferenced)  type data DistReferencedNoGen@@ -448,7 +449,7 @@  data instance XXCTree MonoReferenced   = MRuleRef Name-  | MGenerator CBORGenerator (CTree MonoReferenced)+  | MGenerator (CBORGen WrappedTerm) (CTree MonoReferenced)   | MValidator CBORValidator (CTree MonoReferenced)  type MonoEnv = BindingEnv DistReferenced MonoReferenced
src/Codec/CBOR/Cuddle/Huddle.hs view
@@ -91,8 +91,9 @@   callToDef,    -- * Generators-  withGenerator,   withAntiGen,+  withGenerator,+  withCBORGen,    -- * Validators   withValidator,@@ -111,9 +112,10 @@ import Codec.CBOR.Cuddle.CDDL (CDDL, GenericParameter (..), HasName (..), Name (..), XRule) import Codec.CBOR.Cuddle.CDDL qualified as C import Codec.CBOR.Cuddle.CDDL.CBORGenerator (-  CBORGenerator (..),+  CBORGen (..),   CBORValidator (..),   CustomValidatorResult,+  GenEnv (..),   GenPhase,   HasGenerator (..),   HasValidator (..),@@ -125,6 +127,7 @@ import Codec.CBOR.Cuddle.Comments qualified as C import Codec.CBOR.Term (Term) import Control.Monad (when)+import Control.Monad.Reader (ReaderT (..)) import Control.Monad.State (MonadState (get), State, execState, modify) import Data.ByteString (ByteString) import Data.ByteString.Base16 qualified as Base16@@ -146,7 +149,7 @@ import Optics.Core (lens, view, (%), (%~), (&)) import Optics.Core qualified as L import Test.AntiGen (AntiGen)-import Test.QuickCheck (Gen)+import Test.QuickCheck.Gen (Gen) import Test.QuickCheck.GenT (MonadGen (liftGen)) import Prelude hiding ((/)) @@ -160,7 +163,7 @@  data instance C.XRule HuddleStage = HuddleXRule   { hxrComment :: C.Comment-  , hxrGenerator :: Maybe CBORGenerator+  , hxrGenerator :: Maybe (CBORGen WrappedTerm)   , hxrValidator :: Maybe CBORValidator   }   deriving (Generic)@@ -1377,13 +1380,20 @@ -- | Use a custom `QuickCheck` generator to generate the term. Will override -- the generator passed via `withAntiGen` withGenerator :: HasGenerator a => (CTreeRoot GenPhase -> Gen WrappedTerm) -> a -> a-withGenerator f = L.set generatorL (Just . CBORGenerator $ liftGen . f)+withGenerator f = L.set generatorL (Just . CBORGen . ReaderT $ \GenEnv {geRoot} -> liftGen $ f geRoot)+{-# DEPRECATED withGenerator "Use withCBORGen instead" #-}  -- | Use a custom `AntiGen` generator to generate the term. Will override -- the custom generator passed via `withGenerator`. The advantage of using -- `AntiGen` generator is that it can also be used to generate negative examples. withAntiGen :: HasGenerator a => (CTreeRoot GenPhase -> AntiGen WrappedTerm) -> a -> a-withAntiGen f = L.set generatorL (Just $ CBORGenerator f)+withAntiGen f = L.set generatorL (Just . CBORGen . ReaderT $ \GenEnv {geRoot} -> f geRoot)+{-# DEPRECATED withAntiGen "Use withCBORGen instead" #-}++-- | Use a custom `CBORGen` generator to generate the term. Will override+-- the custom generator passed via `withGenerator`.+withCBORGen :: HasGenerator a => CBORGen WrappedTerm -> a -> a+withCBORGen gen = L.set generatorL (Just gen)  withValidator :: HasValidator a => (Term -> CustomValidatorResult) -> a -> a withValidator p = L.set validatorL . Just $ CBORValidator p
test/Test/Codec/CBOR/Cuddle/CDDL/Examples/Huddle.hs view
@@ -6,8 +6,8 @@   huddleArray,   huddleMap,   huddleRangeMap,-  simpleRule,-  simpleTermExample,+  customGenRule,+  customGenExample,   refTermExample,   bytesExample,   opCertExample,@@ -17,6 +17,15 @@   rangeMapExample,   optionalMapExample,   choicesExample,+  cborControlExample,+  listTooShortExample,+  listSkippedRuleExample,+  listSkippedRuleNestedExample,+  mapLeftoverKVExample,+  mapNoMatchingKeyExample,+  listZeroOrMoreExample,+  mapNestedValueExample,+  deeplyNestedRefExample, ) where  import Codec.CBOR.Cuddle.CDDL (Name)@@ -34,8 +43,7 @@   idx,   mp,   opt,-  sized,-  withGenerator,+  withCBORGen,   (...),   (=:=),   (==>),@@ -43,7 +51,7 @@ import Codec.CBOR.Cuddle.Huddle qualified as H import Codec.CBOR.Term qualified as C import Data.Word (Word64)-import Test.QuickCheck.Gen (choose)+import Test.QuickCheck.GenT (MonadGen (..))  huddleRangeArray :: Huddle huddleRangeArray =@@ -95,18 +103,21 @@     ]  simpleRule :: Name -> Rule-simpleRule n = withGenerator (\_ -> S . C.TInt <$> choose (4, 6)) $ n =:= arr [1, 2, 3]+simpleRule n = n =:= arr [1, 2, 3] -simpleTermExample :: Huddle-simpleTermExample =+customGenRule :: Name -> Rule+customGenRule = withCBORGen (S . C.TInt <$> choose (4, 6)) . simpleRule++customGenExample :: Huddle+customGenExample =   collectFrom-    [ HIRule $ simpleRule "root"+    [ HIRule $ customGenRule "root"     ]  refTermExample :: Huddle refTermExample =   collectFrom-    [ HIRule $ "root" =:= arr [0, a $ simpleRule "bar"]+    [ HIRule $ "root" =:= arr [0, a $ customGenRule "bar"]     ]  bytesExample :: Huddle@@ -121,22 +132,22 @@     [ HIRule $         "root"           =:= arr-            [ a (VBytes `sized` (32 :: Word64))+            [ a (VBytes `H.sized` (32 :: Word64))             , a VUInt             , a VUInt-            , a (VBytes `sized` (64 :: Word64))+            , a (VBytes `H.sized` (64 :: Word64))             ]     ]  sizeTextExample :: Huddle sizeTextExample =   collectFrom-    [HIRule $ "root" =:= VText `sized` (0 :: Word64, 32 :: Word64)]+    [HIRule $ "root" =:= VText `H.sized` (0 :: Word64, 32 :: Word64)]  sizeBytesExample :: Huddle sizeBytesExample =   collectFrom-    [HIRule $ "root" =:= VBytes `sized` (0 :: Word64, 32 :: Word64)]+    [HIRule $ "root" =:= VBytes `H.sized` (0 :: Word64, 32 :: Word64)]  rangeListExample :: Huddle rangeListExample =@@ -164,7 +175,7 @@     [ HIRule $         "root"           =:= mp-            [ 10 <+ asKey ((0 :: Integer) ... (10 :: Integer)) ==> VBool+            [ 10 <+ asKey ((1 :: Integer) ... (10 :: Integer)) ==> VBool             ]     ] @@ -177,3 +188,120 @@           H./ arr [1, a VBool, 6]           H./ arr [1, a VText]     ]++cborControlExample :: Huddle+cborControlExample =+  collectFrom+    [ HIRule $+        "root"+          =:= VBytes+          `H.cbor` simpleRule "simpleRule"+    ]++listTooShortExample :: Huddle+listTooShortExample =+  collectFrom+    [ HIRule $+        "root"+          =:= arr+            [ a VInt+            , a VText+            , a VBool+            ]+    ]++listSkippedRuleExample :: Huddle+listSkippedRuleExample =+  let bar = "bar" =:= VText+   in collectFrom+        [ HIRule $+            "root"+              =:= arr+                [ a VInt+                , opt $ a bar+                ]+        , HIRule bar+        ]++listSkippedRuleNestedExample :: Huddle+listSkippedRuleNestedExample =+  let baz = "baz" =:= arr [a VInt, a VText]+      qux = "qux" =:= VText+      quux = "quux" =:= VInt+   in collectFrom+        [ HIRule $+            "root"+              =:= arr+                [ a VInt+                , opt $ a qux+                , opt $ a baz+                , opt $ a quux+                ]+        , HIRule baz+        , HIRule qux+        , HIRule quux+        ]++mapLeftoverKVExample :: Huddle+mapLeftoverKVExample =+  collectFrom+    [ HIRule $+        "root"+          =:= mp+            [ opt $ idx 1 ==> VText+            , opt $ idx 2 ==> VInt+            , opt $ idx 3 ==> VBool+            ]+    ]++mapNoMatchingKeyExample :: Huddle+mapNoMatchingKeyExample =+  collectFrom+    [ HIRule $+        "root"+          =:= mp+            [ opt $ idx 1 ==> VText+            , opt $ idx 2 ==> VInt+            ]+    ]++listZeroOrMoreExample :: Huddle+listZeroOrMoreExample =+  collectFrom+    [ HIRule $+        "root"+          =:= arr+            [ 0 <+ a VInt+            , a VText+            ]+    ]++mapNestedValueExample :: Huddle+mapNestedValueExample =+  let bar = "bar" =:= arr [a VInt, a VText]+   in collectFrom+        [ HIRule $+            "root"+              =:= mp+                [ opt $ idx 1 ==> bar+                ]+        , HIRule bar+        ]++-- | A rule with deeply nested references competing against a rule that makes+-- actual term progress. Used to test that stacked references don't incorrectly+-- beat rules with real progress.+deeplyNestedRefExample :: Huddle+deeplyNestedRefExample =+  collectFrom+    [ HIRule $ "root" =:= arr [a VInt, opt $ a aRef, opt $ a listRule]+    , HIRule aRef+    , HIRule barRef+    , HIRule bazRef+    , HIRule listRule+    ]+  where+    bazRef = "baz" =:= VText+    barRef = "bar" =:= bazRef+    aRef = "a_ref" =:= barRef+    listRule = "list_rule" =:= arr [a VInt, a VInt]
test/Test/Codec/CBOR/Cuddle/CDDL/GeneratorSpec.hs view
@@ -16,21 +16,22 @@ import Codec.CBOR.Term (Term (..), decodeTerm, encodeTerm) import Codec.CBOR.Write (toStrictByteString) import Data.ByteString.Lazy qualified as LBS+import Data.Text qualified as T import Data.Text.Lazy qualified as TL-import Test.AntiGen (runAntiGen, zapAntiGen)+import Test.AntiGen (ZapResult (..), prettyZapResult, runAntiGen, zapAntiGenResult) import Test.Codec.CBOR.Cuddle.CDDL.Examples.Huddle (   bytesExample,+  customGenExample,   opCertExample,   optionalMapExample,   rangeListExample,   rangeMapExample,   refTermExample,-  simpleTermExample,   sizeBytesExample,   sizeTextExample,  )-import Test.Codec.CBOR.Cuddle.CDDL.Validator (expectInvalid)-import Test.Hspec (HasCallStack, Spec, describe, runIO, shouldBe, shouldSatisfy)+import Test.Codec.CBOR.Cuddle.CDDL.Validator (expectInvalid, genAndValidateRule)+import Test.Hspec (HasCallStack, Spec, describe, runIO, shouldSatisfy, xdescribe) import Test.Hspec.Core.Spec (SpecM) import Test.Hspec.QuickCheck (prop) import Test.QuickCheck (Gen, Property, Testable (..), counterexample)@@ -47,13 +48,18 @@  expectZapInvalidates :: CTreeRoot MonoReferenced -> Name -> Property expectZapInvalidates cddl name = property $ do-  value <- zapAntiGen 1 $ generateFromName (mapIndex cddl) name+  res@ZapResult {zrValue} <- zapAntiGenResult 1 $ generateFromName (mapIndex cddl) name   let-    bs = toStrictByteString $ encodeTerm value+    bs = toStrictByteString $ encodeTerm zrValue     validationRes = validateCBOR bs name $ mapIndex cddl-    failMsg = case deserialiseFromBytes decodeTerm (LBS.fromStrict bs) of-      Right (_, t) -> prettyHexEnc (encodeTerm t)-      Left _ -> mempty+    failMsg =+      unlines+        [ case deserialiseFromBytes decodeTerm (LBS.fromStrict bs) of+            Right (_, t) -> prettyHexEnc (encodeTerm t)+            Left _ -> mempty+        , mempty+        , T.unpack $ prettyZapResult res+        ]   pure . counterexample failMsg $ expectInvalid validationRes  zapInvalidatesHuddle :: String -> Huddle -> Spec@@ -62,24 +68,43 @@   prop n . counterexample (TL.unpack . pShow $ mapIndex @_ @_ @MonoSimple cddl) $     expectZapInvalidates cddl "root" +-- | Test that generated values are valid for a Huddle schema+genAndValidateHuddle :: String -> Huddle -> Spec+genAndValidateHuddle n huddle = do+  cddl <- tryResolveHuddle huddle+  genAndValidateRule n "root" cddl+ spec :: Spec spec = do+  describe "Positive generator" $ do+    describe "Generated value validates" $ do+      -- Note: simpleTermExample and refTermExample use custom generators+      -- that intentionally produce type-mismatched values, so they're excluded+      genAndValidateHuddle "opCert" opCertExample+      genAndValidateHuddle "sizeText" sizeTextExample+      genAndValidateHuddle "sizeBytes" sizeBytesExample+      genAndValidateHuddle "rangeList" rangeListExample+      genAndValidateHuddle "rangeMap" rangeMapExample+      xdescribe "Generator cannot reliably produce unique keys for maps" $ do+        genAndValidateHuddle "optionalMapExample" optionalMapExample+   describe "Negative generator" $ do     describe "Zapped value fails to validate" $ do-      zapInvalidatesHuddle "simpleTerm" simpleTermExample+      zapInvalidatesHuddle "customGen" customGenExample       zapInvalidatesHuddle "refTerm" refTermExample       zapInvalidatesHuddle "opCert" opCertExample       zapInvalidatesHuddle "sizeText" sizeTextExample       zapInvalidatesHuddle "sizeBytes" sizeBytesExample       zapInvalidatesHuddle "rangeList" rangeListExample       zapInvalidatesHuddle "rangeMap" rangeMapExample-      zapInvalidatesHuddle "optionalMapExample" optionalMapExample+      xdescribe "Generator cannot reliably produce unique keys for maps" $ do+        zapInvalidatesHuddle "optionalMapExample" optionalMapExample    describe "Custom generators" $ do     describe "Huddle" $ do-      simpleTermExampleCddl <- tryResolveHuddle simpleTermExample+      customGenExampleCddl <- tryResolveHuddle customGenExample       prop "If a term has a custom generator then it is used" $ do-        res <- generateCDDL $ mapIndex simpleTermExampleCddl+        res <- generateCDDL $ mapIndex customGenExampleCddl         pure $           res `shouldSatisfy` \case             TInt i -> i > 3 && i < 7@@ -90,8 +115,13 @@         pure $           res `shouldSatisfy` \case             TList [TInt 0, TInt i] -> i > 3 && i < 7+            TListI [TInt 0, TInt i] -> i > 3 && i < 7             _ -> False       bytesExampleCddl <- tryResolveHuddle bytesExample       prop "Bytes are generated correctly" $ do         res <- generateCDDL $ mapIndex bytesExampleCddl-        pure $ res `shouldBe` TBytes "\x01\x02\x03\xff"+        pure $+          res `shouldSatisfy` \case+            TBytes "\x01\x02\x03\xff" -> True+            TBytesI "\x01\x02\x03\xff" -> True+            _ -> False
test/Test/Codec/CBOR/Cuddle/CDDL/Validator.hs view
@@ -6,6 +6,8 @@   spec,   expectValid,   expectInvalid,+  genAndValidateCddl,+  genAndValidateRule, ) where  import Codec.CBOR.Cuddle.CBOR.Gen (generateFromName)@@ -24,7 +26,7 @@ import Codec.CBOR.Cuddle.CDDL.CTree (CTreeRoot (..)) import Codec.CBOR.Cuddle.CDDL.CTree qualified as CTree import Codec.CBOR.Cuddle.CDDL.Postlude (appendPostlude)-import Codec.CBOR.Cuddle.CDDL.Resolve (fullResolveCDDL)+import Codec.CBOR.Cuddle.CDDL.Resolve (MonoReferenced, fullResolveCDDL) import Codec.CBOR.Cuddle.Huddle (   Huddle,   HuddleItem (..),@@ -69,6 +71,7 @@   Spec,   describe,   expectationFailure,+  it,   runIO,  ) import Test.Hspec.QuickCheck@@ -90,6 +93,31 @@  ) import Text.Megaparsec (runParser) +-- | Test that a specific rule in a resolved CDDL generates valid values+genAndValidateRule :: String -> Name -> CTreeRoot MonoReferenced -> Spec+genAndValidateRule description name resolvedCddl =+  prop description $ do+    cborTerm <- runAntiGen $ generateFromName (mapIndex resolvedCddl) name+    let+      generatedCbor = toStrictByteString $ encodeTerm cborTerm+      res = validateCBOR generatedCbor name (mapIndex resolvedCddl)+      extraInfo =+        unlines+          [ "CBOR term:"+          , prettyHexEnc $ encodeTerm cborTerm+          ]+    pure . counterexample extraInfo $ expectValid res++-- | Test that all rules in a resolved CDDL generate valid values+genAndValidateCddl :: String -> CTreeRoot MonoReferenced -> Spec+genAndValidateCddl description resolvedCddl@(CTreeRoot m) = do+  let+    isRule CTree.Group {} = False+    isRule _ = True+  describe description $ do+    forM_ (Map.keys $ Map.filter isRule m) $ \name@(Name n) ->+      genAndValidateRule (T.unpack n) name resolvedCddl+ genAndValidateFromFile :: FilePath -> Spec genAndValidateFromFile path = do   contents <- runIO $ T.readFile =<< getDataFileName path@@ -97,24 +125,10 @@     cddl = fromRight (error "Failed to parse CDDL") $ runParser pCDDL path contents     resolverError x =       error $ "Failed to resolve the CDDL from file " <> show path <> ":\n" <> show x-    resolvedCddl@(CTreeRoot m) =+    resolvedCddl =       either resolverError id . fullResolveCDDL . appendPostlude $         mapCDDLDropExt cddl-    isRule CTree.Group {} = False-    isRule _ = True-  describe path $ do-    forM_ (Map.keys $ Map.filter isRule m) $ \name@(Name n) ->-      prop (T.unpack n) $ do-        cborTerm <- runAntiGen $ generateFromName (mapIndex resolvedCddl) name-        let-          generatedCbor = toStrictByteString $ encodeTerm cborTerm-          res = validateCBOR generatedCbor name (mapIndex resolvedCddl)-          extraInfo =-            unlines-              [ "CBOR term:"-              , prettyHexEnc $ encodeTerm cborTerm-              ]-        pure . counterexample extraInfo $ expectValid res+  genAndValidateCddl path resolvedCddl  genInfiniteUniqueList :: Ord a => Gen a -> Gen [a] genInfiniteUniqueList = fmap nubOrd . infiniteListOf@@ -307,6 +321,29 @@         prop "Fails to validate map with too many range elements"           . forAll (genHuddleRangeMap (11, 20))           $ expectInvalid . validateHuddle huddleRangeMap "a"++    describe "Bignums" $ do+      let+        bignum = 2 ^ (64 :: Int) :: Integer+        bignumCBOR = toStrictByteString . encodeTerm $ TInteger bignum+        resolveCDDL cddlText =+          let cddl = fromRight (error "Failed to parse CDDL") $ runParser pCDDL "" cddlText+           in either (error . show) id . fullResolveCDDL . appendPostlude $ mapCDDLDropExt cddl+      it "Validates a bignum >= 2^64 against biguint" $+        expectValid $+          validateCBOR bignumCBOR "a" (mapIndex $ resolveCDDL "a = biguint")+      it "Validates a bignum >= 2^64 against bigint" $+        expectValid $+          validateCBOR bignumCBOR "a" (mapIndex $ resolveCDDL "a = bigint")+      it "Validates a bignum >= 2^64 against integer" $+        expectValid $+          validateCBOR bignumCBOR "a" (mapIndex $ resolveCDDL "a = integer")+      it "Rejects a bignum >= 2^64 against int" $+        expectInvalid $+          validateCBOR bignumCBOR "a" (mapIndex $ resolveCDDL "a = int")+      it "Rejects a bignum >= 2^64 against uint" $+        expectInvalid $+          validateCBOR bignumCBOR "a" (mapIndex $ resolveCDDL "a = uint")    describe "Custom validator" $ do     describe "Positive" $ do
test/Test/Codec/CBOR/Cuddle/CDDL/Validator/Golden.hs view
@@ -14,6 +14,7 @@ import Codec.CBOR.Cuddle.Huddle (Huddle, toCDDL) import Codec.CBOR.Cuddle.IndexMappable (mapCDDLDropExt, mapIndex) import Codec.CBOR.Term (Term (..), encodeTerm)+import Codec.CBOR.Write (toStrictByteString) import Codec.CBOR.Write qualified as CBOR import Control.Monad ((<=<)) import Data.Either (fromRight)@@ -24,13 +25,25 @@ import Prettyprinter.Render.Terminal qualified as Ansi import System.FilePath ((</>)) import Test.Codec.CBOR.Cuddle.CDDL.Examples.Huddle (+  cborControlExample,   choicesExample,+  deeplyNestedRefExample,   huddleRangeArray,+  listSkippedRuleExample,+  listSkippedRuleNestedExample,+  listTooShortExample,+  listZeroOrMoreExample,+  mapLeftoverKVExample,+  mapNestedValueExample,+  mapNoMatchingKeyExample,   refTermExample,  ) import Test.Hspec (Spec, describe, it) import Test.Hspec.Golden (Golden (..)) +listTooShortTerm :: Term+listTooShortTerm = TList [TInt 42]+ huddleRangeArrayTermTwoStrings :: Term huddleRangeArrayTermTwoStrings =   TList@@ -60,6 +73,27 @@     , TInt 1     ] +listSkippedRuleTerm :: Term+listSkippedRuleTerm = TList [TInt 1, TInt 2]++listSkippedRuleNestedTerm :: Term+listSkippedRuleNestedTerm = TList [TInt 1, TList [TInt 2, TInt 3]]++mapLeftoverKVTerm :: Term+mapLeftoverKVTerm = TMap [(TInt 2, TString "hello")]++mapNoMatchingKeyTerm :: Term+mapNoMatchingKeyTerm = TMap [(TInt 99, TString "hello")]++listZeroOrMoreTerm :: Term+listZeroOrMoreTerm = TList [TInt 1, TInt 2, TString "hello", TBool True]++mapNestedValueTerm :: Term+mapNestedValueTerm = TMap [(TInt 1, TList [TInt 2, TInt 3])]++cborControlBad :: Term+cborControlBad = TBytes . toStrictByteString $ encodeTerm (TList [TInt 1, TInt 2, TInt 4])+ validatorPrettyGolden :: String -> Huddle -> Name -> Term -> Spec validatorPrettyGolden testName huddle n term =   it testName $@@ -102,3 +136,50 @@       choicesExample       "root"       choiceAlmostSecond+    validatorPrettyGolden+      "cborControlBad"+      cborControlExample+      "root"+      cborControlBad+    validatorPrettyGolden+      "listTooShort"+      listTooShortExample+      "root"+      listTooShortTerm+    validatorPrettyGolden+      "listSkippedRule"+      listSkippedRuleExample+      "root"+      listSkippedRuleTerm+    validatorPrettyGolden+      "listSkippedRuleNested"+      listSkippedRuleNestedExample+      "root"+      listSkippedRuleNestedTerm+    validatorPrettyGolden+      "mapLeftoverKV"+      mapLeftoverKVExample+      "root"+      mapLeftoverKVTerm+    validatorPrettyGolden+      "mapNoMatchingKey"+      mapNoMatchingKeyExample+      "root"+      mapNoMatchingKeyTerm+    validatorPrettyGolden+      "listZeroOrMore"+      listZeroOrMoreExample+      "root"+      listZeroOrMoreTerm+    validatorPrettyGolden+      "mapNestedValue"+      mapNestedValueExample+      "root"+      mapNestedValueTerm+    -- Deeply nested refs competing against a rule that makes actual term+    -- progress. The stacked refs should NOT win over real term progress.+    validatorPrettyGolden+      "deeplyNestedRefVsRealProgress"+      deeplyNestedRefExample+      "root"+      (TList [TInt 1, TList [TInt 1, TString "hello"]])