state_machines-graphviz 0.0.1
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/.gitignore +22 -0
- data/.rspec +3 -0
- data/.travis.yaml +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +29 -0
- data/Rakefile +10 -0
- data/lib/state_machines/graphviz.rb +5 -0
- data/lib/state_machines/graphviz/graph.rb +72 -0
- data/lib/state_machines/graphviz/monkeypatch.rb +161 -0
- data/lib/state_machines/graphviz/version.rb +6 -0
- data/spec/files/switch.rb +15 -0
- data/spec/graph_spec.rb +92 -0
- data/spec/machine_drawing_spec.rb +215 -0
- data/spec/machine_spec.rb +19 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/state_spec.rb +142 -0
- data/state_machines-graphviz.gemspec +25 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b375be8fce2b404843e60ffc947edb7d7b37a5e4
|
4
|
+
data.tar.gz: e46a7734205fd1ff0946dacb72195e82e1863d5a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db7731a6a0236d51ca0a888870ab4f0bf1670ade1832051b2e84bd7220290b8ac5b86d473c089608aeb667388c70665fb9f2acf10cc89d59ee57d7f76fe59965
|
7
|
+
data.tar.gz: 854bb07b3eff8a50165b6bbc7012e2304259711d768238d5a0fbf88f02898f6e5a9730f36971a5c5d6f810cc87d30a649e9bfbf33a62230e1ff1ef99c7eda6c8
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.travis.yaml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2006-2012 Aaron Pfeifer
|
2
|
+
Copyright (c) 2014 Abdelkader Boudih
|
3
|
+
|
4
|
+
MIT License
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# StateMachines::Graphviz
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'state_machine2-graphviz' , group: :development
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install state_machine2-graphviz
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/seuros/state_machine2-graphviz/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module StateMachines
|
2
|
+
# Provides a set of higher-order features on top of the raw GraphViz graphs
|
3
|
+
class Graph < GraphViz
|
4
|
+
# The name of the font to draw state names in
|
5
|
+
attr_reader :font
|
6
|
+
|
7
|
+
# The graph's full filename
|
8
|
+
attr_reader :file_path
|
9
|
+
|
10
|
+
# The image format to generate the graph in
|
11
|
+
attr_reader :file_format
|
12
|
+
|
13
|
+
# Creates a new graph with the given name.
|
14
|
+
#
|
15
|
+
# Configuration options:
|
16
|
+
# * <tt>:path</tt> - The path to write the graph file to. Default is the
|
17
|
+
# current directory (".").
|
18
|
+
# * <tt>:format</tt> - The image format to generate the graph in.
|
19
|
+
# Default is "png'.
|
20
|
+
# * <tt>:font</tt> - The name of the font to draw state names in.
|
21
|
+
# Default is "Arial".
|
22
|
+
# * <tt>:orientation</tt> - The direction of the graph ("portrait" or
|
23
|
+
# "landscape"). Default is "portrait".
|
24
|
+
def initialize(name, options = {})
|
25
|
+
options = { path: 'doc/state_machines', format: 'png', font: 'Arial', orientation: 'portrait' }.merge(options)
|
26
|
+
options.assert_valid_keys(:path, :format, :font, :orientation)
|
27
|
+
|
28
|
+
# TODO fail if path cannot be created or readonly
|
29
|
+
unless Dir.exist? options[:path]
|
30
|
+
FileUtils.mkpath(options[:path])
|
31
|
+
end
|
32
|
+
@font = options[:font]
|
33
|
+
@file_path = File.join(options[:path], "#{name}.#{options[:format]}")
|
34
|
+
@file_format = options[:format]
|
35
|
+
|
36
|
+
super('G', rankdir: options[:orientation] == 'landscape' ? 'LR' : 'TB')
|
37
|
+
end
|
38
|
+
|
39
|
+
# Generates the actual image file based on the nodes / edges added to the
|
40
|
+
# graph. The path to the file is based on the configuration options for
|
41
|
+
# this graph.
|
42
|
+
def output
|
43
|
+
super(@file_format => @file_path)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Adds a new node to the graph. The font for the node will be automatically
|
47
|
+
# set based on the graph configuration. The generated node will be returned.
|
48
|
+
#
|
49
|
+
# For example,
|
50
|
+
#
|
51
|
+
# graph = StateMachines::Graph.new('test')
|
52
|
+
# graph.add_nodes('parked', :label => 'Parked', :width => '1', :height => '1', :shape => 'ellipse')
|
53
|
+
def add_nodes(*args)
|
54
|
+
node = super
|
55
|
+
node.fontname = @font
|
56
|
+
node
|
57
|
+
end
|
58
|
+
|
59
|
+
# Adds a new edge to the graph. The font for the edge will be automatically
|
60
|
+
# set based on the graph configuration. The generated edge will be returned.
|
61
|
+
#
|
62
|
+
# For example,
|
63
|
+
#
|
64
|
+
# graph = StateMachines::Graph.new('test')
|
65
|
+
# graph.add_edges('parked', 'idling', :label => 'ignite')
|
66
|
+
def add_edges(*args)
|
67
|
+
edge = super
|
68
|
+
edge.fontname = @font
|
69
|
+
edge
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
#TODO register graphviz as render engine
|
2
|
+
module StateMachines
|
3
|
+
class Machine
|
4
|
+
class << self
|
5
|
+
# Draws the state machines defined in the given classes using GraphViz.
|
6
|
+
# The given classes must be a comma-delimited string of class names.
|
7
|
+
#
|
8
|
+
# Configuration options:
|
9
|
+
# * <tt>:file</tt> - A comma-delimited string of files to load that
|
10
|
+
# contain the state machine definitions to draw
|
11
|
+
# * <tt>:path</tt> - The path to write the graph file to
|
12
|
+
# * <tt>:format</tt> - The image format to generate the graph in
|
13
|
+
# * <tt>:font</tt> - The name of the font to draw state names in
|
14
|
+
def draw(class_names, options = {})
|
15
|
+
raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
|
16
|
+
|
17
|
+
# Load any files
|
18
|
+
if files = options.delete(:file)
|
19
|
+
files.split(',').each { |file| require file }
|
20
|
+
end
|
21
|
+
|
22
|
+
class_names.split(',').each do |class_name|
|
23
|
+
# Navigate through the namespace structure to get to the class
|
24
|
+
klass = Object
|
25
|
+
class_name.split('::').each do |name|
|
26
|
+
klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Draw each of the class's state machines
|
30
|
+
klass.state_machines.each_value do |machine|
|
31
|
+
machine.draw(options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Draws a directed graph of the machine for visualizing the various events,
|
38
|
+
# states, and their transitions.
|
39
|
+
#
|
40
|
+
# This requires both the Ruby graphviz gem and the graphviz library be
|
41
|
+
# installed on the system.
|
42
|
+
#
|
43
|
+
# Configuration options:
|
44
|
+
# * <tt>:name</tt> - The name of the file to write to (without the file extension).
|
45
|
+
# Default is "#{owner_class.name}_#{name}"
|
46
|
+
# * <tt>:path</tt> - The path to write the graph file to. Default is the
|
47
|
+
# current directory (".").
|
48
|
+
# * <tt>:format</tt> - The image format to generate the graph in.
|
49
|
+
# Default is "png'.
|
50
|
+
# * <tt>:font</tt> - The name of the font to draw state names in.
|
51
|
+
# Default is "Arial".
|
52
|
+
# * <tt>:orientation</tt> - The direction of the graph ("portrait" or
|
53
|
+
# "landscape"). Default is "portrait".
|
54
|
+
# * <tt>:human_names</tt> - Whether to use human state / event names for
|
55
|
+
# node labels on the graph instead of the internal name. Default is false.
|
56
|
+
def draw(graph_options = {})
|
57
|
+
name = graph_options.delete(:name) || "#{owner_class.name}_#{self.name}"
|
58
|
+
draw_options = {:human_name => false}
|
59
|
+
draw_options[:human_name] = graph_options.delete(:human_names) if graph_options.include?(:human_names)
|
60
|
+
|
61
|
+
graph = Graph.new(name, graph_options)
|
62
|
+
|
63
|
+
# Add nodes / edges
|
64
|
+
states.by_priority.each { |state| state.draw(graph, draw_options) }
|
65
|
+
events.each { |event| event.draw(graph, draw_options) }
|
66
|
+
|
67
|
+
# Output result
|
68
|
+
graph.output
|
69
|
+
graph
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class State
|
74
|
+
# Draws a representation of this state on the given machine. This will
|
75
|
+
# create a new node on the graph with the following properties:
|
76
|
+
# * +label+ - The human-friendly description of the state.
|
77
|
+
# * +width+ - The width of the node. Always 1.
|
78
|
+
# * +height+ - The height of the node. Always 1.
|
79
|
+
# * +shape+ - The actual shape of the node. If the state is a final
|
80
|
+
# state, then "doublecircle", otherwise "ellipse".
|
81
|
+
#
|
82
|
+
# Configuration options:
|
83
|
+
# * <tt>:human_name</tt> - Whether to use the state's human name for the
|
84
|
+
# node's label that gets drawn on the graph
|
85
|
+
def draw(graph, options = {})
|
86
|
+
node = graph.add_nodes(name ? name.to_s : 'nil',
|
87
|
+
:label => description(options),
|
88
|
+
:width => '1',
|
89
|
+
:height => '1',
|
90
|
+
:shape => final? ? 'doublecircle' : 'ellipse'
|
91
|
+
)
|
92
|
+
|
93
|
+
# Add open arrow for initial state
|
94
|
+
graph.add_edges(graph.add_nodes('starting_state', :shape => 'point'), node) if initial?
|
95
|
+
|
96
|
+
true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Event
|
101
|
+
# Draws a representation of this event on the given graph. This will
|
102
|
+
# create 1 or more edges on the graph for each branch (i.e. transition)
|
103
|
+
# configured.
|
104
|
+
#
|
105
|
+
# Configuration options:
|
106
|
+
# * <tt>:human_name</tt> - Whether to use the event's human name for the
|
107
|
+
# node's label that gets drawn on the graph
|
108
|
+
def draw(graph, options = {})
|
109
|
+
valid_states = machine.states.by_priority.map {|state| state.name}
|
110
|
+
branches.each do |branch|
|
111
|
+
branch.draw(graph, options[:human_name] ? human_name : name, valid_states)
|
112
|
+
end
|
113
|
+
|
114
|
+
true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Branch
|
119
|
+
# Draws a representation of this branch on the given graph. This will draw
|
120
|
+
# an edge between every state this branch matches *from* to either the
|
121
|
+
# configured to state or, if none specified, then a loopback to the from
|
122
|
+
# state.
|
123
|
+
#
|
124
|
+
# For example, if the following from states are configured:
|
125
|
+
# * +idling+
|
126
|
+
# * +first_gear+
|
127
|
+
# * +backing_up+
|
128
|
+
#
|
129
|
+
# ...and the to state is +parked+, then the following edges will be created:
|
130
|
+
# * +idling+ -> +parked+
|
131
|
+
# * +first_gear+ -> +parked+
|
132
|
+
# * +backing_up+ -> +parked+
|
133
|
+
#
|
134
|
+
# Each edge will be labeled with the name of the event that would cause the
|
135
|
+
# transition.
|
136
|
+
def draw(graph, event, valid_states)
|
137
|
+
state_requirements.each do |state_requirement|
|
138
|
+
# From states determined based on the known valid states
|
139
|
+
from_states = state_requirement[:from].filter(valid_states)
|
140
|
+
|
141
|
+
# If a to state is not specified, then it's a loopback and each from
|
142
|
+
# state maps back to itself
|
143
|
+
if state_requirement[:to].values.empty?
|
144
|
+
loopback = true
|
145
|
+
else
|
146
|
+
to_state = state_requirement[:to].values.first
|
147
|
+
to_state = to_state ? to_state.to_s : 'nil'
|
148
|
+
loopback = false
|
149
|
+
end
|
150
|
+
|
151
|
+
# Generate an edge between each from and to state
|
152
|
+
from_states.each do |from_state|
|
153
|
+
from_state = from_state ? from_state.to_s : 'nil'
|
154
|
+
graph.add_edges(from_state, loopback ? from_state : to_state, :label => event.to_s)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
true
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/spec/graph_spec.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe StateMachines::Graph do
|
3
|
+
context 'GraphDefault' do
|
4
|
+
before(:each) do
|
5
|
+
@graph = StateMachines::Graph.new('test')
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should_have_a_default_font' do
|
9
|
+
expect('Arial').to eq(@graph.font)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should_use_current_directory_for_filepath' do
|
13
|
+
expect('doc/state_machines/test.png').to eq(@graph.file_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should_have_a_default_file_format' do
|
17
|
+
expect('png').to eq(@graph.file_format)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should_have_a_default_orientation' do
|
21
|
+
expect('TB').to eq(@graph[:rankdir].source)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'GraphNodes' do
|
26
|
+
before(:each) do
|
27
|
+
@graph = StateMachines::Graph.new('test')
|
28
|
+
@node = @graph.add_nodes('parked', :shape => 'ellipse')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should_return_generated_node' do
|
32
|
+
expect(@node).to_not be_nil
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should_use_specified_name' do
|
36
|
+
expect(@node).to eq(@graph.get_node('parked'))
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should_use_specified_options' do
|
40
|
+
expect('ellipse').to eq(@node['shape'].to_s.gsub('"', ''))
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should_set_default_font' do
|
44
|
+
expect('Arial').to eq(@node['fontname'].to_s.gsub('"', ''))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'GraphEdges' do
|
49
|
+
before(:each) do
|
50
|
+
@graph = StateMachines::Graph.new('test')
|
51
|
+
@graph.add_nodes('parked', :shape => 'ellipse')
|
52
|
+
@graph.add_nodes('idling', :shape => 'ellipse')
|
53
|
+
@edge = @graph.add_edges('parked', 'idling', :label => 'ignite')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should_return_generated_edge' do
|
57
|
+
expect(@edge).to_not be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should_use_specified_nodes' do
|
61
|
+
expect('parked').to eq(@edge.node_one(false))
|
62
|
+
expect('idling').to eq(@edge.node_two(false))
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should_use_specified_options' do
|
66
|
+
expect('ignite').to eq(@edge['label'].to_s.gsub('"', ''))
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should_set_default_font' do
|
70
|
+
expect('Arial').to eq(@edge['fontname'].to_s.gsub('"', ''))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'GraphOutput' do
|
75
|
+
before(:each) do
|
76
|
+
@graph_name = "test_#{rand(1000000)}"
|
77
|
+
@graph = StateMachines::Graph.new(@graph_name)
|
78
|
+
@graph.add_nodes('parked', :shape => 'ellipse')
|
79
|
+
@graph.add_nodes('idling', :shape => 'ellipse')
|
80
|
+
@graph.add_edges('parked', 'idling', :label => 'ignite')
|
81
|
+
@graph.output
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should_save_file' do
|
85
|
+
expect(File.exist?("doc/state_machines/#{@graph_name}.png")).to be_truthy
|
86
|
+
end
|
87
|
+
|
88
|
+
after(:each) do
|
89
|
+
FileUtils.rm Dir["doc/state_machines/#{@graph_name}.png"]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
describe StateMachines::Graphviz do
|
2
|
+
context 'Drawing' do
|
3
|
+
let(:klass) do
|
4
|
+
Class.new do
|
5
|
+
def self.name
|
6
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
let(:machine) { StateMachines::Machine.new(klass, initial: :parked) }
|
11
|
+
|
12
|
+
before(:each) do
|
13
|
+
machine.event :ignite do
|
14
|
+
transition parked: :idling
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should_raise_exception_if_invalid_option_specified' do
|
19
|
+
expect { machine.draw(invalid: true) }.to raise_error(ArgumentError)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should_save_file_with_class_name_by_default' do
|
23
|
+
machine.draw
|
24
|
+
expect(File.exist?("doc/state_machines/#{klass.name}_state.png")).to be_truthy
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should_allow_base_name_to_be_customized' do
|
28
|
+
name = "machine_#{rand(1_000_000)}"
|
29
|
+
machine.draw(name: name)
|
30
|
+
@path = "doc/state_machines/#{name}.png"
|
31
|
+
expect(File.exist?(@path)).to be_truthy
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should_allow_format_to_be_customized' do
|
35
|
+
machine.draw(format: 'jpg')
|
36
|
+
@path = "doc/state_machines/#{klass.name}_state.jpg"
|
37
|
+
expect(File.exist?(@path)).to be_truthy
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should_allow_path_to_be_customized' do
|
41
|
+
machine.draw(path: "#{File.dirname(__FILE__)}/")
|
42
|
+
@path = "#{File.dirname(__FILE__)}/#{klass.name}_state.png"
|
43
|
+
expect(File.exist?(@path)).to be_truthy
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should_allow_orientation_to_be_landscape' do
|
47
|
+
graph = machine.draw(orientation: 'landscape')
|
48
|
+
expect(graph['rankdir'].to_s.gsub('"', '')).to eq('LR')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should_allow_orientation_to_be_portrait' do
|
52
|
+
graph = machine.draw(orientation: 'portrait')
|
53
|
+
expect(graph['rankdir'].to_s.gsub('"', '')).to eq('TB')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should_allow_human_names_to_be_displayed' do
|
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
|
+
expect(parked_node['label'].to_s.gsub('"', '')).to eq('Parked')
|
64
|
+
|
65
|
+
idling_node = graph.get_node('idling')
|
66
|
+
expect(idling_node['label'].to_s.gsub('"', '')).to eq('Idling')
|
67
|
+
end
|
68
|
+
|
69
|
+
after(:each) do
|
70
|
+
FileUtils.rm Dir[@path || "doc/state_machines/#{klass.name}_state.png"]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'DrawingWithIntegerStates' do
|
75
|
+
let(:klass) do
|
76
|
+
Class.new do
|
77
|
+
def self.name
|
78
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
let(:machine) { StateMachines::Machine.new(klass, :state_id, initial: :parked) }
|
84
|
+
before(:each) do
|
85
|
+
machine.event :ignite do
|
86
|
+
transition parked: :idling
|
87
|
+
end
|
88
|
+
machine.state :parked, value: 1
|
89
|
+
machine.state :idling, value: 2
|
90
|
+
end
|
91
|
+
|
92
|
+
let!(:graph) { machine.draw }
|
93
|
+
|
94
|
+
it 'should_draw_all_states' do
|
95
|
+
expect(graph.node_count).to eq(3)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should_draw_all_events' do
|
99
|
+
expect(graph.edge_count).to eq(2)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should_draw_machine' do
|
103
|
+
expect(File.exist?("doc/state_machines/#{klass.name}_state_id.png")).to be_truthy
|
104
|
+
end
|
105
|
+
|
106
|
+
after(:each) do
|
107
|
+
FileUtils.rm Dir["doc/state_machines/#{klass.name}_state_id.png"]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'DrawingWithNilStates' do
|
112
|
+
let(:klass) do
|
113
|
+
Class.new do
|
114
|
+
def self.name
|
115
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
let(:machine) { StateMachines::Machine.new(klass, initial: :parked) }
|
120
|
+
|
121
|
+
before(:each) do
|
122
|
+
machine.event :ignite do
|
123
|
+
transition parked: :idling
|
124
|
+
end
|
125
|
+
machine.state :parked, value: nil
|
126
|
+
end
|
127
|
+
|
128
|
+
let!(:graph) { machine.draw }
|
129
|
+
|
130
|
+
it 'should_draw_all_states' do
|
131
|
+
expect(graph.node_count).to eq(3)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'should_draw_all_events' do
|
135
|
+
expect(graph.edge_count).to eq(2)
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should_draw_machine' do
|
139
|
+
expect(File.exist?("doc/state_machines/#{klass.name}_state.png")).to be_truthy
|
140
|
+
end
|
141
|
+
|
142
|
+
after(:each) do
|
143
|
+
FileUtils.rm Dir["doc/state_machines/#{klass.name}_state.png"]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context 'DrawingWithDynamicStates' do
|
148
|
+
let(:klass) do
|
149
|
+
Class.new do
|
150
|
+
def self.name
|
151
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
let(:machine) { StateMachines::Machine.new(klass, initial: :parked) }
|
157
|
+
|
158
|
+
before(:each) do
|
159
|
+
machine.event :activate do
|
160
|
+
transition parked: :idling
|
161
|
+
end
|
162
|
+
machine.state :idling, value: lambda { Time.now }
|
163
|
+
end
|
164
|
+
|
165
|
+
let!(:graph) { machine.draw }
|
166
|
+
|
167
|
+
it 'should_draw_all_states' do
|
168
|
+
expect(graph.node_count).to eq(3)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should_draw_all_events' do
|
172
|
+
expect(graph.edge_count).to eq(2)
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'should_draw_machine' do
|
176
|
+
expect(File.exist?("doc/state_machines/#{klass.name}_state.png")).to be_truthy
|
177
|
+
end
|
178
|
+
|
179
|
+
after(:each) do
|
180
|
+
FileUtils.rm Dir["doc/state_machines/#{klass.name}_state.png"]
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'ClassDrawing' do
|
186
|
+
before(:each) do
|
187
|
+
klass = Class.new do
|
188
|
+
def self.name
|
189
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
machine = StateMachines::Machine.new(klass)
|
193
|
+
machine.event :ignite do
|
194
|
+
transition parked: :idling
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'should_raise_exception_if_no_class_names_specified' do
|
199
|
+
expect { StateMachines::Machine.draw(nil) }.to raise_error(ArgumentError)
|
200
|
+
# FixMe
|
201
|
+
# assert_equal 'At least one class must be specified', exception.message
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should_load_files' do
|
205
|
+
StateMachines::Machine.draw('Switch', file: File.expand_path("#{File.dirname(__FILE__)}/files/switch.rb"))
|
206
|
+
expect(defined?(::Switch)).to be_truthy
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'should_allow_path_and_format_to_be_customized' do
|
210
|
+
StateMachines::Machine.draw('Switch', file: File.expand_path("#{File.dirname(__FILE__)}/files/switch.rb"), path: "#{File.dirname(__FILE__)}/", format: 'jpg')
|
211
|
+
expect(File.exist?("#{File.dirname(__FILE__)}/#{Switch.name}_state.jpg")).to be_truthy
|
212
|
+
FileUtils.rm Dir["{.,#{File.dirname(__FILE__)}}/#{Switch.name}_state.{jpg,png}"]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
describe StateMachines::Machine do
|
2
|
+
before(:each) do
|
3
|
+
klass = Class.new do
|
4
|
+
def self.name
|
5
|
+
@name ||= "Vehicle_#{rand(1_000_000)}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
@machine = StateMachines::Machine.new(klass, initial: :parked)
|
10
|
+
@machine.event :ignite do
|
11
|
+
transition parked: :idling
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should not raise exception' do
|
16
|
+
expect { @machine.draw }.not_to raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/state_spec.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
context 'Drawing' do
|
2
|
+
before(:each) do
|
3
|
+
@machine = StateMachines::Machine.new(Class.new)
|
4
|
+
@machine.states << @state = StateMachines::State.new(@machine, :parked, :value => 1)
|
5
|
+
@machine.event :ignite do
|
6
|
+
transition :parked => :idling
|
7
|
+
end
|
8
|
+
|
9
|
+
graph = StateMachines::Graph.new('test')
|
10
|
+
@state.draw(graph)
|
11
|
+
@node = graph.get_node('parked')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should_use_ellipse_shape' do
|
15
|
+
expect(@node['shape'].to_s.gsub('"', '')).to eq('ellipse')
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should_set_width_to_one' do
|
19
|
+
expect('1').to eq(@node['width'].to_s.gsub('"', ''))
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should_set_height_to_one' do
|
23
|
+
expect('1').to eq(@node['height'].to_s.gsub('"', ''))
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should_use_description_as_label' do
|
27
|
+
expect('parked (1)').to eq(@node['label'].to_s.gsub('"', ''))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'DrawingInitial' do
|
32
|
+
before(:each) do
|
33
|
+
@machine = StateMachines::Machine.new(Class.new)
|
34
|
+
@machine.states << @state = StateMachines::State.new(@machine, :parked, :initial => true)
|
35
|
+
@machine.event :ignite do
|
36
|
+
transition :parked => :idling
|
37
|
+
end
|
38
|
+
|
39
|
+
@graph = StateMachines::Graph.new('test')
|
40
|
+
@state.draw(@graph)
|
41
|
+
@node = @graph.get_node('parked')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should_use_ellipse_as_shape' do
|
45
|
+
expect('ellipse').to eq(@node['shape'].to_s.gsub('"', ''))
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should_draw_edge_between_point_and_state' do
|
49
|
+
expect(2).to eq(@graph.node_count)
|
50
|
+
expect(1).to eq(@graph.edge_count)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'DrawingNilName' do
|
55
|
+
before(:each) do
|
56
|
+
@machine = StateMachines::Machine.new(Class.new)
|
57
|
+
@machine.states << @state = StateMachines::State.new(@machine, nil)
|
58
|
+
|
59
|
+
graph = StateMachines::Graph.new('test')
|
60
|
+
@state.draw(graph)
|
61
|
+
@node = graph.get_node('nil')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should_have_a_node' do
|
65
|
+
expect(@node).to be_truthy
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should_use_description_as_label' do
|
69
|
+
expect('nil').to eq(@node['label'].to_s.gsub('"', ''))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'DrawingLambdaValue' do
|
74
|
+
before(:each) do
|
75
|
+
@machine = StateMachines::Machine.new(Class.new)
|
76
|
+
@machine.states << @state = StateMachines::State.new(@machine, :parked, :value => lambda {})
|
77
|
+
|
78
|
+
graph = StateMachines::Graph.new('test')
|
79
|
+
@state.draw(graph)
|
80
|
+
@node = graph.get_node('parked')
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should_have_a_node' do
|
84
|
+
expect(@node).to be_truthy
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should_use_description_as_label' do
|
88
|
+
expect('parked (*)').to eq(@node['label'].to_s.gsub('"', ''))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'DrawingNonFinal' do
|
93
|
+
before(:each) do
|
94
|
+
@machine = StateMachines::Machine.new(Class.new)
|
95
|
+
@machine.states << @state = StateMachines::State.new(@machine, :parked)
|
96
|
+
@machine.event :ignite do
|
97
|
+
transition :parked => :idling
|
98
|
+
end
|
99
|
+
|
100
|
+
graph = StateMachines::Graph.new('test')
|
101
|
+
@state.draw(graph)
|
102
|
+
@node = graph.get_node('parked')
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should_use_ellipse_as_shape' do
|
106
|
+
expect('ellipse').to eq(@node['shape'].to_s.gsub('"', ''))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'DrawingFinal' do
|
111
|
+
before(:each) do
|
112
|
+
@machine = StateMachines::Machine.new(Class.new)
|
113
|
+
@machine.states << @state = StateMachines::State.new(@machine, :parked)
|
114
|
+
|
115
|
+
graph = StateMachines::Graph.new('test')
|
116
|
+
@state.draw(graph)
|
117
|
+
@node = graph.get_node('parked')
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should_use_doublecircle_as_shape' do
|
121
|
+
expect('doublecircle').to eq(@node['shape'].to_s.gsub('"', ''))
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'DrawingWithHumanName' do
|
126
|
+
before(:each) do
|
127
|
+
@machine = StateMachines::Machine.new(Class.new)
|
128
|
+
@machine.states << @state = StateMachines::State.new(@machine, :parked, :human_name => 'Parked')
|
129
|
+
@machine.event :ignite do
|
130
|
+
transition :parked => :idling
|
131
|
+
end
|
132
|
+
|
133
|
+
graph = StateMachines::Graph.new('test')
|
134
|
+
@state.draw(graph, :human_name => true)
|
135
|
+
@node = graph.get_node('parked')
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should_use_description_with_human_name_as_label' do
|
139
|
+
expect('Parked').to eq(@node['label'].to_s.gsub('"', ''))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'state_machines/graphviz/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'state_machines-graphviz'
|
8
|
+
spec.version = StateMachines::Graphviz::VERSION
|
9
|
+
spec.authors = ['Abdelkader Boudih', 'Aaron Pfeifer']
|
10
|
+
spec.email = ['terminale@gmail.com']
|
11
|
+
spec.summary = %q(Drawing module for state machine2)
|
12
|
+
spec.description = %q(Graphviz module for state machine2)
|
13
|
+
spec.homepage = 'https://github.com/seuros/state_machines-graphviz'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.test_files = spec.files.grep(%r{/^spec\//})
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_dependency 'state_machines'
|
21
|
+
spec.add_dependency 'ruby-graphviz'
|
22
|
+
spec.add_development_dependency 'bundler'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec' , '3.0.0.beta2'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: state_machines-graphviz
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Abdelkader Boudih
|
8
|
+
- Aaron Pfeifer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-04-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: state_machines
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: ruby-graphviz
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bundler
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rake
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rspec
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - '='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.0.0.beta2
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - '='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 3.0.0.beta2
|
84
|
+
description: Graphviz module for state machine2
|
85
|
+
email:
|
86
|
+
- terminale@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yaml"
|
94
|
+
- Gemfile
|
95
|
+
- LICENSE.txt
|
96
|
+
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- lib/state_machines/graphviz.rb
|
99
|
+
- lib/state_machines/graphviz/graph.rb
|
100
|
+
- lib/state_machines/graphviz/monkeypatch.rb
|
101
|
+
- lib/state_machines/graphviz/version.rb
|
102
|
+
- spec/files/switch.rb
|
103
|
+
- spec/graph_spec.rb
|
104
|
+
- spec/machine_drawing_spec.rb
|
105
|
+
- spec/machine_spec.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- spec/state_spec.rb
|
108
|
+
- state_machines-graphviz.gemspec
|
109
|
+
homepage: https://github.com/seuros/state_machines-graphviz
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.2.2
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: Drawing module for state machine2
|
133
|
+
test_files: []
|
134
|
+
has_rdoc:
|