tap 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. data/History +35 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README +16 -15
  4. data/bin/tap +1 -1
  5. data/cmd/console.rb +4 -3
  6. data/cmd/manifest.rb +2 -2
  7. data/cmd/run.rb +12 -15
  8. data/doc/Class Reference +120 -117
  9. data/doc/Command Reference +27 -27
  10. data/doc/Syntax Reference +55 -111
  11. data/doc/Tutorial +69 -26
  12. data/lib/tap.rb +3 -8
  13. data/lib/tap/app.rb +122 -146
  14. data/lib/tap/constants.rb +2 -2
  15. data/lib/tap/env.rb +178 -252
  16. data/lib/tap/exe.rb +67 -30
  17. data/lib/tap/file_task.rb +224 -411
  18. data/lib/tap/generator/arguments.rb +13 -0
  19. data/lib/tap/generator/base.rb +112 -30
  20. data/lib/tap/generator/destroy.rb +36 -13
  21. data/lib/tap/generator/generate.rb +69 -48
  22. data/lib/tap/generator/generators/command/templates/command.erb +3 -3
  23. data/lib/tap/generator/generators/config/config_generator.rb +82 -10
  24. data/lib/tap/generator/generators/generator/generator_generator.rb +16 -6
  25. data/lib/tap/generator/generators/generator/templates/task.erb +2 -2
  26. data/lib/tap/generator/generators/generator/templates/test.erb +26 -0
  27. data/lib/tap/generator/generators/root/root_generator.rb +24 -13
  28. data/lib/tap/generator/generators/root/templates/Rakefile +4 -4
  29. data/lib/tap/generator/generators/root/templates/{tapfile → Rapfile} +6 -6
  30. data/lib/tap/generator/generators/root/templates/gemspec +0 -1
  31. data/lib/tap/generator/generators/task/task_generator.rb +3 -3
  32. data/lib/tap/generator/generators/task/templates/test.erb +1 -1
  33. data/lib/tap/generator/manifest.rb +7 -1
  34. data/lib/tap/generator/preview.rb +76 -0
  35. data/lib/tap/root.rb +222 -156
  36. data/lib/tap/spec.rb +41 -0
  37. data/lib/tap/support/aggregator.rb +25 -28
  38. data/lib/tap/support/audit.rb +278 -357
  39. data/lib/tap/support/constant.rb +2 -1
  40. data/lib/tap/support/constant_manifest.rb +28 -25
  41. data/lib/tap/support/dependency.rb +1 -1
  42. data/lib/tap/support/executable.rb +52 -183
  43. data/lib/tap/support/executable_queue.rb +50 -20
  44. data/lib/tap/support/gems.rb +1 -1
  45. data/lib/tap/support/intern.rb +0 -6
  46. data/lib/tap/support/join.rb +49 -83
  47. data/lib/tap/support/joins.rb +0 -3
  48. data/lib/tap/support/joins/switch.rb +13 -11
  49. data/lib/tap/support/joins/sync_merge.rb +25 -50
  50. data/lib/tap/support/manifest.rb +1 -0
  51. data/lib/tap/support/node.rb +140 -20
  52. data/lib/tap/support/parser.rb +56 -42
  53. data/lib/tap/support/schema.rb +183 -157
  54. data/lib/tap/support/templater.rb +9 -1
  55. data/lib/tap/support/versions.rb +39 -0
  56. data/lib/tap/task.rb +150 -177
  57. data/lib/tap/tasks/dump.rb +4 -4
  58. data/lib/tap/tasks/load.rb +29 -29
  59. data/lib/tap/test.rb +66 -53
  60. data/lib/tap/test/env_vars.rb +3 -3
  61. data/lib/tap/test/extensions.rb +11 -17
  62. data/lib/tap/test/file_test.rb +74 -132
  63. data/lib/tap/test/file_test_class.rb +4 -1
  64. data/lib/tap/test/regexp_escape.rb +2 -2
  65. data/lib/tap/test/script_test.rb +2 -2
  66. data/lib/tap/test/subset_test.rb +6 -6
  67. data/lib/tap/test/tap_test.rb +28 -154
  68. metadata +30 -51
  69. data/bin/rap +0 -118
  70. data/cgi/run.rb +0 -97
  71. data/lib/tap/declarations.rb +0 -229
  72. data/lib/tap/generator/generators/config/templates/doc.erb +0 -12
  73. data/lib/tap/generator/generators/config/templates/nodoc.erb +0 -8
  74. data/lib/tap/generator/generators/file_task/file_task_generator.rb +0 -27
  75. data/lib/tap/generator/generators/file_task/templates/file.txt +0 -11
  76. data/lib/tap/generator/generators/file_task/templates/result.yml +0 -6
  77. data/lib/tap/generator/generators/file_task/templates/task.erb +0 -33
  78. data/lib/tap/generator/generators/file_task/templates/test.erb +0 -29
  79. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +0 -5
  80. data/lib/tap/patches/optparse/summarize.rb +0 -62
  81. data/lib/tap/support/assignments.rb +0 -173
  82. data/lib/tap/support/class_configuration.rb +0 -182
  83. data/lib/tap/support/combinator.rb +0 -125
  84. data/lib/tap/support/configurable.rb +0 -113
  85. data/lib/tap/support/configurable_class.rb +0 -271
  86. data/lib/tap/support/configuration.rb +0 -170
  87. data/lib/tap/support/gems/rake.rb +0 -111
  88. data/lib/tap/support/instance_configuration.rb +0 -173
  89. data/lib/tap/support/joins/fork.rb +0 -19
  90. data/lib/tap/support/joins/merge.rb +0 -22
  91. data/lib/tap/support/joins/sequence.rb +0 -21
  92. data/lib/tap/support/lazy_attributes.rb +0 -45
  93. data/lib/tap/support/lazydoc.rb +0 -386
  94. data/lib/tap/support/lazydoc/comment.rb +0 -503
  95. data/lib/tap/support/lazydoc/config.rb +0 -17
  96. data/lib/tap/support/lazydoc/definition.rb +0 -36
  97. data/lib/tap/support/lazydoc/document.rb +0 -152
  98. data/lib/tap/support/lazydoc/method.rb +0 -24
  99. data/lib/tap/support/tdoc.rb +0 -409
  100. data/lib/tap/support/tdoc/tdoc_html_generator.rb +0 -38
  101. data/lib/tap/support/tdoc/tdoc_html_template.rb +0 -42
  102. data/lib/tap/support/validation.rb +0 -479
  103. data/lib/tap/tasks/rake.rb +0 -57
@@ -0,0 +1,41 @@
1
+ $:.unshift File.expand_path("#{File.dirname(__FILE__)}/..")
2
+ require 'tap/test/extensions'
3
+
4
+ require 'rubygems'
5
+ require 'minitest/spec'
6
+
7
+ # :stopdoc:
8
+ class MiniTest::Unit::TestCase
9
+ extend Tap::Test::Extensions
10
+
11
+ class << self
12
+ # Causes a test suite to be skipped. If a message is given, it will
13
+ # print and notify the user the test suite has been skipped.
14
+ def skip_test(msg=nil)
15
+ @@test_suites.delete(self)
16
+ puts "Skipping #{self}#{msg.empty? ? '' : ': ' + msg}"
17
+ end
18
+
19
+ private
20
+
21
+ # Infers the test root directory from the calling file.
22
+ # 'some_class.rb' => 'some_class'
23
+ # 'some_class_test.rb' => 'some_class'
24
+ def test_root_dir # :nodoc:
25
+ # caller[1] is considered the calling file (which should be the test case)
26
+ # note that caller entries are like this:
27
+ # ./path/to/file.rb:10
28
+ # ./path/to/file.rb:10:in 'method'
29
+
30
+ calling_file = caller[1].gsub(/:\d+(:in .*)?$/, "")
31
+ calling_file.chomp(File.extname(calling_file)).chomp("_spec")
32
+ end
33
+ end
34
+
35
+ def method_name
36
+ @method_name ||= name.gsub(/\s/, "_").gsub(/^test_/, "")
37
+ end
38
+ end
39
+
40
+ MiniTest::Unit.autorun
41
+ # :startdoc:
@@ -1,68 +1,65 @@
1
1
  module Tap
2
2
  module Support
3
3
 
4
- # Aggregator allows thread-safe collection of Audits, organized
5
- # by Audit#_current_source.
4
+ # Aggregator allows thread-safe collection of Audits, organized by
5
+ # Audit#key.
6
6
  #
7
- # a = Audit.new
8
- # a._record(:src, 'a')
9
- #
10
- # b = Audit.new
11
- # b._record(:src, 'b')
7
+ # a = Audit.new(:key, 'a')
8
+ # b = Audit.new(:key, 'b')
12
9
  #
13
10
  # agg = Aggregator.new
14
11
  # agg.store(a)
15
12
  # agg.store(b)
16
- # agg.retrieve(:src) # => [a, b]
13
+ # agg.retrieve(:key) # => [a, b]
17
14
  #
18
15
  class Aggregator < Monitor
19
16
 
20
17
  # Creates a new Aggregator.
21
18
  def initialize
22
19
  super
23
- clear
20
+ @hash = {}
24
21
  end
25
22
 
26
- # Clears self of all audits.
23
+ # Clears self of all audits. Returns the existing audits as a hash
24
+ # of (key, audits) pairs.
27
25
  def clear
28
- synchronize { self.hash = Hash.new }
26
+ synchronize do
27
+ current, @hash = @hash, {}
28
+ current
29
+ end
29
30
  end
30
31
 
31
32
  # The total number of audits recorded in self.
32
33
  def size
33
- synchronize { hash.values.inject(0) {|sum, array| sum + array.length} }
34
+ synchronize { @hash.values.inject(0) {|sum, array| sum + array.length} }
34
35
  end
35
36
 
36
37
  # True if size == 0
37
38
  def empty?
38
- synchronize { hash.empty? }
39
+ synchronize { size == 0 }
39
40
  end
40
41
 
41
- # Stores the Audit according to _result._current_source
42
- def store(_result)
43
- synchronize { (hash[_result._current_source] ||= []) << _result }
42
+ # Stores the Audit according to _audit.key.
43
+ def store(_audit)
44
+ synchronize { (@hash[_audit.key] ||= []) << _audit }
44
45
  end
45
46
 
46
- # Retreives all aggregated audits for the specified source.
47
- def retrieve(source)
48
- synchronize { hash[source] }
47
+ # Retreives all audits for the specified key.
48
+ def retrieve(key)
49
+ synchronize { @hash[key] }
49
50
  end
50
51
 
51
- # Retreives all audits for the input sources, joined as an array.
52
- def retrieve_all(*sources)
52
+ # Retreives all audits for the input keys, joined as an array.
53
+ def retrieve_all(*keys)
53
54
  synchronize do
54
- sources.collect {|src| hash[src] }.flatten.compact
55
+ keys.collect {|src| @hash[src] }.flatten.compact
55
56
  end
56
57
  end
57
58
 
58
- # Converts self to a hash of (source, audits) pairs.
59
+ # Converts self to a hash of (key, audits) pairs.
59
60
  def to_hash
60
- hash.dup
61
+ synchronize { @hash.dup }
61
62
  end
62
-
63
- protected
64
-
65
- attr_accessor :hash # :nodoc:
66
63
  end
67
64
  end
68
65
  end
@@ -1,410 +1,331 @@
1
1
  module Tap
2
- module Support
2
+ module Support
3
3
 
4
- # Marks the merge of multiple Audit trails
5
- class AuditMerge < Array
6
-
7
- # True if another is an AuditMerge and passes Array#==
8
- def ==(another)
9
- another.kind_of?(AuditMerge) && super
10
- end
11
- end
12
-
13
- # Marks the expansion of an Audit trail
14
- class AuditIterate
15
- attr_reader :index
16
- def initialize(index) @index = index end
17
-
18
- # True if another is an AuditIterate with the same index.
19
- def ==(another)
20
- another.kind_of?(AuditIterate) && another.index == index
21
- end
22
-
23
- # Returns a string like '_iterate(<index>)'.
24
- def to_s
25
- "_iterate(#{index})"
26
- end
27
- end
28
-
29
- # Audit provides a way to track the values (inputs and results) passed among
30
- # tasks or, more generally, any Executable. Audits allow you to track inputs
31
- # as they make their way through a workflow, and have great utility in
32
- # debugging and record keeping.
4
+ # Audit provides a way to track the values passed among tasks or, more
5
+ # generally, any Executable. Audits collectively build a {directed
6
+ # acyclic graph}[http://en.wikipedia.org/wiki/Directed_acyclic_graph]
7
+ # of task execution and have great utility in debugging and record keeping.
33
8
  #
34
- # During execution, the inputs to a task are used to initialize an Audit.
35
- # These inputs are the original value of the audit and mark the begining
36
- # of an audit trail; every task adds to the trail by recording it's result
37
- # and itself as the 'source' of the result.
9
+ # Audits record a key, a current value, and the previous audit(s) in the
10
+ # trail. Keys are arbitrary identifiers of where the value comes from.
11
+ # To illustrate, lets use symbols as keys.
38
12
  #
39
- # Audits can take any object as a source, so for illustration lets use some
40
- # symbols:
41
- #
42
13
  # # initialize a new audit
43
- # a = Audit.new(1, nil)
44
- #
45
- # # record some values
46
- # a._record(:A, 2)
47
- # a._record(:B, 3)
48
- #
49
- # Now you can pull up the source and value trails, as well as the current
50
- # and original values:
51
- #
52
- # a._source_trail # => [nil, :A, :B]
53
- # a._value_trail # => [1, 2, 3]
54
- #
55
- # a._original # => 1
56
- # a._original_source # => nil
14
+ # _a = Audit.new(:one, 1)
15
+ # _a.key # => :one
16
+ # _a.value # => 1
57
17
  #
58
- # a._current # => 3
59
- # a._current_source # => :B
18
+ # # build a short trail
19
+ # _b = Audit.new(:two, 2, _a)
20
+ # _c = Audit.new(:three, 3, _b)
60
21
  #
61
- # Merges are supported by using an array of the merged trails (actually
62
- # an AuditMerge) as the source, and an array of the merged values as the
63
- # original value.
22
+ # _a.sources # => []
23
+ # _b.sources # => [_a]
24
+ # _c.sources # => [_b]
64
25
  #
65
- # b = Audit.new(10, nil)
66
- # b._record(:C, 11)
67
- # b._record(:D, 12)
26
+ # Audits allow you track back through the sources of each audit to build
27
+ # a trail describing how a particular value was produced.
68
28
  #
69
- # c = Audit.merge(a, b)
70
- # c._source_trail # => [ [[nil, :A, :B], [nil, :C, :D]] ]
71
- # c._value_trail # => [ [[1,2,3], [10, 11, 12]] ]
72
- # c._current # => [3, 12]
29
+ # _c.trail # => [_a,_b,_c]
30
+ # _c.trail {|audit| audit.key } # => [:one, :two, :three]
31
+ # _c.trail {|audit| audit.value } # => [1,2,3]
73
32
  #
74
- # c._record(:E, "a string value")
75
- # c._record(:F, {'a' => 'hash value'})
76
- # c._record(:G, ['an', 'array', 'value'])
33
+ # Any number of audits may share the same source, so forks are naturally
34
+ # supported.
77
35
  #
78
- # c._source_trail # => [ [[nil, :A, :B], [nil, :C, :D]], :E, :F, :G]
79
- # c._value_trail # => [ [[1,2,3], [10, 11, 12]], "a string value", {'a' => 'hash value'}, ['an', 'array', 'value']]
36
+ # _d = Audit.new(:four, 4, _b)
37
+ # _d.trail # => [_a,_b,_d]
80
38
  #
81
- # Audit supports forks by duplicating the source and value trails. Forks
82
- # can be developed independently. Audits are also forked during a merge;
83
- # notice the additional record in 'a' doesn't change the source trail for
84
- # 'c':
39
+ # _e = Audit.new(:five, 5, _b)
40
+ # _e.trail # => [_a,_b,_e]
85
41
  #
86
- # a1 = a._fork
42
+ # Merges are supported by specifying more than one source. Merges have
43
+ # the effect of nesting audit trails within an array:
87
44
  #
88
- # a._record(:X, -1)
89
- # a1._record(:Y, -2)
90
- #
91
- # a._source_trail # => [nil, :A, :B, :X]
92
- # a1._source_trail # => [nil, :A, :B, :Y]
93
- # c._source_trail # => [ [[nil, :A, :B], [nil, :C, :D]], :E, :F, :G]
94
- #
95
- # The data structure for an audit gets nasty after a few merges because
96
- # the lead array gets more and more nested. Audit provides iterators
97
- # to help gain access, as well as a printing method to visualize the
98
- # audit trail:
45
+ # _f = Audit.new(:six, 6)
46
+ # _g = Audit.new(:seven, 7, _f)
47
+ # _h = Audit.new(:eight, 8, [_c,_d,_g])
48
+ # _h.trail # => [[[_a,_b,_c], [_a,_b,_d], [_f,_g]], _h]
49
+ #
50
+ # Nesting can get quite ugly after a couple merges so Audit provides a
51
+ # scalable pretty-print dump that helps visualize the audit trail.
99
52
  #
100
- # c._to_s
101
- # # =>
102
- # # o-[] 1
103
- # # o-[A] 2
104
- # # o-[B] 3
105
- # # |
106
- # # | o-[] 10
107
- # # | o-[C] 11
108
- # # | o-[D] 12
109
- # # | |
110
- # # `-`-o-[E] "a string value"
111
- # # o-[F] {"a"=>"hash value"}
112
- # # o-[G] ["an", "array", "value"]
53
+ # "\n" + _h.dump
54
+ # # => %q{
55
+ # # o-[one] 1
56
+ # # o-[two] 2
57
+ # # |
58
+ # # |-o-[three] 3
59
+ # # | |
60
+ # # `---o-[four] 4
61
+ # # | |
62
+ # # | | o-[six] 6
63
+ # # | | o-[seven] 7
64
+ # # | | |
65
+ # # `-`-`-o-[eight] 8
66
+ # # }
113
67
  #
114
- # In practice, tasks are recored as sources. Thus source trails can be used
115
- # to access task configurations and other information that may be useful
116
- # when creating reports or making workflow decisions.
68
+ # In practice, tasks are recorded as keys. Thus audit trails can be used
69
+ # to access task configurations and other information that may be useful
70
+ # when creating reports or making workflow decisions. Note that by
71
+ # convention Audits and non-Audit methods that return Audits are
72
+ # prefixed with an underscore.
117
73
  #
118
74
  #--
119
- # TODO:
120
- # Track nesting level of ams; see if you can hook this into the _to_s process to make
121
- # extraction/presentation of audits more managable.
122
- #
123
- # Create a FirstLastArray to minimize the audit data collected. Allow different audit
124
- # modes:
125
- # - full ([] both)
126
- # - source_only (fl value)
127
- # - minimal (fl source and value)
128
- #
129
- # Try to work a _to_s that doesn't repeat the same audit twice. Think about a format
130
- # like:
131
- # |
132
- # ------|-----+
133
- # | |
134
- # ------|-----|-----+
135
- # | | |
136
- # `-----`-----`-o-[j] j5
75
+ # Note Audit could easily be expanded to track sinks as well as sources.
76
+ # In initialize:
137
77
  #
78
+ # @sinks = []
79
+ # sources.each do |source|
80
+ # source.sinks << self
81
+ # end
82
+ #
83
+ # The downside is that this may not circumvent cleanly if you want light
84
+ # or no auditing. It also adds additonal references which will prevent
85
+ # garbage collection. On the plus side, sinks will make it easier to
86
+ # truly use Audits as a DAG
138
87
  class Audit
139
88
  class << self
140
-
141
- # Creates a new Audit by merging the input audits. The value of the new
142
- # Audit will be an array of the _current values of the inputs. The source
143
- # will be an AuditMerge whose values are forks of the inputs. Non-Audit
144
- # sources may be provided; they are initialized to Audits before merging.
145
- #
146
- # a = Audit.new
147
- # a._record(:a, 'a')
148
- #
149
- # b = Audit.new
150
- # b._record(:b, 'b')
151
- #
152
- # c = Audit.merge(a, b, 1)
153
- # c._record(:c, 'c')
154
- #
155
- # c._values # => [['a','b', 1], 'c']
156
- # c._sources # => [AuditMerge[a, b, Audit.new(1)], :c]
157
- #
158
- # If no audits are provided, merge returns a new Audit. If only one
159
- # audit is provided, merge returns a fork of that audit.
160
- def merge(*audits)
161
- case audits.length
162
- when 0 then Audit.new
163
- when 1 then audits[0]._fork
164
- else
165
- sources = AuditMerge.new
166
- audits.each {|a| sources << (a.kind_of?(Audit) ? a._fork : Audit.new(a)) }
167
- values = audits.collect {|a| a.kind_of?(Audit) ? a._current : a}
89
+
90
+ # Produces a pretty-print dump of the specified audits to target.
91
+ # A block may be provided to format the trailer of each line.
92
+ def dump(audits, target=$stdout) # :yields: audit
93
+ return dump(audits, target) do |audit|
94
+ "o-[#{audit.key}] #{audit.value.inspect}"
95
+ end unless block_given?
96
+
97
+ # arrayify audits
98
+ audits = [audits].flatten
99
+
100
+ # the order of audits
101
+ order = []
102
+
103
+ # (audit, sinks) hash preventing double iteration over
104
+ # audits, and identifying sinks for a particular audit
105
+ sinks = {}
106
+
107
+ # iterate over all audits, collecting in order
108
+ audits.each do |audit|
109
+ traverse(audit, order, sinks)
110
+ end
111
+
112
+ # visit each audit, collecting audits into indent groups
113
+ groups = []
114
+ current = nil
115
+ order.each do |audit|
116
+ sources = audit.sources
117
+ unless sources.length == 1 && sinks[sources[0]].length <= 1
118
+ current = []
119
+ groups << current
120
+ end
121
+
122
+ current << audit
123
+ end
124
+
125
+ # identify nodes at which a fork occurs... these are audits
126
+ # that have more than one sink, and they cause a fork-style
127
+ # leader to be printed
128
+ forks = {}
129
+ sinks.each_pair do |audit, audit_sinks|
130
+ n = audit_sinks.length
131
+ forks[audit] = [0, n] if n > 1
132
+ end
133
+
134
+ # setup print
135
+ index = 0
136
+ leader = ""
137
+
138
+ # print each group
139
+ groups.each do |group|
140
+ sources = group[0].sources
141
+ complete = audits.include?(group[-1])
142
+
143
+ case
144
+ when sources.length > 1
145
+ # print a merge
146
+ # `-`-`-o-[merge]
147
+
148
+ leader =~ /^(.*)((\| *){#{sources.length}})$/
149
+ leader = "#{$1}#{' ' * $2.length} "
150
+ target << "#{$1}#{$2.gsub('|', '`').gsub(' ', '-')}-#{yield(group.shift)}\n"
151
+
152
+ when fork = forks[sources[0]]
153
+ # print a fork
154
+ # |-o-[a]
155
+ # |
156
+ # `---o-[b]
157
+
158
+ n = fork[0] += 1
159
+ base = leader[0, leader.length - (2 * n - 1)]
160
+ target << "#{base}#{fork[0] == fork[1] ? '`-' : '|-'}#{'--' * (n-1)}#{yield(group.shift)}\n"
161
+ leader = "#{base}#{fork[0] == fork[1] ? ' ' : '| '}#{'| ' * (n-1)}"
162
+
163
+ when index > 0
164
+ # simply get ready to print the next series of audits
165
+ # o-[a]
166
+ # o-[b]
167
+
168
+ leader = "#{leader} "
169
+ leader = "" if leader.strip.empty?
170
+ end
171
+
172
+ # print the next series of audits
173
+ group.each do |audit|
174
+ target << "#{leader}#{yield(audit)}\n"
175
+ end
176
+
177
+ # add a continuation line, if necessary
178
+ unless group == groups.last
179
+ if complete
180
+ leader = "#{leader} "
181
+ else
182
+ leader = "#{leader}|"
183
+ end
184
+ target << "#{leader}\n"
185
+ end
186
+
187
+ index += 1
188
+ end
189
+
190
+ target
191
+ end
192
+
193
+ protected
194
+
195
+ # helper to determine the order and sinks for a node
196
+ def traverse(node, order=[], sinks={}) # :nodoc:
197
+ return if sinks.has_key?(node)
168
198
 
169
- Audit.new(values, sources)
199
+ node.sources.each do |source|
200
+ traverse(source, order, sinks)
201
+ (sinks[source] ||= []) << node
170
202
  end
203
+
204
+ order << node
171
205
  end
172
206
  end
173
207
 
174
- # An array of the sources in self
175
- attr_reader :_sources
176
-
177
- # An array of the values in self
178
- attr_reader :_values
208
+ # A key for self (typically the task producing value, or
209
+ # nil if the value has an unknown origin)
210
+ attr_reader :key
179
211
 
180
- # An arbitrary object used to identify when no inputs have been
181
- # provided to Audit.new. (nil cannot be used since nil is a valid
182
- # initial value)
183
- AUDIT_NIL = Object.new
212
+ # The current value
213
+ attr_reader :value
184
214
 
185
- # A new audit takes a value and/or source. A nil source is typically given
186
- # for the original value.
187
- def initialize(value=AUDIT_NIL, source=nil)
188
- @_sources = []
189
- @_values = []
190
-
191
- _record(source, value) unless value == AUDIT_NIL
192
- end
193
-
194
- # Records the next value produced by the source. When an audit is
195
- # passed as a value, record will record the current value of the audit.
196
- # Record will similarly resolve every audit in an array containing audits.
215
+ # Initializes a new Audit. Sources may be an array, a single value
216
+ # (which is turned into an array), or nil (indicating no sources).
197
217
  #
198
- # Example:
218
+ # _a = Audit.new(nil, nil, nil)
219
+ # _a.sources # => []
199
220
  #
200
- # a = Audit.new(1)
201
- # b = Audit.new(2)
202
- # c = Audit.new(3)
221
+ # _b = Audit.new(nil, nil, _a)
222
+ # _b.sources # => [_a]
203
223
  #
204
- # c.record(:a, a)
205
- # c.sources # => [:a]
206
- # c.values # => [1]
207
- #
208
- # c.record(:ab, [a,b])
209
- # c.sources # => [:a, :ab]
210
- # c.values # => [1, [1, 2]]
211
- def _record(source, value)
212
- _sources << source
213
- _values << value
214
- self
215
- end
216
-
217
- # The original value used to initialize the Audit
218
- def _original
219
- _values.first
220
- end
221
-
222
- # The current (ie last) value recorded in the Audit
223
- def _current
224
- _values.last
225
- end
226
-
227
- # The original source used to initialize the Audit
228
- def _original_source
229
- _sources.first
230
- end
231
-
232
- # The current (ie last) source recorded in the Audit
233
- def _current_source
234
- _sources.last
235
- end
236
-
237
- # Searches back and recursively (if the source is an audit) collects all sources
238
- # for the current value.
239
- def _source_trail
240
- _collect_records {|source, value| source}
224
+ # _c = Audit.new(nil, nil, [_a,_b])
225
+ # _c.sources # => [_a,_b]
226
+ #
227
+ def initialize(key=nil, value=nil, sources=nil)
228
+ @key = key
229
+ @value = value
230
+ @source = singularize(sources)
241
231
  end
242
232
 
243
- # Searches back and recursively (if the source is an audit) collects all values
244
- # leading to the current value.
245
- def _value_trail
246
- _collect_records {|source, value| value}
233
+ # An array of source audits for self. Sources may be empty.
234
+ def sources
235
+ arrayify(@source)
247
236
  end
248
237
 
249
- def _collect_records(&block) # :yields: source, value
238
+ # Produces a fork of self for each item in value, using the index of
239
+ # the item as a key. Splat is useful for developing each item of an
240
+ # array value along different paths.
241
+ #
242
+ # _a = Audit.new(nil, [:x, :y, :z])
243
+ # _b,_c,_d = _a.splat
244
+ #
245
+ # _b.key # => 0
246
+ # _b.value # => :x
247
+ #
248
+ # _c.key # => 1
249
+ # _c.value # => :y
250
+ #
251
+ # _d.key # => 2
252
+ # _d.value # => :z
253
+ # _d.trail # => [_a,_d]
254
+ #
255
+ # If value does not respond to 'each', an array with self as the only
256
+ # member will be returned. This ensures that the result of splat
257
+ # is an array of audits ready for further development.
258
+ #
259
+ # _a = Audit.new(nil, :value)
260
+ # _a.splat # => [_a]
261
+ #
262
+ def splat
263
+ return [self] unless value.respond_to?(:each)
264
+
250
265
  collection = []
251
- 0.upto(_sources.length-1) do |i|
252
- collection << collect_records(_sources[i], _values[i], &block)
266
+ index = 0
267
+ value.each do |obj|
268
+ collection << Audit.new(index, obj, self)
269
+ index += 1
253
270
  end
254
271
  collection
255
272
  end
256
273
 
257
- def _each_record(merge_level=0, merge_index=0, &block) # :yields: source, value, merge_level, merge_index, index
258
- 0.upto(_sources.length-1) do |i|
259
- each_record(_sources[i], _values[i], merge_level, merge_index, i, &block)
260
- end
261
- end
262
-
263
- # Creates a new Audit by merging self and the input audits, using Audit#merge.
264
- def _merge(*audits)
265
- Audit.merge(self, *audits)
266
- end
267
-
268
- # Produces a new Audit with duplicate sources and values, suitable for
269
- # independent development.
270
- def _fork
271
- a = Audit.new
272
- a._sources = _sources.dup
273
- a._values = _values.dup
274
- a
275
- end
276
-
277
- # Produces a fork of self for each item in the current value (_current).
278
- # Iterate is useful for developing each item of (say) an array along
279
- # different paths.
280
- #
281
- # Records the next value of each fork as [item, AuditIterate.new(<index of item>)].
282
- # Raises an error if _current does not respond to each.
283
- def _iterate
284
- expanded = []
285
- _current.each do |value|
286
- expanded << _fork._record(AuditIterate.new(expanded.length), value)
287
- end
288
- expanded
289
- end
290
-
291
- # Returns true if the _sources and _values for self are equal
292
- # to those of another.
293
- def ==(another)
294
- another.kind_of?(Audit) && self._sources == another._sources && self._values == another._values
295
- end
296
-
297
- # A kind of pretty-print for Audits. See the example in the overview.
298
- def _to_s
299
- # TODO -- find a way to avoid repeating groups
300
-
301
- group = []
302
- groups = [group]
303
- extended_groups = [groups]
304
- group_merges = []
305
- extended_group_merges = []
306
- current_level = nil
307
- current_index = nil
274
+ # Recursively collects an audit trail leading to self. Single sources
275
+ # are collected into the trail directly, while multiple sources are
276
+ # collected into arrays.
277
+ #
278
+ # _a = Audit.new(:one, 1)
279
+ # _b = Audit.new(:two, 2, _a)
280
+ # _b.trail # => [_a,_b]
281
+ #
282
+ # _a = Audit.new(:one, 1)
283
+ # _b = Audit.new(:two, 2)
284
+ # _c = Audit.new(:three, 3, [_a, _b])
285
+ # _c.trail # => [[[_a],[_b]],_c]
286
+ #
287
+ # A block may be provided to collect a specific audit attribute
288
+ # instead of the audit itself.
289
+ #
290
+ # _c.trail {|audit| audit.value } # => [[[1],[2]],3]
291
+ #
292
+ def trail(trail=[], &block)
293
+ trail.unshift(block_given? ? block.call(self) : self)
308
294
 
309
- _each_record do |source, value, merge_level, merge_index, index|
310
- source_str, value_str = if block_given?
311
- yield(source, value)
312
- else
313
- [source, value == nil ? '' : PP.singleline_pp(value, '')]
314
- end
315
-
316
- if !group.empty? && (merge_level != current_level || index == 0)
317
- unless merge_level <= current_level
318
- groups = []
319
- extended_groups << groups
320
- end
321
-
322
- group = []
323
- groups << group
324
-
325
- if merge_level < current_level
326
- if merge_index == 0
327
- extended_group_merges << group.object_id
328
- end
329
-
330
- unless index == 0
331
- group_merges << group.object_id
332
- end
333
- end
334
- end
335
-
336
- group << "o-[#{source_str}] #{value_str}"
337
- current_level = merge_level
338
- current_index = merge_index
339
- end
340
-
341
- lines = []
342
- group_prefix = ""
343
- extended_groups.each do |ext_groups|
344
- indentation = 0
345
-
346
- ext_groups.each_with_index do |ext_group, group_num|
347
- ext_group.each_with_index do |line, line_num|
348
- if line_num == 0
349
- unless lines.empty?
350
- lines << group_prefix + " " * indentation + "| " * (group_num-indentation)
351
- end
352
-
353
- if group_merges.include?(ext_group.object_id)
354
- lines << group_prefix + " " * indentation + "`-" * (group_num-indentation) + line
355
- indentation = group_num
356
-
357
- if extended_group_merges.include?(ext_group.object_id)
358
- lines.last.gsub!(/\| \s*/) {|match| "`-" + "-" * (match.length - 2)}
359
- group_prefix.gsub!(/\| /, " ")
360
- end
361
- next
362
- end
363
- end
364
-
365
- lines << group_prefix + " " * indentation + "| " * (group_num-indentation) + line
366
- end
367
- end
368
-
369
- group_prefix += " " * (ext_groups.length-1) + "| "
295
+ case @source
296
+ when Audit
297
+ @source.trail(trail, &block)
298
+ when Array
299
+ trail.unshift @source.collect {|audit| audit.trail(&block) }
370
300
  end
371
301
 
372
- lines.join("\n") + "\n"
302
+ trail
303
+ end
304
+
305
+ # A kind of pretty-print for Audits.
306
+ def dump(&block)
307
+ Audit.dump(self, "", &block)
373
308
  end
374
309
 
375
- protected
376
-
377
- attr_writer :_sources, :_values # :nodoc:
378
-
379
310
  private
380
311
 
381
- # helper method to recursively collect the value trail for a given source
382
- def collect_records(source, value, &block)
383
- case source
384
- when AuditMerge
385
- collection = []
386
- 0.upto(source.length-1) do |i|
387
- collection << collect_records(source[i], value[i], &block)
388
- end
389
- collection
390
- when Audit
391
- source._collect_records(&block)
392
- else
393
- yield(source, value)
312
+ # helper to optimize storage of nodes
313
+ def singularize(obj) # :nodoc:
314
+ return obj unless obj.kind_of?(Array)
315
+
316
+ case obj.length
317
+ when 0 then nil
318
+ when 1 then obj[0]
319
+ else obj
394
320
  end
395
321
  end
396
322
 
397
- def each_record(source, value, merge_level, merge_index, index, &block)
398
- case source
399
- when AuditMerge
400
- merge_level += 1
401
- 0.upto(source.length-1) do |i|
402
- each_record(source[i], value[i], merge_level, i, index, &block)
403
- end
404
- when Audit
405
- source._each_record(merge_level, merge_index, &block)
406
- else
407
- yield(source, value, merge_level, merge_index, index)
323
+ # helper to optimize storage of nodes
324
+ def arrayify(obj) # :nodoc:
325
+ case obj
326
+ when nil then []
327
+ when Array then obj
328
+ else [obj]
408
329
  end
409
330
  end
410
331
  end