-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathJson.hs
395 lines (349 loc) · 12.5 KB
/
Json.hs
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
-- no head/tail warnings
{-# OPTIONS_GHC -Wno-x-partial #-}
module Test.Booster.Syntax.Json (
-- Tasty wrappers
test_prettyPattern,
test_JsonRoundTrips,
test_Unit_tests_for_json_failure_modes,
test_headerFailures,
-- Hedgehog things
roundTripTests,
showExamples,
writeExamples,
-- generators
genKorePattern,
genMultiKorePattern,
genAllKorePatterns,
between,
upTo,
) where
import Control.Monad (forever, void)
import Data.Bifunctor qualified as Bifunctor
import Data.ByteString.Lazy.Char8 qualified as BS
import Data.Char (isAlpha, isAlphaNum, isPrint)
import Data.Either (isRight)
import Data.List (isInfixOf, isPrefixOf)
import Data.List.NonEmpty qualified as NE
import Data.String (IsString)
import Data.Text (Text)
import Data.Text qualified as T
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((<.>), (</>))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Hedgehog
import Text.Printf (printf)
import Prelude hiding (Left, Right)
import Prelude qualified
import Booster.Syntax.Json
import Kore.Syntax.Json.Types -- for testing and generating test data
genKoreJson :: Gen KorePattern -> Gen KoreJson
genKoreJson = fmap (KoreJson KORE KJ1)
genKorePattern :: Gen KorePattern
genKorePattern =
Gen.recursive
Gen.choice
[ KJEVar <$> genId <*> genSort
, KJSVar <$> (('@' -:) <$> genId) <*> genSort
, KJString <$> genStringLiteral
, KJTop <$> genSort
, KJBottom <$> genSort
, KJDV <$> genSort <*> genStringLiteral
]
[ do
sorts <- between 1 10 genSort
args <- exactly (length sorts - 1) genKorePattern
name <- Gen.element [('\\' -:), id] <*> genId
pure KJApp{name, sorts, args}
, KJNot <$> genSort <*> genKorePattern
, KJAnd <$> genSort <*> between 2 10 genKorePattern
, KJOr <$> genSort <*> between 2 10 genKorePattern
, KJImplies <$> genSort <*> genKorePattern <*> genKorePattern
, KJIff <$> genSort <*> genKorePattern <*> genKorePattern
, KJForall <$> genSort <*> genId <*> genSort <*> genKorePattern
, KJExists <$> genSort <*> genId <*> genSort <*> genKorePattern
, KJMu . ('@' -:) <$> genId <*> genSort <*> genKorePattern
, KJNu . ('@' -:) <$> genId <*> genSort <*> genKorePattern
, KJCeil <$> genSort <*> genSort <*> genKorePattern
, KJFloor <$> genSort <*> genSort <*> genKorePattern
, KJEquals <$> genSort <*> genSort <*> genKorePattern <*> genKorePattern
, KJIn <$> genSort <*> genSort <*> genKorePattern <*> genKorePattern
, KJNext <$> genSort <*> genKorePattern
, KJRewrites <$> genSort <*> genKorePattern <*> genKorePattern
]
-- | special generator which yields "multi-X" patterns (breaks round-trip testing)
genMultiKorePattern :: Gen KorePattern
genMultiKorePattern =
Gen.choice
[ KJMultiOr
<$> Gen.element [Left, Right]
<*> genSort
<*> (NE.fromList <$> between 3 12 (Gen.small genAllKorePatterns))
, KJLeftAssoc
<$> (Gen.element [('\\' -:), id] <*> genId)
<*> exactly 2 genSort
<*> (NE.fromList <$> between 3 12 (Gen.small genAllKorePatterns))
, KJRightAssoc
<$> (Gen.element [('\\' -:), id] <*> genId)
<*> exactly 2 genSort
<*> (NE.fromList <$> between 3 12 (Gen.small genAllKorePatterns))
]
(-:) :: Char -> Id -> Id
c -: (Id x) = Id $ T.cons c x
genAllKorePatterns :: Gen KorePattern
genAllKorePatterns =
Gen.frequency [(21, genKorePattern), (2, genMultiKorePattern)]
genSort :: Gen Sort
genSort =
Gen.recursive
Gen.choice
[SortVar <$> genId]
[SortApp <$> genId <*> upTo 10 genSort]
genId :: Gen Id
genId =
fmap Id $ (<>) <$> genVarName <*> Gen.text (Range.constant 0 5) Gen.digit
genVarName :: Gen Text
genVarName =
T.cons <$> Gen.alpha <*> Gen.text (Range.linear 0 128) genIdChar
genIdChar :: Gen Char
genIdChar =
Gen.frequency
[ (10, Gen.alpha)
, (3, Gen.digit)
, (1, Gen.element ['-', '\''])
]
genStringLiteral :: Gen Text
genStringLiteral = Gen.text (Range.linear 0 32) Gen.latin1
exactly :: Int -> Gen a -> Gen [a]
exactly n g
| n >= 0 = Gen.list (Range.singleton n) g
| otherwise = error "negative count requested"
upTo :: Int -> Gen a -> Gen [a]
upTo n g
| n >= 0 = Gen.list (Range.linear 0 n) g
| otherwise = error "negative range limit requested"
between :: Int -> Int -> Gen a -> Gen [a]
between n m g
| n < 0 || m < 0 =
error "negative range limits requested"
| n <= m =
Gen.list (Range.linear n m) g
| otherwise =
pure []
----------------------------------------
-- helpers for the repl
showExamples :: IO ()
showExamples =
forever $ do
koreJson <- Gen.sample (genKoreJson genKorePattern)
BS.putStr $ encodeKoreJson koreJson
void getLine
writeExamples :: Bool -> FilePath -> FilePath -> Int -> IO ()
writeExamples withMultiThings dir basename n
| n <= 0 =
error $ unwords ["Requested", show n, "<=0 examples, nothing to do."]
| n >= 100 =
error "Cowardly refusing to create more than 99 files."
| otherwise =
do
createDirectoryIfMissing False dir
mapM_ generateFile [1 .. n]
where
generateFile :: Int -> IO ()
generateFile i =
Gen.sample (genKoreJson generator)
>>= BS.writeFile (file i) . encodeKoreJson
generator =
if withMultiThings then genAllKorePatterns else genKorePattern
file i = dir </> basename ++ printf "_%02d" i <.> "json"
----------------------------------------
-- Tests
-- pretty-printer tests
test_prettyPattern :: TestTree
test_prettyPattern =
testProperty "Can pretty-print any KorePattern" . property $ do
korePattern <- forAll genAllKorePatterns
diff (T.length $ prettyPattern korePattern) (>=) 0
-- Tasty interface, whole group only
test_JsonRoundTrips :: [TestTree]
test_JsonRoundTrips =
[ testProperty "KorePattern -> json -> KorePattern" jsonRoundTrip
]
roundTripTests :: Group
roundTripTests = roundTripTestsWith 1000
roundTripTestsWith :: TestLimit -> Group
roundTripTestsWith n =
Group
"Json -> KorePattern -> ParsedPattern Round trip tests"
[ ("KorePattern -> json -> KorePattern", withTests n jsonRoundTrip)
]
jsonRoundTrip :: Property
jsonRoundTrip =
property $ do
koreJson <- forAll $ genKoreJson genAllKorePatterns
-- this is testing To/FromJSON instances and lexical checks
tripping koreJson encodeKoreJson decodeKoreJson
----------------------------------------
-- unit tests for specific properties
test_Unit_tests_for_json_failure_modes :: [TestTree]
test_Unit_tests_for_json_failure_modes =
[ eVarChecks
, sVarChecks
]
-- lexical check for identifiers
eVarChecks :: TestTree
eVarChecks =
testGroup
"Element variable lexical checks"
[ testProperty "A set variable name must not be empty" testNotEmpty
, testProperty "A valid element variable is accepted" $
property $ do
Id valid <- forAll genId
diff (checkIdChars valid) (==) []
, testProperty "A variable name has to start by a character" $
withTests 1000 testEVarInitial
, testProperty "A variable name must not contain non-alphanumeric characters" $
withTests 1000 testEVarCharSet
]
testNotEmpty :: Property
testNotEmpty =
withTests 1 . property $ diff (checkIdChars T.empty) (==) ["Empty"]
testEVarInitial :: Property
testEVarInitial =
property $
do
Id valid <- forAll genId
notLetter <- forAll $ Gen.filter (not . isAlpha) Gen.ascii
let nonLetterStart = checkIdChars $ T.cons notLetter valid
length nonLetterStart === 1
assert ("Illegal initial character" `isPrefixOf` head nonLetterStart)
notPrintable <- forAll $ Gen.filter (not . isPrint) Gen.ascii
let nonPrintableStart = checkIdChars $ T.cons notPrintable valid
length nonPrintableStart === 1
assert ("Illegal initial character" `isPrefixOf` head nonLetterStart)
testEVarCharSet :: Property
testEVarCharSet =
property $
do
initial <- forAll Gen.alpha
notAlphaNum <- forAll $ Gen.filter (not . isAllowedChar) Gen.ascii
let nonAlphaNumChars = checkIdChars $ T.pack [initial, notAlphaNum]
length nonAlphaNumChars === 1
assert ("Contains illegal characters: " `isPrefixOf` head nonAlphaNumChars)
isAllowedChar :: Char -> Bool
isAllowedChar c = isAlphaNum c || c `elem` ['-', '\'']
sVarChecks :: TestTree
sVarChecks =
testGroup
"Set variable lexical checks"
[ testProperty "A valid set variable is accepted" $
property $ do
Id valid <- forAll genId
diff (checkSVarName $ T.cons '@' valid) (==) []
, testProperty
"A set variable name has to start by `@'"
testSVarInitial
, testProperty
"A set variable must be a valid identifier after the `@'"
testSVarSuffix
]
testSVarInitial :: Property
testSVarInitial =
property $
do
wrongInitial <- forAll $ Gen.filter (/= '@') Gen.ascii
Id valid <- forAll genId
let withWrongInitial = checkSVarName $ T.cons wrongInitial valid
withWrongInitial === ["Must start with `@'"]
testSVarSuffix :: Property
testSVarSuffix =
property $
do
notAlphaNum <- forAll $ Gen.filter (not . isAllowedChar) Gen.ascii
let withWrongSuffix = checkSVarName $ T.pack ['@', 'X', notAlphaNum]
length withWrongSuffix === 1
assert ("Contains illegal characters: " `isPrefixOf` head withWrongSuffix)
----------------------------------------
test_headerFailures :: TestTree
test_headerFailures =
testGroup "Header (format and version) checks" $
map (uncurry testProperty) headerTests
headerTests :: IsString name => [(name, Property)]
headerTests =
map
(Bifunctor.second (withTests 1 . property))
[
( "Correct test data parses"
, assert . isRight $
decodeKoreJson $
withHeader "KORE" "1" aString
)
,
( "Format string errors are reported"
, diffLeft "Error in $.format: expected \"KORE\"" $
decodeKoreJson $
withHeader "Gore" "1" aString
)
,
( "Version string errors are reported"
, diffLeft "Error in $.version: expected 1.0" $
decodeKoreJson $
withHeader "KORE" "2" aString
)
,
( "Payload errors are reported"
, expectLeft ("key \"tag\" not found" `isInfixOf`) $
decodeKoreJson $
withHeader "KORE" "1" rubbish
)
,
( "Format errors take precedence"
, diffLeft "Error in $.format: expected \"KORE\"" $
decodeKoreJson $
withHeader "Gore" "42" rubbish
)
,
( "Version errors take precedence"
, diffLeft "Error in $.version: expected 1.0" $
decodeKoreJson $
withHeader "KORE" "42" rubbish
-- NB if the payload is not an object, the version
-- error does _not_ take precedence!
)
]
where
rubbish = "{ \"rubbish\": 42 }"
diffLeft ::
(MonadTest m, Eq a, Eq b, Show a, Show b) =>
a ->
Either a b ->
m ()
diffLeft expected result =
diff result (==) (Prelude.Left expected)
expectLeft ::
(MonadTest m, MonadFail m, Show b) =>
(a -> Bool) ->
Either a b ->
m ()
expectLeft predicate = \case
Prelude.Left a -> assert $ predicate a
Prelude.Right x -> fail $ "Unexpected " <> show x
withHeader :: BS.ByteString -> BS.ByteString -> BS.ByteString -> BS.ByteString
withHeader format version payload =
BS.unlines
[ "{"
, " \"format\": \"" <> format <> "\","
, " \"version\": " <> version <> ","
, " \"term\": " <> payload
, "}"
]
aString :: BS.ByteString
aString =
BS.unlines
[ " {"
, " \"tag\": \"String\","
, " \"value\": \"parse me!\""
, " }"
]