pg-verify 0.1.0
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 +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
|