dogviz 0.0.21 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b9eb357510d3a4de43f3cc7af60919bc6540915
4
- data.tar.gz: 4bceb85921dd199a159d7f6a3340041c31ce3eb9
3
+ metadata.gz: a428fcd04a6f1cc90bf9225b887c94889c7667ef
4
+ data.tar.gz: 26363eb38c24901ae048103f99179fb7d5e2807a
5
5
  SHA512:
6
- metadata.gz: 222029facb991efdcebe0bb8877c3cbbc46d8516e130255199c973809b0a018b2a6eda659e6320ff8167d2f629353149043bb318b81a1ceca38b764d3e7db966
7
- data.tar.gz: 78912d906cea50c5861c39c658c97e5a0a9833a3113307a2d16519510c9544a5754dd59e35a7ef90382a41b3624a1edcf25cee8b9967f63086fa13bb6140533b
6
+ metadata.gz: 1e363ecd7f98e01a3218c0bcd389f952f7350fa4488fd30e1e4bc3ff7b512624208576137a05e2cd26b48f51235e9f34954b95b28b4f004eb468b16cc909f684
7
+ data.tar.gz: 5f9ae410f02002a7148fa66c0ecead46b0a8db9980cbf84eab671298cf3fb85a36e4de4ac83162a7a70dd23f7bd092322b9453ae67cf9606c8a37f372dd1b4a0
data/Rakefile CHANGED
@@ -3,7 +3,6 @@ require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new do |t|
5
5
  t.test_files = FileList['tests/test*.rb']
6
- t.verbose = true
7
6
  end
8
7
 
9
8
  require 'colorize'
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", "~> 10.0"
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 /\s/
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 = {} if @attributes.nil?
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? &:skip?
69
+ ancestors.any?(&:skip?)
69
70
  end
70
71
 
71
72
  def under_rollup?
72
- ancestors.any? &:rollup?
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
- @calls = []
19
+ @commands = []
20
+ @actors = []
21
+ @caller_stack = []
22
+ @executor = nil
11
23
  end
12
24
 
13
25
  def make_connections
14
- calls.each { |from, to, label|
15
- thing_of(from).points_to thing_of(to), label: label
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
- calls << [from, to, label]
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: #{calls}" unless step.is_a?(Thing) || step.is_a?(Process)
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 sequence, not: '#{type}'" unless type == :sequence
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 = SequenceRenderer.new(@name)
52
- calls.each do |from, to, label|
53
- renderer.render_edge from, to, {label: label}
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 :calls, :sys
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
@@ -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
- @graph = GraphViz.digraph(title)
10
- @graph[hints]
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
- node = parent_node(parent).add_nodes(id, merged_attributes)
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 &matcher
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 &matcher
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
@@ -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 &matcher
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 &matcher
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
- text = @lines.map(&:strip).join "\n"
9
- File.write type_to_file.values.first, text
10
- text
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
- detail = process_annotations(detail, sender_label, receiver_label, other.description)
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
- lines << "#{sender_label} -> #{receiver_label}: #{detail}"
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
- RenderedSequence.new lines
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 process_annotations(detail, sender, receiver, process_description)
41
- detail = [detail,
42
- "note right of #{receiver}",
43
- " #{process_description}",
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 *args
27
- puts "Created output: #{args.join ' '}" if run_from_command_line?
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 |other|
109
- edge_heads << other
114
+ others.each do |other_to_render|
115
+ edge_heads << other_to_render
110
116
  render_options = pointer[:options]
111
- renderer.render_edge(from, other, render_options)
117
+ renderer.render_edge(from, other_to_render, render_options)
112
118
  end
113
119
  end
114
120
 
@@ -1,3 +1,3 @@
1
1
  module Dogviz
2
- VERSION = '0.0.21'
2
+ VERSION = '0.0.22'
3
3
  end
@@ -4,7 +4,7 @@ module GraphChecking
4
4
  end
5
5
 
6
6
  def subgraph_ids_without_cluster_prefixes
7
- subgraph_ids.map {|id| id.gsub /^cluster_/, '' }
7
+ subgraph_ids.map {|id| id.gsub(/^cluster_/, '') }
8
8
  end
9
9
 
10
10
  def subgraph(id)
data/tests/setup_tests.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  require "test/unit"
2
2
  require_relative '../lib/dogviz'
3
- SimpleCov.root('..') if Object.const_defined?('SimpleCov')
3
+ SimpleCov.root('..') if Object.const_defined?('SimpleCov')
data/tests/svg_graph.rb CHANGED
@@ -66,7 +66,7 @@ module Tests
66
66
  private
67
67
 
68
68
  def comments
69
- @doc.children.select &:comment?
69
+ @doc.children.select(&:comment?)
70
70
  end
71
71
  end
72
72
  end
@@ -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 test_flow_generates_precise_sequence
19
- sys = System.new 'takeaway'
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 'order'
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
- order.output sequence: outfile('seq.txt')
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
- definition = read_outfile('seq.txt')
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
- 'eater -> server: gimme burger',
35
- 'server -> chef: passes order',
36
- 'chef -> server:',
37
- 'server -> eater:',
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 create_food_flow
42
- @sys = System.new 'takeaway'
43
- eater = sys.thing 'eater'
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, 'orders',
49
- server, 'creates order',
50
- cook.does('cooks burger'),
51
- 'burger', server,
52
- 'burger', eater
53
- order
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 test_flow_generates_precise_sequence_with_action
57
- order = create_food_flow
227
+ def test_flow_generates_precise_sequence_with_action_with_deprecated_flows
228
+ order = create_food_flow_with_deprecated_flows
58
229
 
59
- order.output sequence: outfile('seq.txt')
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 test_flow_can_be_used_to_make_connections_in_dog
74
- order = create_food_flow
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 *%i(cat dog mum son)
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 /rank=sink/, dotspec
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 'test', auto_nominate: true
74
+ Dogviz::System.new('test', auto_nominate: true).suppress_messages!
74
75
  end
75
76
  end
76
77
  end
@@ -163,7 +163,7 @@ class TestDogvizGraph < Test::Unit::TestCase
163
163
  def test_root
164
164
  group = sys.group('g')
165
165
  nested_group = group.group('nested group')
166
- thing1 = group.thing('n1')
166
+ thing1 = nested_group.thing('n1')
167
167
 
168
168
  assert_equal sys, thing1.root
169
169
  end
@@ -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
- nested = top.container('nested')
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.21
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-04-27 00:00:00.000000000 Z
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: '10.0'
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: '10.0'
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