cuddle-1.8.1.0: test/Test/Codec/CBOR/Cuddle/CDDL/Validator.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module Test.Codec.CBOR.Cuddle.CDDL.Validator (
spec,
expectValid,
expectInvalid,
genAndValidateCddl,
genAndValidateRule,
validateCBOR_,
) where
import Codec.CBOR.Cuddle.CBOR.Canonical (toCanonical)
import Codec.CBOR.Cuddle.CBOR.Gen (generateFromName)
import Codec.CBOR.Cuddle.CBOR.Validator (
ValidatorPhase,
validateCBOR,
)
import Codec.CBOR.Cuddle.CBOR.Validator.Trace (
Evidenced (..),
SValidity (..),
ValidationTrace,
defaultTraceOptions,
prettyValidationTrace,
)
import Codec.CBOR.Cuddle.CDDL (Name (..))
import Codec.CBOR.Cuddle.CDDL.CTree (CTreeRoot (..))
import Codec.CBOR.Cuddle.CDDL.CTree qualified as CTree
import Codec.CBOR.Cuddle.CDDL.Custom.Core (RuleTerm (..))
import Codec.CBOR.Cuddle.CDDL.Custom.Generator (GenConfig (..), runCBORGen)
import Codec.CBOR.Cuddle.CDDL.Custom.Validator (TermValidator)
import Codec.CBOR.Cuddle.CDDL.Postlude (appendPostlude)
import Codec.CBOR.Cuddle.CDDL.Resolve (MonoReferenced, fullResolveCDDL)
import Codec.CBOR.Cuddle.Huddle (
CanQuantify (..),
GroupDef,
Huddle,
HuddleItem (..),
Value (..),
a,
arr,
collectFrom,
int,
opt,
toCDDL,
withValidator,
(=:=),
(=:~),
)
import Codec.CBOR.Cuddle.Huddle qualified as H
import Codec.CBOR.Cuddle.IndexMappable (mapCDDLDropExt, mapIndex)
import Codec.CBOR.Cuddle.Parser (pCDDL)
import Codec.CBOR.Pretty (prettyHexEnc)
import Codec.CBOR.Term (Term (..), encodeTerm)
import Codec.CBOR.Write (toStrictByteString)
import Control.Monad (forM_)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as LBS
import Data.Containers.ListUtils (nubOrdOn)
import Data.Either (fromRight)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Data.Text.Lazy qualified as LT
import Paths_cuddle (getDataFileName)
import Prettyprinter (defaultLayoutOptions, layoutPretty)
import Prettyprinter.Render.Terminal (renderStrict)
import Test.AntiGen (runAntiGen)
import Test.Codec.CBOR.Cuddle.CDDL.Examples.Huddle (
huddleArray,
huddleMap,
huddleRangeArray,
huddleRangeMap,
)
import Test.Hspec (
Expectation,
HasCallStack,
Spec,
describe,
expectationFailure,
it,
runIO,
)
import Test.Hspec.QuickCheck
import Test.QuickCheck (
Arbitrary (..),
Gen,
NonNegative (..),
choose,
counterexample,
elements,
forAll,
infiniteListOf,
listOf,
listOf1,
oneof,
scale,
shuffle,
vectorOf,
)
import Text.Megaparsec (runParser)
validateCBOR_ ::
HasCallStack =>
BS.ByteString ->
Name ->
CTreeRoot ValidatorPhase ->
Evidenced ValidationTrace
validateCBOR_ bs n cddl = either (\e -> error $ "validateCBOR failed:\n" <> show e) id $ validateCBOR bs n cddl
-- | 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
let
genCfg =
GenConfig
{ gcRoot = mapIndex resolvedCddl
, gcTwiddle = True
}
cborTerm <- runAntiGen . runCBORGen genCfg $ generateFromName 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
let
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 =
either resolverError id . fullResolveCDDL . appendPostlude $
mapCDDLDropExt cddl
genAndValidateCddl path resolvedCddl
genInfiniteUniqueListOn :: Ord b => (a -> b) -> Gen a -> Gen [a]
genInfiniteUniqueListOn f = fmap (nubOrdOn f) . infiniteListOf
genHuddleRangeMap :: (Int, Int) -> Gen Term
genHuddleRangeMap rng@(lo, hi) = do
n <- choose rng
let genKV = (,) <$> fmap TInt arbitrary <*> fmap TBool arbitrary
genMapTerm . take n
=<< scale (const $ max lo hi) (genInfiniteUniqueListOn (toCanonical . fst) genKV)
genHuddleArrayRequiredTerms :: Gen [Term]
genHuddleArrayRequiredTerms = do
ints <- listOf1 $ TInt <$> arbitrary
text <-
oneof
[ (: []) <$> (genStringTerm . T.pack =<< arbitrary)
, pure []
]
lastInt <- TInt . getNonNegative <$> arbitrary
pure $ ints <> text <> [lastInt]
genHuddleArrayTerms :: Gen [Term]
genHuddleArrayTerms = do
bools <- listOf $ TBool <$> arbitrary
required <- genHuddleArrayRequiredTerms
pure $ bools <> required
genHuddleArray :: Gen Term
genHuddleArray = genHuddleArrayTerms >>= genArrayTerm
genBadArrayReversed :: Gen Term
genBadArrayReversed = genHuddleArrayTerms >>= genArrayTerm . reverse . (TBool True :)
genBadArrayMissingLastInt :: Gen Term
genBadArrayMissingLastInt =
genHuddleArrayTerms >>= genArrayTerm . reverse . dropWhile isNonNegativeInt . reverse
where
isNonNegativeInt (TInt x) | x >= 0 = True
isNonNegativeInt _ = False
genHuddleRangeArray :: Gen Term
genHuddleRangeArray = do
numInts <- choose (3, 4)
ints <- vectorOf numInts $ TInt <$> arbitrary
numBools <- choose (0, 3)
bools <- vectorOf numBools $ TBool <$> arbitrary
numTexts <- choose (3, 10)
texts <- vectorOf numTexts $ genStringTerm . T.pack =<< arbitrary
genArrayTerm $ ints <> bools <> texts
genArrayTerm :: [Term] -> Gen Term
genArrayTerm xs = elements [TList xs, TListI xs]
genMapTerm :: [(Term, Term)] -> Gen Term
genMapTerm x = elements [TMap x, TMapI x]
genStringTerm :: Text -> Gen Term
genStringTerm x = elements [TString x, TStringI (LT.fromStrict x)]
genBytesTerm :: ByteString -> Gen Term
genBytesTerm x = elements [TBytes x, TBytesI $ LBS.fromStrict x]
arbitraryByteString :: Gen ByteString
arbitraryByteString = BS.pack <$> arbitrary
arbitraryTerm :: Gen Term
arbitraryTerm =
oneof
[ TInt <$> arbitrary
, TInteger <$> arbitrary
, TBytes . BS.pack <$> arbitrary
, TBytesI . LBS.pack <$> arbitrary
, TString . T.pack <$> arbitrary
, TStringI . LT.pack <$> arbitrary
, TList <$> listOf (scale (`div` 2) arbitraryTerm)
, TListI <$> listOf (scale (`div` 2) arbitraryTerm)
, TMap <$> listOf (scale (`div` 2) $ (,) <$> arbitraryTerm <*> arbitraryTerm)
, TMapI <$> listOf (scale (`div` 2) $ (,) <$> arbitraryTerm <*> arbitraryTerm)
, -- TODO properly implement tagged generation
-- , TTagged <$> arbitrary <*> arbitraryTerm
TBool <$> arbitrary
, pure TNull
, pure $ TSimple 23 -- TODO add other values once they are supported by cuddle
, THalf <$> arbitrary
, TFloat <$> arbitrary
, TDouble <$> arbitrary
]
genFullMap :: Gen Term
genFullMap = do
field1 <- do
es <- listOf $ TInt . getNonNegative <$> arbitrary
pure (TInt 1, TList es)
lField2 <-
oneof
[ do
b <- arbitrary
pure [(TInt 2, TBool b)]
, pure []
]
strFields <-
nubOrdOn (toCanonical . fst)
<$> listOf ((,) <$> (genStringTerm . T.pack =<< arbitrary) <*> (TInt <$> arbitrary))
bytesFields <-
nubOrdOn (toCanonical . fst)
<$> listOf1 ((,) <$> (genBytesTerm =<< arbitraryByteString) <*> arbitraryTerm)
allFields <- shuffle $ field1 : lField2 <> strFields <> bytesFields
genMapTerm allFields
genBadMapInvalidIndex :: Gen Term
genBadMapInvalidIndex =
pure $
TMap
[ (TInt 1, TList [])
, (TInt 99, TList [])
, (TBytes "foo", TBytes "bar")
]
validateHuddle ::
HasCallStack =>
Huddle ->
Name ->
Term ->
Evidenced ValidationTrace
validateHuddle huddle name term = do
let
resolvedCddl = case fullResolveCDDL . mapCDDLDropExt $ toCDDL huddle of
Right root -> root
Left err -> error $ show err
bs = toStrictByteString $ encodeTerm term
validateCBOR_ bs name (mapIndex resolvedCddl)
expectValid :: Evidenced ValidationTrace -> Expectation
expectValid (Evidenced SValid _) = pure ()
expectValid (Evidenced SInvalid t) =
expectationFailure . T.unpack $
"Expected a success, got failure:\n"
<> renderStrict (layoutPretty defaultLayoutOptions $ prettyValidationTrace defaultTraceOptions t)
expectInvalid :: Evidenced ValidationTrace -> Expectation
expectInvalid (Evidenced SValid t) =
expectationFailure . T.unpack $
"Expected a failure, but got success:\n"
<> renderStrict (layoutPretty defaultLayoutOptions $ prettyValidationTrace defaultTraceOptions t)
expectInvalid _ = pure ()
stringValidator :: TermValidator
stringValidator (SingleTerm (TString _)) = pure ()
stringValidator (SingleTerm (TStringI _)) = pure ()
stringValidator t = fail $ "Expected a string, got\n" <> show t
bytesValidator :: TermValidator
bytesValidator (SingleTerm (TBytes _)) = pure ()
bytesValidator (SingleTerm (TBytesI _)) = pure ()
bytesValidator t = fail $ "Expected bytes, got\n" <> show t
spec :: Spec
spec = describe "Validator" $ do
describe "Generate and validate from file" $ do
genAndValidateFromFile "cddl/basic_assign.cddl"
genAndValidateFromFile "cddl/conway.cddl"
genAndValidateFromFile "cddl/costmdls_min.cddl"
genAndValidateFromFile "cddl/issue80-min.cddl"
genAndValidateFromFile "cddl/pretty.cddl"
genAndValidateFromFile "cddl/shelley.cddl"
genAndValidateFromFile "cddl/validator.cddl"
describe "Term tests" $ do
describe "Maps and arrays" $ do
describe "Positive" $ do
prop "Validates a full map" . forAll genFullMap $
expectValid . validateHuddle huddleMap "a"
prop "Validates array" . forAll genHuddleArray $
expectValid . validateHuddle huddleArray "a"
prop "Validates map with correct number of range elements"
. forAll (genHuddleRangeMap (5, 10))
$ expectValid . validateHuddle huddleRangeMap "a"
prop "Validates array with ranges" . forAll genHuddleRangeArray $
expectValid . validateHuddle huddleRangeArray "a"
describe "Negative" $ do
prop "Fails to validate a map with an unexpected index"
. forAll genBadMapInvalidIndex
$ expectInvalid . validateHuddle huddleMap "a"
prop "Fails to validate reversed array" . forAll genBadArrayReversed $
expectInvalid . validateHuddle huddleArray "a"
prop "Fails to validate array with missing non-negative int at the end"
. forAll genBadArrayMissingLastInt
$ expectInvalid . validateHuddle huddleArray "a"
prop "Fails to validate map with too few range elements"
. forAll (genHuddleRangeMap (0, 4))
$ expectInvalid . validateHuddle huddleRangeMap "a"
prop "Fails to validate map with too many range elements"
. forAll (genHuddleRangeMap (11, 20))
$ expectInvalid . validateHuddle huddleRangeMap "a"
-- Regression tests showing that optional ('?') validation is broken when
-- the optional element is inside a group (`grp` / `=:~`) that is itself
-- referenced inside an array. When the optional element is absent,
-- `consumeGroup` receives a corrupted leftover-term list (due to `(<>)`
-- on the result tuple accumulating terms from the failed consume path),
-- causing the continuation to see phantom extra terms and report them as
-- `ListValidationLeftoverTerms` instead of succeeding.
describe "Group" $ do
describe "Optional inside a group referenced from an array" $ do
let
-- @
-- root = [innerGrp, int]
-- innerGrp = (int, ? bool)
-- @
-- => [int, ? bool, int]
optionalInGroupHuddle :: Huddle
optionalInGroupHuddle =
let
innerGrp :: GroupDef
innerGrp = "innerGrp" =:~ [a VInt, opt $ a VBool]
in
collectFrom
[ HIRule $ "root" =:= arr [a innerGrp, a VInt]
]
it "Validates when optional entry is present" $
expectValid $
validateHuddle optionalInGroupHuddle "root" (TList [TInt 1, TBool True, TInt 2])
it "Validates when optional entry is absent" $
expectValid $
validateHuddle optionalInGroupHuddle "root" (TList [TInt 1, TInt 2])
describe "ZeroOrMore (*) inside a group referenced from an array" $ do
let
-- @
-- root = [zeroOrMoreGrp, text]
-- zeroOrMoreGrp = (* bool, int)
-- @
-- => [* bool, int, text]
zeroOrMoreInGroupHuddle :: Huddle
zeroOrMoreInGroupHuddle =
let
zeroOrMoreGrp :: GroupDef
zeroOrMoreGrp = "zeroOrMoreGrp" =:~ [0 <+ a VBool, a VInt]
in
collectFrom
[ HIRule $ "root" =:= arr [a zeroOrMoreGrp, a VText]
]
it "Validates when zero-or-more entry has elements" $
expectValid $
validateHuddle
zeroOrMoreInGroupHuddle
"root"
(TList [TBool True, TBool False, TInt 42, TString "hi"])
it "Validates when zero-or-more entry has zero elements" $
expectValid $
validateHuddle zeroOrMoreInGroupHuddle "root" (TList [TInt 42, TString "hi"])
describe "Bounded (n..m) inside a group referenced from an array" $ do
-- @
-- root = [boundedGrp, text]
-- boundedGrp = (0..2 bool, int)
-- @
-- => [0..2 bool, int, text]
let boundedInGroupHuddle :: Huddle
boundedInGroupHuddle =
let boundedGrp :: GroupDef
boundedGrp = "boundedGrp" =:~ [0 <+ a VBool +> 2, a VInt]
in collectFrom
[ HIRule $ "root" =:= arr [a boundedGrp, a VText]
]
it "Validates when bounded entry has elements (within bounds)" $
expectValid $
validateHuddle
boundedInGroupHuddle
"root"
(TList [TBool True, TInt 42, TString "hi"])
it "Validates when bounded entry has zero elements (lower bound is 0)" $
expectValid $
validateHuddle boundedInGroupHuddle "root" (TList [TInt 42, TString "hi"])
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
prop "Validates with custom validator" $ do
term <- genStringTerm . T.pack =<< arbitrary
num <- arbitrary
let
huddle =
collectFrom
[ HIRule . withValidator stringValidator $ "a" =:= int num
]
pure . expectValid $ validateHuddle huddle "a" term
prop "Validates with custom validator behind a reference" $ do
stringTerm <- genStringTerm . T.pack =<< arbitrary
arrTerm <- genArrayTerm [stringTerm]
num <- arbitrary
let
ruleA = withValidator stringValidator $ "a" =:= int num
huddle =
collectFrom
[ HIRule ruleA
, HIRule $ "b" =:= arr [a ruleA]
]
pure . expectValid $ validateHuddle huddle "b" arrTerm
prop "Validates with custom validator in a choice within a group" $ do
bs <- BS.pack <$> listOf1 arbitrary
let
ruleA = withValidator bytesValidator $ "a" =:= VText
huddle =
collectFrom
[ HIRule ruleA
, HIRule $ "b" =:= arr [0, a (ruleA H./ VNil)]
]
arrTerm <- genArrayTerm [TInt 0, TBytes bs]
pure . expectValid $ validateHuddle huddle "b" arrTerm
describe "Negative" $ do
prop "Fails if term is valid against the Huddle, but not the custom validator" $ do
stringTerm <- genStringTerm . T.pack =<< arbitrary
let
huddle =
collectFrom
[ HIRule . withValidator bytesValidator $ "a" =:= VText
]
pure . expectInvalid $ validateHuddle huddle "a" stringTerm
prop "Fails if term is valid against the Huddle, but not the custom validator, behind a reference" $ do
stringTerm <- genStringTerm . T.pack =<< arbitrary
let
ruleA = withValidator bytesValidator $ "a" =:= VText
ruleB = "b" =:= arr [a ruleA]
huddle =
collectFrom
[ HIRule ruleA
, HIRule ruleB
]
pure . expectInvalid $ validateHuddle huddle "a" stringTerm