packages feed

cuddle-1.7.0.0: test/Test/Codec/CBOR/Cuddle/CDDL/GeneratorSpec.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}

module Test.Codec.CBOR.Cuddle.CDDL.GeneratorSpec (spec) where

import Codec.CBOR.Cuddle.CBOR.Gen (GenPhase, generateFromName)
import Codec.CBOR.Cuddle.CBOR.Validator (validateCBOR)
import Codec.CBOR.Cuddle.CDDL (Name)
import Codec.CBOR.Cuddle.CDDL.CTree (CTreeRoot (..))
import Codec.CBOR.Cuddle.CDDL.Custom.Generator (GenConfig (..), runCBORGen)
import Codec.CBOR.Cuddle.CDDL.Resolve (MonoReferenced, MonoSimple, fullResolveCDDL)
import Codec.CBOR.Cuddle.Huddle (Huddle, toCDDL)
import Codec.CBOR.Cuddle.IndexMappable (IndexMappable (..), mapCDDLDropExt)
import Codec.CBOR.Pretty (prettyHexEnc)
import Codec.CBOR.Read (deserialiseFromBytes)
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 (ZapResult (..), prettyZapResult, runAntiGen, zapAntiGenResult)
import Test.Codec.CBOR.Cuddle.CDDL.Examples.Huddle (
  bytesExample,
  customGenExample,
  opCertExample,
  optionalMapExample,
  rangeListExample,
  rangeMapExample,
  refTermExample,
  sizeBytesExample,
  sizeTextExample,
  tagRangeExample,
  taggedUintExample,
 )
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 (..), classify, counterexample)
import Text.Pretty.Simple (pShow)

generateCDDL :: CTreeRoot GenPhase -> Gen Term
generateCDDL cddl = runAntiGen . runCBORGen cfg $ generateFromName "root"
  where
    cfg =
      GenConfig
        { gcRoot = cddl
        , gcTwiddle = True
        }

tryResolveHuddle :: HasCallStack => Huddle -> SpecM () (CTreeRoot MonoReferenced)
tryResolveHuddle huddle = do
  case fullResolveCDDL . mapCDDLDropExt $ toCDDL huddle of
    Right x -> pure x
    Left err -> runIO . fail $ "Failed to resolve CDDL:\n" <> show err

expectZapInvalidates :: CTreeRoot MonoReferenced -> Name -> Property
expectZapInvalidates cddl name = property $ do
  let
    cfg =
      GenConfig
        { gcRoot = mapIndex cddl
        , gcTwiddle = True
        }
  res@ZapResult {zrValue} <- zapAntiGenResult 1 . runCBORGen cfg $ generateFromName name
  let
    bs = toStrictByteString $ encodeTerm zrValue
    validationRes = validateCBOR bs name $ mapIndex cddl
    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
zapInvalidatesHuddle n huddle = do
  cddl <- tryResolveHuddle huddle
  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
      genAndValidateHuddle "tagRange" tagRangeExample
      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 "customGen" customGenExample
      zapInvalidatesHuddle "refTerm" refTermExample
      zapInvalidatesHuddle "opCert" opCertExample
      zapInvalidatesHuddle "sizeText" sizeTextExample
      zapInvalidatesHuddle "sizeBytes" sizeBytesExample
      zapInvalidatesHuddle "rangeList" rangeListExample
      zapInvalidatesHuddle "rangeMap" rangeMapExample
      xdescribe "Generator cannot reliably produce unique keys for maps" $ do
        zapInvalidatesHuddle "optionalMapExample" optionalMapExample

  describe "Custom generators" $ do
    describe "Huddle" $ do
      customGenExampleCddl <- tryResolveHuddle customGenExample
      prop "If a term has a custom generator then it is used" $ do
        res <- generateCDDL $ mapIndex customGenExampleCddl
        pure $
          res `shouldSatisfy` \case
            TInt i -> i > 3 && i < 7
            _ -> False
      refTermExampleCddl <- tryResolveHuddle refTermExample
      prop "Custom generator works when called via reference" $ do
        res <- generateCDDL $ mapIndex refTermExampleCddl
        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 `shouldSatisfy` \case
            TBytes "\x01\x02\x03\xff" -> True
            TBytesI "\x01\x02\x03\xff" -> True
            _ -> False
      tagRangeExampleCddl <- tryResolveHuddle tagRangeExample
      prop "tagRange generator covers edges and middle" $ do
        res <- generateCDDL $ mapIndex tagRangeExampleCddl
        let tags = case res of
              TList xs -> [t | TTagged t _ <- xs]
              TListI xs -> [t | TTagged t _ <- xs]
              _ -> []
            classifyTag t p =
              classify (t == 1280 || t == 1400) "edge tag (1280 or 1400)" $
                classify (t > 1280 && t < 1400) "middle tag (1281..1399)" p
            base =
              counterexample ("tags: " <> show tags) $
                all (\t -> t >= 1280 && t <= 1400) tags
        pure $ foldr classifyTag (property base) tags

  describe "Tagged bytes zapping" $ do
    taggedBytesCddl <- tryResolveHuddle taggedUintExample
    let cfg =
          GenConfig
            { gcRoot = mapIndex taggedBytesCddl
            , gcTwiddle = True
            }
    prop "labels zapped result" $ do
      ZapResult {zrValue} <-
        zapAntiGenResult 1 . runCBORGen cfg $ generateFromName "root"
      let
        isBytes TBytes {} = True
        isBytes TBytesI {} = True
        isBytes _ = False
        tagOmitted = case zrValue of
          TTagged {} -> False
          _ -> True
        tagChanged = case zrValue of
          TTagged t _ -> t /= 42
          _ -> False
        innerValueZapped = case zrValue of
          TTagged 42 inner -> not (isBytes inner)
          _ -> False
      pure
        . classify tagOmitted "tag omitted"
        . classify tagChanged "tag changed"
        . classify innerValueZapped "inner value zapped"
        $ expectInvalid
          (validateCBOR (toStrictByteString $ encodeTerm zrValue) "root" $ mapIndex taggedBytesCddl)