ograph 0.1.0 → 0.2.0

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.
@@ -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: