dogviz 0.0.3

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c7fd14011b87419590d6395eaed53feebf8e5485
4
+ data.tar.gz: 692dbf2318025b7176b3deb470aadcbbc3539f4d
5
+ SHA512:
6
+ metadata.gz: de38e540d402cf39df368688e07b1a405b1558b906074a925c844b2830a44988736f9d199491f5cdae43f55cd2589a745156b08f04fa2dac1c61830019ca997d
7
+ data.tar.gz: 676fa2fff0072567e6210001605f0286069a91629432e6941d2d86d56d41eb015000436f8252d79b3c4e2678d0c994df223d538c58bceb3322187040b41f9770
@@ -0,0 +1,6 @@
1
+ .idea
2
+ *-generated.*
3
+ coverage
4
+ sys.rb
5
+ dogviz*gem
6
+ Gemfile.lock
@@ -0,0 +1 @@
1
+ 2.1.5
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Dan Moore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # dogviz
2
+ domain object graph visualisation built on graphviz
3
+
4
+ bundle install
5
+
6
+ bundle exec ruby examples/website.rb
@@ -0,0 +1,16 @@
1
+ require 'bundler/setup'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = FileList['tests/test*.rb']
6
+ t.verbose = true
7
+ end
8
+
9
+ require 'colorize'
10
+ at_exit {
11
+ if $?.exitstatus == 0
12
+ puts 'PASSED'.green
13
+ else
14
+ puts 'FAILED'.red
15
+ end
16
+ }
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dogviz/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dogviz"
8
+ spec.version = Dogviz::VERSION
9
+ spec.authors = ["damned"]
10
+ spec.email = ["writetodan@yahoo.com"]
11
+
12
+ spec.summary = %q{domain object graph visualisation}
13
+ spec.description = %q{leverages graphviz to generate multiple views of a domain-specific graph}
14
+ spec.homepage = "https://github.com/damned/dogviz"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.10"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency 'simplecov', '~> 0'
23
+ spec.add_development_dependency 'colorize', '~> 0'
24
+
25
+ spec.add_dependency 'ruby-graphviz', '~> 0'
26
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'website_domain'
2
+
3
+ def describe_tw_com(sys)
4
+ website = sys.thing('website')
5
+ sys.user('visitor').points_to website
6
+ end
7
+
8
+ def output(sys, name)
9
+ sys.output(dot: "#{name}-generated.dot")
10
+ sys.output(png: "#{name}-generated.png")
11
+ sys.output(svg: "#{name}-generated.svg")
12
+ end
13
+
14
+ include WebsiteDomain
15
+
16
+ render_hints = {
17
+ splines: false
18
+ }
19
+ sys = WebsiteSystem.new 'website', render_hints
20
+
21
+ describe_tw_com sys
22
+ output sys, 'website'
@@ -0,0 +1,162 @@
1
+ require 'dogviz'
2
+
3
+ module WebsiteDomain
4
+ include Dogviz
5
+ module Creators
6
+ def box(name, options={})
7
+ add Box.new(self, name, options)
8
+ end
9
+ def pipeline(name)
10
+ add Pipeline.new(self, name)
11
+ end
12
+ def lb(name)
13
+ add LoadBalancer.new self, name
14
+ end
15
+ def process(name)
16
+ add Process.new self, name
17
+ end
18
+ def external(name, options={})
19
+ add External.new self, name, options
20
+ end
21
+ def grouping(name, options = {})
22
+ add Grouping.new self, name, options
23
+ end
24
+ def data_centre(name)
25
+ add DataCentre.new self, name
26
+ end
27
+ def user(name)
28
+ add User.new self, name
29
+ end
30
+ end
31
+ class Grouping < LogicalContainer
32
+ include Creators
33
+ end
34
+ class WebsiteSystem < System
35
+ include Creators
36
+ def render
37
+ puts 'Rendering...' unless @rendered
38
+ super
39
+ end
40
+ def rollup_by_class(type)
41
+ find_all { |n|
42
+ n.is_a?(type)
43
+ }.each &:rollup!
44
+ end
45
+ def rollup_names_starting(start)
46
+ find_all { |n|
47
+ n.name.start_with? start
48
+ }.each &:rollup!
49
+ end
50
+ def rollup_names_including(substring)
51
+ find_all { |n|
52
+ n.name.include? substring
53
+ }.each &:rollup!
54
+ end
55
+ end
56
+ class Box < Container
57
+ def initialize(parent, name, options={})
58
+ super parent, name, {style: 'filled', color: '#ffaaaa'}.merge(options)
59
+ end
60
+ def service(name, options={})
61
+ add Service.new self, name, options
62
+ end
63
+ def process(name)
64
+ add Process.new self, name
65
+ end
66
+ end
67
+ class Service < Container
68
+ def initialize(parent, name, options={})
69
+ super parent, name, options.merge(color: '#cccccc', style: 'filled')
70
+ end
71
+ include Creators
72
+ end
73
+ class DataCentre < Container
74
+ def initialize(parent, name, options={})
75
+ super parent, name
76
+ end
77
+ include Creators
78
+ end
79
+ class Pipeline < Container
80
+ def initialize(parent, name, options={})
81
+ super parent, name, options.merge(color: '#c8f8c8', style: 'filled')
82
+ end
83
+ include Creators
84
+ def stage(name)
85
+ add Stage.new self, name
86
+ end
87
+ end
88
+ class Stage < Thing
89
+ def initialize(parent, name)
90
+ super parent, name, color: '#aaccaa', style: 'filled'
91
+ doclink("http://go/#{name}")
92
+ end
93
+
94
+ def deploys(*apps)
95
+ apps.each {|app|
96
+ points_to app, name: 'deploys', style: 'dotted'
97
+ }
98
+ end
99
+ def triggers(*stages)
100
+ stages.each {|stage|
101
+ points_to stage, name: 'triggers', color: '#00bb00'
102
+ }
103
+ end
104
+ def configures(*things)
105
+ things.each {|thing|
106
+ points_to thing, name: 'configures', style: 'dashed'
107
+ }
108
+ end
109
+ end
110
+ class Process < Thing
111
+ def initialize(parent, name)
112
+ super parent, name, style: 'filled'
113
+ end
114
+ def calls_all(*callees)
115
+ points_to_all *callees
116
+ end
117
+ def calls(callee, options={})
118
+ points_to callee, options
119
+ end
120
+ end
121
+ class External < Thing
122
+ def initialize(parent, name, options={})
123
+ super parent, name, options.merge(color: 'lightyellow', style: 'filled')
124
+ end
125
+ end
126
+ class User < Thing
127
+ def initialize(parent, name)
128
+ super parent, name, shape: 'circle', color: 'brown'
129
+ end
130
+ def uses(used)
131
+ points_to used
132
+ end
133
+ def uses_all(*useds)
134
+ points_to_all *useds
135
+ end
136
+ end
137
+ class LoadBalancer < Thing
138
+ def initialize(parent, name)
139
+ super parent, name, color: '#ff6666', style: 'filled'
140
+ end
141
+ end
142
+
143
+ class Repo
144
+ def initialize(organisation, name)
145
+ @name = name
146
+ @organisation = organisation
147
+ end
148
+
149
+ def url
150
+ "https://github.com/#{@organisation}/#{@name}"
151
+ end
152
+
153
+ def source(path)
154
+ "#{url}/blob/master/#{path}"
155
+ end
156
+
157
+ def to_s
158
+ url
159
+ end
160
+ end
161
+
162
+ end
@@ -0,0 +1,428 @@
1
+ require 'ruby-graphviz'
2
+
3
+ module Dogviz
4
+ module Common
5
+ def create_id(name, parent)
6
+ parts = []
7
+ parts << parent.id if parent.respond_to? :id
8
+ parts += name.split /\s/
9
+ parts.join '_'
10
+ end
11
+ def graph
12
+ parent.graph
13
+ end
14
+ def root
15
+ ancestors.last
16
+ end
17
+ def ancestors
18
+ ancestors = [parent]
19
+ loop do
20
+ break unless ancestors.last.respond_to?(:parent)
21
+ ancestors << ancestors.last.parent
22
+ end
23
+ ancestors
24
+ end
25
+ def doclink(url)
26
+ setup_render_attributes(URL: url)
27
+ end
28
+ def setup_render_attributes(attributes)
29
+ @attributes = {} if @attributes.nil?
30
+ @attributes.merge!(attributes)
31
+ end
32
+ def rollup?
33
+ @rollup
34
+ end
35
+ def rollup!
36
+ @rollup = true
37
+ self
38
+ end
39
+ def under_rollup?
40
+ ancestors.any? &:rollup?
41
+ end
42
+ def in_rollup?
43
+ rollup? || under_rollup?
44
+ end
45
+ def on_top_rollup?
46
+ rollup? && !under_rollup?
47
+ end
48
+ end
49
+ module Parent
50
+ def find_all(&matcher)
51
+ raise MissingMatchBlockError.new unless block_given?
52
+ @by_name.find_all &matcher
53
+ end
54
+ def find(name=nil, &matcher)
55
+ if block_given?
56
+ @by_name.find &matcher
57
+ else
58
+ raise 'Need to provide name or block' if name.nil?
59
+ @by_name.lookup name
60
+ end
61
+ end
62
+ def thing(name, options={})
63
+ add Thing.new self, name, options
64
+ end
65
+ def container(name, options={})
66
+ add Container.new self, name, options
67
+ end
68
+ def logical_container(name, options={})
69
+ add LogicalContainer.new self, name, options
70
+ end
71
+ def group(name, options={})
72
+ logical_container name, options
73
+ end
74
+ def add(child)
75
+ @children << child
76
+ child
77
+ end
78
+ end
79
+
80
+ class Thing
81
+ include Common
82
+ attr_reader :parent
83
+ attr_reader :name, :id, :pointers, :edge_heads
84
+
85
+ def initialize(parent, name, options = {})
86
+ @parent = parent
87
+ @name = name
88
+ @id = create_id(name, parent)
89
+ @pointers = []
90
+ @rollup = false
91
+ @edge_heads = []
92
+
93
+ rollup! if options[:rollup]
94
+ options.delete(:rollup)
95
+
96
+ @render_options = options
97
+ setup_render_attributes label: name
98
+
99
+ parent.register name, self
100
+ end
101
+
102
+ def do_render_node(renderer)
103
+ render_options = @render_options
104
+ attributes = @attributes
105
+ renderer.render_node(parent, id, render_options, attributes)
106
+ end
107
+
108
+ def node
109
+ graph.find_node(id)
110
+ end
111
+
112
+ def points_to_all(*others)
113
+ others.each {|other|
114
+ points_to other
115
+ }
116
+ end
117
+
118
+ def points_to(other, options = {})
119
+ setup_render_edge(other, options)
120
+ end
121
+
122
+ def pointees
123
+ pointers.map {|e|
124
+ e[:other]
125
+ }
126
+ end
127
+
128
+ def render(renderer)
129
+ do_render_node(renderer) unless in_rollup?
130
+ end
131
+
132
+ def render_edges(renderer)
133
+ pointers.each {|p|
134
+ render_pointer p, renderer
135
+ }
136
+ end
137
+
138
+ private
139
+
140
+ def setup_render_edge(other, options)
141
+ pointers << {
142
+ other: other,
143
+ options: {
144
+ label: options[:name],
145
+ style: options[:style]
146
+ }
147
+ }
148
+ end
149
+
150
+ def render_pointer(pointer, renderer)
151
+ other = pointer[:other]
152
+ while (other.in_rollup? && !other.on_top_rollup?) do
153
+ other = other.parent
154
+ end
155
+ return if other.under_rollup?
156
+
157
+ from = self
158
+ while (from.in_rollup? && !from.on_top_rollup?) do
159
+ from = from.parent
160
+ end
161
+
162
+ return if from == self && from.in_rollup?
163
+
164
+ return if from == other
165
+ return if edge_heads.include? other
166
+
167
+ edge_heads << other
168
+ render_options = pointer[:options]
169
+ renderer.render_edge(from, other, render_options)
170
+ end
171
+ end
172
+
173
+ class Container
174
+ include Common
175
+ include Parent
176
+ attr_reader :parent
177
+ attr_reader :name, :id, :node, :render_id, :render_type, :render_options, :children
178
+
179
+ def initialize(parent, name, options = {})
180
+ @children = []
181
+ @by_name = Registry.new
182
+ @parent = parent
183
+ @name = name
184
+ @id = create_id(name, parent)
185
+
186
+ init_rollup options
187
+
188
+ setup_render_attributes label: name
189
+
190
+ @render_options = options
191
+
192
+ parent.register name, self
193
+ end
194
+
195
+ def register(name, thing)
196
+ @by_name.register name, thing
197
+ parent.register name, thing
198
+ end
199
+
200
+ def render(renderer)
201
+ if on_top_rollup?
202
+ do_render_node renderer
203
+ elsif !under_rollup?
204
+ do_render_subgraph renderer
205
+ end
206
+
207
+ children.each {|c|
208
+ c.render renderer
209
+ }
210
+ end
211
+
212
+ def render_edges(renderer)
213
+ children.each {|c|
214
+ c.render_edges renderer
215
+ }
216
+ end
217
+
218
+ def node
219
+ if render_type == :node
220
+ graph.find_node(render_id)
221
+ elsif render_type == :subgraph
222
+ @subgraph
223
+ end
224
+ end
225
+
226
+ private
227
+
228
+ def do_render_subgraph(renderer)
229
+ @render_type = :subgraph
230
+ render_id = cluster_prefix + id
231
+ attributes = @attributes
232
+ @render_id = render_id
233
+ @subgraph = renderer.render_subgraph(parent, render_id, render_options, attributes)
234
+ end
235
+
236
+ def do_render_node(renderer)
237
+ @render_type = :node
238
+ @render_id = id
239
+ render_id = @render_id
240
+ attributes = @attributes
241
+ renderer.render_node(parent, render_id, render_options, attributes)
242
+ end
243
+
244
+ def init_rollup(options)
245
+ @rollup = false
246
+ rollup! if options[:rollup]
247
+ options.delete(:rollup)
248
+ end
249
+
250
+ def cluster_prefix
251
+ is_cluster = true
252
+ if @render_options.has_key? :cluster
253
+ is_cluster = @render_options[:cluster]
254
+ @render_options.delete :cluster
255
+ end
256
+ cluster_prefix = (is_cluster ? 'cluster_' : '')
257
+ end
258
+
259
+ end
260
+
261
+ class LogicalContainer < Container
262
+ def initialize(parent, name, options)
263
+ super parent, name, options.merge(cluster: false)
264
+ end
265
+ end
266
+
267
+ require 'date'
268
+
269
+ class GraphvizRenderer
270
+ attr_reader :graph
271
+
272
+ def initialize(title, hints)
273
+ @graph = GraphViz.digraph(title)
274
+ @graph[hints]
275
+ @subgraphs = {}
276
+ @nodes = {}
277
+ end
278
+
279
+ def render_edge(from, other, options)
280
+ edge = graph.add_edges from.id, other.id
281
+ options.each { |key, value|
282
+ edge[key] = value unless value.nil?
283
+ }
284
+ edge
285
+ end
286
+
287
+ def render_node(parent, id, options, attributes)
288
+ clean_node_options options
289
+ default_options = {:shape => 'box', :style => ''}
290
+ node = parent_node(parent).add_nodes(id, default_options.merge(options))
291
+ apply_render_attributes node, attributes
292
+ end
293
+
294
+ def render_subgraph(parent, id, options, attributes)
295
+ subgraph = parent_node(parent).add_graph(id, options)
296
+ apply_render_attributes subgraph, attributes
297
+ @subgraphs[id] = subgraph
298
+ subgraph
299
+ end
300
+
301
+ private
302
+
303
+ def clean_node_options(options)
304
+ options.delete(:rank)
305
+ options.delete(:cluster)
306
+ options
307
+ end
308
+
309
+ def parent_node(parent)
310
+ return graph unless parent.respond_to?(:render_id)
311
+ node = graph.search_node(parent.render_id)
312
+ return node unless node.nil?
313
+ subgraph = @subgraphs[parent.render_id]
314
+ raise "couldn't find node or graph: #{parent.render_id}, out of graphs: #{graph_ids}" if subgraph.nil?
315
+ subgraph
316
+ end
317
+
318
+ def apply_render_attributes(node, attributes)
319
+ attributes.each do |key, value|
320
+ node[key] = value
321
+ end
322
+ end
323
+ end
324
+
325
+ class System
326
+ include Parent
327
+
328
+ attr_reader :render_hints, :title, :children, :graph
329
+
330
+ alias :name :title
331
+
332
+ def initialize(name, hints = {splines: 'line'})
333
+ @children = []
334
+ @by_name = Registry.new
335
+ @render_hints = hints
336
+ @title = create_title(name)
337
+ @rendered = false
338
+ end
339
+
340
+ def node
341
+ graph
342
+ end
343
+
344
+ def output(*args)
345
+ render
346
+ out = graph.output *args
347
+ puts "Created output: #{args.join ' '}"
348
+ out
349
+ end
350
+
351
+ def render(type=:graphviz)
352
+ return @graph if @rendered
353
+ raise "dunno bout that '#{type}', only know :graphviz" unless type == :graphviz
354
+
355
+ renderer = GraphvizRenderer.new @title, render_hints
356
+
357
+ children.each {|c|
358
+ c.render renderer
359
+ }
360
+ children.each {|c|
361
+ c.render_edges renderer
362
+ }
363
+ @rendered = true
364
+ @graph = renderer.graph
365
+ end
366
+
367
+ def rollup?
368
+ false
369
+ end
370
+
371
+ def register(name, thing)
372
+ @by_name.register name, thing
373
+ end
374
+
375
+ private
376
+
377
+ def create_title(name)
378
+ now = DateTime.now
379
+ "#{now.strftime '%H:%M'} #{name} #{now.strftime '%F'}"
380
+ end
381
+ end
382
+
383
+ class LookupError < StandardError
384
+ end
385
+ class MissingMatchBlockError < LookupError
386
+ def initialize
387
+ super 'need to provide match block'
388
+ end
389
+ end
390
+ class DuplicateLookupError < LookupError
391
+ def initialize(name)
392
+ super "More than one object registered of name '#{name}' - you'll need to search in a narrower context"
393
+ end
394
+ end
395
+ class Registry
396
+ def initialize
397
+ @by_name = {}
398
+ @all = []
399
+ end
400
+
401
+ def register(name, thing)
402
+ @all << thing
403
+ if @by_name.has_key?(name)
404
+ @by_name[name] = DuplicateLookupError.new name
405
+ else
406
+ @by_name[name] = thing
407
+ end
408
+ end
409
+
410
+ def find(&matcher)
411
+ raise LookupError.new("need to provide match block") unless block_given?
412
+ @all.find &matcher
413
+ end
414
+
415
+ def find_all(&matcher)
416
+ raise MissingMatchBlockError.new unless block_given?
417
+ @all.select &matcher
418
+ end
419
+
420
+ def lookup(name)
421
+ found = @by_name[name]
422
+ raise LookupError.new("could not find '#{name}'") if found.nil?
423
+ raise found if found.is_a?(Exception)
424
+ found
425
+ end
426
+ end
427
+
428
+ end
@@ -0,0 +1,3 @@
1
+ module Dogviz
2
+ VERSION = '0.0.3'
3
+ end
@@ -0,0 +1,83 @@
1
+ require "test/unit"
2
+
3
+ require_relative '../lib/dogviz'
4
+
5
+ class TestDogvizGraph < Test::Unit::TestCase
6
+ include Dogviz
7
+
8
+ attr_reader :sys
9
+
10
+ def setup
11
+ @sys = Dogviz::System.new 'test'
12
+ end
13
+
14
+ def test_container_gets_rolled_up
15
+ g = sys.group('g')
16
+ assert_equal(false, g.rollup?)
17
+ g.rollup!
18
+ assert_equal(true, g.rollup?)
19
+ end
20
+
21
+ def test_stuff_isnt_under_or_on_top_of_rollup_without_rollup
22
+ g = sys.group('g')
23
+ a = g.thing('a')
24
+
25
+ assert_equal(false, a.under_rollup?)
26
+ assert_equal(false, a.on_top_rollup?)
27
+ assert_equal(false, g.under_rollup?)
28
+ assert_equal(false, g.on_top_rollup?)
29
+ end
30
+
31
+ def test_rolled_up_containers_arent_under_rollup_when_on_top
32
+ g = sys.group('g')
33
+ g.rollup!
34
+
35
+ assert_equal(false, g.under_rollup?)
36
+ assert_equal(true, g.on_top_rollup?)
37
+ end
38
+
39
+ def test_nested_containers_and_things_are_under_rollup
40
+ g = sys.group('g')
41
+ g.rollup!
42
+ nested = g.group('nested')
43
+ a = g.thing('a')
44
+
45
+ assert_equal(true, nested.under_rollup?)
46
+ assert_equal(true, a.under_rollup?)
47
+ assert_equal(false, nested.on_top_rollup?)
48
+ end
49
+
50
+ def test_nested_things_are_in_rollup_if_under_one
51
+ g = sys.group('g').rollup!
52
+ a = g.thing('a')
53
+
54
+ assert_equal(true, a.in_rollup?)
55
+ end
56
+
57
+ def test_nested_things_are_in_rollup_if_rolled_up_themselves
58
+ a = sys.thing('a').rollup!
59
+ assert_equal(true, a.in_rollup?)
60
+ end
61
+
62
+ def test_find_with_match_block
63
+ nested_group = sys.group('g').group('nested group')
64
+ nested_thing = nested_group.thing('nested thing')
65
+ nested_group.thing('other thing')
66
+
67
+ assert_equal(nested_thing, sys.find {|n|
68
+ n.is_a?(Thing) && n.name.start_with?('nested')
69
+ })
70
+ end
71
+
72
+ def test_find_all
73
+ group = sys.group('g')
74
+ nested_group = group.group('nested group')
75
+ thing1 = group.thing('n1')
76
+ thing2 = nested_group.thing('n2')
77
+
78
+ assert_equal([thing1, thing2], sys.find_all {|n|
79
+ n.is_a?(Thing)
80
+ })
81
+ end
82
+
83
+ end
@@ -0,0 +1,242 @@
1
+ require "test/unit"
2
+
3
+ require_relative '../lib/dogviz'
4
+
5
+ class TestDogvizGraphvizRendering < Test::Unit::TestCase
6
+ include Dogviz
7
+
8
+ attr_reader :sys
9
+
10
+ def setup
11
+ @sys = Dogviz::System.new 'test'
12
+ end
13
+
14
+ def graph
15
+ sys.render
16
+ end
17
+
18
+ def test_points_to_links_nodes
19
+ sys.thing('a').points_to sys.thing('b')
20
+
21
+ assert_equal('a->b', connections)
22
+ end
23
+
24
+ def test_points_to_all_makes_multiple_links_to_nodes
25
+ sys.thing('a').points_to_all sys.thing('b'), sys.thing('c')
26
+
27
+ assert_equal(2, edges.size)
28
+ assert_equal(find('a').id, edges[0].tail_node)
29
+ assert_equal(find('b').id, edges[0].head_node)
30
+ assert_equal(find('a').id, edges[1].tail_node)
31
+ assert_equal(find('c').id, edges[1].head_node)
32
+ assert_equal('a->b a->c', connections)
33
+ end
34
+
35
+ def test_containers_are_subgraphs_prefixed_with_cluster_for_visual_containment_in_GraphViz
36
+ top = sys.container('top')
37
+ nested = top.container('nested')
38
+
39
+ assert_equal('cluster_top', subgraph_ids.first)
40
+ assert_equal('cluster_top_nested', subgraph_ids.last)
41
+ end
42
+
43
+ def test_logical_containers_have_no_cluster_prefix_so_will_not_be_visible_in_Graphviz
44
+ top = sys.logical_container('top')
45
+ top_thing = top.thing('top thing')
46
+
47
+ assert_equal(['top'], subgraph_ids)
48
+ assert_equal(top_thing.id, subgraph('top').get_node("#{top_thing.id}").id)
49
+ end
50
+
51
+ def test_nested_containers_have_things
52
+ top = sys.container('top')
53
+ top_thing = top.thing('top thing')
54
+ nested = top.container('nested')
55
+ nested_thing = nested.thing('nested thing')
56
+
57
+ graph
58
+
59
+ assert_equal([top.render_id, nested.render_id], subgraph_ids)
60
+
61
+ top_subgraph = subgraph(top.render_id)
62
+ nested_subgraph = subgraph(nested.render_id)
63
+
64
+ assert_equal(top_thing.id, top_subgraph.get_node(top_thing.id).id)
65
+ assert_equal(nested_thing.id, nested_subgraph.get_node(nested_thing.id).id)
66
+ assert_nil(top_subgraph.get_node(nested_thing.id), 'should not be in other container')
67
+ assert_nil(nested_subgraph.get_node(top_thing.id), 'should not be in other container')
68
+ end
69
+
70
+ def test_point_into_target_in_container
71
+ container = sys.container('container')
72
+ target = container.thing('target')
73
+ pointer = sys.thing('pointer')
74
+
75
+ pointer.points_to target
76
+
77
+ assert_equal("pointer->#{target.id}", connections)
78
+ end
79
+
80
+ def test_node_names_are_displayed
81
+ thing = sys.container('whatever').thing('the thing')
82
+ assert_equal('whatever_the_thing', thing.id)
83
+ assert_equal('"the thing"', find(thing.id)[:label].to_s)
84
+ end
85
+
86
+ def test_point_into_target_in_nested_containers
87
+ top = sys.container('top')
88
+ target_parent = top.container('nested').container('subnested')
89
+ target = target_parent.thing('target')
90
+ pointer = sys.thing('pointer')
91
+
92
+ pointer.points_to target
93
+
94
+ assert_equal('pointer->top_nested_subnested_target', connections)
95
+ end
96
+
97
+ def test_points_to_rolled_up_container_of_target
98
+ group = sys.container('group')
99
+ group.rollup!
100
+ target = group.thing('target')
101
+ pointer = sys.thing('pointer')
102
+
103
+ pointer.points_to target
104
+
105
+ assert_equal('pointer->group', connections)
106
+ end
107
+
108
+ def test_do_not_render_rolled_up_thing
109
+ sys.thing('a').rollup!
110
+
111
+ assert_nil(find('a'))
112
+ end
113
+
114
+ def test_points_from_thing_in_rolled_up_container
115
+ group = sys.group('group')
116
+ group.rollup!
117
+
118
+ pointer = group.thing('pointer')
119
+ target = sys.thing('target')
120
+
121
+ pointer.points_to target
122
+
123
+ assert_equal('group->target', connections)
124
+ end
125
+
126
+ def test_points_to_rolled_up_nested_containers_of_target
127
+ top = sys.container('top', rollup: false)
128
+ nested = top.container('nested', rollup: true)
129
+ target = nested.container('subnested').thing('target')
130
+ pointer = sys.thing('pointer')
131
+
132
+ top.thing('thing in top')
133
+ nested.thing('thing in nested')
134
+
135
+ pointer.points_to target
136
+
137
+ assert_equal('pointer->top_nested', connections)
138
+ assert_not_nil(graph.find_node('top_thing_in_top'))
139
+ end
140
+
141
+ def test_points_to_multiple_things_in_rolled_up_group
142
+ group = sys.group('group', rollup: true)
143
+ pointer = sys.thing('pointer')
144
+
145
+ pointer.points_to_all group.thing('a'), group.thing('b'), group.thing('c')
146
+
147
+ assert_equal('pointer->group', connections)
148
+ end
149
+
150
+ def test_pointing_from_rolled_up_thing_in_non_rolled_up_group_creates_no_links
151
+ a = sys.thing('a', rollup: true)
152
+ b = sys.thing('b')
153
+ c = sys.thing('c')
154
+ a.points_to b
155
+ b.points_to c
156
+ assert_equal('b->c', connections)
157
+ end
158
+
159
+ def test_point_to_between_and_from_things_in_rolled_up_container
160
+ entry = sys.thing('entry')
161
+ group = sys.group('group')
162
+ a = group.thing('a')
163
+ b = group.thing('b')
164
+ exit = sys.thing('exit')
165
+ entry.points_to a
166
+ a.points_to b
167
+ b.points_to exit
168
+
169
+ group.rollup!
170
+
171
+ assert_equal('entry->group group->exit', connections)
172
+ end
173
+
174
+ def test_find_thing
175
+ sys.group('top').thing('needle')
176
+
177
+ assert_equal('needle', sys.find('needle').name)
178
+ end
179
+
180
+ def test_find_duplicate_show_blow_up
181
+ sys.group('A').thing('needle')
182
+ sys.group('B').thing('needle')
183
+
184
+ assert_raise DuplicateLookupError do
185
+ sys.find('needle').name
186
+ end
187
+ end
188
+
189
+ def test_find_nothing_show_blow_up
190
+ sys.group('A').thing('needle')
191
+
192
+ assert_raise LookupError do
193
+ sys.find('not a needle')
194
+ end
195
+ end
196
+
197
+ def test_doclinks_create_links
198
+ a = sys.thing('a')
199
+ doc_url = 'http://some.url/'
200
+ a.doclink doc_url
201
+
202
+ assert_equal(doc_url, find('a')['URL'].to_ruby)
203
+ end
204
+
205
+ private
206
+
207
+ def subgraph_ids
208
+ subgraphs.map(&:id)
209
+ end
210
+
211
+ def subgraph(id)
212
+ subgraphs.find {|sub| sub.id == id }
213
+ end
214
+
215
+ def subgraphs(from=graph)
216
+ subs = []
217
+ from.each_graph {|sub_name, sub|
218
+ subs << sub
219
+ subs += subgraphs(sub)
220
+ }
221
+ subs
222
+ end
223
+
224
+ def connections(sep=' ')
225
+ edges.map {|e|
226
+ "#{e.tail_node}->#{e.head_node}"
227
+ }.join sep
228
+ end
229
+
230
+ def connected_ids
231
+ (edges.map(&:tail_node) + edges.map(&:head_node)).uniq
232
+ end
233
+
234
+ def edges
235
+ graph.each_edge
236
+ end
237
+
238
+ def find(name)
239
+ graph.find_node name
240
+ end
241
+
242
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dogviz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - damned
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: simplecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: colorize
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ruby-graphviz
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: leverages graphviz to generate multiple views of a domain-specific graph
84
+ email:
85
+ - writetodan@yahoo.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".ruby-version"
92
+ - Gemfile
93
+ - Gemfile.lock
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - dogviz.gemspec
98
+ - examples/website.rb
99
+ - examples/website_domain.rb
100
+ - lib/dogviz.rb
101
+ - lib/dogviz/version.rb
102
+ - tests/test_sisvis_graph.rb
103
+ - tests/test_sisvis_graphviz_rendering.rb
104
+ homepage: https://github.com/damned/dogviz
105
+ licenses:
106
+ - MIT
107
+ metadata: {}
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 2.4.3
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: domain object graph visualisation
128
+ test_files: []