dogviz 0.0.21 → 0.0.22
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 +4 -4
- data/Rakefile +0 -1
- data/dogviz.gemspec +3 -1
- data/lib/dogviz/common.rb +5 -4
- data/lib/dogviz/flow.rb +93 -12
- data/lib/dogviz/flowable.rb +50 -0
- data/lib/dogviz/graphviz_renderer.rb +9 -3
- data/lib/dogviz/parent.rb +2 -2
- data/lib/dogviz/registry.rb +2 -2
- data/lib/dogviz/rendered_sequence.rb +48 -4
- data/lib/dogviz/sequence_renderer.rb +79 -9
- data/lib/dogviz/system.rb +61 -6
- data/lib/dogviz/thing.rb +10 -4
- data/lib/dogviz/version.rb +1 -1
- data/tests/graph_checking.rb +1 -1
- data/tests/setup_tests.rb +1 -1
- data/tests/svg_graph.rb +1 -1
- data/tests/test_dogviz_flows.rb +239 -33
- data/tests/test_dogviz_functionally.rb +4 -3
- data/tests/test_dogviz_graph.rb +1 -1
- data/tests/test_dogviz_graphviz_rendering.rb +1 -1
- data/todo.txt +45 -4
- metadata +32 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a428fcd04a6f1cc90bf9225b887c94889c7667ef
|
4
|
+
data.tar.gz: 26363eb38c24901ae048103f99179fb7d5e2807a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e363ecd7f98e01a3218c0bcd389f952f7350fa4488fd30e1e4bc3ff7b512624208576137a05e2cd26b48f51235e9f34954b95b28b4f004eb468b16cc909f684
|
7
|
+
data.tar.gz: 5f9ae410f02002a7148fa66c0ecead46b0a8db9980cbf84eab671298cf3fb85a36e4de4ac83162a7a70dd23f7bd092322b9453ae67cf9606c8a37f372dd1b4a0
|
data/Rakefile
CHANGED
data/dogviz.gemspec
CHANGED
@@ -18,10 +18,12 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
20
|
spec.add_development_dependency "bundler", "~> 1.10"
|
21
|
-
spec.add_development_dependency "rake", "~>
|
21
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
22
22
|
spec.add_development_dependency 'simplecov', '~> 0'
|
23
23
|
spec.add_development_dependency 'colorize', '~> 0'
|
24
24
|
spec.add_development_dependency 'nokogiri', '~> 1.6'
|
25
|
+
spec.add_development_dependency 'test-unit'
|
25
26
|
|
26
27
|
spec.add_dependency 'ruby-graphviz', '~> 1'
|
28
|
+
spec.add_dependency 'json', '~> 2.1.0'
|
27
29
|
end
|
data/lib/dogviz/common.rb
CHANGED
@@ -3,7 +3,7 @@ module Dogviz
|
|
3
3
|
def create_id(name, parent)
|
4
4
|
parts = []
|
5
5
|
parts << parent.id if parent.respond_to? :id
|
6
|
-
parts += name.split
|
6
|
+
parts += name.split(/\s/)
|
7
7
|
parts.join '_'
|
8
8
|
end
|
9
9
|
|
@@ -23,6 +23,7 @@ module Dogviz
|
|
23
23
|
def info(fields)
|
24
24
|
@info.merge! fields
|
25
25
|
setup_render_attributes(label: label_with_info)
|
26
|
+
self
|
26
27
|
end
|
27
28
|
|
28
29
|
def doclink(url)
|
@@ -38,7 +39,7 @@ module Dogviz
|
|
38
39
|
end
|
39
40
|
|
40
41
|
def setup_render_attributes(attributes)
|
41
|
-
@attributes
|
42
|
+
@attributes ||= {}
|
42
43
|
@attributes.merge!(attributes)
|
43
44
|
end
|
44
45
|
|
@@ -65,11 +66,11 @@ module Dogviz
|
|
65
66
|
end
|
66
67
|
|
67
68
|
def under_skip?
|
68
|
-
ancestors.any?
|
69
|
+
ancestors.any?(&:skip?)
|
69
70
|
end
|
70
71
|
|
71
72
|
def under_rollup?
|
72
|
-
ancestors.any?
|
73
|
+
ancestors.any?(&:rollup?)
|
73
74
|
end
|
74
75
|
|
75
76
|
def in_rollup?
|
data/lib/dogviz/flow.rb
CHANGED
@@ -4,19 +4,85 @@ require_relative 'process'
|
|
4
4
|
|
5
5
|
module Dogviz
|
6
6
|
class Flow
|
7
|
+
FLOW_RENDERERS = {
|
8
|
+
sequence: WebSequenceDiagramsSequenceRenderer,
|
9
|
+
plantuml: PlantUmlSequenceRenderer,
|
10
|
+
png: PngSequenceRenderer
|
11
|
+
}
|
12
|
+
|
13
|
+
attr_reader :sys
|
14
|
+
attr_accessor :executor
|
15
|
+
|
7
16
|
def initialize(sys, name)
|
8
17
|
@sys = sys
|
9
18
|
@name = name
|
10
|
-
@
|
19
|
+
@commands = []
|
20
|
+
@actors = []
|
21
|
+
@caller_stack = []
|
22
|
+
@executor = nil
|
11
23
|
end
|
12
24
|
|
13
25
|
def make_connections
|
14
|
-
|
15
|
-
thing_of(from).points_to
|
26
|
+
commands.each { |type, from, to, label|
|
27
|
+
thing_of(from).points_to(thing_of(to), name: label.split('\n').first) if type == :call
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def involves(*actors)
|
32
|
+
@actors += actors
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def from(initial_actor, &flowspec)
|
37
|
+
@actors.each { |actor|
|
38
|
+
actor.start_flow self
|
16
39
|
}
|
40
|
+
@caller_stack << initial_actor
|
41
|
+
begin
|
42
|
+
flowspec.call
|
43
|
+
rescue NoMethodError => nme
|
44
|
+
raise "Did you call #involves for all actors? It's a common cause of the caught exception: #{nme}"
|
45
|
+
ensure
|
46
|
+
@caller_stack.pop
|
47
|
+
@actors.each { |actor|
|
48
|
+
actor.stop_flow
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_note(from, where, what)
|
54
|
+
# yukk next lets move to command classes, e.g. OptCommand, NoteCommand, CallCommand etc.
|
55
|
+
commands << [:note, from, where, what]
|
56
|
+
end
|
57
|
+
|
58
|
+
def optional(text, &block)
|
59
|
+
commands << [:opt, nil, nil, text]
|
60
|
+
block.call
|
61
|
+
commands << [:end, nil, nil, nil]
|
62
|
+
end
|
63
|
+
|
64
|
+
def divider(text)
|
65
|
+
commands << [:divider, nil, nil, text]
|
66
|
+
end
|
67
|
+
|
68
|
+
alias :opt :optional
|
69
|
+
|
70
|
+
def add_call(from, to, label)
|
71
|
+
commands << [:call, from, to, label]
|
72
|
+
end
|
73
|
+
|
74
|
+
def next_call(to, label)
|
75
|
+
add_call @caller_stack.last, to, label
|
76
|
+
@caller_stack << to
|
77
|
+
end
|
78
|
+
|
79
|
+
def end_call(label)
|
80
|
+
current_actor = @caller_stack.pop
|
81
|
+
add_call(current_actor, @caller_stack.last, label) unless label.nil?
|
17
82
|
end
|
18
83
|
|
19
84
|
def flows(*steps)
|
85
|
+
sys.warn_on_exit 'deprecation warning: flow#flows deprecated, should use flow#from(actor) { <nested flow spec> }'
|
20
86
|
from = nil
|
21
87
|
to = nil
|
22
88
|
label = nil
|
@@ -29,7 +95,7 @@ module Dogviz
|
|
29
95
|
to = ensure_is_thing(step)
|
30
96
|
end
|
31
97
|
unless to.nil?
|
32
|
-
|
98
|
+
add_call from, to, label
|
33
99
|
from = to
|
34
100
|
to = label = nil
|
35
101
|
end
|
@@ -37,31 +103,46 @@ module Dogviz
|
|
37
103
|
end
|
38
104
|
|
39
105
|
def ensure_is_thing(step)
|
40
|
-
raise "Expected some thing or process: '#{step}' already got: #{
|
106
|
+
raise "Expected some thing or process: '#{step}' already got: #{commands}" unless step.is_a?(Thing) || step.is_a?(Process)
|
41
107
|
step
|
42
108
|
end
|
43
109
|
|
44
110
|
def output(type_to_file)
|
45
111
|
type = type_to_file.keys.first
|
46
|
-
raise "Only support
|
47
|
-
render.output(type_to_file)
|
112
|
+
raise "Only support #{FLOW_RENDERERS.keys}, not: '#{type}'" unless FLOW_RENDERERS.has_key?(type)
|
113
|
+
render(FLOW_RENDERERS[type]).output(type_to_file, executor)
|
48
114
|
end
|
49
115
|
|
50
|
-
def render
|
51
|
-
renderer =
|
52
|
-
|
53
|
-
|
116
|
+
def render(renderer_class = SequenceRenderer)
|
117
|
+
renderer = renderer_class.new(@name)
|
118
|
+
commands.each do |type, from, to, label|
|
119
|
+
if type == :call
|
120
|
+
renderer.render_edge(from, to, {label: label})
|
121
|
+
elsif type == :end
|
122
|
+
renderer.end_combination
|
123
|
+
elsif type == :note
|
124
|
+
renderer.note(from, to, label)
|
125
|
+
elsif type == :divider
|
126
|
+
renderer.divider(label)
|
127
|
+
else
|
128
|
+
renderer.start_combination(type, label)
|
129
|
+
end
|
54
130
|
end
|
55
131
|
renderer.rendered
|
56
132
|
end
|
57
133
|
|
134
|
+
def suppress_messages!
|
135
|
+
sys.suppress_messages!
|
136
|
+
end
|
137
|
+
|
58
138
|
private
|
59
139
|
|
60
|
-
attr_reader :
|
140
|
+
attr_reader :commands
|
61
141
|
|
62
142
|
def thing_of(it)
|
63
143
|
return it.processor if it.is_a?(Process)
|
64
144
|
it
|
65
145
|
end
|
146
|
+
|
66
147
|
end
|
67
148
|
end
|
data/lib/dogviz/flowable.rb
CHANGED
@@ -5,5 +5,55 @@ module Dogviz
|
|
5
5
|
def does(action)
|
6
6
|
Process.new(self, action)
|
7
7
|
end
|
8
|
+
|
9
|
+
def receives(requests, &block)
|
10
|
+
|
11
|
+
@requests = self.requests.merge requests if requests.is_a?(Hash)
|
12
|
+
self.requests[requests] = block if requests.is_a?(Symbol)
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def note(where, what)
|
17
|
+
@flow.add_note(self, where, what)
|
18
|
+
end
|
19
|
+
|
20
|
+
def start_flow(flow)
|
21
|
+
@flow = flow
|
22
|
+
end
|
23
|
+
|
24
|
+
def stop_flow
|
25
|
+
@flow = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_missing(m, *args, &block)
|
29
|
+
if requests.has_key?(m)
|
30
|
+
@flow ||= nil
|
31
|
+
|
32
|
+
request_def = requests[m]
|
33
|
+
if request_def.is_a?(String)
|
34
|
+
label = request_def
|
35
|
+
return_label = nil
|
36
|
+
else
|
37
|
+
request_def = request_def.call(*args) if request_def.is_a?(Proc)
|
38
|
+
label = request_def.keys.first
|
39
|
+
return_label = request_def.values.first
|
40
|
+
end
|
41
|
+
|
42
|
+
@flow.next_call self, label
|
43
|
+
block.call if block_given?
|
44
|
+
@flow.end_call(return_label)
|
45
|
+
else
|
46
|
+
raise "this flowable does not know about receiving '#{m}', only know about: #{requests.keys}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def requests
|
51
|
+
@requests ||= {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def request_handlers
|
55
|
+
@request_handlers ||= {}
|
56
|
+
end
|
57
|
+
|
8
58
|
end
|
9
59
|
end
|
@@ -6,8 +6,14 @@ module Dogviz
|
|
6
6
|
attr_reader :graph
|
7
7
|
|
8
8
|
def initialize(title, hints)
|
9
|
-
|
10
|
-
|
9
|
+
construction_hints = {}
|
10
|
+
after_hints = hints.clone
|
11
|
+
if hints.has_key?(:use)
|
12
|
+
construction_hints[:use] = hints[:use]
|
13
|
+
after_hints.delete :use
|
14
|
+
end
|
15
|
+
@graph = GraphViz.digraph(title, construction_hints)
|
16
|
+
@graph[after_hints]
|
11
17
|
@subgraphs = {}
|
12
18
|
@nodes = {}
|
13
19
|
@rendered_subgraph_ids = {}
|
@@ -25,7 +31,7 @@ module Dogviz
|
|
25
31
|
clean_node_attributes attributes
|
26
32
|
default_attributes = {:shape => 'box', :style => ''}
|
27
33
|
merged_attributes = default_attributes.merge(attributes)
|
28
|
-
|
34
|
+
parent_node(parent).add_nodes(id, merged_attributes)
|
29
35
|
end
|
30
36
|
|
31
37
|
def render_subgraph(parent, id, attributes)
|
data/lib/dogviz/parent.rb
CHANGED
@@ -2,12 +2,12 @@ module Dogviz
|
|
2
2
|
module Parent
|
3
3
|
def find_all(&matcher)
|
4
4
|
raise MissingMatchBlockError.new unless block_given?
|
5
|
-
@by_name.find_all
|
5
|
+
@by_name.find_all(&matcher)
|
6
6
|
end
|
7
7
|
|
8
8
|
def find(name=nil, &matcher)
|
9
9
|
if block_given?
|
10
|
-
@by_name.find
|
10
|
+
@by_name.find(&matcher)
|
11
11
|
else
|
12
12
|
raise 'Need to provide name or block' if name.nil?
|
13
13
|
@by_name.lookup name
|
data/lib/dogviz/registry.rb
CHANGED
@@ -21,12 +21,12 @@ module Dogviz
|
|
21
21
|
|
22
22
|
def find(&matcher)
|
23
23
|
raise LookupError.new(@context, "need to provide match block") unless block_given?
|
24
|
-
@all.find
|
24
|
+
@all.find(&matcher)
|
25
25
|
end
|
26
26
|
|
27
27
|
def find_all(&matcher)
|
28
28
|
raise MissingMatchBlockError.new(@context) unless block_given?
|
29
|
-
@all.select
|
29
|
+
@all.select(&matcher)
|
30
30
|
end
|
31
31
|
|
32
32
|
def lookup(name)
|
@@ -4,10 +4,54 @@ module Dogviz
|
|
4
4
|
@lines = lines
|
5
5
|
end
|
6
6
|
|
7
|
-
def output(type_to_file)
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def output(type_to_file, executor = nil)
|
8
|
+
File.write type_to_file.values.first, body
|
9
|
+
body
|
10
|
+
end
|
11
|
+
|
12
|
+
def body
|
13
|
+
@lines.map(&:rstrip).join "\n"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class WebSequenceDiagramsRenderedSequence < RenderedSequence
|
18
|
+
end
|
19
|
+
|
20
|
+
class PlantUmlRenderedSequence < RenderedSequence
|
21
|
+
|
22
|
+
def body
|
23
|
+
raw_body = super
|
24
|
+
['@startuml', raw_body, '@enduml'].join "\n"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
class Executor
|
30
|
+
def execute(cmd)
|
31
|
+
system cmd
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class PngRenderedSequence < PlantUmlRenderedSequence
|
36
|
+
def output(type_to_file, executor = nil)
|
37
|
+
image_type, image_filename = type_to_file.first
|
38
|
+
plantuml_definition_filename = without_extension(image_filename) + '.plantuml'
|
39
|
+
|
40
|
+
super plantuml: plantuml_definition_filename
|
41
|
+
|
42
|
+
executor = Executor.new if executor.nil?
|
43
|
+
executor.execute(plantuml_cmd image_type, plantuml_definition_filename)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def plantuml_cmd(image_type, plantuml_definition_filename)
|
49
|
+
"plantuml -t#{image_type} #{plantuml_definition_filename}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def without_extension(filename)
|
53
|
+
filename.gsub(/\.[a-zA-Z]*$/, '')
|
11
54
|
end
|
12
55
|
end
|
56
|
+
|
13
57
|
end
|
@@ -7,28 +7,63 @@ module Dogviz
|
|
7
7
|
|
8
8
|
def initialize(title)
|
9
9
|
@lines = []
|
10
|
+
@indents = 0
|
11
|
+
@rendered_class = RenderedSequence
|
12
|
+
add_title title
|
10
13
|
end
|
11
14
|
|
12
15
|
def render_edge(from, other, options)
|
13
|
-
|
14
16
|
detail = options[:label]
|
15
17
|
receiver_label = other.name
|
16
18
|
sender_label = from.name
|
19
|
+
annotations = nil
|
17
20
|
if other.is_a?(Process)
|
18
|
-
|
21
|
+
annotations = extract_annotations(detail, sender_label, receiver_label, other.description)
|
19
22
|
receiver_label = process_start_label(receiver_label)
|
20
23
|
elsif from.is_a?(Process)
|
21
24
|
receiver_label = process_end_label(receiver_label)
|
22
25
|
end
|
23
|
-
|
26
|
+
line = "#{escape sender_label} -> #{escape receiver_label}: #{detail}"
|
27
|
+
line = [ line, annotations ].join("\n") unless annotations.nil?
|
28
|
+
add_line line
|
29
|
+
end
|
30
|
+
|
31
|
+
def start_combination(operator, guard)
|
32
|
+
add_line "#{operator} #{escape guard}"
|
33
|
+
@indents += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def end_combination
|
37
|
+
@indents -= 1
|
38
|
+
add_line 'end'
|
39
|
+
end
|
40
|
+
|
41
|
+
def note(from, where, what)
|
42
|
+
add_line "note #{where} of #{escape from.name}"
|
43
|
+
@indents += 1
|
44
|
+
add_line what
|
45
|
+
@indents -= 1
|
46
|
+
add_line "end note"
|
47
|
+
end
|
48
|
+
|
49
|
+
def divider(text)
|
50
|
+
#nop
|
24
51
|
end
|
25
52
|
|
26
53
|
def rendered
|
27
|
-
|
54
|
+
@rendered_class.new lines
|
28
55
|
end
|
29
56
|
|
30
57
|
private
|
31
58
|
|
59
|
+
def add_line(line)
|
60
|
+
lines << (' ' * @indents + line)
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_title(title)
|
64
|
+
add_line "title #{title}"
|
65
|
+
end
|
66
|
+
|
32
67
|
def process_start_label(receiver_label)
|
33
68
|
"+#{receiver_label}"
|
34
69
|
end
|
@@ -37,11 +72,46 @@ module Dogviz
|
|
37
72
|
"-#{receiver_label}"
|
38
73
|
end
|
39
74
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
'end note'].join("\n")
|
75
|
+
def extract_annotations(detail, sender, receiver, process_description)
|
76
|
+
[ "note right of #{escape receiver}",
|
77
|
+
" #{escape process_description}",
|
78
|
+
'end note' ].join("\n")
|
45
79
|
end
|
80
|
+
|
81
|
+
def escape(s)
|
82
|
+
if (/\s/).match(s)
|
83
|
+
"\"#{s}\""
|
84
|
+
else
|
85
|
+
s
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
class WebSequenceDiagramsSequenceRenderer < SequenceRenderer
|
92
|
+
def initialize title
|
93
|
+
super title
|
94
|
+
@rendered_class = WebSequenceDiagramsRenderedSequence
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class PlantUmlSequenceRenderer < SequenceRenderer
|
99
|
+
def initialize title
|
100
|
+
super title
|
101
|
+
@rendered_class = PlantUmlRenderedSequence
|
102
|
+
end
|
103
|
+
|
104
|
+
def divider(text)
|
105
|
+
add_line "== #{text} =="
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class PngSequenceRenderer < PlantUmlSequenceRenderer
|
110
|
+
def initialize title
|
111
|
+
super title
|
112
|
+
@rendered_class = PngRenderedSequence
|
113
|
+
end
|
46
114
|
end
|
115
|
+
|
116
|
+
|
47
117
|
end
|
data/lib/dogviz/system.rb
CHANGED
@@ -19,12 +19,23 @@ module Dogviz
|
|
19
19
|
@render_hints = hints
|
20
20
|
@title = create_title(name)
|
21
21
|
@rendered = false
|
22
|
+
|
23
|
+
@warnings = Set.new
|
24
|
+
@messages = Set.new
|
25
|
+
|
26
|
+
@suppress_warnings = false
|
27
|
+
@suppress_messages = false
|
28
|
+
|
29
|
+
on_exit {
|
30
|
+
output_messages
|
31
|
+
output_warnings
|
32
|
+
}
|
22
33
|
end
|
23
34
|
|
24
35
|
def output(*args)
|
25
36
|
render
|
26
|
-
out = graph.output
|
27
|
-
|
37
|
+
out = graph.output(*args)
|
38
|
+
@messages << "Created output: #{args.join ' '}" unless suppress_messages?
|
28
39
|
out
|
29
40
|
end
|
30
41
|
|
@@ -77,10 +88,58 @@ module Dogviz
|
|
77
88
|
@non_render_hints[:auto_nominate]
|
78
89
|
end
|
79
90
|
|
91
|
+
def warn_on_exit(warning)
|
92
|
+
@warnings << warning
|
93
|
+
end
|
80
94
|
|
95
|
+
def warnings
|
96
|
+
@warnings.to_a
|
97
|
+
end
|
98
|
+
|
99
|
+
def messages
|
100
|
+
@messages.to_a
|
101
|
+
end
|
102
|
+
|
103
|
+
def suppress_warnings!
|
104
|
+
@suppress_warnings = true
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
def suppress_messages!
|
109
|
+
@suppress_messages = true
|
110
|
+
self
|
111
|
+
end
|
81
112
|
|
82
113
|
private
|
83
114
|
|
115
|
+
def on_exit(&block)
|
116
|
+
Kernel.at_exit(&block)
|
117
|
+
end
|
118
|
+
|
119
|
+
def output_messages
|
120
|
+
unless suppress_messages?
|
121
|
+
messages.each {|message|
|
122
|
+
STDERR.puts message
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def output_warnings
|
128
|
+
unless suppress_warnings?
|
129
|
+
warnings.each {|warning|
|
130
|
+
STDERR.puts warning
|
131
|
+
}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def suppress_warnings?
|
136
|
+
@suppress_warnings
|
137
|
+
end
|
138
|
+
|
139
|
+
def suppress_messages?
|
140
|
+
@suppress_messages
|
141
|
+
end
|
142
|
+
|
84
143
|
def remove_dogviz_hints!(hints)
|
85
144
|
dogviz_only_hints = {}
|
86
145
|
%i(colorize_edges auto_nominate).each { |k|
|
@@ -93,9 +152,5 @@ module Dogviz
|
|
93
152
|
now = DateTime.now
|
94
153
|
"#{now.strftime '%H:%M'} #{name} #{now.strftime '%F'}"
|
95
154
|
end
|
96
|
-
|
97
|
-
def run_from_command_line?
|
98
|
-
$stdout.isatty
|
99
|
-
end
|
100
155
|
end
|
101
156
|
end
|
data/lib/dogviz/thing.rb
CHANGED
@@ -63,11 +63,17 @@ module Dogviz
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def setup_render_edge(other, options)
|
66
|
+
fontsize = 14
|
67
|
+
fontsize += options[:stroke] if options.has_key?(:stroke)
|
66
68
|
pointers << {
|
67
69
|
other: other,
|
68
70
|
options: {
|
69
71
|
xlabel: options[:name],
|
70
|
-
style: options[:style]
|
72
|
+
style: options[:style],
|
73
|
+
color: options[:color],
|
74
|
+
fontcolor: options[:color],
|
75
|
+
penwidth: options[:stroke],
|
76
|
+
fontsize: fontsize
|
71
77
|
}.merge(inherited_render_options)
|
72
78
|
}
|
73
79
|
|
@@ -105,10 +111,10 @@ module Dogviz
|
|
105
111
|
others = [other]
|
106
112
|
end
|
107
113
|
|
108
|
-
others.each do |
|
109
|
-
edge_heads <<
|
114
|
+
others.each do |other_to_render|
|
115
|
+
edge_heads << other_to_render
|
110
116
|
render_options = pointer[:options]
|
111
|
-
renderer.render_edge(from,
|
117
|
+
renderer.render_edge(from, other_to_render, render_options)
|
112
118
|
end
|
113
119
|
end
|
114
120
|
|
data/lib/dogviz/version.rb
CHANGED
data/tests/graph_checking.rb
CHANGED
data/tests/setup_tests.rb
CHANGED
data/tests/svg_graph.rb
CHANGED
data/tests/test_dogviz_flows.rb
CHANGED
@@ -9,83 +9,289 @@ module Tests
|
|
9
9
|
"/tmp/dogviz_flow_test.#{ext}"
|
10
10
|
end
|
11
11
|
|
12
|
+
def delete_outfile(ext)
|
13
|
+
FileUtils.rm_f outfile(ext)
|
14
|
+
end
|
15
|
+
|
12
16
|
def read_outfile(ext)
|
13
17
|
File.read outfile(ext)
|
14
18
|
end
|
15
19
|
|
16
20
|
include Dogviz
|
17
21
|
|
18
|
-
def
|
19
|
-
|
20
|
-
eater = sys.thing 'eater'
|
21
|
-
server = sys.thing 'server'
|
22
|
-
cook = sys.thing 'chef'
|
22
|
+
def test_nested_flow_syntax
|
23
|
+
create_takeaway
|
23
24
|
|
24
|
-
order = sys.flow
|
25
|
-
order.flows eater, 'gimme burger',
|
26
|
-
server, 'passes order',
|
27
|
-
cook, server, eater
|
25
|
+
order = sys.flow('order').involves(sys.server, sys.cook)
|
28
26
|
|
29
|
-
|
27
|
+
sys.server.receives burger: { 'gimme burger' => 'here ya go' },
|
28
|
+
dessert: 'gimme dessert'
|
29
|
+
sys.cook.receives order: { 'passes order' => '' }
|
30
|
+
|
31
|
+
order.from(sys.eater) {
|
32
|
+
sys.server.burger {
|
33
|
+
sys.cook.order
|
34
|
+
}
|
35
|
+
sys.server.dessert
|
36
|
+
}
|
37
|
+
|
38
|
+
definition = sequence_definition(order)
|
39
|
+
|
40
|
+
assert_equal [
|
41
|
+
'eater -> server: gimme burger',
|
42
|
+
'server -> cook: passes order',
|
43
|
+
'cook -> server:',
|
44
|
+
'server -> eater: here ya go',
|
45
|
+
'eater -> server: gimme dessert'
|
46
|
+
].join("\n"), definition
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_get_useful_error_if_not_called_involves_for_called_actor
|
50
|
+
create_takeaway
|
51
|
+
|
52
|
+
order = sys.flow('order')
|
53
|
+
|
54
|
+
sys.server.receives burger: 'gimme burger'
|
55
|
+
|
56
|
+
assert_raise_message(/call #involves for all actors/) {
|
57
|
+
|
58
|
+
order.from(sys.eater) {
|
59
|
+
sys.server.burger
|
60
|
+
}
|
61
|
+
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_nested_flow_with_optional_part_of_sequence
|
66
|
+
create_takeaway
|
67
|
+
|
68
|
+
order = sys.flow('order').involves sys.server
|
69
|
+
|
70
|
+
sys.server.receives burger: { 'gimme burger' => 'here you go' }
|
71
|
+
|
72
|
+
order.from(sys.eater) {
|
73
|
+
order.opt('if hungry') {
|
74
|
+
sys.server.burger
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
definition = sequence_definition(order)
|
79
|
+
|
80
|
+
assert_equal [
|
81
|
+
'opt "if hungry"',
|
82
|
+
' eater -> server: gimme burger',
|
83
|
+
' server -> eater: here you go',
|
84
|
+
'end'
|
85
|
+
].join("\n"), definition
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_nested_flow_dynamic_receives_definition
|
89
|
+
create_takeaway
|
90
|
+
|
91
|
+
order = sys.flow('order').involves sys.server
|
92
|
+
|
93
|
+
sys.server.receives(:order) do |order_no|
|
94
|
+
{ "make order #{order_no}" => "deliver order #{order_no}" }
|
95
|
+
end
|
96
|
+
|
97
|
+
order.from(sys.eater) {
|
98
|
+
sys.server.order(1)
|
99
|
+
sys.server.order(2)
|
100
|
+
}
|
101
|
+
|
102
|
+
definition = sequence_definition(order)
|
103
|
+
|
104
|
+
assert_equal [
|
105
|
+
'eater -> server: make order 1',
|
106
|
+
'server -> eater: deliver order 1',
|
107
|
+
'eater -> server: make order 2',
|
108
|
+
'server -> eater: deliver order 2'
|
109
|
+
].join("\n"), definition
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_plantuml_text_output
|
113
|
+
create_takeaway
|
114
|
+
|
115
|
+
order = sys.flow('the order').involves sys.server
|
116
|
+
|
117
|
+
sys.server.receives burger: 'gimme'
|
118
|
+
|
119
|
+
order.from(sys.eater) {
|
120
|
+
sys.server.burger
|
121
|
+
}
|
122
|
+
|
123
|
+
order.output plantuml: outfile('seq.plantuml')
|
124
|
+
definition = read_outfile('seq.plantuml')
|
125
|
+
|
126
|
+
assert_equal [
|
127
|
+
'@startuml',
|
128
|
+
'title the order',
|
129
|
+
'eater -> server: gimme',
|
130
|
+
'@enduml'
|
131
|
+
].join("\n"), definition
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_plantuml_dividers
|
135
|
+
create_takeaway
|
136
|
+
|
137
|
+
order = sys.flow('order')
|
138
|
+
|
139
|
+
sys.server.receives burger: 'gimme'
|
140
|
+
|
141
|
+
order.from(sys.eater) {
|
142
|
+
order.divider('bob')
|
143
|
+
}
|
144
|
+
|
145
|
+
definition = order.output plantuml: outfile('seq.plantuml')
|
146
|
+
|
147
|
+
assert_equal [
|
148
|
+
'@startuml',
|
149
|
+
'title order',
|
150
|
+
'== bob ==',
|
151
|
+
'@enduml'
|
152
|
+
].join("\n"), definition
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
class MockExecutor
|
157
|
+
def execute(cmd)
|
158
|
+
@cmd = cmd
|
159
|
+
end
|
160
|
+
attr_reader :cmd
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
def test_flow_png_image_output_via_plantuml
|
165
|
+
create_takeaway
|
166
|
+
|
167
|
+
order = sys.flow('order').involves sys.server
|
168
|
+
|
169
|
+
sys.server.receives burger: 'gimme'
|
170
|
+
|
171
|
+
order.from(sys.eater) {
|
172
|
+
sys.server.burger
|
173
|
+
}
|
174
|
+
|
175
|
+
plantuml_definition_file = outfile('seq.plantuml')
|
176
|
+
FileUtils.rm_f plantuml_definition_file
|
177
|
+
mock_executor = MockExecutor.new
|
178
|
+
order.executor = mock_executor
|
179
|
+
order.output png: outfile('seq.png')
|
180
|
+
|
181
|
+
assert_equal true, File.exist?(plantuml_definition_file)
|
182
|
+
assert_equal 'plantuml -tpng ' + plantuml_definition_file, mock_executor.cmd
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_nested_flow_with_note_on_right
|
186
|
+
create_takeaway
|
187
|
+
|
188
|
+
order = sys.flow('order').involves sys.server, sys.eater
|
189
|
+
|
190
|
+
sys.server.receives burger: 'gimme burger'
|
30
191
|
|
31
|
-
|
192
|
+
order.from(sys.eater) {
|
193
|
+
sys.server.burger
|
194
|
+
sys.server.note(:right, 'a note')
|
195
|
+
}
|
196
|
+
|
197
|
+
definition = sequence_definition(order)
|
32
198
|
|
33
199
|
assert_equal [
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
200
|
+
'eater -> server: gimme burger',
|
201
|
+
'note right of server',
|
202
|
+
' a note',
|
203
|
+
'end note',
|
38
204
|
].join("\n"), definition
|
39
205
|
end
|
40
206
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
server = sys.thing 'server'
|
45
|
-
cook = sys.thing 'cook'
|
207
|
+
def test_flow_generates_precise_sequence_with_deprecated_flows
|
208
|
+
create_takeaway
|
209
|
+
sys.suppress_warnings!
|
46
210
|
|
47
211
|
order = sys.flow 'order'
|
48
|
-
order.flows eater, '
|
49
|
-
server, '
|
50
|
-
cook.
|
51
|
-
|
52
|
-
|
53
|
-
|
212
|
+
order.flows sys.eater, 'gimme burger',
|
213
|
+
sys.server, 'passes order',
|
214
|
+
sys.cook, sys.server, sys.eater
|
215
|
+
|
216
|
+
definition = sequence_definition(order, without_lines_starting: [])
|
217
|
+
|
218
|
+
assert_equal [
|
219
|
+
'title order',
|
220
|
+
'eater -> server: gimme burger',
|
221
|
+
'server -> cook: passes order',
|
222
|
+
'cook -> server:',
|
223
|
+
'server -> eater:',
|
224
|
+
].join("\n"), definition
|
54
225
|
end
|
55
226
|
|
56
|
-
def
|
57
|
-
order =
|
227
|
+
def test_flow_generates_precise_sequence_with_action_with_deprecated_flows
|
228
|
+
order = create_food_flow_with_deprecated_flows
|
58
229
|
|
59
|
-
|
60
|
-
definition = read_outfile('seq.txt')
|
230
|
+
definition = sequence_definition order
|
61
231
|
|
62
232
|
assert_equal([
|
63
233
|
'eater -> server: orders',
|
64
234
|
'server -> +cook: creates order',
|
65
235
|
'note right of cook',
|
66
|
-
' cooks burger',
|
236
|
+
' "cooks burger"',
|
67
237
|
'end note',
|
68
238
|
'cook -> -server: burger',
|
69
239
|
'server -> eater: burger',
|
70
240
|
].join("\n"), definition)
|
71
241
|
end
|
72
242
|
|
73
|
-
def
|
74
|
-
order =
|
243
|
+
def test_flow_can_be_used_to_make_connections_in_dog_with_deprecated_flows
|
244
|
+
order = create_food_flow_with_deprecated_flows
|
75
245
|
|
76
246
|
order.make_connections
|
77
247
|
|
78
248
|
assert_equal('eater->server server->cook server->eater cook->server', connections)
|
79
249
|
end
|
80
250
|
|
251
|
+
def test_deprecated_flows_generates_warnings
|
252
|
+
order = create_food_flow_with_deprecated_flows
|
253
|
+
|
254
|
+
assert_equal(1, order.sys.warnings.select {|w| w.include?('flow#flows deprecated')}.size)
|
255
|
+
end
|
256
|
+
|
81
257
|
private
|
82
258
|
|
83
259
|
attr_accessor :sys
|
84
260
|
|
261
|
+
def sequence_definition(order, without_lines_starting: ['title'])
|
262
|
+
order.output sequence: outfile('seq.txt')
|
263
|
+
|
264
|
+
lines = read_outfile('seq.txt').split "\n"
|
265
|
+
without_lines_starting.each {|prefix|
|
266
|
+
lines = lines.reject {|line| line.start_with?(prefix) }
|
267
|
+
}
|
268
|
+
lines.join "\n"
|
269
|
+
end
|
270
|
+
|
85
271
|
def graph
|
86
272
|
g = sys.render
|
87
273
|
sys.output svg: outfile('svg')
|
88
274
|
g
|
89
275
|
end
|
276
|
+
|
277
|
+
def create_takeaway
|
278
|
+
@sys = System.new('takeaway', auto_nominate: true).suppress_messages!
|
279
|
+
sys.thing 'eater'
|
280
|
+
sys.thing 'server'
|
281
|
+
sys.thing 'cook'
|
282
|
+
end
|
283
|
+
|
284
|
+
def create_food_flow_with_deprecated_flows
|
285
|
+
create_takeaway
|
286
|
+
order = sys.flow 'order'
|
287
|
+
sys.suppress_warnings!
|
288
|
+
order.flows sys.eater, 'orders',
|
289
|
+
sys.server, 'creates order',
|
290
|
+
sys.cook.does('cooks burger'),
|
291
|
+
'burger', sys.server,
|
292
|
+
'burger', sys.eater
|
293
|
+
order
|
294
|
+
end
|
295
|
+
|
90
296
|
end
|
91
297
|
end
|
@@ -18,9 +18,10 @@ module Tests
|
|
18
18
|
|
19
19
|
include Dogviz
|
20
20
|
class Family < Dogviz::System
|
21
|
-
attr_reader
|
21
|
+
attr_reader(*%i(cat dog mum son))
|
22
22
|
def initialize
|
23
23
|
super 'family'
|
24
|
+
suppress_messages!
|
24
25
|
|
25
26
|
house = container 'household'
|
26
27
|
|
@@ -60,7 +61,7 @@ module Tests
|
|
60
61
|
|
61
62
|
dotspec = File.read outfile('dot')
|
62
63
|
|
63
|
-
assert_match
|
64
|
+
assert_match(/rank=sink/, dotspec)
|
64
65
|
end
|
65
66
|
|
66
67
|
def test_can_render_auto_nominate_graph
|
@@ -70,7 +71,7 @@ module Tests
|
|
70
71
|
end
|
71
72
|
|
72
73
|
def system_with_auto_nominate
|
73
|
-
Dogviz::System.new
|
74
|
+
Dogviz::System.new('test', auto_nominate: true).suppress_messages!
|
74
75
|
end
|
75
76
|
end
|
76
77
|
end
|
data/tests/test_dogviz_graph.rb
CHANGED
@@ -34,7 +34,7 @@ class TestDogvizGraphvizRendering < Test::Unit::TestCase
|
|
34
34
|
|
35
35
|
def test_containers_are_subgraphs_prefixed_with_cluster_for_visual_containment_in_GraphViz
|
36
36
|
top = sys.container('top')
|
37
|
-
|
37
|
+
top.container('nested')
|
38
38
|
|
39
39
|
assert_equal('cluster_top', subgraph_ids.first)
|
40
40
|
assert_equal('cluster_top_nested', subgraph_ids.last)
|
data/todo.txt
CHANGED
@@ -1,7 +1,21 @@
|
|
1
|
-
* spike 'autostrap': automatically strap-on reader methods using normalised name
|
1
|
+
* [DONE] spike 'autostrap': automatically strap-on reader methods using normalised name (auto_nominate)
|
2
2
|
* [DONE] render generic output (sigma.js json) for ingestion -> space-dog
|
3
|
-
* sort out fore vs back color
|
4
3
|
* [DONE] move 'cluster_' prefix to graphviz renderer
|
4
|
+
* flow / sequence improvements
|
5
|
+
[DONE] new flow syntax
|
6
|
+
[DONE] opt combined fragments
|
7
|
+
[DONE] titles
|
8
|
+
[DONE] indent text output
|
9
|
+
- alt combined fragments
|
10
|
+
- allow unbalanced combined fragments (explicit start + end, not just by block)
|
11
|
+
* plantuml flow output
|
12
|
+
[DONE] text output
|
13
|
+
[DONE] direct image output if plantuml installed
|
14
|
+
[DONE] sort double quotes
|
15
|
+
[ ] allow multiline
|
16
|
+
* output generated flow file
|
17
|
+
* deprecate old flows
|
18
|
+
* sort out HACK (search for commit in git log) around flow detail used in make_connections
|
5
19
|
* clean up init options + render attributes - not v clear / overlapping
|
6
20
|
* make style attributes well defined -> less leaky + explicit graphviz passthroughs
|
7
21
|
* separate style from domain-specific elements (can use same view-manipulation selection mechanisms)
|
@@ -15,6 +29,33 @@
|
|
15
29
|
- maybe makes more seamless when collapsing/hiding for different views?
|
16
30
|
* add sankey diagrams?
|
17
31
|
* other renderer integrations:
|
18
|
-
- plantuml?
|
19
32
|
- cyctoscape?
|
20
|
-
* switch to proper graph lib under covers - this is just a builder + view wrapper...
|
33
|
+
* switch to proper graph lib under covers - this is just a builder + view wrapper... https://github.com/monora/rgl?
|
34
|
+
* #repeat(count) on containers to give multiple instances, by default expanded (to show each instance)
|
35
|
+
* ...then can #collapse (1 instance showing, number labelled)
|
36
|
+
* ...then can #template or #prototype or ?? (1 instance detailed, rest simplified)
|
37
|
+
* ...make count of repeat be complex e.g. to show auto-scaling, 1 per customer etc. etc.
|
38
|
+
* make render 2 stages (rollups + repeats, then render) enabling:
|
39
|
+
- render multiple views from same dog instance
|
40
|
+
- separate and simplify rollup logic, which is a bit complex
|
41
|
+
- pave way for simplified repeat expansion logic, which is similarly complex (+ almost the reverse)
|
42
|
+
- pave way for cleaning graphviz specifics out of main dog ('cluster_', subgraphs)
|
43
|
+
* edge attributes?
|
44
|
+
i - pass on any attributes passed as _graphviz: on to graphviz renderer for relevant entity
|
45
|
+
ii - map in simpler named attributes
|
46
|
+
- color -> edge + font color
|
47
|
+
- stroke -> edge size and font size
|
48
|
+
* render to d3 force directed graph
|
49
|
+
* "rollup edges": where many edges from a node, rollup those to certain destination (e.g. from 'website' to all
|
50
|
+
in '3rd party' container) so that not so crowded around source (would add extra view node nearer target)
|
51
|
+
* "via": app -> service, via: load_balancer... then can omit load_balancer and get direct app -> service connection
|
52
|
+
* capture arbitrary info
|
53
|
+
- filter primitives for views
|
54
|
+
- default info_level to 'all', could be set to 'none'
|
55
|
+
- info_level applies to:
|
56
|
+
- entire view by default
|
57
|
+
- specified per node
|
58
|
+
- optionally inherited?
|
59
|
+
- allow nodes to assign summary fields? then could be set to 'summary'
|
60
|
+
* sort out fore vs back color
|
61
|
+
* check out structurizr.com
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dogviz
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.22
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- damned
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-06-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '12.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '12.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: simplecov
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '1.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: test-unit
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: ruby-graphviz
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +108,20 @@ dependencies:
|
|
94
108
|
- - "~>"
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: json
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 2.1.0
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 2.1.0
|
97
125
|
description: leverages graphviz to generate multiple views of a domain-specific graph
|
98
126
|
email:
|
99
127
|
- writetodan@yahoo.com
|