pg-verify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +98 -0
- data/README.md +29 -0
- data/Rakefile +62 -0
- data/bin/console +15 -0
- data/bin/pg-verify.rb +18 -0
- data/bin/setup +8 -0
- data/calc.ebnf +21 -0
- data/data/config/pg-verify.yml +66 -0
- data/data/nusmv.sample.smv +179 -0
- data/data/project-template/.gitignore.resource +4 -0
- data/data/project-template/.pg-verify.yml +0 -0
- data/data/project-template/README.md +18 -0
- data/data/project-template/addon/.keep +0 -0
- data/data/project-template/program-graph.rb.resource +103 -0
- data/devpg +5 -0
- data/doc/examples/railroad_crossing.rb +61 -0
- data/doc/examples/train-tree.rb +43 -0
- data/doc/examples/weidezaun.rb +99 -0
- data/doc/examples/weidezaun.txt +29 -0
- data/doc/expose/definition.png +0 -0
- data/doc/expose/diagram.png +0 -0
- data/doc/expose/expose.md +359 -0
- data/doc/expose/validity.png +0 -0
- data/exe/pg-verify +4 -0
- data/integration_tests/ruby_dsl/001_states.rb +10 -0
- data/integration_tests/ruby_dsl/002_transitions.rb +10 -0
- data/integration_tests/ruby_dsl/003_actions.rb +14 -0
- data/integration_tests/ruby_dsl/004_guards.rb +18 -0
- data/integration_tests/ruby_dsl/005_variables.rb +16 -0
- data/integration_tests/ruby_dsl/006_state_variables.rb +26 -0
- data/integration_tests/ruby_dsl/007_variable_initialization.rb +28 -0
- data/integration_tests/ruby_dsl/008_state_initialization.rb +19 -0
- data/integration_tests/ruby_dsl/009_shared_variables.rb +26 -0
- data/integration_tests/ruby_dsl/010_complex_guards.rb +18 -0
- data/integration_tests/ruby_dsl/011_complex_actions.rb +16 -0
- data/integration_tests/ruby_dsl/012_error_components.rb +9 -0
- data/integration_tests/ruby_dsl/013_hazards.rb +25 -0
- data/integration_tests/ruby_dsl/014_tau_transitions.rb +26 -0
- data/integration_tests/ruby_dsl/015_basic_dcca.rb +19 -0
- data/integration_tests/ruby_dsl/016_pressure_tank.rb +146 -0
- data/lib/pg-verify/cli/cli.rb +235 -0
- data/lib/pg-verify/core/cmd_runner.rb +151 -0
- data/lib/pg-verify/core/core.rb +38 -0
- data/lib/pg-verify/core/extensions/array_extensions.rb +11 -0
- data/lib/pg-verify/core/extensions/enumerable_extensions.rb +19 -0
- data/lib/pg-verify/core/extensions/nil_extensions.rb +7 -0
- data/lib/pg-verify/core/extensions/string_extensions.rb +84 -0
- data/lib/pg-verify/core/shell/colorizer.rb +136 -0
- data/lib/pg-verify/core/shell/shell.rb +0 -0
- data/lib/pg-verify/core/util.rb +146 -0
- data/lib/pg-verify/doctor/doctor.rb +180 -0
- data/lib/pg-verify/ebnf_parser/ast.rb +31 -0
- data/lib/pg-verify/ebnf_parser/ebnf_parser.rb +26 -0
- data/lib/pg-verify/ebnf_parser/expression_parser.rb +177 -0
- data/lib/pg-verify/ebnf_parser/expression_parser2.rb +422 -0
- data/lib/pg-verify/ebnf_parser/expressions.ebnf +33 -0
- data/lib/pg-verify/ebnf_parser/expressions.peg +52 -0
- data/lib/pg-verify/ebnf_parser/parser_result.rb +26 -0
- data/lib/pg-verify/interpret/component_context.rb +125 -0
- data/lib/pg-verify/interpret/graph_context.rb +85 -0
- data/lib/pg-verify/interpret/interpret.rb +142 -0
- data/lib/pg-verify/interpret/pg_script.rb +72 -0
- data/lib/pg-verify/interpret/spec/ltl_builder.rb +90 -0
- data/lib/pg-verify/interpret/spec/spec_context.rb +32 -0
- data/lib/pg-verify/interpret/spec/spec_set_context.rb +67 -0
- data/lib/pg-verify/interpret/transition_context.rb +55 -0
- data/lib/pg-verify/model/allocation_set.rb +28 -0
- data/lib/pg-verify/model/assignment.rb +34 -0
- data/lib/pg-verify/model/component.rb +40 -0
- data/lib/pg-verify/model/dcca/hazard.rb +16 -0
- data/lib/pg-verify/model/dcca.rb +67 -0
- data/lib/pg-verify/model/expression.rb +106 -0
- data/lib/pg-verify/model/graph.rb +58 -0
- data/lib/pg-verify/model/model.rb +10 -0
- data/lib/pg-verify/model/parsed_expression.rb +77 -0
- data/lib/pg-verify/model/simulation/trace.rb +43 -0
- data/lib/pg-verify/model/simulation/variable_state.rb +23 -0
- data/lib/pg-verify/model/source_location.rb +45 -0
- data/lib/pg-verify/model/specs/spec.rb +44 -0
- data/lib/pg-verify/model/specs/spec_result.rb +25 -0
- data/lib/pg-verify/model/specs/spec_set.rb +43 -0
- data/lib/pg-verify/model/specs/specification.rb +50 -0
- data/lib/pg-verify/model/transition.rb +41 -0
- data/lib/pg-verify/model/validation/assignment_to_state_variable_validation.rb +26 -0
- data/lib/pg-verify/model/validation/empty_state_set_validation.rb +18 -0
- data/lib/pg-verify/model/validation/errors.rb +119 -0
- data/lib/pg-verify/model/validation/foreign_assignment_validation.rb +30 -0
- data/lib/pg-verify/model/validation/unknown_token_validation.rb +35 -0
- data/lib/pg-verify/model/validation/validation.rb +23 -0
- data/lib/pg-verify/model/variable.rb +47 -0
- data/lib/pg-verify/model/variable_set.rb +84 -0
- data/lib/pg-verify/nusmv/nusmv.rb +23 -0
- data/lib/pg-verify/nusmv/runner.rb +124 -0
- data/lib/pg-verify/puml/puml.rb +23 -0
- data/lib/pg-verify/shell/loading/line_animation.rb +36 -0
- data/lib/pg-verify/shell/loading/loading_animation.rb +80 -0
- data/lib/pg-verify/shell/loading/loading_prompt.rb +43 -0
- data/lib/pg-verify/shell/loading/no_animation.rb +20 -0
- data/lib/pg-verify/shell/shell.rb +30 -0
- data/lib/pg-verify/simulation/simulation.rb +7 -0
- data/lib/pg-verify/simulation/simulator.rb +90 -0
- data/lib/pg-verify/simulation/state.rb +53 -0
- data/lib/pg-verify/transform/hash_transformation.rb +104 -0
- data/lib/pg-verify/transform/nusmv_transformation.rb +261 -0
- data/lib/pg-verify/transform/puml_transformation.rb +89 -0
- data/lib/pg-verify/transform/transform.rb +8 -0
- data/lib/pg-verify/version.rb +5 -0
- data/lib/pg-verify.rb +47 -0
- data/pg-verify.gemspec +38 -0
- data/sig/pg-verify.rbs +4 -0
- metadata +226 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module PgVerify
|
2
|
+
module Model
|
3
|
+
module Validation
|
4
|
+
|
5
|
+
module EmptyStateSetValidation
|
6
|
+
|
7
|
+
def self.validate(model)
|
8
|
+
errors = model.components.map { |cmp|
|
9
|
+
next if cmp.states.length > 0
|
10
|
+
EmptyStateSetError.new(cmp)
|
11
|
+
}.compact
|
12
|
+
return errors
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module PgVerify
|
2
|
+
module Model
|
3
|
+
module Validation
|
4
|
+
|
5
|
+
class ValidationError < PgVerify::Core::Error
|
6
|
+
def initialize(model, errors)
|
7
|
+
@model, @errors = model, errors
|
8
|
+
end
|
9
|
+
|
10
|
+
def formatted()
|
11
|
+
title = "There are #{@errors.length} errors for model #{@model.name}"
|
12
|
+
|
13
|
+
summary = @errors.map{ |e| e.formatted.first }
|
14
|
+
summary = summary.each_with_index.map { |e, i| "#{i + 1}: #{e}" }.join("\n")
|
15
|
+
|
16
|
+
body = @errors.map(&:to_formatted).join("\n")
|
17
|
+
return title, "#{summary}\n\n#{body}"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class UnknownTokenError < PgVerify::Core::Error
|
23
|
+
def initialize(token, expression, varset)
|
24
|
+
@token, @expression, @varset = token, expression, varset
|
25
|
+
end
|
26
|
+
def formatted()
|
27
|
+
title = "Unknown token"
|
28
|
+
|
29
|
+
body = []
|
30
|
+
body << @expression.source_location.to_s.c_sidenote unless @expression.source_location.blank?
|
31
|
+
body << @expression.source_location.render_code_block() unless @expression.source_location.blank?
|
32
|
+
body << ""
|
33
|
+
body << "The expression '#{@expression.to_s.c_expression}' uses token #{@token.to_s.c_string}."
|
34
|
+
body << "This is neither a known variable, nor literal."
|
35
|
+
body << "Known variables: #{@varset.names.map(&:to_s).map(&:c_var).join(", ")}"
|
36
|
+
body << "Known literals: #{@varset.values.reject{ |v| v.is_a?(Numeric) }.map(&:to_s).map(&:c_literal).join(", ")}"
|
37
|
+
|
38
|
+
return title, body.join("\n")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class UnknownVariableError < PgVerify::Core::Error
|
43
|
+
def initialize(assigned_variable, assignment_expression, var_set)
|
44
|
+
@assigned_variable, @assignment_expression, @var_set = assigned_variable, assignment_expression, var_set
|
45
|
+
end
|
46
|
+
def formatted()
|
47
|
+
title = "Unknown variable '#{@assigned_variable}'"
|
48
|
+
|
49
|
+
body = []
|
50
|
+
body << @assignment_expression.source_location.to_s.c_sidenote
|
51
|
+
body << @assignment_expression.source_location.render_code_block()
|
52
|
+
body << ""
|
53
|
+
body << "The expression '#{@assignment_expression.to_s.c_expression}' uses variable #{@assigned_variable.to_s.c_var}."
|
54
|
+
body << "However there is no such variable in the context of this model."
|
55
|
+
body << "Known variables: #{@var_set.names.map(&:to_s).map(&:c_var).join(', ')}."
|
56
|
+
|
57
|
+
hint = "Declare variables before using them in expressions"
|
58
|
+
|
59
|
+
return title, body.join("\n"), hint
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ForeignVariableAssignmentError < PgVerify::Core::Error
|
64
|
+
def initialize(variable, expression, varset, component)
|
65
|
+
@variable, @expression, @varset = variable, expression, varset
|
66
|
+
@component = component
|
67
|
+
end
|
68
|
+
def formatted()
|
69
|
+
title = "Foreign variable assignment"
|
70
|
+
|
71
|
+
body = []
|
72
|
+
body << @expression.source_location.to_s.c_sidenote
|
73
|
+
body << @expression.source_location.render_code_block()
|
74
|
+
body << ""
|
75
|
+
body << "Component #{@component.name.to_s.c_cmp} assigns a value to variable #{@variable.to_s.c_var},"
|
76
|
+
body << "using this expression '#{@expression.to_s.c_expression}'."
|
77
|
+
body << "However #{@variable.to_s.c_var} is owned by component #{@varset[@variable].owner_name.to_s.c_cmp}"
|
78
|
+
|
79
|
+
hint = "A variable can only be written to by the component which declared it"
|
80
|
+
|
81
|
+
return title, body.join("\n"), hint
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class AssignmentToStateVariableError < PgVerify::Core::Error
|
86
|
+
def initialize(variable, expression, varset)
|
87
|
+
@variable, @expression, @varset = variable, expression, varset
|
88
|
+
end
|
89
|
+
def formatted()
|
90
|
+
title = "Assignment to state variable"
|
91
|
+
|
92
|
+
body = []
|
93
|
+
body << @expression.source_location.to_s.c_sidenote
|
94
|
+
body << @expression.source_location.render_code_block()
|
95
|
+
body << ""
|
96
|
+
body << "The expression '#{@expression.to_s.c_expression}' assigns a value to variable #{@variable.to_s.c_var}."
|
97
|
+
body << "However this variable is a state variable and can not be written to."
|
98
|
+
|
99
|
+
hint = "State variables are read only"
|
100
|
+
|
101
|
+
return title, body.join("\n"), hint
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class EmptyStateSetError < PgVerify::Core::Error
|
106
|
+
def initialize(component)
|
107
|
+
@component = component
|
108
|
+
end
|
109
|
+
def formatted()
|
110
|
+
title = "Empty state set"
|
111
|
+
body = "The graph component '#{@component.name.to_s.c_cmp}' does not define any states."
|
112
|
+
hint = "Each graph component needs at least one state"
|
113
|
+
return title, body, hint
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module PgVerify
|
2
|
+
module Model
|
3
|
+
module Validation
|
4
|
+
|
5
|
+
module ForeignAssignmentValidation
|
6
|
+
|
7
|
+
def self.validate(model)
|
8
|
+
errors = []
|
9
|
+
varset = model.all_variables()
|
10
|
+
|
11
|
+
model.components.each { |component|
|
12
|
+
actions = component.transitions.map(&:action).compact
|
13
|
+
actions.each { |a|
|
14
|
+
var_strings = a.assigned_variables()
|
15
|
+
# Null values (variable not known is not an error for this validation)
|
16
|
+
vars = var_strings.map { |vs| varset[vs] }.compact
|
17
|
+
vars.map { |var|
|
18
|
+
next if var.owner_name == component.name
|
19
|
+
errors << ForeignVariableAssignmentError.new(var.name, a, varset, component)
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
return errors
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module PgVerify
|
2
|
+
module Model
|
3
|
+
module Validation
|
4
|
+
|
5
|
+
module UnknownTokenValidation
|
6
|
+
|
7
|
+
def self.validate(model)
|
8
|
+
errors = []
|
9
|
+
varset = model.all_variables()
|
10
|
+
transitions = model.components.map(&:transitions).flatten
|
11
|
+
transitions.each { |tx|
|
12
|
+
errors += validate_expression(varset, tx.precon)
|
13
|
+
errors += validate_expression(varset, tx.guard)
|
14
|
+
errors += validate_expression(varset, tx.action)
|
15
|
+
|
16
|
+
}
|
17
|
+
return errors
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.validate_expression(varset, expression)
|
21
|
+
return [] if expression.nil?
|
22
|
+
tokens = expression.word_tokens()
|
23
|
+
tokens = tokens.reject { |token|
|
24
|
+
varset.varname?(token) || varset.constname?(token)
|
25
|
+
}
|
26
|
+
return tokens.map { |token|
|
27
|
+
UnknownTokenError.new(token, expression, varset)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module PgVerify
|
2
|
+
module Model
|
3
|
+
module Validation
|
4
|
+
|
5
|
+
def self.validate(model)
|
6
|
+
errors = []
|
7
|
+
errors += EmptyStateSetValidation.validate(model)
|
8
|
+
errors += UnknownTokenValidation.validate(model)
|
9
|
+
errors += ForeignAssignmentValidation.validate(model)
|
10
|
+
errors += AssignmentToStateVariableValidation.validate(model)
|
11
|
+
return errors
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.validate!(model)
|
15
|
+
errors = validate(model)
|
16
|
+
return if errors.empty?
|
17
|
+
raise ValidationError.new(model, errors)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module PgVerify
|
2
|
+
module Model
|
3
|
+
|
4
|
+
class Variable
|
5
|
+
|
6
|
+
# Name of this variable as a symbol
|
7
|
+
attr_accessor :name
|
8
|
+
|
9
|
+
# The name of the component which owns this variable as a symbol
|
10
|
+
attr_accessor :owner_name
|
11
|
+
|
12
|
+
# Range of this variable. This can either be a range like (0..2)
|
13
|
+
# an array of numbers like [0, 1, 2] or an array of symbols representing
|
14
|
+
# states of components like [:Idle, :Breaking]
|
15
|
+
attr_accessor :range
|
16
|
+
|
17
|
+
# An expression to be used to initialize this variable
|
18
|
+
# While this can be something simple like "my_var = 10", it can
|
19
|
+
# also be something more involved like "my_var > 10 && my_var < 15".
|
20
|
+
attr_accessor :init_expression
|
21
|
+
|
22
|
+
# The location in source, where this variable was defined
|
23
|
+
attr_accessor :source_location
|
24
|
+
|
25
|
+
def initialize(name, range, owner_name, source_location, init: nil)
|
26
|
+
init = Model::ParsedExpression.new(init, Model::ParsedExpression::TYPE_PL) if init.is_a?(String)
|
27
|
+
@name, @range, @owner_name, @init_expression = name, range, owner_name, init
|
28
|
+
@source_location = source_location
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns all possible values of this variable as an array
|
32
|
+
def values()
|
33
|
+
return @range if @range.is_a?(Array)
|
34
|
+
return @range.to_a
|
35
|
+
end
|
36
|
+
|
37
|
+
# A state variable represents the states of a component.
|
38
|
+
# e.g: Switch with range [:on, :off] for component switch.
|
39
|
+
def state_variable?()
|
40
|
+
return @name == @owner_name
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
|
2
|
+
module PgVerify
|
3
|
+
module Model
|
4
|
+
|
5
|
+
class VariableSet
|
6
|
+
|
7
|
+
attr_accessor :map
|
8
|
+
|
9
|
+
def initialize(*variables)
|
10
|
+
@map = {}
|
11
|
+
self << variables
|
12
|
+
end
|
13
|
+
|
14
|
+
def <<(var)
|
15
|
+
return @map[var.name] = var if var.is_a?(Variable)
|
16
|
+
return var.map { |v| self << v } if var.is_a?(Array)
|
17
|
+
raise "Not a variable '#{var}'"
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(*names)
|
21
|
+
return names.map { |n| self[n] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def +(varset)
|
25
|
+
varset = VariableSet.new(*varset) if varset.is_a?(Array)
|
26
|
+
raise "Not a variable set '#{varset}'" unless varset.is_a?(VariableSet)
|
27
|
+
return VariableSet.new(*(self.to_a() + varset.to_a()))
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_a()
|
31
|
+
return @map.values()
|
32
|
+
end
|
33
|
+
|
34
|
+
def names()
|
35
|
+
return @map.keys()
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](name)
|
39
|
+
name = name.to_sym
|
40
|
+
raise "No such variable #{name}!" unless @map.key?(name)
|
41
|
+
return @map[name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def include?(name_or_var)
|
45
|
+
return varname?(name_or_var) || constname?(name_or_var)
|
46
|
+
end
|
47
|
+
|
48
|
+
def empty?()
|
49
|
+
return @map.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def map(&blk)
|
53
|
+
return @map.values.map(&blk)
|
54
|
+
end
|
55
|
+
|
56
|
+
def varname?(name)
|
57
|
+
return @map.key?(name.to_sym)
|
58
|
+
end
|
59
|
+
|
60
|
+
def constname?(name)
|
61
|
+
values().map(&:to_s).include?(name.to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s()
|
65
|
+
return "{#{@map.map { |name, var| "#{name}:#{var.range}(#{var.owner_name})" }.join(", ")}}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def select_by_owner(owner)
|
69
|
+
owner = owner.name if owner.is_a?(Model::Component)
|
70
|
+
owner = owner.to_sym if owner.is_a?(String)
|
71
|
+
found = self.to_a().select { |var| var.owner_name == owner }
|
72
|
+
return VariableSet.new(*found)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns all possible values of variables in this set as an array
|
76
|
+
def values()
|
77
|
+
return self.to_a().map(&:values).flatten
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Require all module files
|
2
|
+
Dir[File.join(__dir__, "**", '*.rb')].sort.each { |file| require file }
|
3
|
+
|
4
|
+
module PgVerify
|
5
|
+
module NuSMV
|
6
|
+
|
7
|
+
class RawNuSMVError < PgVerify::Core::Error
|
8
|
+
def initialize(cmd, out, err, status, file)
|
9
|
+
@cmd, @out, @err, @status, @file = cmd, out, err, status, file
|
10
|
+
end
|
11
|
+
|
12
|
+
def formatted()
|
13
|
+
title = "Execution of NuSMV exited with #{@status}"
|
14
|
+
|
15
|
+
body = "The exact command was\n#{@cmd.c_string}\n\n"
|
16
|
+
body += "#{@out}\n#{@err}"
|
17
|
+
|
18
|
+
return title, body
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
|
2
|
+
module PgVerify
|
3
|
+
module NuSMV
|
4
|
+
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
def run_specs(program_graph)
|
8
|
+
nusmv_s = Transform::NuSmvTransformation.new.transform_graph(program_graph)
|
9
|
+
output = eval_nusmv(nusmv_s)
|
10
|
+
specs = program_graph.specification.flatten()
|
11
|
+
return parse_spec_results(program_graph, specs, output)
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse_spec_results(program_graph, specs, nusmv_output)
|
15
|
+
block = Struct.new(:success, :lines)
|
16
|
+
|
17
|
+
# Split the output into blocks which describe the
|
18
|
+
# specifications
|
19
|
+
blocks, current_block = [], nil
|
20
|
+
nusmv_output.split("\n").each { |line|
|
21
|
+
result = line[/-- specification .* is (true|false)/, 1]
|
22
|
+
if !result.nil?
|
23
|
+
blocks << current_block unless current_block.nil?
|
24
|
+
current_block = block.new(result == "true", [])
|
25
|
+
next
|
26
|
+
end
|
27
|
+
current_block.lines << line unless current_block.nil?
|
28
|
+
}
|
29
|
+
blocks << current_block unless current_block.nil?
|
30
|
+
|
31
|
+
return blocks.each_with_index.map { |block, index|
|
32
|
+
trace = block.success ? nil : block.lines.select { |l| l.start_with?(/\s+/) }
|
33
|
+
trace = parse_trace(program_graph, trace.join("\n")) unless trace.nil?
|
34
|
+
|
35
|
+
Model::SpecResult.new(specs[index], block.success, trace)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Calls NuSMV to calculate a possible state trace.
|
40
|
+
# Returns an array of states, where each state is a hash from
|
41
|
+
# variable to the value of that variable in that state
|
42
|
+
def run_simulation(program_graph, steps, random: false)
|
43
|
+
commands = []
|
44
|
+
commands << "read_model"
|
45
|
+
commands << "flatten_hierarchy"
|
46
|
+
commands << "encode_variables"
|
47
|
+
commands << "build_model"
|
48
|
+
commands << "pick_state #{random ? '-r' : ''}"
|
49
|
+
commands << "simulate -k #{steps.to_s.to_i} -v #{random ? '-r' : ''}"
|
50
|
+
commands << "quit"
|
51
|
+
nusmv_s = Transform::NuSmvTransformation.new.transform_graph(program_graph)
|
52
|
+
output = eval_nusmv(nusmv_s, commands: commands)
|
53
|
+
return parse_trace(program_graph, output)
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_trace(program_graph, nusmv_output)
|
57
|
+
var_states, current_var_state = [], nil
|
58
|
+
|
59
|
+
loop_index = -1
|
60
|
+
|
61
|
+
nusmv_output.split("\n").each { |line|
|
62
|
+
# Wait for heading of new state
|
63
|
+
if line.match(/\s*-> State: .+ <-/)
|
64
|
+
# Complete and store the old state if any
|
65
|
+
unless current_var_state.nil?
|
66
|
+
missing_keys = var_states.empty? ? [] : var_states[-1].keys - current_var_state.keys
|
67
|
+
missing_keys.each { |key|
|
68
|
+
current_var_state[key] = var_states[-1][key]
|
69
|
+
}
|
70
|
+
var_states << current_var_state
|
71
|
+
end
|
72
|
+
# Create a new state
|
73
|
+
current_var_state = {}
|
74
|
+
next
|
75
|
+
end
|
76
|
+
# Skip lines before the first state
|
77
|
+
next if current_var_state.nil?
|
78
|
+
|
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_", "")
|
87
|
+
current_var_state[key] = val
|
88
|
+
}
|
89
|
+
return Model::Trace.new(program_graph, var_states)
|
90
|
+
end
|
91
|
+
|
92
|
+
def eval_nusmv(nusmv_string, commands: [])
|
93
|
+
tmp_file = PgVerify.tmp_file("pg.smv")
|
94
|
+
File.write(tmp_file, nusmv_string)
|
95
|
+
return eval_file(tmp_file, commands: commands)
|
96
|
+
end
|
97
|
+
|
98
|
+
def eval_file(file, commands: [])
|
99
|
+
nusmv_path = find_nusmv_path()
|
100
|
+
raise "NuSMV not integrated! (TODO: Make a better error message)" if nusmv_path.blank?
|
101
|
+
if commands.blank?
|
102
|
+
nusmv_cmd = "#{nusmv_path} #{file}"
|
103
|
+
else
|
104
|
+
tmp_cmd_file = PgVerify.tmp_file("nusmv_commands")
|
105
|
+
File.write(tmp_cmd_file, commands.join("\n"))
|
106
|
+
nusmv_cmd = "#{nusmv_path} -source '#{tmp_cmd_file}' #{file}"
|
107
|
+
end
|
108
|
+
output, err, status = Open3.capture3({}, nusmv_cmd)
|
109
|
+
raise RawNuSMVError.new(nusmv_cmd, output, err, status, file) unless status.success?
|
110
|
+
return output
|
111
|
+
end
|
112
|
+
|
113
|
+
def find_nusmv_path
|
114
|
+
# Return by settings path if that exists
|
115
|
+
return Settings.nusmv.path if !Settings.nusmv.path.blank? && File.file?(Settings.nusmv.path)
|
116
|
+
|
117
|
+
# Fall back to looking in the addon directory
|
118
|
+
candidates = Dir[File.join(PgVerify.addon_dir, "*", "bin", "NuSMV*")]
|
119
|
+
return candidates.sort.first unless candidates.empty?
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Require all module files
|
2
|
+
Dir[File.join(__dir__, "**", '*.rb')].sort.each { |file| require file }
|
3
|
+
|
4
|
+
|
5
|
+
module PgVerify
|
6
|
+
module Puml
|
7
|
+
|
8
|
+
def self.find_path
|
9
|
+
# Return by settings path if that exists
|
10
|
+
return Settings.puml.path if !Settings.puml.path.blank? && File.file?(Settings.puml.path)
|
11
|
+
|
12
|
+
# Fall back to looking in the addon directory
|
13
|
+
candidates = Dir[File.join(PgVerify.addon_dir, "plantuml-*.jar")]
|
14
|
+
return candidates.sort.first unless candidates.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.convert_file(in_path, out_path)
|
18
|
+
# TODO: The PlantUML jar switches focus to the desktop is if it would
|
19
|
+
# attempt to open a window each time it is invoked.
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'loading_animation.rb'
|
2
|
+
|
3
|
+
module PgVerify
|
4
|
+
module Shell
|
5
|
+
|
6
|
+
class LineAnimation < LoadingAnimation
|
7
|
+
|
8
|
+
def initialize(loading_message)
|
9
|
+
super(loading_message)
|
10
|
+
@last_line = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_anim(passed_time)
|
14
|
+
color = (passed_time * 2).to_i % 2 == 0 ? :loading : :white
|
15
|
+
circle = (passed_time * 2).to_i % 2 == 0 ? "▶" : "-"
|
16
|
+
circle = circle.color(color)
|
17
|
+
" #{circle} #{string = format_line(@last_line)} "
|
18
|
+
end
|
19
|
+
|
20
|
+
def printl(string)
|
21
|
+
return unless started?()
|
22
|
+
string = string.to_s
|
23
|
+
string = (string ||= "").gsub("\n", '')
|
24
|
+
string = string.c_sidenote unless Colorizer.color?(string)
|
25
|
+
@last_line = string
|
26
|
+
# print_anim(string)
|
27
|
+
end
|
28
|
+
|
29
|
+
def format_line(string)
|
30
|
+
return loading_message() if string.blank?
|
31
|
+
"#{loading_message()} [ #{string} ]"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module PgVerify
|
2
|
+
module Shell
|
3
|
+
|
4
|
+
class LoadingAnimation
|
5
|
+
|
6
|
+
attr_reader :loading_message, :last_output_length, :start_time
|
7
|
+
|
8
|
+
def initialize(loading_message)
|
9
|
+
@loading_message = loading_message
|
10
|
+
@last_output_length = 0
|
11
|
+
@animation_thread = nil
|
12
|
+
@start_time = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
return unless @animation_thread.nil?
|
17
|
+
@start_time = Time.now
|
18
|
+
on_start()
|
19
|
+
@animation_thread = Thread.new {
|
20
|
+
begin
|
21
|
+
while true do
|
22
|
+
passed_time = Time.now - @start_time
|
23
|
+
string = on_anim(passed_time)
|
24
|
+
print_anim(string) unless string.nil?
|
25
|
+
sleep(1.0 / 10)
|
26
|
+
end
|
27
|
+
rescue => e
|
28
|
+
puts e
|
29
|
+
puts e.backtrace
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
}
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop(status, finish_message)
|
37
|
+
return if @animation_thread.nil?
|
38
|
+
@animation_thread.exit
|
39
|
+
prompt = Shell.gen_prompt(status)
|
40
|
+
string = on_stop(prompt, finish_message)
|
41
|
+
@animation_thread = nil
|
42
|
+
print_anim(string)
|
43
|
+
puts
|
44
|
+
end
|
45
|
+
|
46
|
+
def print_anim(string)
|
47
|
+
return if string.blank?
|
48
|
+
string = string.to_s
|
49
|
+
# Fill the string with spaces to overwrite the last printed string
|
50
|
+
print_string = string + ( " " * [0, @last_output_length - string.length].max )
|
51
|
+
# Set the length of the last print to the string length without spaces,
|
52
|
+
# since they do not need to be overwritten.
|
53
|
+
@last_output_length = string.length
|
54
|
+
# Print the string with spaces.
|
55
|
+
print("\r#{print_string}")
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_anim(passed_time); end
|
59
|
+
def on_start(); end
|
60
|
+
def printl(string); end
|
61
|
+
|
62
|
+
def on_stop(prompt, finish_message)
|
63
|
+
message = "#{prompt} #{@loading_message}"
|
64
|
+
message += " | #{finish_message}".c_sidenote unless finish_message.nil? || finish_message.empty?
|
65
|
+
if Settings.print_loading_times
|
66
|
+
duration = TimeUtil.duration_string((Time.now - @start_time).to_i)
|
67
|
+
duration_string = duration.nil? ? "" : "Took #{duration}".c_sidenote
|
68
|
+
return Shell.expand_to_console(message, duration_string)
|
69
|
+
else
|
70
|
+
return message
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def started?()
|
75
|
+
!@animation_thread.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|