ograph 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/Manifest.txt +4 -0
- data/README.txt +6 -1
- data/Rakefile +1 -1
- data/examples/ascendants_graph.rb +10 -0
- data/examples/diff_graph.rb +11 -0
- data/examples/regular_graph.rb +11 -0
- data/lib/ograph.rb +251 -85
- data/test/test_all.rb +10 -0
- metadata +10 -4
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
data/README.txt
CHANGED
@@ -12,6 +12,7 @@ give you a graph of your object and its relationships.
|
|
12
12
|
For sample output and more sample code see:
|
13
13
|
|
14
14
|
* http://flickr.com/photos/aaronp/tags/graphviz/
|
15
|
+
* http://tenderlovemaking.com/2007/06/17/graphing-ruby-objects/
|
15
16
|
* http://tenderlovemaking.com/2007/01/13/graphing-objects-in-memory-with-ruby/
|
16
17
|
|
17
18
|
== PROBLEMS
|
@@ -22,7 +23,11 @@ None currently known.
|
|
22
23
|
|
23
24
|
list = %w{ hello world how are you? }
|
24
25
|
hash = { :list => list, :string => "tenderlovemaking.com" }
|
25
|
-
|
26
|
+
ograph = ObjectGraph.new
|
27
|
+
ograph.graph(hash) do |h|
|
28
|
+
h.delete(:string)
|
29
|
+
end
|
30
|
+
puts ograph
|
26
31
|
|
27
32
|
== INSTALL
|
28
33
|
|
data/Rakefile
CHANGED
@@ -9,6 +9,6 @@ Hoe.new('ograph', ObjectGraph::VERSION) do |p|
|
|
9
9
|
p.summary = p.paragraphs_of('README.txt', 3).join("\n\n")
|
10
10
|
p.description = p.paragraphs_of('README.txt', 3..5).join("\n\n")
|
11
11
|
p.url = p.paragraphs_of('README.txt', 1).first.split(/\n/)[1..-1].first.strip
|
12
|
-
p.changes = p.paragraphs_of('History.txt', 0..
|
12
|
+
p.changes = p.paragraphs_of('History.txt', 0..2).join("\n\n")
|
13
13
|
end
|
14
14
|
|
data/lib/ograph.rb
CHANGED
@@ -41,11 +41,12 @@
|
|
41
41
|
# end
|
42
42
|
|
43
43
|
class ObjectGraph
|
44
|
+
attr_accessor :nodes
|
44
45
|
|
45
46
|
##
|
46
47
|
# The version of ObjectGraph you are currently using.
|
47
48
|
|
48
|
-
VERSION = '0.
|
49
|
+
VERSION = '0.2.0'
|
49
50
|
|
50
51
|
##
|
51
52
|
# Characters we need to escape
|
@@ -59,7 +60,7 @@ class ObjectGraph
|
|
59
60
|
|
60
61
|
def self.graph(target, opts = {})
|
61
62
|
graph = new opts
|
62
|
-
graph.
|
63
|
+
graph.graph target
|
63
64
|
graph.to_s
|
64
65
|
end
|
65
66
|
|
@@ -72,94 +73,199 @@ class ObjectGraph
|
|
72
73
|
# :show_nil:: Draw nil (and edges to nil.)
|
73
74
|
|
74
75
|
def initialize(opts = {})
|
75
|
-
|
76
|
-
|
76
|
+
@opts = { :class_filter => //,
|
77
|
+
:show_ivars => true,
|
78
|
+
:show_nil => true,
|
79
|
+
:ascendants => false,
|
80
|
+
:descendants => true,
|
81
|
+
}.merge opts
|
82
|
+
|
83
|
+
@nodes = {}
|
84
|
+
@preprocess_callback = nil
|
85
|
+
end
|
77
86
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
87
|
+
##
|
88
|
+
# Adds +target+ to the graph
|
89
|
+
def graph(target)
|
90
|
+
GC.start
|
91
|
+
add_ascendants(target) if @opts[:ascendants]
|
92
|
+
add_descendants(target) if @opts[:descendants]
|
93
|
+
if block_given?
|
94
|
+
yield target
|
95
|
+
GC.start
|
96
|
+
diff(target)
|
97
|
+
end
|
98
|
+
end
|
82
99
|
|
83
|
-
|
100
|
+
##
|
101
|
+
# Combines +target+ with the current graph and highlights differences
|
102
|
+
|
103
|
+
def diff(target)
|
104
|
+
old_nodes = @nodes
|
105
|
+
@nodes = {}
|
106
|
+
graph(target)
|
107
|
+
new_nodes = @nodes.keys - old_nodes.keys
|
108
|
+
lost_nodes = old_nodes.keys - @nodes.keys
|
109
|
+
|
110
|
+
(@nodes.keys & old_nodes.keys).each do |object_id|
|
111
|
+
old = old_nodes[object_id].pointers.values
|
112
|
+
new = @nodes[object_id].pointers.values
|
113
|
+
|
114
|
+
(old - new).flatten.each do |lost_link|
|
115
|
+
lost_link.options = { :color => 'red' }
|
116
|
+
@nodes[object_id].lost_links << lost_link
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Color the lost nodes and add them to the graph
|
121
|
+
lost_nodes.each do |object_id|
|
122
|
+
old_nodes[object_id].options['color'] = 'hotpink2'
|
123
|
+
@nodes[object_id] = old_nodes[object_id]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Color the new nodes
|
127
|
+
new_nodes.each do |object_id|
|
128
|
+
@nodes[object_id].options['color'] = 'yellowgreen'
|
129
|
+
end
|
84
130
|
end
|
85
131
|
|
86
132
|
##
|
87
|
-
# Adds +target+ to
|
133
|
+
# Adds +target+ plus ascendants to graph
|
134
|
+
|
135
|
+
def add_ascendants(target)
|
136
|
+
ascendants = []
|
137
|
+
stack = [target]
|
138
|
+
|
139
|
+
while stack.length > 0
|
140
|
+
o_target = stack.pop
|
141
|
+
ascendants << o_target
|
142
|
+
|
143
|
+
GC.start
|
144
|
+
ObjectSpace.each_object do |object|
|
145
|
+
next if @nodes.key? object.object_id
|
146
|
+
next if object.equal? o_target
|
147
|
+
next if object.equal? ascendants
|
148
|
+
next if object.equal? stack
|
149
|
+
next if ascendants.any? { |x| x.equal? object }
|
150
|
+
|
151
|
+
case object
|
152
|
+
when IO, String, ARGF then
|
153
|
+
# Do nothing
|
154
|
+
when Hash then
|
155
|
+
object.each do |k,v|
|
156
|
+
if k.equal?(o_target) || v.equal?(o_target)
|
157
|
+
stack << object
|
158
|
+
break
|
159
|
+
end
|
160
|
+
end
|
161
|
+
when Enumerable then
|
162
|
+
object.each do |v|
|
163
|
+
if v.equal?(o_target)
|
164
|
+
stack << object
|
165
|
+
break
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
88
169
|
|
89
|
-
|
90
|
-
|
170
|
+
if object.instance_variables.length > 0
|
171
|
+
stack << object if object.instance_variables.any? do |iv_sym|
|
172
|
+
object.instance_variable_get(iv_sym).equal?(o_target)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
add_object_to_graph(target, { :style => 'dashed' })
|
179
|
+
(ascendants - [target]).each { |x| add_object_to_graph(x) }
|
180
|
+
ascendants.clear
|
181
|
+
stack.clear
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Adds +target+ to the graph, descending in to the target
|
91
186
|
|
92
|
-
|
187
|
+
def add_descendants(target)
|
188
|
+
stack = add_object_to_graph(target, { :style => 'dashed' })
|
93
189
|
|
94
|
-
while
|
95
|
-
object =
|
96
|
-
|
97
|
-
next if @seen_hash.key? @name
|
98
|
-
@seen_hash[@name] = 1
|
190
|
+
while stack.length > 0
|
191
|
+
object = stack.pop
|
192
|
+
next if @nodes.key? object.object_id
|
99
193
|
@preprocess_callback.call object if @preprocess_callback
|
100
194
|
|
101
|
-
|
102
|
-
@record << @name
|
103
|
-
|
104
|
-
case object
|
105
|
-
when String then
|
106
|
-
str = object[0..15].gsub(ESCAPE, '\\\\\1').gsub(/[\r\n]/,' ')
|
107
|
-
str << '...' if object.length > 15
|
108
|
-
@record.push "'#{str}'"
|
109
|
-
when Numeric then
|
110
|
-
@record.push object
|
111
|
-
when Hash then
|
112
|
-
if object.respond_to? :empty? and object.empty? then
|
113
|
-
@record.push 'Empty!'
|
114
|
-
else
|
115
|
-
object.each_with_index do |(k,v),i|
|
116
|
-
next if v.nil? && ! @opts[:show_nil]
|
117
|
-
next unless v.class.to_s =~ @opts[:class_filter]
|
195
|
+
stack += add_object_to_graph(object, {:style => 'filled'})
|
118
196
|
|
119
|
-
|
120
|
-
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Adds +object+ to tree
|
202
|
+
|
203
|
+
def add_object_to_graph(object, opts = {})
|
204
|
+
record = Node.new(object.object_id, object_name(object))
|
205
|
+
record.options = opts
|
206
|
+
|
207
|
+
stack = []
|
208
|
+
case object
|
209
|
+
when String then
|
210
|
+
str = object[0..15].gsub(ESCAPE, '\\\\\1').gsub(/[\r\n]/,' ')
|
211
|
+
str << '...' if object.length > 15
|
212
|
+
record.values << "'#{str}'"
|
213
|
+
when Numeric then
|
214
|
+
record.values << object
|
215
|
+
when IO then
|
216
|
+
when Hash then
|
217
|
+
if object.respond_to? :empty? and object.empty? then
|
218
|
+
record.values << 'Empty!'
|
219
|
+
else
|
220
|
+
object.each_with_index do |(k,v),i|
|
221
|
+
next if v.nil? && ! @opts[:show_nil]
|
222
|
+
next unless v.class.to_s =~ @opts[:class_filter]
|
223
|
+
|
224
|
+
stack += add_to_record(record, k, v)
|
121
225
|
end
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
226
|
+
end
|
227
|
+
when Enumerable then
|
228
|
+
if object.respond_to? :empty? and object.empty? then
|
229
|
+
record.values.push 'Empty!'
|
230
|
+
else
|
231
|
+
object.each_with_index do |v, i|
|
232
|
+
next if v.nil? && ! @opts[:show_nil]
|
233
|
+
if v.class.to_s =~ @opts[:class_filter] or
|
234
|
+
(v.is_a? Enumerable and not v.is_a? String) then
|
235
|
+
stack += add_to_record(record, i, v)
|
132
236
|
end
|
133
237
|
end
|
134
238
|
end
|
239
|
+
end
|
135
240
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
end
|
241
|
+
# This is a HACK around bug 1345 (I think)
|
242
|
+
if object.class and object.class.ancestors.include? Array and
|
243
|
+
not Enumerable === object then
|
244
|
+
if object.respond_to? :empty? and object.empty? then
|
245
|
+
record.values.push 'Empty!'
|
246
|
+
else
|
247
|
+
object.each_with_index do |v, i|
|
248
|
+
next if v.nil? && ! @opts[:show_nil]
|
249
|
+
if v.class.to_s =~ @opts[:class_filter] or
|
250
|
+
(v.is_a? Enumerable and not v.is_a? String) then
|
251
|
+
stack += add_to_record(record, i, v)
|
148
252
|
end
|
149
253
|
end
|
150
254
|
end
|
255
|
+
end
|
151
256
|
|
152
|
-
|
257
|
+
stack += add_ivars(record, object)
|
153
258
|
|
154
|
-
|
155
|
-
|
259
|
+
@nodes[record.node_id] = record
|
260
|
+
stack
|
156
261
|
end
|
157
262
|
|
158
263
|
##
|
159
264
|
# Adds ivars for +object+ to the graph.
|
160
265
|
|
161
|
-
def add_ivars(object)
|
266
|
+
def add_ivars(record, object)
|
162
267
|
return unless object.instance_variables # HACK WTF Rails?
|
268
|
+
stack = []
|
163
269
|
|
164
270
|
ivars = object.instance_variables.sort
|
165
271
|
|
@@ -172,48 +278,52 @@ class ObjectGraph
|
|
172
278
|
next if v.nil? && ! @opts[:show_nil]
|
173
279
|
next unless v.class.to_s =~ @opts[:class_filter]
|
174
280
|
|
175
|
-
add_to_record k, v
|
281
|
+
stack += add_to_record(record, k, v)
|
176
282
|
end
|
177
283
|
end
|
178
284
|
end
|
179
285
|
|
180
|
-
ivars.
|
286
|
+
ivars.each do |iv_sym|
|
181
287
|
iv = object.instance_variable_get iv_sym
|
182
288
|
next if iv.nil? && ! @opts[:show_nil]
|
183
289
|
if iv.class.to_s =~ @opts[:class_filter] ||
|
184
290
|
(object.is_a?(Enumerable) && !object.is_a?(String))
|
185
|
-
|
186
|
-
|
187
|
-
|
291
|
+
|
292
|
+
obj_name = object_name(iv)
|
293
|
+
record.pointers["#{iv_sym}"] << Pointer.new(obj_name, iv.object_id)
|
294
|
+
record.values.push("#{iv_sym}")
|
295
|
+
stack.push(iv)
|
188
296
|
end
|
189
297
|
end
|
190
298
|
|
191
|
-
|
299
|
+
stack
|
192
300
|
end
|
193
301
|
|
194
302
|
##
|
195
|
-
# Adds +key+ and +value+ to
|
196
|
-
# processed.
|
303
|
+
# Adds +key+ and +value+ to +record+
|
197
304
|
|
198
|
-
def add_to_record(key, value)
|
199
|
-
|
305
|
+
def add_to_record(record, key, value)
|
306
|
+
stack = []
|
307
|
+
record.values.push(
|
200
308
|
[key, value].map { |val|
|
201
309
|
case val
|
202
310
|
when NilClass
|
203
311
|
'nil'
|
204
312
|
when Fixnum
|
205
313
|
"#{val}"
|
206
|
-
when String
|
314
|
+
when String, Symbol
|
315
|
+
val = ":#{val}" if val.is_a? Symbol
|
207
316
|
string_val = val[0..12].gsub(ESCAPE, '\\\\\1').gsub(/[\r\n]/, ' ')
|
208
317
|
string_val << '...' if val.length > 12
|
209
318
|
string_val
|
210
319
|
else
|
211
320
|
string_val = object_name val
|
212
|
-
|
213
|
-
|
321
|
+
record.pointers[string_val] << Pointer.new(string_val, val.object_id)
|
322
|
+
stack.push val
|
214
323
|
string_val
|
215
324
|
end
|
216
325
|
}.join(' is '))
|
326
|
+
stack
|
217
327
|
end
|
218
328
|
|
219
329
|
##
|
@@ -261,24 +371,80 @@ digraph g {
|
|
261
371
|
|
262
372
|
END
|
263
373
|
|
264
|
-
@
|
265
|
-
s << "\"#{
|
374
|
+
@nodes.values.sort.each do |node|
|
375
|
+
s << "\"#{node.name}\" [\n\tlabel="
|
266
376
|
list = []
|
267
|
-
|
268
|
-
|
269
|
-
|
377
|
+
list << "<f0>#{node.name}"
|
378
|
+
node.values.each_with_index do |value,i|
|
379
|
+
list << "<f#{i + 1}>#{value}"
|
380
|
+
end
|
270
381
|
s << "\"#{list.join('|')}\"\n"
|
382
|
+
node.options.each do |k,v|
|
383
|
+
s << "#{k}=#{v}\n"
|
384
|
+
end
|
271
385
|
s << "\tshape = \"record\"\n]\n\n"
|
272
|
-
|
386
|
+
end
|
273
387
|
|
274
|
-
@
|
275
|
-
|
276
|
-
|
388
|
+
@nodes.values.each do |node|
|
389
|
+
node.pointers.each_with_index do |(from,pointer),i|
|
390
|
+
pointer.each do |p|
|
391
|
+
to = @nodes[p.object_id]
|
392
|
+
next unless to
|
393
|
+
s<< "\"#{node.name}\":f#{i + 1} -> \"#{p.name}\":f0 [ id = #{i}\n"
|
394
|
+
p.options.each do |k,v|
|
395
|
+
s << "#{k}=#{v}\n"
|
396
|
+
end
|
397
|
+
s << "]\n"
|
398
|
+
end
|
399
|
+
end
|
400
|
+
node.lost_links.each do |p|
|
401
|
+
s<< "\"#{node.name}\":f0 -> \"#{p.name}\":f0 [\n"
|
402
|
+
p.options.each do |k,v|
|
403
|
+
s << "#{k}=#{v}\n"
|
404
|
+
end
|
405
|
+
s << "]\n"
|
406
|
+
end
|
407
|
+
end
|
277
408
|
|
278
409
|
s << "}\n"
|
279
410
|
|
280
411
|
s
|
281
412
|
end
|
282
413
|
|
414
|
+
class Pointer
|
415
|
+
attr_accessor :name, :object_id, :options
|
416
|
+
def initialize(name, object_id, options = {})
|
417
|
+
@name = name
|
418
|
+
@object_id = object_id
|
419
|
+
@options = options
|
420
|
+
end
|
421
|
+
|
422
|
+
def hash
|
423
|
+
name.hash
|
424
|
+
end
|
425
|
+
|
426
|
+
def eql?(other)
|
427
|
+
name.eql? other.name
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
class Node
|
432
|
+
include Comparable
|
433
|
+
attr_accessor :node_id, :name, :values, :pointers, :options, :lost_links
|
434
|
+
|
435
|
+
def initialize(node_id, name)
|
436
|
+
@node_id = node_id
|
437
|
+
@name = name
|
438
|
+
@values = []
|
439
|
+
@pointers = Hash.new { |h,k| h[k] = [] }
|
440
|
+
@options = {}
|
441
|
+
@lost_links = []
|
442
|
+
end
|
443
|
+
|
444
|
+
def <=>(other)
|
445
|
+
self.node_id <=> other.node_id
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
283
449
|
end
|
284
450
|
|
data/test/test_all.rb
ADDED
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
4
4
|
name: ograph
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2007-
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2007-06-17 00:00:00 -07:00
|
8
8
|
summary: ObjectGraph will output Graphviz dot files of your objects in memory. It will ferret out your instance variables and enumerate over your enumerables to give you a graph of your object and its relationships.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -15,6 +15,7 @@ description: "ObjectGraph will output Graphviz dot files of your objects in memo
|
|
15
15
|
ferret out your instance variables and enumerate over your enumerables to give
|
16
16
|
you a graph of your object and its relationships. For sample output and more
|
17
17
|
sample code see: * http://flickr.com/photos/aaronp/tags/graphviz/ *
|
18
|
+
http://tenderlovemaking.com/2007/06/17/graphing-ruby-objects/ *
|
18
19
|
http://tenderlovemaking.com/2007/01/13/graphing-objects-in-memory-with-ruby/"
|
19
20
|
autorequire:
|
20
21
|
default_executable:
|
@@ -38,8 +39,13 @@ files:
|
|
38
39
|
- Manifest.txt
|
39
40
|
- README.txt
|
40
41
|
- Rakefile
|
42
|
+
- examples/ascendants_graph.rb
|
43
|
+
- examples/diff_graph.rb
|
44
|
+
- examples/regular_graph.rb
|
41
45
|
- lib/ograph.rb
|
42
|
-
|
46
|
+
- test/test_all.rb
|
47
|
+
test_files:
|
48
|
+
- test/test_all.rb
|
43
49
|
rdoc_options: []
|
44
50
|
extra_rdoc_files: []
|
45
51
|
executables: []
|
@@ -54,5 +60,5 @@ dependencies:
|
|
54
60
|
-
|
55
61
|
- ">="
|
56
62
|
- !ruby/object:Gem::Version
|
57
|
-
version: 1.
|
63
|
+
version: 1.2.0
|
58
64
|
version:
|