ograph 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,8 @@
1
+ == 0.2.0
2
+
3
+ * ObjectGraph now can graph ascendants of your objects
4
+ * Support for color diffing trees was added
5
+
1
6
  == 0.1.0
2
7
 
3
8
  Tons of changes thanks to Eric Hodel!
@@ -2,4 +2,8 @@ History.txt
2
2
  Manifest.txt
3
3
  README.txt
4
4
  Rakefile
5
+ examples/ascendants_graph.rb
6
+ examples/diff_graph.rb
7
+ examples/regular_graph.rb
5
8
  lib/ograph.rb
9
+ test/test_all.rb
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
- puts ObjectGraph.graph(hash)
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..1).join("\n\n")
12
+ p.changes = p.paragraphs_of('History.txt', 0..2).join("\n\n")
13
13
  end
14
14
 
@@ -0,0 +1,10 @@
1
+ require 'ograph'
2
+
3
+ class A; end
4
+ a = A.new
5
+ struct = { :one => [], :two => [a] }
6
+
7
+ grapher = ObjectGraph.new(:ascendants => true)
8
+ grapher.graph(a)
9
+ puts grapher
10
+
@@ -0,0 +1,11 @@
1
+ require 'ograph'
2
+
3
+ class A; end
4
+ struct = { :one => [], :two => [A.new]}
5
+
6
+ grapher = ObjectGraph.new
7
+ grapher.graph(struct) do |s|
8
+ s[:one] << A.new
9
+ s[:two].pop
10
+ end
11
+ puts grapher
@@ -0,0 +1,11 @@
1
+ require 'ograph'
2
+
3
+ # Example of graphing an arbitrary data structure.
4
+
5
+ class A; end
6
+ struct = { :one => [A.new] }
7
+
8
+ grapher = ObjectGraph.new
9
+ grapher.graph(struct)
10
+ puts grapher
11
+
@@ -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.1.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.add target
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
- defaults = { :class_filter => //, :show_ivars => true, :show_nil => true }
76
- @opts = defaults.merge opts
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
- @stack = []
79
- @object_links = []
80
- @seen_objects = []
81
- @seen_hash = {}
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
- @preprocess_callback = nil
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 the graph.
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
- def add(target)
90
- return if @seen_hash.key? object_name(target)
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
- @stack << target
187
+ def add_descendants(target)
188
+ stack = add_object_to_graph(target, { :style => 'dashed' })
93
189
 
94
- while @stack.length > 0
95
- object = @stack.pop
96
- @name = object_name object
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
- @record = []
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
- add_to_record k, v
120
- end
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
- when Enumerable then
123
- if object.respond_to? :empty? and object.empty? then
124
- @record.push 'Empty!'
125
- else
126
- object.each_with_index do |v, i|
127
- next if v.nil? && ! @opts[:show_nil]
128
- if v.class.to_s =~ @opts[:class_filter] or
129
- (v.is_a? Enumerable and not v.is_a? String) then
130
- add_to_record i, v
131
- end
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
- # This is a HACK around bug 1345 (I think)
137
- if object.class and object.class.ancestors.include? Array and
138
- not Enumerable === object then
139
- if object.respond_to? :empty? and object.empty? then
140
- @record.push 'Empty!'
141
- else
142
- object.each_with_index do |v, i|
143
- next if v.nil? && ! @opts[:show_nil]
144
- if v.class.to_s =~ @opts[:class_filter] or
145
- (v.is_a? Enumerable and not v.is_a? String) then
146
- add_to_record i, v
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
- add_ivars object
257
+ stack += add_ivars(record, object)
153
258
 
154
- @seen_objects.push @record
155
- end
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.each_with_index do |iv_sym, i|
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
- index = @opts[:show_ivars] ? @record.length + i : nil
186
- @object_links << [@name, object_name(iv), index]
187
- @stack.push(iv)
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
- @record.push(*ivars) if @opts[:show_ivars]
299
+ stack
192
300
  end
193
301
 
194
302
  ##
195
- # Adds +key+ and +value+ to the record for the object currently being
196
- # processed.
303
+ # Adds +key+ and +value+ to +record+
197
304
 
198
- def add_to_record(key, value)
199
- @record.push(
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
- @object_links.push [@name, string_val, @record.length]
213
- @stack.push val
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
- @seen_objects.each { |id, *rest|
265
- s << "\"#{id}\" [\n\tlabel="
374
+ @nodes.values.sort.each do |node|
375
+ s << "\"#{node.name}\" [\n\tlabel="
266
376
  list = []
267
- [id, *rest].each_with_index { |field, i|
268
- list << "<f#{i}>#{field}"
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
- @object_links.each_with_index { |(from, to, x), i|
275
- s << "\"#{from}\":f#{x || 0} -> \"#{to}\":f0 [ id = #{i} ]\n"
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
 
@@ -0,0 +1,10 @@
1
+ require 'test/unit'
2
+ require 'ograph'
3
+
4
+ class FooTest < Test::Unit::TestCase
5
+ def test_foo
6
+ a = ObjectGraph::Pointer.new(1234, 'asdfasdf')
7
+ b = ObjectGraph::Pointer.new(1234, 'asdfasdf')
8
+ assert_equal(true, a.eql?(b))
9
+ end
10
+ end
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.1.0
7
- date: 2007-02-06 00:00:00 -08:00
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
- test_files: []
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.1.7
63
+ version: 1.2.0
58
64
  version: