This repository has been archived by the owner on Jun 18, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathProcessRegistry.hs
578 lines (488 loc) · 19.6 KB
/
ProcessRegistry.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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
-----------------------------------------------------------------------------
-- |
-- Module : ProcessRegistry
-- Copyright : (C) 2017, Jacob Stanley; 2018, Stevan Andjelkovic
-- License : BSD-style (see the file LICENSE)
--
-- Maintainer : Stevan Andjelkovic <[email protected]>
-- Stability : provisional
-- Portability : non-portable (GHC extensions)
--
-- This module contains the process registry example that is commonly
-- used in the papers on Erlang QuickCheck, e.g. "Finding Race
-- Conditions in Erlang with QuickCheck and PULSE". Parts of the code
-- are stolen from an example in Hedgehog.
--
-----------------------------------------------------------------------------
module ProcessRegistry
( prop_processRegistry
, printLabelledExamples
)
where
import Control.Exception
(catch)
import Control.Monad
(when)
import Control.Monad.IO.Class
(MonadIO(..))
import Data.Foldable
(traverse_)
import Data.Functor.Classes
(Ord1)
import Data.Hashable
(Hashable)
import qualified Data.HashTable.IO as HashTable
import Data.IORef
(IORef)
import qualified Data.IORef as IORef
import Data.Kind
(Type)
import Data.List
((\\))
import Data.Map
(Map)
import qualified Data.Map.Strict as Map
import Data.Maybe
(isJust, isNothing)
import Data.Set
(Set)
import qualified Data.Set as Set
import Data.Tuple
(swap)
import GHC.Generics
(Generic, Generic1)
import Prelude
import System.IO.Error
(ioeGetErrorString)
import System.IO.Unsafe
(unsafePerformIO)
import System.Random
(randomRIO)
import Test.QuickCheck
(Arbitrary, Gen, Property, arbitrary, elements,
tabulate, (.&&.), (===))
import Test.QuickCheck.Monadic
(monadicIO)
import Test.StateMachine
import Test.StateMachine.Labelling
import qualified Test.StateMachine.Types.Rank2 as Rank2
------------------------------------------------------------------------
-- Implementation
--
-- The following code is stolen from an Hedgehog example:
--
-- Fake Process Registry
--
-- /These are global to simulate some kind of external system we're
-- testing./
--
newtype Name = Name String
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (ToExpr)
newtype Pid = Pid Int
deriving newtype (Num)
deriving stock (Eq, Ord, Generic, Show)
deriving anyclass (ToExpr)
type ProcessTable = HashTable.CuckooHashTable String Int
pidRef :: IORef Pid
pidRef =
unsafePerformIO $ IORef.newIORef 0
{-# NOINLINE pidRef #-}
procTable :: ProcessTable
procTable =
unsafePerformIO $ HashTable.new
{-# NOINLINE procTable #-}
killedPidsRef :: IORef [Pid]
killedPidsRef =
unsafePerformIO $ IORef.newIORef []
{-# NOINLINE killedPidsRef #-}
ioReset :: IO ()
ioReset = do
IORef.writeIORef pidRef 0
ks <- fmap fst <$> HashTable.toList procTable
traverse_ (HashTable.delete procTable) ks
IORef.writeIORef killedPidsRef []
ioSpawn :: IO Pid
ioSpawn = do
pid <- IORef.readIORef pidRef
IORef.writeIORef pidRef (pid + 1)
die <- randomRIO (1, 6) :: IO Int
if die == -1
then error "ioSpawn"
else pure pid
ioKill :: Pid -> IO ()
ioKill pid =
IORef.modifyIORef killedPidsRef (pid :)
reverseLookup :: (Eq k, Eq v, Hashable k, Hashable v)
=> HashTable.CuckooHashTable k v -> v -> IO (Maybe k)
reverseLookup tbl val = do
lbt <- swapTable tbl
HashTable.lookup lbt val
where
-- Swap the keys and values in a hashtable.
swapTable :: (Eq k, Eq v, Hashable k, Hashable v)
=> HashTable.CuckooHashTable k v -> IO (HashTable.CuckooHashTable v k)
swapTable t = HashTable.fromList =<< fmap (map swap) (HashTable.toList t)
ioRegister :: Name -> Pid -> IO ()
ioRegister (Name name) pid'@(Pid pid) = do
mpid <- HashTable.lookup procTable name
when (isJust mpid) $
fail "ioRegister: name already registered"
mname <- reverseLookup procTable pid
when (isJust mname) $
fail "ioRegister: pid already registered"
killedPids <- IORef.readIORef killedPidsRef
when (pid' `elem` killedPids) $
fail "ioRegister: pid is dead"
HashTable.insert procTable name pid
ioUnregister :: Name -> IO ()
ioUnregister (Name name) = do
m <- HashTable.lookup procTable name
when (isNothing m) $
fail "ioUnregister: not registered"
HashTable.delete procTable name
-- Here we extend the Hedgehog example with a looking up names in the
-- registry.
ioWhereIs :: Name -> IO Pid
ioWhereIs (Name name) = do
mpid <- HashTable.lookup procTable name
case mpid of
Nothing -> fail "ioWhereIs: not registered"
Just pid -> return (Pid pid)
------------------------------------------------------------------------
-- Specification
data Action (r :: Type -> Type)
= Spawn
| Kill (Reference Pid r)
| Register Name (Reference Pid r)
| BadRegister Name (Reference Pid r)
| Unregister Name
| BadUnregister Name
| WhereIs Name
| Exit
deriving stock (Show, Generic1)
deriving anyclass (Rank2.Functor, Rank2.Foldable, Rank2.Traversable, CommandNames)
data Action_
= Spawn_
| Kill_
| Register_
| BadRegister_
| Unregister_
| BadUnregister_
| WhereIs_
| Exit_
deriving stock (Show, Eq, Ord, Generic)
constructor :: Action r -> Action_
constructor act = case act of
Spawn {} -> Spawn_
Kill {} -> Kill_
Register {} -> Register_
BadRegister {} -> BadRegister_
Unregister {} -> Unregister_
BadUnregister {} -> BadUnregister_
WhereIs {} -> WhereIs_
Exit {} -> Exit_
newtype Response (r :: Type -> Type) = Response
{ _getResponse :: Either Error (Success r) }
deriving stock (Show, Generic1)
deriving anyclass Rank2.Foldable
data Success (r :: Type -> Type)
= Spawned (Reference Pid r)
| Killed
| Registered
| Unregistered
| HereIs (Reference Pid r)
| Exited
deriving stock (Show, Generic1)
deriving anyclass (Rank2.Foldable)
data Error
= NameAlreadyRegisteredError
| PidAlreadyRegisteredError
| PidDeadRegisterError
| NameNotRegisteredError
| UnknownError
deriving stock Show
success :: Success r -> Response r
success = Response . Right
failure :: Error -> Response r
failure = Response . Left
data Model (r :: Type -> Type) = Model
{ pids :: [Reference Pid r]
, registry :: Map Name (Reference Pid r)
, killed :: [Reference Pid r]
, stop :: Bool
}
deriving stock (Show, Generic)
instance ToExpr (Model Concrete)
initModel :: Model r
initModel = Model [] Map.empty [] False
transition :: Model r -> Action r -> Response r -> Model r
transition m act (Response (Left _err)) = case act of
BadRegister {} -> m
BadUnregister {} -> m
_otherwise -> error "transition: good command throws error"
transition Model {..} act (Response (Right resp)) = case (act, resp) of
(Spawn, Spawned pid) ->
Model { pids = pids ++ [pid], .. }
(Kill pid, Killed) ->
Model { killed = killed ++ [pid], .. }
(Register name pid, Registered) ->
Model { registry = Map.insert name pid registry, .. }
(BadRegister _name _pid, _) -> error "transition: BadRegister"
(Unregister name, Unregistered) ->
Model { registry = Map.delete name registry, .. }
(BadUnregister _name, _) -> error "transition: BadUnregister"
(WhereIs _name, HereIs _pid) ->
Model {..}
(Exit, Exited) ->
Model { stop = True, .. }
(_, _) -> error "transition"
precondition :: Model Symbolic -> Action Symbolic -> Logic
precondition Model {..} act = case act of
Spawn -> Top
Kill pid -> pid `member` pids
Register name pid -> pid `member` pids
.&& name `notMember` Map.keys registry
.&& pid `notMember` Map.elems registry
BadRegister name pid -> pid `member` killed
.|| name `member` Map.keys registry
.|| pid `member` Map.elems registry
Unregister name -> name `member` Map.keys registry
BadUnregister name -> name `notMember` Map.keys registry
WhereIs name -> name `member` Map.keys registry
Exit -> Top
postcondition :: Model Concrete -> Action Concrete -> Response Concrete -> Logic
postcondition Model {..} act (Response (Left err)) = case act of
BadRegister _name _pid -> Top
BadUnregister _name -> Top
_ -> Bot .// show err
postcondition Model {..} act (Response (Right resp)) = case (act, resp) of
(Spawn, Spawned _pid) -> Top
(Kill _pid, Killed) -> Top
(Register _name _pid, Registered) -> Top
(Unregister _name, Unregistered) -> Top
(WhereIs name, HereIs pid) -> registry Map.! name .== pid
(Exit, Exited) -> Top
(_, _) -> Bot
semantics' :: Action Concrete -> IO (Success Concrete)
semantics' Spawn = Spawned . reference <$> ioSpawn
semantics' (Kill pid) = Killed <$ ioKill (concrete pid)
semantics' (Register name pid) = Registered <$ ioRegister name (concrete pid)
semantics' (BadRegister name pid) = Registered <$ ioRegister name (concrete pid)
semantics' (Unregister name) = Unregistered <$ ioUnregister name
semantics' (BadUnregister name) = Unregistered <$ ioUnregister name
semantics' (WhereIs name) = HereIs . reference <$> ioWhereIs name
semantics' Exit = return Exited
semantics :: Action Concrete -> IO (Response Concrete)
semantics act = fmap success (semantics' act)
`catch`
(return . failure . handler)
where
handler :: IOError -> Error
handler err = case ioeGetErrorString err of
"ioRegister: name already registered" -> NameAlreadyRegisteredError
"ioRegister: pid already registered" -> PidAlreadyRegisteredError
"ioRegister: pid is dead" -> PidDeadRegisterError
"ioUnregister: not registered" -> NameNotRegisteredError
_ -> UnknownError
data Fin2
= Zero
| One
| Two
deriving stock (Enum, Bounded, Show, Eq, Read, Ord)
data State = Fin2 :*: Fin2 | Stop
deriving stock (Show, Eq, Ord, Generic)
partition :: Model r -> State
partition Model {..}
| stop = Stop
| otherwise = ( toEnum (length pids - length killed)
:*: toEnum (length (Map.keys registry))
)
sinkState :: State -> Bool
sinkState = (== Stop)
_initState :: State
_initState = Zero :*: Zero
allNames :: [Name]
allNames = map Name ["A", "B", "C"]
instance Arbitrary Name where
arbitrary = elements allNames
genSpawn, genKill, genRegister, genBadRegister, genUnregister, genBadUnregister,
genWhereIs, genExit :: Model Symbolic -> Gen (Action Symbolic)
genSpawn _model = return Spawn
genKill model = Kill <$> elements (pids model)
genRegister model = Register <$> arbitrary <*> elements (pids model \\ killed model)
genBadRegister model = BadRegister <$> arbitrary <*> elements (pids model ++ killed model)
genUnregister model = Unregister <$> elements (Map.keys (registry model))
genBadUnregister model = BadUnregister <$> elements (allNames \\ Map.keys (registry model))
genWhereIs model = WhereIs <$> elements (Map.keys (registry model))
genExit _model = return Exit
gens :: Map Action_ (Model Symbolic -> Gen (Action Symbolic))
gens = Map.fromList
[ (Spawn_, genSpawn)
, (Kill_, genKill)
, (Register_, genRegister)
, (BadRegister_, genBadRegister)
, (Unregister_, genUnregister)
, (BadUnregister_, genBadUnregister)
, (WhereIs_, genWhereIs)
, (Exit_, genExit)
]
generator :: Model Symbolic -> Maybe (Gen (Action Symbolic))
generator = markovGenerator markov gens partition sinkState
shrinker :: Model Symbolic -> Action Symbolic -> [Action Symbolic]
shrinker _model _act = []
mock :: Model Symbolic -> Action Symbolic -> GenSym (Response Symbolic)
mock m act = case act of
Spawn -> success . Spawned <$> genSym
Kill _pid -> pure (success Killed)
Register _name _pid -> pure (success Registered)
BadRegister name pid
| name `elem` Map.keys (registry m) -> pure (failure NameAlreadyRegisteredError)
| pid `elem` Map.elems (registry m) -> pure (failure PidAlreadyRegisteredError)
| pid `elem` killed m -> pure (failure PidDeadRegisterError)
| otherwise -> error "mock: BadRegister"
Unregister _name -> pure (success Unregistered)
BadUnregister _name -> pure (failure NameNotRegisteredError)
WhereIs _name -> success . HereIs <$> genSym
Exit -> pure (success Exited)
sm :: StateMachine Model Action IO Response
sm = StateMachine initModel transition precondition postcondition
Nothing generator shrinker semantics mock noCleanup
markov :: Markov State Action_ Double
markov = makeMarkov
[ Zero :*: Zero -< [ Spawn_ /- One :*: Zero ]
, One :*: Zero -< [ Spawn_ /- Two :*: Zero
, Register_ /- One :*: One
, (BadRegister_, 10) >- One :*: Zero
, (Kill_, 20) >- Zero :*: Zero
]
, One :*: One -< [ (Spawn_, 40) >- Two :*: One
, BadRegister_ /- One :*: One
, (Unregister_, 20) >- One :*: Zero
, BadUnregister_ /- One :*: One
, (WhereIs_, 20) >- One :*: One
]
, Two :*: Zero -< [ (Register_, 80) >- Two :*: One
, (Kill_, 20) >- One :*: Zero
]
, Two :*: One -< [ (Register_, 30) >- Two :*: Two
, (Kill_, 10) >- One :*: One
, (Unregister_, 20) >- Two :*: Zero
, (BadUnregister_, 10) >- Two :*: One
, (WhereIs_, 20) >- Two :*: One
, (Exit_, 10) >- Stop
]
, Two :*: Two -< [ (Exit_, 30) >- Stop
, (Unregister_, 20) >- Two :*: One
, (WhereIs_, 50) >- Two :*: Two
]
]
------------------------------------------------------------------------
-- Requirements from the paper "How well are your requirements tested?"
-- (2016) by Arts and Hughes.
data Req
= RegisterNewNameAndPid_REG001
| RegisterExistingName_REG002
| RegisterExistingPid_REG003
| RegisterDeadPid_REG004
| UnregisterRegisteredName_UNR001
| UnregisterNotRegisteredName_UNR002
| WHE001
| WHE002
| DIE001
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (ToExpr)
type EventPred r = Predicate (Event Model Action Response r) Req
-- Convenience combinator for creating classifiers for successful commands.
successful :: (Event Model Action Response r -> Success r -> Either Req (EventPred r))
-> EventPred r
successful f = predicate $ \ev ->
case eventResp ev of
Response (Left _ ) -> Right $ successful f
Response (Right ok) -> f ev ok
tag :: forall r. Ord1 r => [Event Model Action Response r] -> [Req]
tag = classify
[ tagRegisterNewNameAndPid
, tagRegisterExistingName Set.empty
, tagRegisterExistingPid Set.empty
, tagRegisterDeadPid Set.empty
, tagUnregisterRegisteredName Set.empty
, tagUnregisterNotRegisteredName Set.empty
]
where
tagRegisterNewNameAndPid :: EventPred r
tagRegisterNewNameAndPid = successful $ \ev _ -> case eventCmd ev of
Register _ _ -> Left RegisterNewNameAndPid_REG001
_otherwise -> Right tagRegisterNewNameAndPid
tagRegisterExistingName :: Set Name -> EventPred r
tagRegisterExistingName existingNames = predicate $ \ev ->
case (eventCmd ev, eventResp ev) of
(Register name _pid, Response (Right Registered)) ->
Right (tagRegisterExistingName (Set.insert name existingNames))
(BadRegister name _pid, Response (Left NameAlreadyRegisteredError))
| name `Set.member` existingNames -> Left RegisterExistingName_REG002
_otherwise
-> Right (tagRegisterExistingName existingNames)
tagRegisterExistingPid :: Set (Reference Pid r) -> EventPred r
tagRegisterExistingPid existingPids = predicate $ \ev ->
case (eventCmd ev, eventResp ev) of
(Register _name pid, Response (Right Registered)) ->
Right (tagRegisterExistingPid (Set.insert pid existingPids))
(BadRegister _name pid, Response (Left PidAlreadyRegisteredError))
| pid `Set.member` existingPids -> Left RegisterExistingPid_REG003
_otherwise
-> Right (tagRegisterExistingPid existingPids)
tagRegisterDeadPid :: Set (Reference Pid r) -> EventPred r
tagRegisterDeadPid killedPids = predicate $ \ev ->
case (eventCmd ev, eventResp ev) of
(Kill pid, Response (Right Killed)) ->
Right (tagRegisterDeadPid (Set.insert pid killedPids))
(BadRegister _name pid, Response (Left PidDeadRegisterError))
| pid `Set.member` killedPids -> Left RegisterDeadPid_REG004
_otherwise
-> Right (tagRegisterDeadPid killedPids)
tagUnregisterRegisteredName :: Set Name -> EventPred r
tagUnregisterRegisteredName registeredNames = successful $ \ev resp ->
case (eventCmd ev, resp) of
(Register name _pid, Registered) ->
Right (tagUnregisterRegisteredName (Set.insert name registeredNames))
(Unregister name, Unregistered)
| name `Set.member` registeredNames -> Left UnregisterRegisteredName_UNR001
_otherwise
-> Right (tagUnregisterRegisteredName registeredNames)
tagUnregisterNotRegisteredName :: Set Name -> EventPred r
tagUnregisterNotRegisteredName registeredNames = predicate $ \ev ->
case (eventCmd ev, eventResp ev) of
(Register name _pid, Response (Right Registered)) ->
Right (tagUnregisterNotRegisteredName (Set.insert name registeredNames))
(BadUnregister name, Response (Left NameNotRegisteredError))
| name `Set.notMember` registeredNames -> Left UnregisterNotRegisteredName_UNR002
_otherwise
-> Right (tagUnregisterNotRegisteredName registeredNames)
printLabelledExamples :: IO ()
printLabelledExamples = showLabelledExamples sm tag
------------------------------------------------------------------------
prop_processRegistry :: StatsDb IO -> Property
prop_processRegistry sdb = forAllCommands sm (Just 100000) $ \cmds -> monadicIO $ do
liftIO ioReset
(hist, _model, res) <- runCommands sm cmds
let observed = historyObservations sm markov partition constructor hist
reqs = tag (execCmds sm cmds)
persistStats sdb observed
prettyCommands' sm tag cmds hist
$ tabulate "_Requirements" (map show reqs)
$ coverMarkov markov
$ tabulateMarkov sm partition constructor cmds
$ printReliability sdb (transitionMatrix markov) observed
$ res === Ok .&&. reqs === tag (execHistory sm hist)