pg-verify 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b580ce77eaa5f2da5c136688b09db56a3397d810188fcfe8480a59bca92760a
4
- data.tar.gz: 89a4e5af16afc5186326cb12615432cb17d38fb7e986b790b7e061cc53138541
3
+ metadata.gz: 61be58b97dd5e5beb6546f87ef6b2f19b2d372e983fb3b93332c88a0f0b489ad
4
+ data.tar.gz: 6fc1e14c40748353219ca58d69b034b2970c57da5d19f2ad27ee842c66847035
5
5
  SHA512:
6
- metadata.gz: c3d040e30a27cf278ff987157b97abeb045d0cf12d209eacc7f4577e81ca39ae40bff1bff51ffb9581b030f5b28628ca08e84994b554c5ca8ac01773c1a8e4f5
7
- data.tar.gz: 56f901795a267c9fea53f0a203122767cb50003fc18f510df455de9f3df715421a7b788321d3538f19e3e87ac3b5cac4d6d6e8908db16f935bf5481959197e9b
6
+ metadata.gz: cbf7170ad7ebff0f939a1ed3309d82ae45246cfbc670475be37f7c0f72f17e516d5e9a73427ebb23dc5403de48693f4dc26db194ac299523af0c119063c75cc4
7
+ data.tar.gz: c0b0b0c88b8e4a54465c857f5daaaa06826e69c2d9c2149e248863b2d8e627ab6348b59e842352ea24a3c453a0319d9d6054205337ab6de6c5bd88fa6f395047
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg-verify (0.1.1)
4
+ pg-verify (0.1.2)
5
5
  config (~> 4.2.1)
6
6
  plantuml_builder (~> 0.3.0)
7
7
  rainbow (~> 3.0.0)
@@ -10,7 +10,7 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- concurrent-ruby (1.2.2)
13
+ concurrent-ruby (1.2.3)
14
14
  config (4.2.1)
15
15
  deep_merge (~> 1.2, >= 1.2.1)
16
16
  dry-validation (~> 1.0, >= 1.0.0)
@@ -66,7 +66,7 @@ GEM
66
66
  diff-lcs (>= 1.2.0, < 2.0)
67
67
  rspec-support (~> 3.12.0)
68
68
  rspec-support (3.12.0)
69
- thor (1.2.1)
69
+ thor (1.2.2)
70
70
 
71
71
  PLATFORMS
72
72
  x86_64-darwin-19
data/README.md CHANGED
@@ -26,4 +26,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
26
26
 
27
27
  ## Contributing
28
28
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pg-verify.
29
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aweber/pg-verify.
@@ -7,6 +7,7 @@ To get started you can run `pg-verify doctor` to verify your installation and gu
7
7
  you through the steps needed to install external addons like the NuSMV model checker.
8
8
 
9
9
  You can always run `pg-verify help` to get a list of available commands.
10
+ Run `pg-verify help <command>` to get more information about a specific command and its options.
10
11
  For example try running `pg-verify show png` to render a PNG image of your program graph
11
12
  and save that to your working directory.
12
13
 
@@ -15,4 +16,84 @@ and save that to your working directory.
15
16
  There are a couple of prelude files and directories in your project to get you started:
16
17
 
17
18
  - `program-graph.rb`: This file defines the default program graph you will be working on
19
+ - `.pg-verify.yml`: This file can be used to configure pg-verify
18
20
  - `addon/`: This is the directory where you will place addon resources like the NuSMV executable.
21
+
22
+ ## Writing specifications
23
+
24
+ The specification framework for the Ruby DSL is inspired by the popular
25
+ testing library [rspec](https://rspec.info/). Here is an example of the syntax:
26
+
27
+ ```ruby
28
+ # Wrap two specifications which are related to some "Car" model.
29
+ specify "The car" do
30
+ it "won't crash" => :"G distance_to_tree > 0" # Becomes: The car won't crash
31
+ it "will drive" => :"F velocity > 0" # Becomes: The car will drive
32
+ end
33
+ ```
34
+
35
+ You wrap your specification using one or more `specify` blocks.
36
+ Within those blocks you can declare a *specification* using the `it` keyword.
37
+ Each specification consists of two parts:
38
+ - A **text** which is used to describe the spec in natural language
39
+ - An **expression** given in LTL or CTL. This will actually be checked
40
+
41
+ The outer block serves as a wrapper for specifications which are related.
42
+ In the example above we declare two specifications which are both concerning
43
+ some "Car" model.
44
+
45
+ You can read those specifications from outside to inside.
46
+ The `it` semantically refers to the outer block (the car in our case).
47
+ So when reading `it "won't crash"`, **it** refers to **the car**.
48
+ Thus this specification becomes "The car won't crash" when it is expanded.
49
+
50
+ ### Assumption blocks
51
+
52
+ You can consolidate multiple preconditions for your specifications into
53
+ an `assuming` block. Here's the syntax:
54
+
55
+ ```Ruby
56
+ specify "The car" do
57
+ assuming "the breaks don't fail" => :"G BreakFailure == No" do
58
+ it "won't crash" => :"G distance_to_tree > 0" # Becomes: The car (assuming the breaks don't fail) won't crash
59
+ it "will drive" => :"F velocity > 0" # Becomes: The car (assuming the breaks don't fail) will drive
60
+ end
61
+ end
62
+ ```
63
+
64
+ Much like the specifications themselves, the assuming block expects a `text` and an `expression`.
65
+ For each contained specification, the assumption expression will be prepended to the spec
66
+ while expanding. Thus for the example above we get two expanded specifications:
67
+
68
+ ```
69
+ 1) The car (assuming the breaks don't fail) won't crash
70
+ ( G BreakFailure == No ) => G distance_to_tree > 0
71
+
72
+ 2) The car (assuming the breaks don't fail) will drive
73
+ ( G BreakFailure == No ) => F velocity > 0
74
+ ```
75
+
76
+ ### Assuming no errors
77
+
78
+ The imagined use case for the `assume` block is to specify model properties under exclusion of errors.
79
+
80
+ ```Ruby
81
+ specify "The car" do
82
+ assuming no_errors do
83
+ it "won't crash" => :"G distance_to_tree > 0" # Becomes: The car (assuming the breaks don't fail) won't crash
84
+ it "will drive" => :"F velocity > 0" # Becomes: The car (assuming the breaks don't fail) will drive
85
+ end
86
+ end
87
+ ```
88
+
89
+ The thing that's new here is the `no_errors` keyword.
90
+ All this does is generate a *text* and *expression* to be used by the `assuming` block.
91
+ Say you had three error graphs: `BreakFailure`, `UsageFailure` and `MotorFailure`.
92
+ Then `no_errors` would produce this expression:
93
+
94
+ ```
95
+ G ( BreakFailure == No && UsageFailure == No && MotorFailure == No )
96
+ ```
97
+
98
+ Instead of `no_errors` you can also use `only(...)` to limit the possibility of errors to the
99
+ ones you provide. e.g: `assuming only(:BreakFailure, :UsageFailure) do ...`.
@@ -0,0 +1,29 @@
1
+ # Create graphs with default states
2
+
3
+ model :VendingMachine do
4
+
5
+ graph :User do
6
+ states :todo
7
+ end
8
+
9
+ graph :CoinReader do
10
+ states :todo
11
+ end
12
+
13
+ graph :Controller do
14
+ states :todo
15
+ end
16
+
17
+ graph :LED do
18
+ states :todo
19
+ end
20
+
21
+ graph :Dispenser do
22
+ states :todo
23
+ end
24
+
25
+ graph :CoinDispenser do
26
+ states :todo
27
+ end
28
+
29
+ end
@@ -0,0 +1,47 @@
1
+ # Add products and coins
2
+
3
+ model :VendingMachine do
4
+
5
+ products = {
6
+ cola: 1.2,
7
+ chips: 2.5
8
+ }
9
+ products = products.map { |c, v| [c, (v * 10).to_i] }.to_h
10
+
11
+ coins = {
12
+ ten_cents: 10,
13
+ twenty_cents: 20,
14
+ fifty_cents: 50,
15
+ one_euro: 100,
16
+ two_euro: 200
17
+ }
18
+ coins = coins.map { |c, v| [c, (v / 10).to_i] }.to_h
19
+
20
+ MAX_MONEY = 100
21
+ START_MONEY = 20
22
+
23
+ graph :User do
24
+ states :todo
25
+ end
26
+
27
+ graph :CoinReader do
28
+ states :todo
29
+ end
30
+
31
+ graph :Controller do
32
+ states :todo
33
+ end
34
+
35
+ graph :LED do
36
+ states :todo
37
+ end
38
+
39
+ graph :Dispenser do
40
+ states :todo
41
+ end
42
+
43
+ graph :CoinDispenser do
44
+ states :todo
45
+ end
46
+
47
+ end
@@ -0,0 +1,68 @@
1
+ # Add states and variables
2
+
3
+ model :VendingMachine do
4
+
5
+ products = {
6
+ cola: 1.2,
7
+ chips: 2.5
8
+ }
9
+ products = products.map { |c, v| [c, (v * 10).to_i] }.to_h
10
+
11
+ coins = {
12
+ ten_cents: 10,
13
+ twenty_cents: 20,
14
+ fifty_cents: 50,
15
+ one_euro: 100,
16
+ two_euro: 200
17
+ }
18
+ coins = coins.map { |c, v| [c, (v / 10).to_i] }.to_h
19
+
20
+
21
+ MAX_MONEY = 100
22
+ START_MONEY = 20
23
+
24
+ graph :User do
25
+ press_states = products.keys.map { |product| :"press_#{product}" }
26
+ grab_states = products.keys.map { |product| :"grab_#{product}" }
27
+ insert_states = coins.keys.map { |coin| :"insert_#{coin}" }
28
+
29
+ var pocket_money: (0..MAX_MONEY), init: START_MONEY
30
+ var value_in_products: (0..MAX_MONEY), init: 0
31
+
32
+ states :inserting, *insert_states, \
33
+ :pressing, *press_states, \
34
+ :waiting, :grab_product, \
35
+ :grab_change, \
36
+ :done, \
37
+ init: :inserting
38
+ end
39
+
40
+ graph :CoinReader do
41
+ states :reading
42
+
43
+ var read_value: (0..coins.values.max), init: 0
44
+ end
45
+
46
+ graph :Controller do
47
+ var budget: (0..MAX_MONEY), init: 0
48
+
49
+ dispense_states = products.keys.map { |product| :"dispense_#{product}" }
50
+ states :accepting, *dispense_states, :rejected, :done, init: :accepting
51
+
52
+ end
53
+
54
+ graph :LED do
55
+ states :idle, :red, :green, init: :idle
56
+ end
57
+
58
+ graph :Dispenser do
59
+ dispensed_states = products.keys.map { |product| :"dispensed_#{product}" }
60
+ states :empty, *dispensed_states, init: :empty
61
+ end
62
+
63
+ graph :CoinDispenser do
64
+ states :idle, :dispensed_change, init: :idle
65
+ var change: (0..MAX_MONEY), init: 0
66
+ end
67
+
68
+ end
@@ -0,0 +1,202 @@
1
+ # Finish it all up
2
+
3
+ # CONFIG
4
+ # trace:
5
+ # colors:
6
+ # insert_*: green
7
+ # press_*: yellow
8
+ # dispense_*: yellow
9
+ # accepting: sidenote
10
+ # idle: sidenote
11
+ # empty: sidenote
12
+ # reading: sidenote
13
+
14
+ model :VendingMachine do
15
+
16
+ products = {
17
+ cola: 1.2,
18
+ chips: 2.5
19
+ }.map { |c, v| [c, (v * 10).to_i] }.to_h
20
+
21
+ coins = {
22
+ ten_cents: 10,
23
+ twenty_cents: 20,
24
+ fifty_cents: 50,
25
+ one_euro: 100,
26
+ two_euro: 200
27
+ }.map { |c, v| [c, (v / 10).to_i] }.to_h
28
+
29
+ MAX_MONEY = 100
30
+ START_MONEY = 20
31
+
32
+ transient error :CoinReadFails
33
+
34
+ graph :User do
35
+ press_states = products.keys.map { |product| :"press_#{product}" }
36
+ grab_states = products.keys.map { |product| :"grab_#{product}" }
37
+ insert_states = coins.keys.map { |coin| :"insert_#{coin}" }
38
+
39
+ var pocket_money: (0..MAX_MONEY), init: START_MONEY
40
+ var value_in_products: (0..MAX_MONEY), init: 0
41
+
42
+ states :inserting, *insert_states, \
43
+ :pressing, *press_states, \
44
+ :waiting, :grab_product, \
45
+ :grab_change, \
46
+ :done, \
47
+ init: :inserting
48
+
49
+ # The user can insert coins until the money runs out
50
+ coins.each { |coin, value|
51
+ transition :inserting => :"insert_#{coin}" do
52
+ guard "pocket_money - #{value} >= 0"
53
+ action "pocket_money := pocket_money - #{value}"
54
+ end
55
+ transition :"insert_#{coin}" => :inserting
56
+ }
57
+ # The user can decide at any point to start pressing buttons
58
+ transition :inserting => :pressing
59
+
60
+ # Press one button and wait for the result
61
+ products.each { |product, value|
62
+ transition :pressing => :"press_#{product}"
63
+ transition :"press_#{product}" => :waiting
64
+ }
65
+
66
+ # Grab a product and take note of the value
67
+ products.each { |product, value|
68
+ transition :waiting => :grab_product do
69
+ precon "value_in_products + #{value} < #{MAX_MONEY}"
70
+ guard "LED == green && Dispenser == dispensed_#{product}"
71
+ action "value_in_products := value_in_products + #{value}"
72
+ end
73
+ }
74
+ transition :grab_product => :grab_change
75
+
76
+ # Grab change directly on fail
77
+ transition :waiting => :grab_change do
78
+ guard "LED == red"
79
+ end
80
+
81
+ # Grab the change and be done
82
+ transition :grab_change => :done do
83
+ precon "pocket_money + change <= #{MAX_MONEY}"
84
+ action "pocket_money := pocket_money + change"
85
+ end
86
+ end
87
+
88
+ graph :CoinReader do
89
+ states :reading
90
+
91
+ var read_value: (0..coins.values.max), init: 0
92
+
93
+ coins.each { |coin, value|
94
+ transition :reading => :reading do
95
+ guard "User == insert_#{coin} && CoinReadFails == No"
96
+ action "read_value := #{value}"
97
+ end
98
+ }
99
+
100
+ transition :reading => :reading do
101
+ guard coins.keys.map { |coin| "User != insert_#{coin}" }.join(" && ")
102
+ action "read_value := 0"
103
+ end
104
+
105
+ end
106
+
107
+ graph :Controller do
108
+ var budget: (0..MAX_MONEY), init: 0
109
+
110
+ dispense_states = products.keys.map { |product| :"dispense_#{product}" }
111
+
112
+ states :accepting, *dispense_states, :rejected, :done, init: :accepting
113
+
114
+ # Accept coin reads from the reader
115
+ transition :accepting => :accepting do
116
+ precon "budget + read_value <= #{MAX_MONEY}"
117
+ guard "read_value > 0"
118
+ action "budget := budget + read_value"
119
+ end
120
+
121
+
122
+ products.each { |product, value|
123
+
124
+ # Accept button presses when enough money
125
+ transition :accepting => :"dispense_#{product}" do
126
+ guard "User == press_#{product} && budget >= #{value}"
127
+ action "budget := budget - #{value}"
128
+ end
129
+ transition :"dispense_#{product}" => :done
130
+
131
+ # Reject button presses when not enough money
132
+ transition :accepting => :rejected do
133
+ guard "User == press_#{product} && budget < #{value}"
134
+ end
135
+ }
136
+
137
+ # Reset when the coin dispenser has dispensed
138
+ transition :accepting => :accepting do
139
+ guard "CoinDispenser == dispensed_change"
140
+ action "budget := 0"
141
+ end
142
+ transition :done => :accepting do
143
+ guard "CoinDispenser == dispensed_change"
144
+ action "budget := 0"
145
+ end
146
+
147
+
148
+ end
149
+
150
+ graph :LED do
151
+ states :idle, :red, :green, init: :idle
152
+
153
+ transition :idle => :green do
154
+ guard "Controller == done"
155
+ end
156
+ transition :idle => :red do
157
+ guard "Controller == rejected"
158
+ end
159
+
160
+ end
161
+
162
+ graph :Dispenser do
163
+ dispensed_states = products.keys.map { |product| :"dispensed_#{product}" }
164
+ states :empty, *dispensed_states, init: :empty
165
+
166
+ # Dispense the product the Controller tells us to
167
+ products.each { |product, value|
168
+ transition :empty => :"dispensed_#{product}" do
169
+ guard "Controller == dispense_#{product}"
170
+ end
171
+ }
172
+ end
173
+
174
+ graph :CoinDispenser do
175
+ states :idle, :dispensed_change, init: :idle
176
+ var change: (0..MAX_MONEY), init: 0
177
+
178
+ # Eject the change money
179
+ transition :idle => :dispensed_change do
180
+ guard "Controller == rejected || Controller == done"
181
+ action "change := budget"
182
+ end
183
+
184
+ end
185
+
186
+ specify "The vending machine" do
187
+
188
+ it "allows the user to buy something" => :"EF value_in_products > 0"
189
+ it "always completes the transaction" => :"F User == done"
190
+
191
+ products.each { |product, value|
192
+ assuming "the product can be bough" => :"pocket_money >= #{value}" do
193
+ it "allows the user to buy #{product}" => :"EF Dispenser == dispensed_#{product}"
194
+ end
195
+ }
196
+
197
+ end
198
+
199
+ hazard "The user looses money" => :"User == done && pocket_money + value_in_products < #{START_MONEY}"
200
+ hazard "The machine looses money" => :"User == done && pocket_money + value_in_products > #{START_MONEY}"
201
+
202
+ end
@@ -4,7 +4,8 @@ model :TestTransitions do
4
4
  end
5
5
  specify "The state" do
6
6
  it "starts in initial" => :"Test == initial"
7
- it "stays in initial" => :"G Test == initial"
7
+ it "stays in initial (LTL)" => :"G Test == initial"
8
+ it "stays in initial (CTL)" => :"AG Test == initial"
8
9
  it "never leaves initial" => :"! F Test != initial"
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ model :TestTransitions do
5
5
  end
6
6
  specify "The state" do
7
7
  it "starts in one" => :"Test == one"
8
- it "transitions to two" => :"X Test == two"
8
+ it "transitions to two (LTL)" => :"X Test == two"
9
+ it "transitions to two (CTL)" => :"AX Test == two"
9
10
  end
10
11
  end
@@ -0,0 +1,19 @@
1
+ model :CtlSpecifications do
2
+
3
+ graph :Example do
4
+ states :one, :two, :three, init: :one
5
+
6
+ transition :one => :two
7
+ transition :two => :one
8
+ transition :one => :three
9
+
10
+ end
11
+
12
+ specify "The model" do
13
+ it "starts in state one" => :"Example == one"
14
+ it "cannot stay in one" => :"! G Example == one"
15
+ it "can stay in three" => :"EX AG Example == three"
16
+ it "cannot go back to one from three" => :"AG ( (Example == three) => ! EF Example == one)"
17
+ end
18
+
19
+ end
@@ -0,0 +1,17 @@
1
+ model :NonAtomicHazard do
2
+
3
+ persistent error :KeysForgotten
4
+
5
+ graph :Person do
6
+ states :inside, :outside, init: :inside
7
+ transition :inside => :outside
8
+ transition :outside => :inside do
9
+ guard "KeysForgotten == No"
10
+ end
11
+ end
12
+
13
+ # This does not work as at any point the fault could not have occurred
14
+ # (KeysForgotten == No) already but then it happens at a later point
15
+ # Expected Cut Sets: { :"" }
16
+ # hazard "The person can't get in" => :"G Person == outside"
17
+ end
@@ -0,0 +1,21 @@
1
+ model :MultipleActions do
2
+
3
+ graph :GraphWithMultipleActions do
4
+ states :one, :two, init: :one
5
+
6
+ var a: (0..10), init: 0
7
+ var b: (0..10), init: 0
8
+ var c: (0..10), init: 0
9
+
10
+ transition :one => :two do
11
+ action "a := 1 + 1 | b := 0 | c := 25 - (2 * 10)"
12
+ end
13
+ end
14
+
15
+ specify "The graph" do
16
+ it "set a = 2" => :"X a == 2"
17
+ it "set b = 0" => :"X b == 0"
18
+ it "set c = 5" => :"X c == 5"
19
+ end
20
+
21
+ end