bahuvrihi-tap 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History +69 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +119 -0
  4. data/bin/tap +114 -0
  5. data/cmd/console.rb +42 -0
  6. data/cmd/destroy.rb +16 -0
  7. data/cmd/generate.rb +16 -0
  8. data/cmd/run.rb +126 -0
  9. data/doc/Class Reference +362 -0
  10. data/doc/Command Reference +153 -0
  11. data/doc/Tutorial +237 -0
  12. data/lib/tap.rb +32 -0
  13. data/lib/tap/app.rb +720 -0
  14. data/lib/tap/constants.rb +8 -0
  15. data/lib/tap/env.rb +640 -0
  16. data/lib/tap/file_task.rb +547 -0
  17. data/lib/tap/generator/base.rb +109 -0
  18. data/lib/tap/generator/destroy.rb +37 -0
  19. data/lib/tap/generator/generate.rb +61 -0
  20. data/lib/tap/generator/generators/command/command_generator.rb +21 -0
  21. data/lib/tap/generator/generators/command/templates/command.erb +32 -0
  22. data/lib/tap/generator/generators/config/config_generator.rb +26 -0
  23. data/lib/tap/generator/generators/config/templates/doc.erb +12 -0
  24. data/lib/tap/generator/generators/config/templates/nodoc.erb +8 -0
  25. data/lib/tap/generator/generators/file_task/file_task_generator.rb +27 -0
  26. data/lib/tap/generator/generators/file_task/templates/file.txt +11 -0
  27. data/lib/tap/generator/generators/file_task/templates/result.yml +6 -0
  28. data/lib/tap/generator/generators/file_task/templates/task.erb +33 -0
  29. data/lib/tap/generator/generators/file_task/templates/test.erb +29 -0
  30. data/lib/tap/generator/generators/root/root_generator.rb +55 -0
  31. data/lib/tap/generator/generators/root/templates/Rakefile +86 -0
  32. data/lib/tap/generator/generators/root/templates/gemspec +27 -0
  33. data/lib/tap/generator/generators/root/templates/tapfile +8 -0
  34. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +5 -0
  36. data/lib/tap/generator/generators/root/templates/test/tapfile_test.rb +15 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +27 -0
  38. data/lib/tap/generator/generators/task/templates/task.erb +14 -0
  39. data/lib/tap/generator/generators/task/templates/test.erb +21 -0
  40. data/lib/tap/generator/manifest.rb +14 -0
  41. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  42. data/lib/tap/patches/rake/testtask.rb +55 -0
  43. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  44. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  45. data/lib/tap/root.rb +581 -0
  46. data/lib/tap/support/aggregator.rb +55 -0
  47. data/lib/tap/support/assignments.rb +172 -0
  48. data/lib/tap/support/audit.rb +418 -0
  49. data/lib/tap/support/batchable.rb +47 -0
  50. data/lib/tap/support/batchable_class.rb +107 -0
  51. data/lib/tap/support/class_configuration.rb +194 -0
  52. data/lib/tap/support/command_line.rb +98 -0
  53. data/lib/tap/support/comment.rb +270 -0
  54. data/lib/tap/support/configurable.rb +114 -0
  55. data/lib/tap/support/configurable_class.rb +296 -0
  56. data/lib/tap/support/configuration.rb +122 -0
  57. data/lib/tap/support/constant.rb +70 -0
  58. data/lib/tap/support/constant_utils.rb +127 -0
  59. data/lib/tap/support/declarations.rb +111 -0
  60. data/lib/tap/support/executable.rb +111 -0
  61. data/lib/tap/support/executable_queue.rb +82 -0
  62. data/lib/tap/support/framework.rb +71 -0
  63. data/lib/tap/support/framework_class.rb +199 -0
  64. data/lib/tap/support/instance_configuration.rb +147 -0
  65. data/lib/tap/support/lazydoc.rb +428 -0
  66. data/lib/tap/support/manifest.rb +89 -0
  67. data/lib/tap/support/run_error.rb +39 -0
  68. data/lib/tap/support/shell_utils.rb +71 -0
  69. data/lib/tap/support/summary.rb +30 -0
  70. data/lib/tap/support/tdoc.rb +404 -0
  71. data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
  72. data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
  73. data/lib/tap/support/templater.rb +180 -0
  74. data/lib/tap/support/validation.rb +410 -0
  75. data/lib/tap/support/versions.rb +97 -0
  76. data/lib/tap/task.rb +259 -0
  77. data/lib/tap/tasks/dump.rb +56 -0
  78. data/lib/tap/tasks/rake.rb +93 -0
  79. data/lib/tap/test.rb +37 -0
  80. data/lib/tap/test/env_vars.rb +29 -0
  81. data/lib/tap/test/file_methods.rb +377 -0
  82. data/lib/tap/test/script_methods.rb +144 -0
  83. data/lib/tap/test/subset_methods.rb +420 -0
  84. data/lib/tap/test/tap_methods.rb +237 -0
  85. data/lib/tap/workflow.rb +187 -0
  86. metadata +145 -0
@@ -0,0 +1,55 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # Aggregator allows thread-safe collection of Audits, organized
5
+ # by Audit#_current_source.
6
+ class Aggregator < Monitor
7
+
8
+ def initialize
9
+ super
10
+ clear
11
+ end
12
+
13
+ # Clears self of all audits.
14
+ def clear
15
+ synchronize { self.hash = Hash.new }
16
+ end
17
+
18
+ # The total number of audits recorded in self.
19
+ def size
20
+ synchronize { hash.values.inject(0) {|sum, array| sum + array.length} }
21
+ end
22
+
23
+ # True if size == 0
24
+ def empty?
25
+ synchronize { hash.empty? }
26
+ end
27
+
28
+ # Stores the Audit according to _result._current_source
29
+ def store(_result)
30
+ synchronize { (hash[_result._current_source] ||= []) << _result }
31
+ end
32
+
33
+ # Retreives all aggregated audits for the specified source.
34
+ def retrieve(source)
35
+ synchronize { hash[source] }
36
+ end
37
+
38
+ # Retreives all audits for the input sources, joined into an array.
39
+ def retrieve_all(*sources)
40
+ synchronize do
41
+ sources.collect {|src| hash[src] }.flatten.compact
42
+ end
43
+ end
44
+
45
+ # Converts self to a hash of (source, audits) pairs.
46
+ def to_hash
47
+ hash.dup
48
+ end
49
+
50
+ protected
51
+
52
+ attr_accessor :hash # :nodoc:
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,172 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # Assignments defines an array of [key, values] pairs that tracks
5
+ # which values are assigned to a particular key. A value may only
6
+ # be assigned to one key at a time.
7
+ #
8
+ # Assignments tracks the order in which keys are declared, and the
9
+ # order in which values are assigned to a key. This behavior is
10
+ # used by ClassConfiguration to track the order in which configurations
11
+ # are assigned to a class; the order, in turn, is used in the formation
12
+ # of config files, command line documentation, etc.
13
+ #
14
+ # === Example
15
+ #
16
+ # a = Assignments.new
17
+ # a.assign(:one, 'one')
18
+ # a.assign(:two, 'two')
19
+ # a.assign(:one, 'ONE')
20
+ # a.to_a # => [[:one, ['one', 'ONE']], [:two, ['two']]]
21
+ #
22
+ # b = Assignments.new(a)
23
+ # b.to_a # => [[:one, ['one', 'ONE']], [:two, ['two']]]
24
+ #
25
+ # b.unassign('one')
26
+ # b.assign(:one, 1)
27
+ # b.to_a # => [[:one, ['ONE', 1]], [:two, ['two']]]
28
+ # a.to_a # => [[:one, ['one', 'ONE']], [:two, ['two']]]
29
+ #
30
+ #--
31
+ # TODO:
32
+ # Assignments may be optimizable... check if an alternate internal
33
+ # storage can be made faster or to take up less memory. Not that
34
+ # that much can be gained period...
35
+ class Assignments
36
+ include Enumerable
37
+
38
+ def initialize(parent=nil)
39
+ existing_array = case parent
40
+ when Assignments then parent.array
41
+ when Array then parent
42
+ when nil then []
43
+ else
44
+ raise ArgumentError.new("cannot convert #{parent.class} to Assignments, Array, or nil")
45
+ end
46
+
47
+ @array = []
48
+ existing_array.each do |key, values|
49
+ assign(key, *values)
50
+ end
51
+ end
52
+
53
+ # Adds the key to the declarations.
54
+ def declare(key)
55
+ array << [key, []] unless declared?(key)
56
+ end
57
+
58
+ # Removes all values for the specified key and
59
+ # removes the key from declarations.
60
+ def undeclare(key)
61
+ array.delete_if {|k, values| k == key}
62
+ end
63
+
64
+ # Returns true if the key is declared.
65
+ def declared?(key)
66
+ array.each do |k, values|
67
+ return true if k == key
68
+ end
69
+ false
70
+ end
71
+
72
+ # Returns an array of all the declared keys
73
+ def declarations
74
+ array.collect {|key, values| key }
75
+ end
76
+
77
+ # Assigns the specified values to the key. The key will
78
+ # be declared, if necessary. Raises an error if the key
79
+ # is nil.
80
+ def assign(key, *values)
81
+ raise ArgumentError.new("nil keys are not allowed") if key == nil
82
+
83
+ # partition the input values into existing and new
84
+ # values, then check for conflicts.
85
+ current_values = self.values
86
+ existing_values, new_values = values.partition {|value| current_values.include?(value) }
87
+
88
+ conflicts = []
89
+ existing_values.collect do |value|
90
+ current_key = key_for(value)
91
+ if current_key != key
92
+ conflicts << "#{value} (#{key}) already assigned to #{current_key}"
93
+ end
94
+ end
95
+
96
+ unless conflicts.empty?
97
+ raise ArgumentError.new(conflicts.join("\n"))
98
+ end
99
+
100
+ declare(key)
101
+ values_for(key).concat new_values
102
+ end
103
+
104
+ # Removes the specified value.
105
+ def unassign(value)
106
+ array.each do |key, values|
107
+ values.delete(value)
108
+ end
109
+ end
110
+
111
+ # Returns true if the value has been assigned to a key.
112
+ def assigned?(value)
113
+ array.each do |key, values|
114
+ return true if values.include?(value)
115
+ end
116
+ false
117
+ end
118
+
119
+ # Returns the ordered values as an array
120
+ def values
121
+ array.collect {|key, values| values}.flatten
122
+ end
123
+
124
+ # Returns the key for the specified value, or nil
125
+ # if the value is unassigned.
126
+ def key_for(value)
127
+ array.each do |key, values|
128
+ return key if values.include?(value)
129
+ end
130
+ nil
131
+ end
132
+
133
+ # Returns the values for the specified key, or nil if
134
+ # the key cannot be found.
135
+ def values_for(key)
136
+ array.each do |k, values|
137
+ return values if k == key
138
+ end
139
+ nil
140
+ end
141
+
142
+ # Yields each key, value pair in the order in which
143
+ # the keys were declared. Keys with no values are
144
+ # skipped.
145
+ def each
146
+ array.each do |key, values|
147
+ values.each {|value| yield(key, value) }
148
+ end
149
+ end
150
+
151
+ # Yields each key, values pair in the order in which
152
+ # the keys were declared.
153
+ def each_pair
154
+ array.each do |key, values|
155
+ yield(key, values)
156
+ end
157
+ end
158
+
159
+ # Returns the [key, values] as an array
160
+ def to_a
161
+ array.collect {|key, values| [key, values.dup] }
162
+ end
163
+
164
+ protected
165
+
166
+ # An array of [key, values] arrays tracking the
167
+ # key and order in which values were assigned.
168
+ attr_reader :array
169
+
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,418 @@
1
+ autoload(:PP, 'pp')
2
+
3
+ module Tap
4
+ module Support
5
+
6
+ # Marks the merge of multiple Audit trails
7
+ class AuditMerge < Array
8
+ def ==(another)
9
+ another.kind_of?(AuditMerge) && super
10
+ end
11
+ end
12
+
13
+ # Marks a split in an Audit trail
14
+ class AuditSplit
15
+ attr_reader :block
16
+ def initialize(block) @block = block end
17
+
18
+ def ==(another)
19
+ another.kind_of?(AuditSplit) && another.block == block
20
+ end
21
+ end
22
+
23
+ # Marks the expansion of an Audit trail
24
+ class AuditExpand
25
+ attr_reader :index
26
+ def initialize(index) @index = index end
27
+
28
+ def ==(another)
29
+ another.kind_of?(AuditExpand) && another.index == index
30
+ end
31
+ end
32
+
33
+ # Audit provides a way to track the values (inputs and results) passed
34
+ # among tasks or, more generally, any Executable method. Audits allow
35
+ # you to track inputs as they make their way through a workflow, and
36
+ # have great utility in debugging and record keeping.
37
+ #
38
+ # During execution, the group of inputs for a task are used to initialize
39
+ # an Audit. These inputs mark the begining of an audit trail; every
40
+ # task that processes them (including the first) adds to the trail by
41
+ # recording it's result using itself as the 'source' of the result.
42
+ #
43
+ # Since Audits are meant to be fairly general structures, they can take
44
+ # any object as a source, so for illustration lets use some symbols:
45
+ #
46
+ # # initialize a new audit
47
+ # a = Audit.new(1, nil)
48
+ #
49
+ # # record some values
50
+ # a._record(:A, 2)
51
+ # a._record(:B, 3)
52
+ #
53
+ # Now you can pull up the source and value trails, as well as
54
+ # information like the current and original values:
55
+ #
56
+ # a._source_trail # => [nil, :A, :B]
57
+ # a._value_trail # => [1, 2, 3]
58
+ #
59
+ # a._original # => 1
60
+ # a._original_source # => nil
61
+ #
62
+ # a._current # => 3
63
+ # a._current_source # => :B
64
+ #
65
+ # Merges are supported by using an array of the merging trails (internally
66
+ # an AuditMerge) as the source, and an array of the merging values as the
67
+ # initial value.
68
+ #
69
+ # b = Audit.new(10, nil)
70
+ # b._record(:C, 11)
71
+ # b._record(:D, 12)
72
+ #
73
+ # c = Audit.merge(a, b)
74
+ # c._source_trail # => [ [[nil, :A, :B], [nil, :C, :D]] ]
75
+ # c._value_trail # => [ [[1,2,3], [10, 11, 12]] ]
76
+ # c._current # => [3, 12]
77
+ #
78
+ # c._record(:E, "a string value")
79
+ # c._record(:F, {'a' => 'hash value'})
80
+ # c._record(:G, ['an', 'array', 'value'])
81
+ #
82
+ # c._source_trail # => [ [[nil, :A, :B], [nil, :C, :D]], :E, :F, :G]
83
+ # c._value_trail # => [ [[1,2,3], [10, 11, 12]], "a string value", {'a' => 'hash value'}, ['an', 'array', 'value']]
84
+ #
85
+ # Audit supports forks by duplicating the source and value trails. Forks
86
+ # can be developed independently. Importantly, Audits are forked during
87
+ # a merge; notice the additional record in +a+ doesn't change the source
88
+ # trail for +c+:
89
+ #
90
+ # a1 = a._fork
91
+ #
92
+ # a._record(:X, -1)
93
+ # a1._record(:Y, -2)
94
+ #
95
+ # a._source_trail # => [nil, :A, :B, :X]
96
+ # a1._source_trail # => [nil, :A, :B, :Y]
97
+ # c._source_trail # => [ [[nil, :A, :B], [nil, :C, :D]], :E, :F, :G]
98
+ #
99
+ # The data structure for an audit gets nasty after a few merges because
100
+ # the lead array gets more and more nested. Audit provides iterators
101
+ # to help gain access, as well as a printing method to visualize the
102
+ # audit trail:
103
+ #
104
+ # [c._to_s]
105
+ # o-[] 1
106
+ # o-[A] 2
107
+ # o-[B] 3
108
+ # |
109
+ # | o-[] 10
110
+ # | o-[C] 11
111
+ # | o-[D] 12
112
+ # | |
113
+ # `-`-o-[E] "a string value"
114
+ # o-[F] {"a"=>"hash value"}
115
+ # o-[G] ["an", "array", "value"]
116
+ #
117
+ # In practice, tasks are recored as sources. Thus source trails can be used
118
+ # to access task configurations and other information that may be useful
119
+ # when creating reports or making workflow decisions (ex: raise an
120
+ # error after looping to a given task too many times).
121
+ #
122
+ #--
123
+ # TODO:
124
+ # Track nesting level of ams; see if you can hook this into the _to_s process to make
125
+ # extraction/presentation of audits more managable.
126
+ #
127
+ # Create a FirstLastArray to minimize the audit data collected. Allow different audit
128
+ # modes:
129
+ # - full ([] both)
130
+ # - source_only (fl value)
131
+ # - minimal (fl source and value)
132
+ #
133
+ # Try to work a _to_s that doesn't repeat the same audit twice. Think about a format
134
+ # like:
135
+ # |
136
+ # ------|-----+
137
+ # | |
138
+ # ------|-----|-----+
139
+ # | | |
140
+ # `-----`-----`-o-[j] j5
141
+ #
142
+ class Audit
143
+ class << self
144
+
145
+ # Creates a new Audit by merging the input audits. The value of the new
146
+ # Audit will be an array of the _current values of the audits. The source
147
+ # will be an AuditMerge whose values are forks of the audits. Non-Audit
148
+ # sources can be provided; they are initialized to Audits before merging.
149
+ #
150
+ # a = Audit.new
151
+ # a._record(:a, 'a')
152
+ #
153
+ # b = Audit.new
154
+ # b._record(:b, 'b')
155
+ #
156
+ # c = Audit.merge(a, b, 1)
157
+ # c._record(:c, 'c')
158
+ #
159
+ # c._values # => [['a','b', 1], 'c']
160
+ # c._sources # => [AuditMerge[a, b, Audit.new(1)], :c]
161
+ #
162
+ # If no audits are provided, merge returns a new Audit. If only one
163
+ # audit is provided, merge returns a fork of that audit.
164
+ def merge(*audits)
165
+ case audits.length
166
+ when 0 then Audit.new
167
+ when 1 then audits[0]._fork
168
+ else
169
+ sources = AuditMerge.new
170
+ audits.each {|a| sources << (a.kind_of?(Audit) ? a._fork : Audit.new(a)) }
171
+ values = audits.collect {|a| a.kind_of?(Audit) ? a._current : a}
172
+
173
+ Audit.new(values, sources)
174
+ end
175
+ end
176
+ end
177
+
178
+ # An array of the sources in self
179
+ attr_reader :_sources
180
+
181
+ # An array of the values in self
182
+ attr_reader :_values
183
+
184
+ # An arbitrary constant used to identify when no inputs have been
185
+ # provided to Audit.new. (nil itself cannot be used as nil is a
186
+ # valid initial value for an audit trail)
187
+ AUDIT_NIL = Object.new
188
+
189
+ # A new audit takes a value and/or source. A nil source is typically given
190
+ # for the original value.
191
+ def initialize(value=AUDIT_NIL, source=nil)
192
+ @_sources = []
193
+ @_values = []
194
+
195
+ _record(source, value) unless value == AUDIT_NIL
196
+ end
197
+
198
+ # Records the next value produced by the source. When an audit is
199
+ # passed as a value, record will record the current value of the audit.
200
+ # Record will similarly resolve every audit in an array containing audits.
201
+ #
202
+ # Example:
203
+ #
204
+ # a = Audit.new(1)
205
+ # b = Audit.new(2)
206
+ # c = Audit.new(3)
207
+ #
208
+ # c.record(:a, a)
209
+ # c.sources # => [:a]
210
+ # c.values # => [1]
211
+ #
212
+ # c.record(:ab, [a,b])
213
+ # c.sources # => [:a, :ab]
214
+ # c.values # => [1, [1, 2]]
215
+ def _record(source, value)
216
+ _sources << source
217
+ _values << value
218
+ self
219
+ end
220
+
221
+ # The original value used to initialize the Audit
222
+ def _original
223
+ _values.first
224
+ end
225
+
226
+ # The current (ie last) value recorded in the Audit
227
+ def _current
228
+ _values.last
229
+ end
230
+
231
+ # The original source used to initialize the Audit
232
+ def _original_source
233
+ _sources.first
234
+ end
235
+
236
+ # The current (ie last) source recorded in the Audit
237
+ def _current_source
238
+ _sources.last
239
+ end
240
+
241
+ # Searches back and recursively (if the source is an audit) collects all sources
242
+ # for the current value.
243
+ def _source_trail
244
+ _collect_records {|source, value| source}
245
+ end
246
+
247
+ # Searches back and recursively (if the source is an audit) collects all values
248
+ # leading to the current value.
249
+ def _value_trail
250
+ _collect_records {|source, value| value}
251
+ end
252
+
253
+ def _collect_records(&block) # :yields: source, value
254
+ collection = []
255
+ 0.upto(_sources.length-1) do |i|
256
+ collection << collect_records(_sources[i], _values[i], &block)
257
+ end
258
+ collection
259
+ end
260
+
261
+ def _each_record(merge_level=0, merge_index=0, &block) # :yields: source, value, merge_level, merge_index, index
262
+ 0.upto(_sources.length-1) do |i|
263
+ each_record(_sources[i], _values[i], merge_level, merge_index, i, &block)
264
+ end
265
+ end
266
+
267
+ # Creates a new Audit by merging self and the input audits, using Audit#merge.
268
+ def _merge(*audits)
269
+ Audit.merge(self, *audits)
270
+ end
271
+
272
+ # Produces a new Audit with duplicate sources and values, suitable for
273
+ # separate development along a separate path.
274
+ def _fork
275
+ a = Audit.new
276
+ a._sources = _sources.dup
277
+ a._values = _values.dup
278
+ a
279
+ end
280
+
281
+ # _forks self and records the next value as [<return from block>, AuditSplit.new(block)]
282
+ def _split(&block) # :yields: _current
283
+ _fork._record(AuditSplit.new(block), yield(_current))
284
+ end
285
+
286
+ # _forks self for each member in _current. Records the next value as
287
+ # [item, AuditExpand.new(<index of item>)]. Raises an error if _current
288
+ # does not respond to each.
289
+ def _expand
290
+ expanded = []
291
+ _current.each do |value|
292
+ expanded << _fork._record(AuditExpand.new(expanded.length), value)
293
+ end
294
+ expanded
295
+ end
296
+
297
+ # Returns true if the _sources and _values for self are equal
298
+ # to those of another.
299
+ def ==(another)
300
+ another.kind_of?(Audit) && self._sources == another._sources && self._values == another._values
301
+ end
302
+
303
+ # A kind of pretty-print for Audits. See the example in the overview.
304
+ def _to_s
305
+ # TODO -- find a way to avoid repeating groups
306
+
307
+ group = []
308
+ groups = [group]
309
+ extended_groups = [groups]
310
+ group_merges = []
311
+ extended_group_merges = []
312
+ current_level = nil
313
+ current_index = nil
314
+
315
+ _each_record do |source, value, merge_level, merge_index, index|
316
+ source_str, value_str = if block_given?
317
+ yield(source, value)
318
+ else
319
+ [source, value == nil ? '' : PP.singleline_pp(value, '')]
320
+ end
321
+
322
+ if !group.empty? && (merge_level != current_level || index == 0)
323
+ unless merge_level <= current_level
324
+ groups = []
325
+ extended_groups << groups
326
+ end
327
+
328
+ group = []
329
+ groups << group
330
+
331
+ if merge_level < current_level
332
+ if merge_index == 0
333
+ extended_group_merges << group.object_id
334
+ end
335
+
336
+ unless index == 0
337
+ group_merges << group.object_id
338
+ end
339
+ end
340
+ end
341
+
342
+ group << "o-[#{source_str}] #{value_str}"
343
+ current_level = merge_level
344
+ current_index = merge_index
345
+ end
346
+
347
+ lines = []
348
+ group_prefix = ""
349
+ extended_groups.each do |ext_groups|
350
+ indentation = 0
351
+
352
+ ext_groups.each_with_index do |ext_group, group_num|
353
+ ext_group.each_with_index do |line, line_num|
354
+ if line_num == 0
355
+ unless lines.empty?
356
+ lines << group_prefix + " " * indentation + "| " * (group_num-indentation)
357
+ end
358
+
359
+ if group_merges.include?(ext_group.object_id)
360
+ lines << group_prefix + " " * indentation + "`-" * (group_num-indentation) + line
361
+ indentation = group_num
362
+
363
+ if extended_group_merges.include?(ext_group.object_id)
364
+ lines.last.gsub!(/\| \s*/) {|match| "`-" + "-" * (match.length - 2)}
365
+ group_prefix.gsub!(/\| /, " ")
366
+ end
367
+ next
368
+ end
369
+ end
370
+
371
+ lines << group_prefix + " " * indentation + "| " * (group_num-indentation) + line
372
+ end
373
+ end
374
+
375
+ group_prefix += " " * (ext_groups.length-1) + "| "
376
+ end
377
+
378
+ lines.join("\n") + "\n"
379
+ end
380
+
381
+ protected
382
+
383
+ attr_writer :_sources, :_values # :nodoc:
384
+
385
+ private
386
+
387
+ # helper method to recursively collect the value trail for a given source
388
+ def collect_records(source, value, &block)
389
+ case source
390
+ when AuditMerge
391
+ collection = []
392
+ 0.upto(source.length-1) do |i|
393
+ collection << collect_records(source[i], value[i], &block)
394
+ end
395
+ collection
396
+ when Audit
397
+ source._collect_records(&block)
398
+ else
399
+ yield(source, value)
400
+ end
401
+ end
402
+
403
+ def each_record(source, value, merge_level, merge_index, index, &block)
404
+ case source
405
+ when AuditMerge
406
+ merge_level += 1
407
+ 0.upto(source.length-1) do |i|
408
+ each_record(source[i], value[i], merge_level, i, index, &block)
409
+ end
410
+ when Audit
411
+ source._each_record(merge_level, merge_index, &block)
412
+ else
413
+ yield(source, value, merge_level, merge_index, index)
414
+ end
415
+ end
416
+ end
417
+ end
418
+ end