-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathTestlib.hs
More file actions
163 lines (142 loc) · 6.41 KB
/
Copy pathTestlib.hs
File metadata and controls
163 lines (142 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Test suite utilities for the implementor
module Cardano.SCLS.Testlib (
testAllNS,
-- * Hspec helpers
testNS,
validateType,
-- * properties
propNamespaceEntryConformsToSpec,
propNamespaceEntryIsCanonical,
propTypeIsCanonical,
propNamespaceEntryRoundTrip,
propTypeConformsToSpec,
-- * Debug tools
debugValidateType,
debugEncodeType,
) where
import Cardano.SCLS.CBOR.Canonical (getRawDecoder, getRawEncoding)
import Cardano.SCLS.CBOR.Canonical.Encoder
import Cardano.SCLS.CDDL.Validate
import Cardano.SCLS.NamespaceCodec
import Cardano.SCLS.Versioned
import Codec.CBOR.FlatTerm (fromFlatTerm, toFlatTerm)
import Codec.CBOR.Term (decodeTerm)
import Codec.CBOR.Write (toStrictByteString)
import Codec.CBOR.Cuddle.CBOR.Validator.Trace (Evidenced, ValidationTrace)
import Codec.CBOR.Cuddle.CBOR.Validator.Trace qualified as VT
import Data.ByteString qualified as B
import Data.ByteString.Base16 qualified as Base16
import Data.Either (isRight)
import Data.Proxy
import Data.Text qualified as T
import Data.Typeable
import GHC.TypeLits
import Test.Hspec
import Test.Hspec.Expectations.Contrib (annotate)
import Test.Hspec.QuickCheck
import Test.QuickCheck
type ConstrNS a = (KnownNamespace a, Arbitrary (NamespaceEntry a), Eq (NamespaceEntry a), Show (NamespaceEntry a))
-- | Test all supported NS for conformance with SCLS.
testAllNS ::
( ConstrNS "blocks/v0"
, ConstrNS "utxo/v0"
, ConstrNS "snapshots/go/v0"
, ConstrNS "snapshots/mark/v0"
, ConstrNS "snapshots/set/v0"
, ConstrNS "entities/committee/v0"
, ConstrNS "gov/committee/v0"
, ConstrNS "gov/constitution/v0"
, ConstrNS "gov/pparams/v0"
, ConstrNS "gov/proposals/v0"
, ConstrNS "gov/proposals/roots/v0"
) =>
Spec
testAllNS = describe "scls/conformance" $ do
testNS @"blocks/v0"
testNS @"utxo/v0"
testNS @"snapshots/go/v0"
testNS @"snapshots/mark/v0"
testNS @"snapshots/set/v0"
testNS @"entities/committee/v0"
testNS @"gov/committee/v0"
testNS @"gov/constitution/v0"
testNS @"gov/pparams/v0"
testNS @"gov/proposals/v0"
testNS @"gov/proposals/roots/v0"
-- | Validate concrete type against its definition in CDDL
validateType :: forall ns a. (KnownSymbol ns, ToCanonicalCBOR ns a, Arbitrary a, Show a, Typeable a) => T.Text -> Spec
validateType t = prop ("validate type<" ++ n ++ ">") (propTypeConformsToSpec @ns @a t)
where
n = show (typeRep (Proxy @a))
testNS :: forall ns. (KnownSymbol ns, KnownNamespace ns, Arbitrary (NamespaceEntry ns), Eq (NamespaceEntry ns), Show (NamespaceEntry ns)) => Spec
testNS =
describe nsName $ do
prop "conforms to spec" $
propNamespaceEntryConformsToSpec @ns
prop "canonical with regards to its definition" $
propNamespaceEntryRoundTrip @ns
prop "is canonical" $
propNamespaceEntryIsCanonical @ns
where
nsName = (symbolVal (Proxy @ns))
-- | Each value from the known namespace conforms to its spec
propNamespaceEntryConformsToSpec :: forall ns. (KnownSymbol ns, KnownNamespace ns, Arbitrary (NamespaceEntry ns)) => NamespaceEntry ns -> Bool
propNamespaceEntryConformsToSpec = \a ->
case validateBytesAgainst (toStrictByteString (getRawEncoding $ encodeEntry @ns a)) nsName "record_entry" of
Just res -> VT.isValid res
_ -> False
where
nsName = T.pack (symbolVal (Proxy @ns))
propTypeConformsToSpec :: forall ns a. (KnownSymbol ns, ToCanonicalCBOR ns a) => T.Text -> a -> Bool
propTypeConformsToSpec t = \a ->
case validateBytesAgainst (toStrictByteString $ getRawEncoding (toCanonicalCBOR (Proxy @ns) a)) nsName t of
Just res -> VT.isValid res
_ -> False
where
nsName = T.pack (symbolVal (Proxy @ns))
propNamespaceEntryIsCanonical :: forall ns. (KnownSymbol ns, KnownNamespace ns, Arbitrary (NamespaceEntry ns)) => NamespaceEntry ns -> IO ()
propNamespaceEntryIsCanonical = \a ->
let encodedData = toFlatTerm (getRawEncoding $ encodeEntry @ns a)
in case fromFlatTerm decodeTerm encodedData of
Right decodedAsTerm -> annotate "(b, t) = decode @Term (encode x)" $ do
let encodedTerm = toFlatTerm (getRawEncoding $ toCanonicalCBOR (Proxy @ns) decodedAsTerm)
encodedTerm `shouldBe` encodedData
r -> r `shouldSatisfy` isRight
{- | Namespace entry are not contradictory and can roundtrip: `decode.encode.decode.encode = decode.encode`
We do not require `decode.encode=id` because we do not require input type to be in canonical form.
I.e. if we have types:
```
data V a = NoHash a | WithHash a (Maybe Hash)
```
And it's ok to decode `WithHash a Nothing` to `NoHash a`, `decode.encode=id` property will fail, because
decoding will put the value in it's canonical form.
-}
propNamespaceEntryRoundTrip :: forall ns. (KnownNamespace ns, Arbitrary (NamespaceEntry ns), Eq (NamespaceEntry ns), Show (NamespaceEntry ns)) => NamespaceEntry ns -> IO ()
propNamespaceEntryRoundTrip = \a -> do
case fromFlatTerm (getRawDecoder $ decodeEntry @ns) (toFlatTerm (getRawEncoding $ encodeEntry @ns a)) of
Right (Versioned a') -> annotate "(b, a') = decode (encode a)" $ do
a' `shouldBe` a
case fromFlatTerm (getRawDecoder $ decodeEntry @ns) (toFlatTerm (getRawEncoding $ encodeEntry @ns a')) of
Right (Versioned a'') -> annotate "(b', a'') = decode (encode a')" $ do
a'' `shouldBe` a'
r -> r `shouldSatisfy` isRight
r -> r `shouldSatisfy` isRight
debugValidateType :: forall ns a. (KnownSymbol ns, ToCanonicalCBOR ns a) => T.Text -> a -> Maybe (Evidenced ValidationTrace)
debugValidateType t a = validateBytesAgainst (toStrictByteString $ getRawEncoding (toCanonicalCBOR (Proxy @ns) a)) nsName t
where
nsName = T.pack (symbolVal (Proxy @ns))
-- | Serialize value to CBOR (for usage in debug tools)
debugEncodeType :: forall ns a. (KnownSymbol ns, ToCanonicalCBOR ns a) => a -> B.ByteString
debugEncodeType a = Base16.encode $ toStrictByteString $ getRawEncoding (toCanonicalCBOR (Proxy @ns) a)
propTypeIsCanonical :: forall ns a. (KnownSymbol ns, ToCanonicalCBOR ns a) => a -> IO ()
propTypeIsCanonical = \a ->
let encodedData = toFlatTerm (getRawEncoding $ toCanonicalCBOR (Proxy @ns) a)
in case fromFlatTerm decodeTerm encodedData of
Right decodedAsTerm -> annotate "(b, t) = decode @Term (encode x)" $ do
let encodedTerm = toFlatTerm (getRawEncoding $ toCanonicalCBOR (Proxy @ns) decodedAsTerm)
encodedTerm `shouldBe` encodedData
r -> r `shouldSatisfy` isRight