state_machines-graphviz 0.0.2 → 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 +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +14 -2
- data/lib/state_machines/graphviz/graph.rb +5 -5
- data/lib/state_machines/graphviz/renderer.rb +155 -0
- data/lib/state_machines/graphviz/version.rb +3 -1
- data/lib/state_machines/graphviz.rb +33 -1
- data/lib/state_machines/tasks/railtie.rb +3 -2
- data/lib/state_machines/tasks/state_machines.rake +2 -0
- data/lib/state_machines-graphviz.rb +7 -0
- data/{spec → test}/files/switch.rb +2 -0
- data/test/graph_default_test.rb +24 -0
- data/test/graph_edges_test.rb +28 -0
- data/test/graph_nodes_test.rb +25 -0
- data/test/graph_output_test.rb +21 -0
- data/test/machine_class_drawing_test.rb +28 -0
- data/test/machine_drawing_test.rb +72 -0
- data/test/machine_drawing_with_dynamic_states_test.rb +38 -0
- data/test/machine_drawing_with_integer_states_test.rb +38 -0
- data/test/machine_drawing_with_nil_states_test.rb +37 -0
- data/test/state_drawing_final_test.rb +17 -0
- data/test/state_drawing_initial_test.rb +25 -0
- data/test/state_drawing_lambda_value_test.rb +21 -0
- data/test/state_drawing_nil_name_test.rb +22 -0
- data/test/state_drawing_non_final_test.rb +20 -0
- data/test/state_drawing_test.rb +33 -0
- data/test/state_drawing_with_human_name_test.rb +20 -0
- data/test/test_helper.rb +6 -0
- metadata +61 -28
- data/.gitignore +0 -23
- data/.rspec +0 -3
- data/.travis.yml +0 -12
- data/Gemfile +0 -4
- data/Rakefile +0 -13
- data/lib/state_machines/graphviz/monkeypatch.rb +0 -161
- data/lib/state_machines/tasks/state_machines.rb +0 -32
- data/spec/graph_spec.rb +0 -92
- data/spec/machine_drawing_spec.rb +0 -215
- data/spec/machine_spec.rb +0 -19
- data/spec/spec_helper.rb +0 -2
- data/spec/state_spec.rb +0 -142
- data/state_machines-graphviz.gemspec +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 90020c1871d32a31894619ff9883ae27e08210499cd4a1c0361b18ad503c04b1
|
|
4
|
+
data.tar.gz: 9f206b3af3589f1072c8514b182d57c066e1de9179effafb6351a63471f8d61f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cabca272749f9e4e5928cea279b45503e78268b5060e6acb6b50e61fcd5ef31800537ad5da64e7730afceeffaf3f560f18d8e0e96104ab8e00cab15acc27371f
|
|
7
|
+
data.tar.gz: d410ac462911bd82ae5827c71045cef33b4e1413d0abb079f1e1124c1f58fe337b2c4313d2281aabc39bf9a51e92cb7666c191972c365badd83f4feea9749b90
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -19,6 +19,8 @@ Or install it yourself as:
|
|
|
19
19
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
22
|
+
The default output folder for the images is `doc/state_machines`.
|
|
23
|
+
|
|
22
24
|
#### Examples
|
|
23
25
|
|
|
24
26
|
To generate a graph for a specific file / class:
|
|
@@ -67,16 +69,26 @@ in the class. The generated files will use an output filename of the format
|
|
|
67
69
|
For examples of actual images generated using this task, see those under the
|
|
68
70
|
examples folder.
|
|
69
71
|
|
|
72
|
+
#### Example output
|
|
73
|
+
|
|
74
|
+
Generate the sample diagram used in this README:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bundle exec ruby script/generate_example.rb
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+

|
|
81
|
+
|
|
70
82
|
### Interactive graphs
|
|
71
83
|
|
|
72
|
-
Jean Bovet's [Visual Automata Simulator](
|
|
84
|
+
Jean Bovet's [Visual Automata Simulator](https://github.com/NimaGhaedsharafi/VAS)
|
|
73
85
|
is a great tool for "simulating, visualizing and transforming finite state
|
|
74
86
|
automata and Turing Machines". It can help in the creation of states and events
|
|
75
87
|
for your models. It is cross-platform, written in Java.
|
|
76
88
|
|
|
77
89
|
## Contributing
|
|
78
90
|
|
|
79
|
-
1. Fork it ( https://github.com/
|
|
91
|
+
1. Fork it ( https://github.com/state-machines/state_machines-graphviz/fork )
|
|
80
92
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
81
93
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
82
94
|
4. Push to the branch (`git push origin my-new-feature`)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module StateMachines
|
|
2
4
|
# Provides a set of higher-order features on top of the raw GraphViz graphs
|
|
3
5
|
class Graph < GraphViz
|
|
@@ -23,12 +25,10 @@ module StateMachines
|
|
|
23
25
|
# "landscape"). Default is "portrait".
|
|
24
26
|
def initialize(name, options = {})
|
|
25
27
|
options = { path: 'doc/state_machines', format: 'png', font: 'Arial', orientation: 'portrait' }.merge(options)
|
|
26
|
-
|
|
28
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :path, :format, :font, :orientation)
|
|
27
29
|
|
|
28
|
-
# TODO fail if path cannot be created or readonly
|
|
29
|
-
unless Dir.exist? options[:path]
|
|
30
|
-
FileUtils.mkpath(options[:path])
|
|
31
|
-
end
|
|
30
|
+
# TODO: fail if path cannot be created or readonly
|
|
31
|
+
FileUtils.mkpath(options[:path]) unless Dir.exist? options[:path]
|
|
32
32
|
@font = options[:font]
|
|
33
33
|
@file_path = File.join(options[:path], "#{name}.#{options[:format]}")
|
|
34
34
|
@file_format = options[:format]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'state_machines-diagram'
|
|
4
|
+
|
|
5
|
+
module StateMachines
|
|
6
|
+
module Graphviz
|
|
7
|
+
module Renderer
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def draw_machine(machine, io: $stdout, **options)
|
|
11
|
+
diagram, builder = StateMachines::Diagram::Renderer.build_state_diagram(machine, options)
|
|
12
|
+
graph_options = options.dup
|
|
13
|
+
name = graph_options.delete(:name) || "#{machine.owner_class.name}_#{machine.name}"
|
|
14
|
+
|
|
15
|
+
draw_options = { human_name: false }
|
|
16
|
+
draw_options[:human_name] = graph_options.delete(:human_names) if graph_options.include?(:human_names)
|
|
17
|
+
|
|
18
|
+
graph = Graph.new(name, graph_options)
|
|
19
|
+
render_diagram(graph, diagram, builder, draw_options)
|
|
20
|
+
graph.output
|
|
21
|
+
graph
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def draw_state(state, graph, options = {}, io = $stdout)
|
|
25
|
+
node = graph.add_nodes(
|
|
26
|
+
state.name ? state.name.to_s : 'nil',
|
|
27
|
+
label: state.description(options),
|
|
28
|
+
width: '1',
|
|
29
|
+
height: '1',
|
|
30
|
+
shape: state.final? ? 'doublecircle' : 'ellipse'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
graph.add_edges(graph.add_nodes('starting_state', shape: 'point'), node) if state.initial?
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def draw_event(event, graph, options = {}, io = $stdout)
|
|
38
|
+
machine = event.machine
|
|
39
|
+
valid_states = machine.states.by_priority.map(&:name)
|
|
40
|
+
event_label = options[:human_name] ? event.human_name(machine.owner_class) : event.name.to_s
|
|
41
|
+
|
|
42
|
+
event.branches.each do |branch|
|
|
43
|
+
draw_branch_with_label(branch, graph, event_label, machine, valid_states)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def draw_branch(branch, graph, event, valid_states, io = $stdout)
|
|
50
|
+
machine = event.machine
|
|
51
|
+
draw_branch_with_label(branch, graph, event.name.to_s, machine, valid_states)
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def render_diagram(graph, diagram, builder, draw_options)
|
|
58
|
+
state_lookup = build_state_lookup(builder.machine)
|
|
59
|
+
state_metadata = builder&.state_metadata || {}
|
|
60
|
+
start_node = nil
|
|
61
|
+
|
|
62
|
+
diagram.states.each do |state_node|
|
|
63
|
+
metadata = state_metadata[state_node.id] || {}
|
|
64
|
+
state = state_lookup[state_node.id]
|
|
65
|
+
label = state ? state.description(human_name: draw_options[:human_name]) : (state_node.label || state_node.id)
|
|
66
|
+
shape = metadata[:type] == 'final' ? 'doublecircle' : 'ellipse'
|
|
67
|
+
|
|
68
|
+
node = graph.add_nodes(
|
|
69
|
+
graph_node_id(state_node.id),
|
|
70
|
+
label: label,
|
|
71
|
+
width: '1',
|
|
72
|
+
height: '1',
|
|
73
|
+
shape: shape
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if metadata[:type] == 'initial'
|
|
77
|
+
start_node ||= graph.add_nodes('starting_state', shape: 'point')
|
|
78
|
+
graph.add_edges(start_node, node)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
transition_metadata = transition_metadata_map(builder)
|
|
83
|
+
diagram.transitions.each do |transition|
|
|
84
|
+
metadata = transition_metadata[transition] || {}
|
|
85
|
+
callbacks = metadata[:callbacks] || {}
|
|
86
|
+
|
|
87
|
+
graph.add_edges(
|
|
88
|
+
graph_node_id(transition.source_state_id),
|
|
89
|
+
graph_node_id(transition.target_state_id),
|
|
90
|
+
label: transition.label.to_s,
|
|
91
|
+
labelfontsize: 10,
|
|
92
|
+
taillabel: Array(callbacks[:before]).compact.join("\n"),
|
|
93
|
+
headlabel: Array(callbacks[:after]).compact.join("\n")
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_state_lookup(machine)
|
|
99
|
+
machine.states.each_with_object({}) do |state, memo|
|
|
100
|
+
key = state.name ? state.name.to_s : 'nil_state'
|
|
101
|
+
memo[key] = state
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def graph_node_id(diagram_state_id)
|
|
106
|
+
diagram_state_id == 'nil_state' ? 'nil' : diagram_state_id
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def transition_metadata_map(builder)
|
|
110
|
+
Array(builder&.transition_metadata).each_with_object({}) do |metadata, memo|
|
|
111
|
+
transition = metadata[:transition]
|
|
112
|
+
memo[transition] = metadata if transition
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def draw_branch_with_label(branch, graph, event_label, machine, valid_states)
|
|
117
|
+
branch.state_requirements.each do |state_requirement|
|
|
118
|
+
from_states = state_requirement[:from].filter(valid_states)
|
|
119
|
+
|
|
120
|
+
if state_requirement[:to].values.empty?
|
|
121
|
+
loopback = true
|
|
122
|
+
else
|
|
123
|
+
to_state = state_requirement[:to].values.first
|
|
124
|
+
to_state = to_state ? to_state.to_s : 'nil'
|
|
125
|
+
loopback = false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
from_states.each do |from_state|
|
|
129
|
+
from_state = from_state ? from_state.to_s : 'nil'
|
|
130
|
+
graph.add_edges(
|
|
131
|
+
from_state,
|
|
132
|
+
loopback ? from_state : to_state,
|
|
133
|
+
label: event_label,
|
|
134
|
+
labelfontsize: 10,
|
|
135
|
+
taillabel: callback_method_names(machine, branch, :before).join("\n"),
|
|
136
|
+
headlabel: callback_method_names(machine, branch, :after).join("\n")
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def callback_method_names(machine, branch, type)
|
|
143
|
+
event_name = branch.event_requirement.values.first
|
|
144
|
+
machine.callbacks[type].select do |callback|
|
|
145
|
+
callback.branch.matches?(branch,
|
|
146
|
+
from: branch.state_requirements.map { |req| req[:from] },
|
|
147
|
+
to: branch.state_requirements.map { |req| req[:to] },
|
|
148
|
+
on: event_name)
|
|
149
|
+
end.flat_map do |callback|
|
|
150
|
+
callback.instance_variable_get('@methods')
|
|
151
|
+
end.compact
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -1,7 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'state_machines'
|
|
2
4
|
require 'graphviz'
|
|
3
|
-
require 'state_machines/graphviz/monkeypatch'
|
|
4
5
|
require 'state_machines/graphviz/graph'
|
|
6
|
+
require 'state_machines/graphviz/renderer'
|
|
5
7
|
require 'state_machines/graphviz/version'
|
|
6
8
|
|
|
9
|
+
module StateMachines
|
|
10
|
+
module Graphviz
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def draw(class_names, options = {})
|
|
14
|
+
if class_names.nil? || class_names.to_s.split(',').empty?
|
|
15
|
+
raise ArgumentError, 'At least one class must be specified'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Load any files
|
|
19
|
+
if (files = options.delete(:file))
|
|
20
|
+
files.split(',').each { |file| require file }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class_names.to_s.split(',').each do |class_name|
|
|
24
|
+
# Navigate through the namespace structure to get to the class
|
|
25
|
+
klass = Object
|
|
26
|
+
class_name.split('::').each do |name|
|
|
27
|
+
klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Draw each of the class's state machines
|
|
31
|
+
klass.state_machines.each_value do |machine|
|
|
32
|
+
machine.draw(**options)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
7
39
|
require 'state_machines/tasks/railtie' if defined?(Rails)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'state_machines/graphviz'
|
|
2
4
|
require 'rails'
|
|
3
5
|
module StateMachines
|
|
4
|
-
# https://gist.github.com/josevalim/af7e572c2dc973add221
|
|
5
6
|
class Railtie < Rails::Railtie
|
|
6
7
|
rake_tasks do
|
|
7
8
|
load 'state_machines/tasks/state_machines.rake'
|
|
8
9
|
end
|
|
9
10
|
end
|
|
10
|
-
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require './test/test_helper'
|
|
4
|
+
describe StateMachines::Graph do
|
|
5
|
+
def setup
|
|
6
|
+
@graph = StateMachines::Graph.new('test')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_should_have_a_default_font
|
|
10
|
+
assert_equal('Arial', @graph.font)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_should_use_current_directory_for_filepath
|
|
14
|
+
assert_equal('doc/state_machines/test.png', @graph.file_path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_should_have_a_default_file_format
|
|
18
|
+
assert_equal('png', @graph.file_format)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_should_have_a_default_orientation
|
|
22
|
+
assert_equal('TB', @graph[:rankdir].source)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
describe StateMachines::Graph do
|
|
5
|
+
def setup
|
|
6
|
+
@graph = StateMachines::Graph.new('test')
|
|
7
|
+
@graph.add_nodes('parked', shape: 'ellipse')
|
|
8
|
+
@graph.add_nodes('idling', shape: 'ellipse')
|
|
9
|
+
@edge = @graph.add_edges('parked', 'idling', label: 'ignite')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_should_return_generated_edge
|
|
13
|
+
refute_nil(@edge)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_should_use_specified_nodes
|
|
17
|
+
assert_equal('parked', @edge.node_one(false))
|
|
18
|
+
assert_equal('idling', @edge.node_two(false))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_should_use_specified_options
|
|
22
|
+
assert_equal('ignite', @edge['label'].to_s.gsub('"', ''))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_should_set_default_font
|
|
26
|
+
assert_equal('Arial', @edge['fontname'].to_s.gsub('"', ''))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
describe StateMachines::Graph do
|
|
5
|
+
def setup
|
|
6
|
+
@graph = StateMachines::Graph.new('test')
|
|
7
|
+
@node = @graph.add_nodes('parked', shape: 'ellipse')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_should_return_generated_node
|
|
11
|
+
refute_nil @node
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_should_use_specified_name
|
|
15
|
+
assert_equal(@node, @graph.get_node('parked'))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_should_use_specified_options
|
|
19
|
+
assert_equal('ellipse', @node['shape'].to_s.gsub('"', ''))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_should_set_default_font
|
|
23
|
+
assert_equal('Arial', @node['fontname'].to_s.gsub('"', ''))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
describe StateMachines::Graph do
|
|
5
|
+
def setup
|
|
6
|
+
@graph_name = "test_#{rand(1_000_000)}"
|
|
7
|
+
@graph = StateMachines::Graph.new(@graph_name)
|
|
8
|
+
@graph.add_nodes('parked', shape: 'ellipse')
|
|
9
|
+
@graph.add_nodes('idling', shape: 'ellipse')
|
|
10
|
+
@graph.add_edges('parked', 'idling', label: 'ignite')
|
|
11
|
+
@graph.output
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_should_save_file
|
|
15
|
+
assert File.exist?("doc/state_machines/#{@graph_name}.png")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def teardown
|
|
19
|
+
FileUtils.rm Dir["doc/state_machines/#{@graph_name}.png"]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
describe StateMachines::Machine do
|
|
6
|
+
def switch_file
|
|
7
|
+
File.expand_path("#{File.dirname(__FILE__)}/files/switch.rb")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def switch_machine
|
|
11
|
+
require switch_file
|
|
12
|
+
Switch.state_machines.values.first
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_should_load_files
|
|
16
|
+
machine = switch_machine
|
|
17
|
+
assert(defined?(::Switch))
|
|
18
|
+
assert(machine)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_should_allow_path_and_format_to_be_customized
|
|
22
|
+
machine = switch_machine
|
|
23
|
+
output_path = File.dirname(__FILE__)
|
|
24
|
+
machine.draw(path: output_path, format: 'jpg')
|
|
25
|
+
assert(File.exist?("#{output_path}/#{Switch.name}_state.jpg"))
|
|
26
|
+
FileUtils.rm Dir["{.,#{output_path}}/#{Switch.name}_state.{jpg,png}"]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
describe StateMachines::Graph do
|
|
6
|
+
def setup
|
|
7
|
+
@klass = Class.new do
|
|
8
|
+
def self.name
|
|
9
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@machine = StateMachines::Machine.new(@klass, initial: :parked)
|
|
14
|
+
@machine.event :ignite do
|
|
15
|
+
transition parked: :idling
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_should_raise_exception_if_invalid_option_specified
|
|
20
|
+
assert_raises(ArgumentError) { @machine.draw(invalid: true) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_should_save_file_with_class_name_by_default
|
|
24
|
+
@machine.draw
|
|
25
|
+
assert File.exist?("doc/state_machines/#{@klass.name}_state.png"), 'Failed to save file with class name'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_should_allow_base_name_to_be_customized
|
|
29
|
+
name = "@machine_#{rand(1_000_000)}"
|
|
30
|
+
@machine.draw(name: name)
|
|
31
|
+
@path = "doc/state_@machines/#{name}.png"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_should_allow_format_to_be_customized
|
|
35
|
+
@machine.draw(format: 'jpg')
|
|
36
|
+
@path = "doc/state_machines/#{@klass.name}_state.jpg"
|
|
37
|
+
assert File.exist?(@path), 'allow format to be custom'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_should_allow_path_to_be_customized
|
|
41
|
+
@machine.draw(path: "#{File.dirname(__FILE__)}/")
|
|
42
|
+
@path = "#{File.dirname(__FILE__)}/#{@klass.name}_state.png"
|
|
43
|
+
assert(File.exist?(@path))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_should_allow_orientation_to_be_landscape
|
|
47
|
+
@graph = @machine.draw(orientation: 'landscape')
|
|
48
|
+
assert_equal(@graph['rankdir'].to_s.gsub('"', ''), 'LR')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_should_allow_orientation_to_be_portrait
|
|
52
|
+
@graph = @machine.draw(orientation: 'portrait')
|
|
53
|
+
assert_equal(@graph['rankdir'].to_s.gsub('"', ''), 'TB')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_should_allow_human_names_to_be_displayed
|
|
57
|
+
@machine.event :ignite, human_name: 'Ignite'
|
|
58
|
+
@machine.state :parked, human_name: 'Parked'
|
|
59
|
+
@machine.state :idling, human_name: 'Idling'
|
|
60
|
+
@graph = @machine.draw(human_names: true)
|
|
61
|
+
|
|
62
|
+
parked_node = @graph.get_node('parked')
|
|
63
|
+
assert_equal(parked_node['label'].to_s.gsub('"', ''), 'Parked')
|
|
64
|
+
|
|
65
|
+
idling_node = @graph.get_node('idling')
|
|
66
|
+
assert_equal(idling_node['label'].to_s.gsub('"', ''), 'Idling')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def teardown
|
|
70
|
+
FileUtils.rm Dir[@path || "doc/state_@machines/#{@klass.name}_state.png"]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
describe StateMachines::Graph do
|
|
6
|
+
def setup
|
|
7
|
+
@klass = Class.new do
|
|
8
|
+
def self.name
|
|
9
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@machine = StateMachines::Machine.new(@klass, initial: :parked)
|
|
14
|
+
@machine.event :activate do
|
|
15
|
+
transition parked: :idling
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@machine.state :idling, value: -> { Time.now }
|
|
19
|
+
|
|
20
|
+
@graph = @machine.draw
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_should_draw_all_states
|
|
24
|
+
assert(@graph.node_count, 3)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_should_draw_all_events
|
|
28
|
+
assert(@graph.edge_count, 2)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_should_draw_machine
|
|
32
|
+
assert(File.exist?("doc/state_machines/#{@klass.name}_state.png"))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def teardown
|
|
36
|
+
FileUtils.rm Dir["doc/state_machines/#{@klass.name}_state.png"]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
describe StateMachines::Graph do
|
|
6
|
+
def setup
|
|
7
|
+
@klass = Class.new do
|
|
8
|
+
def self.name
|
|
9
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@machine = StateMachines::Machine.new(@klass, :state_id, initial: :parked)
|
|
14
|
+
@machine.event :ignite do
|
|
15
|
+
transition parked: :idling
|
|
16
|
+
end
|
|
17
|
+
@machine.state :parked, value: 1
|
|
18
|
+
@machine.state :idling, value: 2
|
|
19
|
+
|
|
20
|
+
@graph = @machine.draw
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_should_draw_all_states
|
|
24
|
+
assert_equal(@graph.node_count, 3)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_should_draw_all_events
|
|
28
|
+
assert_equal(@graph.edge_count, 2)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_should_draw_machine
|
|
32
|
+
assert(File.exist?("doc/state_machines/#{@klass.name}_state_id.png"))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def teardown
|
|
36
|
+
assert(File.exist?("doc/state_machines/#{@klass.name}_state_id.png"))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
describe StateMachines::Graph do
|
|
6
|
+
def setup
|
|
7
|
+
@klass = Class.new do
|
|
8
|
+
def self.name
|
|
9
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@machine = StateMachines::Machine.new(@klass, initial: :parked)
|
|
14
|
+
@machine.event :ignite do
|
|
15
|
+
transition parked: :idling
|
|
16
|
+
end
|
|
17
|
+
@machine.state :parked, value: nil
|
|
18
|
+
|
|
19
|
+
@graph = @machine.draw
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_should_draw_all_states
|
|
23
|
+
assert(@graph.node_count, 3)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_should_draw_all_events
|
|
27
|
+
assert(@graph.edge_count, 2)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_should_draw_machine
|
|
31
|
+
assert(File.exist?("doc/state_machines/#{@klass.name}_state.png"))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def teardown
|
|
35
|
+
assert(FileUtils.rm(Dir["doc/state_machines/#{@klass.name}_state.png"]))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
describe StateMachines::Graph do
|
|
5
|
+
def setup
|
|
6
|
+
@machine = StateMachines::Machine.new(Class.new)
|
|
7
|
+
@machine.states << @state = StateMachines::State.new(@machine, :parked)
|
|
8
|
+
|
|
9
|
+
graph = StateMachines::Graph.new('test')
|
|
10
|
+
@state.draw(graph)
|
|
11
|
+
@node = graph.get_node('parked')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_should_use_doublecircle_as_shape
|
|
15
|
+
assert_equal('doublecircle', @node['shape'].to_s.gsub('"', ''))
|
|
16
|
+
end
|
|
17
|
+
end
|