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.
@@ -5,9 +5,17 @@ module PgVerify
5
5
  class Runner
6
6
 
7
7
  def run_specs(program_graph)
8
- nusmv_s = Transform::NuSmvTransformation.new.transform_graph(program_graph)
9
- output = eval_nusmv(nusmv_s)
8
+ transformer = Transform::NuSmvTransformation.new
9
+ nusmv_s = transformer.transform_graph(program_graph, include_specs: false)
10
+ commands = [ "go" ]
10
11
  specs = program_graph.specification.flatten()
12
+ transform_map = specs.each { |spec|
13
+ transformed_spec_expr = transformer.transform_expression(spec.expression, program_graph.all_variables)
14
+ cmd = { ltl: "check_ltlspec", ctl: "check_ctlspec" }[spec.expression.predict_type]
15
+ commands << "#{cmd} -p \"#{transformed_spec_expr}\""
16
+ }
17
+ output = eval_nusmv(nusmv_s, commands: commands)
18
+
11
19
  return parse_spec_results(program_graph, specs, output)
12
20
  end
13
21
 
@@ -20,8 +28,8 @@ module PgVerify
20
28
  nusmv_output.split("\n").each { |line|
21
29
  result = line[/-- specification .* is (true|false)/, 1]
22
30
  if !result.nil?
23
- blocks << current_block unless current_block.nil?
24
- current_block = block.new(result == "true", [])
31
+ blocks << current_block unless current_block.nil? # Complete the last block
32
+ current_block = block.new(result == "true", []) # Start a new block
25
33
  next
26
34
  end
27
35
  current_block.lines << line unless current_block.nil?
@@ -41,10 +49,7 @@ module PgVerify
41
49
  # variable to the value of that variable in that state
42
50
  def run_simulation(program_graph, steps, random: false)
43
51
  commands = []
44
- commands << "read_model"
45
- commands << "flatten_hierarchy"
46
- commands << "encode_variables"
47
- commands << "build_model"
52
+ commands << "go"
48
53
  commands << "pick_state #{random ? '-r' : ''}"
49
54
  commands << "simulate -k #{steps.to_s.to_i} -v #{random ? '-r' : ''}"
50
55
  commands << "quit"
@@ -53,12 +58,48 @@ module PgVerify
53
58
  return parse_trace(program_graph, output)
54
59
  end
55
60
 
61
+ def run_check!(program_graph)
62
+ deadlock_state = run_check(program_graph)
63
+ return if deadlock_state.nil?
64
+ raise Model::Validation::DeadlockInFSMError.new(program_graph, deadlock_state)
65
+ end
66
+
67
+ def run_check(program_graph)
68
+ commands = [ "go", "check_fsm", "quit" ]
69
+ nusmv_s = Transform::NuSmvTransformation.new.transform_graph(program_graph, include_specs: false)
70
+ output = eval_nusmv(nusmv_s, commands: commands)
71
+
72
+ # Return "ok" if the FSM has no deadlocks
73
+ return nil if output.include?("The transition relation is total: No deadlock state exists")
74
+
75
+ # Otherwise compute and return the deadlock state
76
+ lines = output.split("\n").drop_while { |str|
77
+ !(str.start_with?("A deadlock state is:") || str.start_with?("successors is:"))
78
+ }
79
+ lines = lines[1, lines.length - 1]
80
+
81
+ var_state = {}
82
+ lines.each { |line|
83
+ key, val = parse_var_assignment_in_trace(line)
84
+ next if key.nil? || val.nil?
85
+ var_state[key] = val
86
+ }
87
+
88
+ return var_state
89
+ end
90
+
56
91
  def parse_trace(program_graph, nusmv_output)
57
92
  var_states, current_var_state = [], nil
58
93
 
59
94
  loop_index = -1
60
95
 
61
96
  nusmv_output.split("\n").each { |line|
97
+ # Mark a loop for the following state
98
+ if line.include?("Loop starts here")
99
+ loop_index = var_states.length
100
+ next
101
+ end
102
+
62
103
  # Wait for heading of new state
63
104
  if line.match(/\s*-> State: .+ <-/)
64
105
  # Complete and store the old state if any
@@ -76,17 +117,28 @@ module PgVerify
76
117
  # Skip lines before the first state
77
118
  next if current_var_state.nil?
78
119
 
79
- if line.include?("Loop starts here")
80
- loop_index == var_states.length - 1
81
- next
82
- end
83
-
84
- key_val = line.split("=").map(&:strip)
85
- key = key_val[0].gsub("v.V_", "").to_sym
86
- val = key_val[1].gsub("L_", "")
120
+ # Parse the variable state
121
+ key, val = parse_var_assignment_in_trace(line)
122
+ next if key.nil? || val.nil?
87
123
  current_var_state[key] = val
88
124
  }
89
- return Model::Trace.new(program_graph, var_states)
125
+ # Finish up
126
+ unless current_var_state.nil?
127
+ missing_keys = var_states.empty? ? [] : var_states[-1].keys - current_var_state.keys
128
+ missing_keys.each { |key|
129
+ current_var_state[key] = var_states[-1][key]
130
+ }
131
+ var_states << current_var_state
132
+ end
133
+ return Model::Trace.new(program_graph, var_states, loop_index: loop_index)
134
+ end
135
+
136
+ def parse_var_assignment_in_trace(line)
137
+ return nil, nil unless line.include?("=")
138
+ key_val = line.split("=").map(&:strip)
139
+ key = key_val[0].gsub("v.V_", "").to_sym
140
+ val = key_val[1].gsub("L_", "")
141
+ return key, val
90
142
  end
91
143
 
92
144
  def eval_nusmv(nusmv_string, commands: [])
@@ -15,6 +15,7 @@ module PgVerify
15
15
  end
16
16
 
17
17
  def self.convert_file(in_path, out_path)
18
+ # CALL WITH java -Djava.awt.headless=true -jar addon/plantuml-1.2023.13.jar --help
18
19
  # TODO: The PlantUML jar switches focus to the desktop is if it would
19
20
  # attempt to open a window each time it is invoked.
20
21
  end
File without changes
@@ -4,16 +4,24 @@ module PgVerify
4
4
  class HashTransformation
5
5
 
6
6
  def transform_graph(graph)
7
- return { graph.name.to_s =>
8
- graph.components.map { |c| transform_component(graph, c) }
9
- }
7
+ components = graph.components.map { |c| transform_component(graph, c) }
8
+ hazards = graph.hazards.map { |h| transform_hazard(h) }
9
+ sub_hash = {}
10
+ sub_hash["components"] = components
11
+ sub_hash["hazards"] = hazards unless hazards.empty?
12
+ sub_hash["specification"] = transform_specification(graph.specification)
13
+ return { graph.name.to_s => sub_hash }
10
14
  end
11
15
 
12
16
  def parse_graph(hash)
13
17
  name = hash.keys.first.to_sym
14
18
  graph = Model::Graph.new(name)
15
- components = hash[hash.keys.first].map { |c| parse_component(graph, c) }
19
+ components = hash[hash.keys.first]["components"].map { |c| parse_component(graph, c) }
20
+ hazards = hash[hash.keys.first]["hazards"]&.map { |h| parse_hazard(h) } || []
21
+ specification = parse_specification(hash[hash.keys.first]["specification"])
16
22
  graph.components = components
23
+ graph.hazards = hazards
24
+ graph.specification = specification
17
25
  return graph
18
26
  end
19
27
 
@@ -22,6 +30,7 @@ module PgVerify
22
30
  return { component.name.to_s => {
23
31
  "states" => component.states.map(&:to_s),
24
32
  "variables" => variables.map { |v| transform_variable(v) },
33
+ "init" => transform_expression(component.init_expression),
25
34
  "transitions" => component.transitions.map { |t| transform_transition(t) },
26
35
  "represents_fault" => component.represents_fault
27
36
  }}
@@ -31,13 +40,14 @@ module PgVerify
31
40
  name = hash.keys.first
32
41
  states = hash[name]["states"].map(&:to_sym)
33
42
  variables = hash[name]["variables"].map { |v| parse_variable(name, v) }
43
+ init_expression = parse_expression(hash[name]["init"])
34
44
  transitions = hash[name]["transitions"].map { |t| parse_transition(t) }
35
45
  represents_fault = hash[name]["represents_fault"]
36
46
 
37
47
  graph.variables += variables
38
48
 
39
49
  return Model::Component.new(name: name.to_sym, states: states,
40
- transitions: transitions, represents_fault: represents_fault)
50
+ transitions: transitions, represents_fault: represents_fault, init_expression: init_expression)
41
51
  end
42
52
 
43
53
  def transform_variable(variable)
@@ -49,7 +59,7 @@ module PgVerify
49
59
 
50
60
  def parse_variable(owner_name, hash)
51
61
  name = hash.keys.first
52
- range = hash[name]["range"]
62
+ range = parse_variable_range(hash[name]["range"])
53
63
  init_expression = parse_expression(hash[name]["init"])
54
64
  return Model::Variable.new(name.to_sym, range, owner_name.to_sym, nil, init: init_expression)
55
65
  end
@@ -60,9 +70,10 @@ module PgVerify
60
70
  end
61
71
 
62
72
  def parse_variable_range(range)
63
- if range.is_a?(String) && range.match(/\A\d+\.\.\d+\z/)
73
+ # Matches ranges like 1..12 or -1..23
74
+ if range.is_a?(String) && range.match(/\A\-?\d+\.\.\-?\d+\z/)
64
75
  first, last = range.split("..")[0], range.split("..")[-1]
65
- return Range.new(first, last)
76
+ return Range.new(first.to_i, last.to_i)
66
77
  end
67
78
  return range
68
79
  end
@@ -85,20 +96,41 @@ module PgVerify
85
96
  def transform_transition(transition)
86
97
  name = "#{transition.src_state} -> #{transition.tgt_state}"
87
98
  return { name => {
88
- "precon" => transition.precon&.to_s,
89
- "guard" => transition.guard&.to_s,
90
- "action" => transition.action&.to_s,
99
+ "precon" => transform_expression(transition.precon),
100
+ "guard" => transform_expression(transition.guard),
101
+ "action" => transform_expression(transition.action),
91
102
  }}
92
103
  end
93
104
 
94
105
  def parse_transition(hash)
95
106
  src_state, tgt_state = hash.keys.first.split("->").map(&:strip).map(&:to_sym)
96
- precon = hash.values.first["precon"]
97
- guard = hash.values.first["guard"]
98
- action = hash.values.first["action"]
107
+ precon = parse_expression(hash.values.first["precon"])
108
+ guard = parse_expression(hash.values.first["guard"] )
109
+ action = parse_expression(hash.values.first["action"])
99
110
  return Model::Transition.new(src_state, tgt_state, precon: precon, guard: guard, action: action)
100
111
  end
101
112
 
113
+ def transform_hazard(hazard)
114
+ return { "label" => hazard.text, "expression" => hazard.expression }
115
+ end
116
+
117
+ def parse_hazard(hash)
118
+ return Model::Hazard.new(hash["label"], hash["expression"])
119
+ end
120
+
121
+ def transform_specification(specification)
122
+ return specification.flatten().map { |spec|
123
+ { "label" => spec.text.strip, "expression" => transform_expression(spec.expression) }
124
+ }
125
+ end
126
+
127
+ def parse_specification(array)
128
+ specs = array.map { |hash|
129
+ Model::Spec.new(hash["label"], parse_expression(hash["expression"]), nil)
130
+ }
131
+ return Model::Specification.new([Model::SpecSet.wrap(specs)])
132
+ end
133
+
102
134
  end
103
135
  end
104
136
  end
@@ -3,7 +3,7 @@ module PgVerify
3
3
 
4
4
  class NuSmvTransformation
5
5
 
6
- def transform_graph(program_graph)
6
+ def transform_graph(program_graph, include_specs: true)
7
7
  variables = program_graph.all_variables
8
8
  components = program_graph.components
9
9
 
@@ -11,7 +11,7 @@ module PgVerify
11
11
  cmp_s = transform_components(components, variables)
12
12
  main_s = transform_main_module(components)
13
13
 
14
- specs = transform_specification(program_graph.specification, variables)
14
+ specs = transform_specification(program_graph.specification, variables) if include_specs
15
15
 
16
16
  return "#{var_s}\n\n#{cmp_s}\n\n#{main_s}\n\n#{specs}\n"
17
17
  end
@@ -251,7 +251,8 @@ module PgVerify
251
251
  def transform_spec(spec, varset)
252
252
  expression_s = "-- #{spec.text}\n"
253
253
  expression_s += "-- Defined in: #{spec.source_location.to_s}\n"
254
- expression_s += "LTLSPEC " + transform_expression(spec.expression, varset)
254
+ keyword = { ltl: "LTLSPEC", ctl: "CTLSPEC" }[spec.expression.predict_type]
255
+ expression_s += keyword + " " + transform_expression(spec.expression, varset)
255
256
  return expression_s
256
257
  end
257
258
 
@@ -5,13 +5,25 @@ module PgVerify
5
5
 
6
6
  class PumlTransformation
7
7
 
8
+ attr_accessor :render_labels
9
+ attr_accessor :render_precons
10
+ attr_accessor :render_guards
11
+ attr_accessor :render_actions
12
+
13
+ def initialize(render_options = {})
14
+ @render_labels = render_options[:render_labels].nil? ? true : render_options[:render_labels]
15
+ @render_precons = render_options[:render_precons].nil? ? true : render_options[:render_precons]
16
+ @render_guards = render_options[:render_guards].nil? ? true : render_options[:render_guards]
17
+ @render_actions = render_options[:render_actions].nil? ? true : render_options[:render_actions]
18
+ end
19
+
8
20
  def transform_graph(graph, variable_state: nil, only: nil)
9
21
 
10
22
  components = graph.components
11
23
  components = components.select { |c| only.map(&:to_s).include?(c.name.to_s) } unless only.nil?
12
24
 
13
25
  parts = []
14
- parts << components.map { |c| transform_component(graph, c) }.join("\n\n")
26
+ parts << components.map { |c| transform_component(graph, c, variable_state) }.join("\n\n")
15
27
  parts << transform_variable_state(graph, variable_state) unless variable_state.nil?
16
28
  parts = parts.compact.join("\n\n")
17
29
  return "@startuml Programmgraph\n#{parts}\n@enduml\n"
@@ -25,14 +37,14 @@ module PgVerify
25
37
  }.join("\n")
26
38
  end
27
39
 
28
- def transform_component(graph, component)
40
+ def transform_component(graph, component, variable_state)
29
41
  # Transform component states
30
42
  states_s = component.states.map { |s| transform_state(component, s) }.join("\n")
31
43
  # Transform component transitions
32
44
  trans_s = component.transitions.map { |t| transform_transition(component, t) }.join("\n")
33
45
  # Transform component variables
34
46
  vars = graph.variables.select_by_owner(component.name)
35
- vars_s = transform_variables(component, vars)
47
+ vars_s = transform_variables(component, vars, variable_state)
36
48
  # Transform the initial state
37
49
  initial_s = transform_initial(graph, component)
38
50
 
@@ -59,10 +71,14 @@ module PgVerify
59
71
  end
60
72
 
61
73
  def transform_transition(component, transition)
62
- label = [ transition.precon, transition.guard ].map(&:to_s).reject(&:empty?).join(" && ")
63
- label += "/ " + transition.action.to_s unless transition.action.nil?
74
+ precon = @render_precons ? transition.precon : nil
75
+ guard = @render_guards ? transition.guard : nil
76
+ action = @render_actions ? transition.action : nil
64
77
 
78
+ label = [ precon, guard ].map(&:to_s).reject(&:empty?).join(" && ")
79
+ label += " / " + action.to_s unless action.nil?
65
80
  label = ": #{label}" unless label.strip.empty?
81
+ label = "" unless @render_labels
66
82
  return "#{component.name}_#{transition.src_state} --> #{component.name}_#{transition.tgt_state} #{label}"
67
83
  end
68
84
 
@@ -70,9 +86,12 @@ module PgVerify
70
86
  str.split("\n").map { |s| "\t#{s}" }.join("\n")
71
87
  end
72
88
 
73
- def transform_variables(component, variables)
74
- return nil if variables.empty?
75
- body = variables.map { |v| "#{v.name} => #{transform_range(v.range)}" }.join("\n")
89
+ def transform_variables(component, variables, variable_state)
90
+ return nil if variables.empty?
91
+ body = variables.map { |var|
92
+ value = variable_state.nil? ? transform_range(var.range) : variable_state[var.name]
93
+ "#{var.name} => #{value}"
94
+ }.join("\n")
76
95
  return "map #{component.name}Variables {\n#{body}\n}"
77
96
  end
78
97
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgVerify
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/vscript.rb ADDED
@@ -0,0 +1,64 @@
1
+
2
+ task :gen_examples do
3
+ files = Dir["integration_tests/ruby_dsl/*.rb"].select(&:file?)
4
+
5
+ target_dir = "examples"
6
+ FileUtils.rm_rf(target_dir); FileUtils.mkdir(target_dir)
7
+
8
+ files.each { |file|
9
+ json = x "./devpg show json --script #{file}", verbose: false
10
+ yaml = x "./devpg show yaml --script #{file}", verbose: false
11
+ json_file = File.join(target_dir, File.basename(file, '.rb') + ".json")
12
+ yaml_file = File.join(target_dir, File.basename(file, '.rb') + ".yaml")
13
+ FileUtils.cp(file, File.join(target_dir, File.basename(file)))
14
+ File.write(json_file, json)
15
+ File.write(yaml_file, yaml)
16
+
17
+ x "tar -czf examples.tar.gz examples/"
18
+ }
19
+ FileUtils.rm_rf(target_dir)
20
+
21
+ done "Generated examples.tar.gz"
22
+ end
23
+
24
+ task :integration_test do
25
+
26
+ work_dir = ".pg-work"
27
+ FileUtils.rm_rf(work_dir); FileUtils.mkdir(work_dir)
28
+
29
+ test_files = Dir["integration_tests/ruby_dsl/*.rb"].sort.select(&:file?)
30
+
31
+
32
+ # Create json files for all tests
33
+ test_files.each { |source|
34
+ log "JSON #{File.basename(source)}"
35
+ target = File.join(work_dir, File.basename(source, '.rb') + ".json")
36
+ content = x "./devpg show json --script #{source}", verbose: false
37
+ File.write(target, content)
38
+ }
39
+
40
+ # Create json files for all tests
41
+ test_files.each { |source|
42
+ log "YAML #{File.basename(source)}"
43
+ target = File.join(work_dir, File.basename(source, '.rb') + ".yaml")
44
+ content = x "./devpg show yaml --script #{source}", verbose: false
45
+ File.write(target, content)
46
+ }
47
+
48
+ # Run tests for all files
49
+ test_files.each { |source|
50
+ args = {
51
+ "script" => source,
52
+ "json-file" => File.join(work_dir, File.basename(source, '.rb') + ".json"),
53
+ }
54
+ commands = [ "show puml", "test", "dcca", "show png", "simulate" ]
55
+ args.each { |argument, file|
56
+ commands.each { |command|
57
+ log "pgv #{command} #{argument} | #{File.basename(file)}"
58
+ x "./devpg #{command} --#{argument} #{file}", verbose: false
59
+ }
60
+ }
61
+ }
62
+
63
+ done "Great success!"
64
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg-verify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arian Weber
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-12 00:00:00.000000000 Z
11
+ date: 2024-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -92,6 +92,10 @@ files:
92
92
  - devpg
93
93
  - doc/examples/railroad_crossing.rb
94
94
  - doc/examples/train-tree.rb
95
+ - doc/examples/vending_machine/checkpoint_1.rb
96
+ - doc/examples/vending_machine/checkpoint_2.rb
97
+ - doc/examples/vending_machine/checkpoint_3.rb
98
+ - doc/examples/vending_machine/checkpoint_4.rb
95
99
  - doc/examples/weidezaun.rb
96
100
  - doc/examples/weidezaun.txt
97
101
  - doc/expose/definition.png
@@ -115,8 +119,13 @@ files:
115
119
  - integration_tests/ruby_dsl/014_tau_transitions.rb
116
120
  - integration_tests/ruby_dsl/015_basic_dcca.rb
117
121
  - integration_tests/ruby_dsl/016_pressure_tank.rb
122
+ - integration_tests/ruby_dsl/017_ctl_specifications.rb
123
+ - integration_tests/ruby_dsl/018_non_atomic_hazard.rb
124
+ - integration_tests/ruby_dsl/019_multiple_actions.rb
125
+ - integration_tests/ruby_dsl/020_vending_machine.rb
118
126
  - lib/pg-verify.rb
119
127
  - lib/pg-verify/cli/cli.rb
128
+ - lib/pg-verify/cli/cli_utils.rb
120
129
  - lib/pg-verify/core/cmd_runner.rb
121
130
  - lib/pg-verify/core/core.rb
122
131
  - lib/pg-verify/core/extensions/array_extensions.rb
@@ -170,6 +179,7 @@ files:
170
179
  - lib/pg-verify/nusmv/nusmv.rb
171
180
  - lib/pg-verify/nusmv/runner.rb
172
181
  - lib/pg-verify/puml/puml.rb
182
+ - lib/pg-verify/puml/runner.rb
173
183
  - lib/pg-verify/shell/loading/line_animation.rb
174
184
  - lib/pg-verify/shell/loading/loading_animation.rb
175
185
  - lib/pg-verify/shell/loading/loading_prompt.rb
@@ -185,6 +195,7 @@ files:
185
195
  - lib/pg-verify/version.rb
186
196
  - pg-verify.gemspec
187
197
  - sig/pg-verify.rbs
198
+ - vscript.rb
188
199
  homepage: https://github.com/ArianWeber/pg-verify
189
200
  licenses: []
190
201
  metadata: