tap 0.7.9

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.
Files changed (146) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README +71 -0
  3. data/Rakefile +117 -0
  4. data/bin/tap +63 -0
  5. data/lib/tap.rb +15 -0
  6. data/lib/tap/app.rb +739 -0
  7. data/lib/tap/file_task.rb +354 -0
  8. data/lib/tap/generator.rb +29 -0
  9. data/lib/tap/generator/generators/config/USAGE +0 -0
  10. data/lib/tap/generator/generators/config/config_generator.rb +23 -0
  11. data/lib/tap/generator/generators/config/templates/config.erb +2 -0
  12. data/lib/tap/generator/generators/file_task/USAGE +0 -0
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
  16. data/lib/tap/generator/generators/root/USAGE +0 -0
  17. data/lib/tap/generator/generators/root/root_generator.rb +36 -0
  18. data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
  19. data/lib/tap/generator/generators/root/templates/app.yml +19 -0
  20. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
  21. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
  22. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  23. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
  24. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
  25. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
  26. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
  27. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
  28. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
  29. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
  30. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  31. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
  32. data/lib/tap/generator/generators/task/USAGE +0 -0
  33. data/lib/tap/generator/generators/task/task_generator.rb +21 -0
  34. data/lib/tap/generator/generators/task/templates/task.erb +21 -0
  35. data/lib/tap/generator/generators/task/templates/test.erb +29 -0
  36. data/lib/tap/generator/generators/workflow/USAGE +0 -0
  37. data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
  38. data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
  39. data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
  40. data/lib/tap/generator/options.rb +26 -0
  41. data/lib/tap/generator/usage.rb +26 -0
  42. data/lib/tap/root.rb +275 -0
  43. data/lib/tap/script/console.rb +7 -0
  44. data/lib/tap/script/destroy.rb +8 -0
  45. data/lib/tap/script/generate.rb +8 -0
  46. data/lib/tap/script/run.rb +111 -0
  47. data/lib/tap/script/server.rb +12 -0
  48. data/lib/tap/support/audit.rb +415 -0
  49. data/lib/tap/support/batch_queue.rb +165 -0
  50. data/lib/tap/support/combinator.rb +114 -0
  51. data/lib/tap/support/logger.rb +91 -0
  52. data/lib/tap/support/rap.rb +38 -0
  53. data/lib/tap/support/run_error.rb +20 -0
  54. data/lib/tap/support/template.rb +81 -0
  55. data/lib/tap/support/templater.rb +155 -0
  56. data/lib/tap/support/versions.rb +63 -0
  57. data/lib/tap/task.rb +448 -0
  58. data/lib/tap/test.rb +320 -0
  59. data/lib/tap/test/env_vars.rb +16 -0
  60. data/lib/tap/test/inference_methods.rb +298 -0
  61. data/lib/tap/test/subset_methods.rb +260 -0
  62. data/lib/tap/version.rb +3 -0
  63. data/lib/tap/workflow.rb +73 -0
  64. data/test/app/config/addition_template.yml +6 -0
  65. data/test/app/config/batch.yml +2 -0
  66. data/test/app/config/empty.yml +0 -0
  67. data/test/app/config/erb.yml +1 -0
  68. data/test/app/config/template.yml +6 -0
  69. data/test/app/config/version-0.1.yml +1 -0
  70. data/test/app/config/version.yml +1 -0
  71. data/test/app/lib/app_test_task.rb +2 -0
  72. data/test/app_class_test.rb +33 -0
  73. data/test/app_test.rb +1372 -0
  74. data/test/file_task/config/batch.yml +2 -0
  75. data/test/file_task/config/configured.yml +1 -0
  76. data/test/file_task/old_file_one.txt +0 -0
  77. data/test/file_task/old_file_two.txt +0 -0
  78. data/test/file_task_test.rb +1041 -0
  79. data/test/root/alt_lib/alt_module.rb +4 -0
  80. data/test/root/lib/absolute_alt_filepath.rb +2 -0
  81. data/test/root/lib/alternative_filepath.rb +2 -0
  82. data/test/root/lib/another_module.rb +2 -0
  83. data/test/root/lib/nested/some_module.rb +4 -0
  84. data/test/root/lib/no_module_included.rb +0 -0
  85. data/test/root/lib/some/module.rb +4 -0
  86. data/test/root/lib/some_class.rb +2 -0
  87. data/test/root/lib/some_module.rb +3 -0
  88. data/test/root/load_path/load_path_module.rb +2 -0
  89. data/test/root/load_path/skip_module.rb +2 -0
  90. data/test/root/mtime/older.txt +0 -0
  91. data/test/root/unload/full_path.rb +2 -0
  92. data/test/root/unload/loaded_by_nested.rb +2 -0
  93. data/test/root/unload/nested/nested_load.rb +6 -0
  94. data/test/root/unload/nested/nested_with_ext.rb +4 -0
  95. data/test/root/unload/nested/relative_path.rb +4 -0
  96. data/test/root/unload/older.rb +2 -0
  97. data/test/root/unload/unload_base.rb +9 -0
  98. data/test/root/versions/another.yml +0 -0
  99. data/test/root/versions/file-0.1.2.yml +0 -0
  100. data/test/root/versions/file-0.1.yml +0 -0
  101. data/test/root/versions/file.yml +0 -0
  102. data/test/root_test.rb +483 -0
  103. data/test/support/audit_test.rb +449 -0
  104. data/test/support/batch_queue_test.rb +320 -0
  105. data/test/support/combinator_test.rb +249 -0
  106. data/test/support/logger_test.rb +31 -0
  107. data/test/support/template_test.rb +122 -0
  108. data/test/support/templater/erb.txt +2 -0
  109. data/test/support/templater/erb.yml +2 -0
  110. data/test/support/templater/somefile.txt +2 -0
  111. data/test/support/templater_test.rb +192 -0
  112. data/test/support/versions_test.rb +71 -0
  113. data/test/tap_test_helper.rb +4 -0
  114. data/test/tap_test_suite.rb +4 -0
  115. data/test/task/config/batch.yml +2 -0
  116. data/test/task/config/batched.yml +2 -0
  117. data/test/task/config/configured.yml +1 -0
  118. data/test/task/config/example.yml +1 -0
  119. data/test/task/config/overriding.yml +2 -0
  120. data/test/task/config/task_with_config.yml +1 -0
  121. data/test/task/config/template.yml +4 -0
  122. data/test/task_class_test.rb +118 -0
  123. data/test/task_execute_test.rb +233 -0
  124. data/test/task_test.rb +424 -0
  125. data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
  126. data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
  127. data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
  128. data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
  129. data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
  130. data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
  131. data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
  132. data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
  133. data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
  134. data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
  135. data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
  136. data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
  137. data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
  138. data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
  139. data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
  140. data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
  141. data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
  142. data/test/test/inference_methods_test.rb +311 -0
  143. data/test/test/subset_methods_test.rb +115 -0
  144. data/test/test_test.rb +233 -0
  145. data/test/workflow_test.rb +108 -0
  146. metadata +274 -0
@@ -0,0 +1,7 @@
1
+ require "irb"
2
+
3
+ def app
4
+ Tap::App.instance
5
+ end
6
+
7
+ IRB.start
@@ -0,0 +1,8 @@
1
+ require 'tap/generator'
2
+ Rails::Generator::Base.use_tap_sources!
3
+
4
+ require 'rails_generator/scripts/destroy'
5
+ generator = ARGV.shift
6
+ script = Rails::Generator::Scripts::Destroy.new
7
+ script.extend Tap::Generator::Usage
8
+ script.run(ARGV, :generator => generator)
@@ -0,0 +1,8 @@
1
+ require 'tap/generator'
2
+ Rails::Generator::Base.use_tap_sources!
3
+
4
+ require 'rails_generator/scripts/generate'
5
+ generator = ARGV.shift
6
+ script = Rails::Generator::Scripts::Generate.new
7
+ script.extend Tap::Generator::Usage
8
+ script.run(ARGV, :generator => generator)
@@ -0,0 +1,111 @@
1
+ app = Tap::App.instance
2
+ task_config = {}
3
+
4
+ #
5
+ # handle options
6
+ #
7
+ require 'getoptlong'
8
+
9
+ opts = GetoptLong.new(
10
+ ['--app-config', '-a', GetoptLong::REQUIRED_ARGUMENT],# "Specifies an application config file"],
11
+ ['--config', '-c', GetoptLong::REQUIRED_ARGUMENT],# "Specifies configurations for the task."],
12
+ ['--quiet', '-q', GetoptLong::NO_ARGUMENT],# "Suppresses logging"],
13
+ ['--force', '-f', GetoptLong::NO_ARGUMENT],# "Force execution at checkpoints."],
14
+ ['--debug', '-d', GetoptLong::NO_ARGUMENT],# "Trace execution and debug."],
15
+ ['--help', '-h', GetoptLong::OPTIONAL_ARGUMENT])#, "Display help for app, or specified task."])
16
+
17
+ opts.each do |opt, value|
18
+ case opt
19
+ when '--help'
20
+ puts "help!"
21
+ exit
22
+
23
+ if value.empty?
24
+ help
25
+ else
26
+ task = task(value)
27
+ puts task.class.help
28
+ puts "Default Config:"
29
+ puts task.class.default_config.stringify_keys.to_yaml
30
+ end
31
+
32
+ when '--usage'
33
+ puts "usage!"
34
+ exit
35
+
36
+ when '--app-config'
37
+ config = Tap::App.parse_yaml(value)
38
+ if config.kind_of?(String)
39
+ raise "application config file does not exist: #{config}" unless File.exists?(config)
40
+ config = Tap::App.read_erb_yaml(config)
41
+ end
42
+ app.reconfigure(config)
43
+
44
+ when '--config'
45
+ task_config = Tap::App.parse_yaml(value)
46
+ if task_config.kind_of?(String)
47
+ raise "task config file does not exist: #{config}" unless File.exists?(config)
48
+ task_config = Tap::App.read_erb_yaml(config)
49
+ end
50
+
51
+ when '--quiet', '--force', '--debug'
52
+ # simply track these have been set
53
+ opt =~ /^-+(\w+)/
54
+ app.options.send("#{$1}=", true)
55
+
56
+ else
57
+ puts "unknown option: #{opt}"
58
+ exit
59
+ end
60
+ end
61
+
62
+ #
63
+ # gather arguments
64
+ # (be sure to clear for gets during interruption)
65
+
66
+ if ARGV.empty?
67
+ puts "no task specified"
68
+ exit
69
+ end
70
+
71
+ td = ARGV.shift
72
+ args = ARGV.collect {|arg| Tap::App.parse_yaml(arg) }
73
+ ARGV.clear
74
+
75
+ task = app.task(td, task_config)
76
+
77
+ #
78
+ # set signals and run!
79
+ #
80
+
81
+ # info signal
82
+ Signal.trap("INFO") do
83
+ puts app.info
84
+ end
85
+
86
+ # interuption signal
87
+ Signal.trap("INT") do
88
+ puts " interrupted!"
89
+ # prompt for decision
90
+ while true
91
+ print "stop, terminate, or resume? (s/t/r):"
92
+ case gets.strip
93
+ when /s(top)?/i
94
+ app.stop
95
+ break
96
+ when /t(erminate)?/i
97
+ app.terminate
98
+ break
99
+ when /r(esume)?/i
100
+ break
101
+ else
102
+ puts "unexpected response..."
103
+ end
104
+ end
105
+ end
106
+
107
+ puts "ctl-i prints information"
108
+ puts "ctl-c interupts execution"
109
+ puts "beginning run..."
110
+
111
+ app.run(task, *args)
@@ -0,0 +1,12 @@
1
+ # change to the server dir so that script/server launches as normal
2
+ # (otherwise Mongrel can raise errors because it can't find a log file)
3
+ Dir.chdir Tap::App.instance[:server]
4
+
5
+ server_script = "script/server"
6
+ unless File.exists?(server_script)
7
+ puts "server script does not exist: #{Tap::App.instance.filepath(:server, server_script)}"
8
+ puts "no tap server available?"
9
+ exit
10
+ end
11
+
12
+ load server_script
@@ -0,0 +1,415 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # === Overview
5
+ #
6
+ # Audit tracks input and result values passed among tasks within a workflow. At the end
7
+ # of a run, each result will have an audit trail detailing the values it has obtained
8
+ # at various stages, and the source of that value. The ability to do track back all the
9
+ # places where a value was changed or modified is very important during workflow debugging.
10
+ #
11
+ # Audit is designed so you can ask a result 'hey where did you come from?' rather than
12
+ # being able to ask an input 'what are all the results you ultimately produce?'. Say your
13
+ # workflowconsists of 3 sequential tasks [:a, :b, :c]. Tasks :a and :b add one to their input
14
+ # value, while :c adds two. Behind the scences, this is what happens when we run the workflow
15
+ # with an initial input value of 3:
16
+ #
17
+ # # task :a initializes a new audit with the original
18
+ # # value upon execution
19
+ # ... run :a with input 3 ...
20
+ # audit = Audit.new(3)
21
+ #
22
+ # # when task :a finishes, it records the new value and
23
+ # # the source of the value (ie task ':a')
24
+ # ... task :a adds one ...
25
+ # audit._record(:a, 4)
26
+ #
27
+ # # next the audit is passed to task :b, then task :c
28
+ # # each of which records the next source and value
29
+ # ... task :b adds one ...
30
+ # audit._record(:b, 5)
31
+ # ... task :c adds two ...
32
+ # audit._record(:c, 7)
33
+ #
34
+ # # at the end, if you want to know how your final
35
+ # # value got to be 7, you can look at the source_trail
36
+ # # (note the very first source is nil)
37
+ # audit._source_trail # => [nil, :a, :b, :c]
38
+ #
39
+ # Audit supports forks by duplicating an audit trail (ie the recorded sources and values) and
40
+ # merges by storing the various sources and values in an array. For example:
41
+ #
42
+ # # now let :a fork its results to both :b and :c
43
+ # audit = Audit.new(3)
44
+ # audit._record(:a, 4)
45
+ # fork_b = audit._fork
46
+ # fork_c = audit._fork
47
+ #
48
+ # ... tasks :b adds one and :c adds two ...
49
+ # fork_b._record(:b, 5)
50
+ # fork_c._record(:c, 6)
51
+ #
52
+ # # at the end you have a separate source trail for
53
+ # # each result.
54
+ # fork_b._source_trail # => [nil, :a, :b]
55
+ # fork_c._source_trail # => [nil, :a, :c]
56
+ #
57
+ # # now lets say you decided to merge both of
58
+ # # these trails into a new task :d which adds
59
+ # # all values that come to it.
60
+ # ... task :d recieves results from :b and :c and adds them ...
61
+ # merged_audit = Audit.merge(fork_b, fork_c)
62
+ # merged_audit._record(:d, 11)
63
+ #
64
+ # # now you can look back at the full source trail
65
+ # # where an array of sources indicates two trails
66
+ # # that merged
67
+ # merged_audit._source_trail # => [[[nil,:a,:b], [nil,:a,:c]], :d]
68
+ #
69
+ # An important thing to note is that while in these examples symbols have been used
70
+ # to represent the tasks, the actual tasks themselves are recorded as sources in practice.
71
+ # Thus the source trails can be used to access task configurations and other information
72
+ # that may be useful when assessing an audit. Incidentally, this is one of the reasons why Tap
73
+ # is designed to be used with configurations that DO NOT change during execution; if they don't
74
+ # change then you're able to look back at your handiwork.
75
+ #
76
+ # === Working with Audits
77
+ #
78
+ # Once an input enters the execution stream, it will be used to initialize an Audit.
79
+ # From this point on, the Audit and not the value will be passed among tasks and ultimately
80
+ # passed out in the results array.
81
+ #
82
+ # This must be kept in mind when building tasks into a workflow. For convenience, Audits are
83
+ # constructed to pass unknown methods and most comparision methods to the current value, such
84
+ # that they behave like the current value. It's important to realize that <em>workflow blocks
85
+ # (ex: on_complete and condition) recieve Audits and NOT values</em>.
86
+ #
87
+ # t = Tap::Task.new
88
+ # t.on_complete do |results|
89
+ # results.each do |result|
90
+ # # you might expect result to be a value like 10 or "str"
91
+ # # in fact it's an Audit, but it passes unknown methods
92
+ # # along to it's current value
93
+ #
94
+ # result.class # => Audit
95
+ # result._current # => "str"
96
+ # result == "str" # => true
97
+ # result.upcase # => "STR"
98
+ #
99
+ # # the forwarding behavior is for convenience when
100
+ # # making decisions about what to do with a result
101
+ # # but be sure you don't get caught! The only object
102
+ # # methods forwarded are '==' and '=~'. Other methods
103
+ # # are NOT forwarded, and =~ cannot (due to context
104
+ # # issues) capture match strings
105
+ #
106
+ # result.kind_of?(String) # => false
107
+ # result =~ /s(\w+)/ # => true
108
+ # $1 # => nil (watch out! you may expect "tr")
109
+ #
110
+ # end
111
+ # end
112
+ #
113
+ # Audits and NOT values are passed into these workflow blocks because you may need to make
114
+ # a workflow decision based on where a value came from (ie you may need the source trail).
115
+ # The same does not hold true when processing inputs. <em>The process method recieves the
116
+ # values themselves.</em>
117
+ #
118
+ # t = Tap::Task.new do |task, input|
119
+ # # here in the process block, the input is the current value
120
+ # # and NOT the Audit tracking the inputs and results
121
+ # input.class # => Fixnum (given that we execute t with 3)
122
+ # input += 1
123
+ # end
124
+ #
125
+ # results = t.execute(3)
126
+ # results.class # => Array
127
+ # results.length # => 1
128
+ # results.first.class # => Audit
129
+ # results.first._current # => 4
130
+ #
131
+ # === Summing it up:
132
+ #
133
+ # - Task inputs may be values or Audits
134
+ # - Task results are always an array of Audits
135
+ # - Workflow blocks (ex: on_complete and condition) recieve Audits and not values
136
+ # - The process method recieves the values themselves
137
+ #
138
+ class Audit
139
+ class << self
140
+
141
+ # Convenience method to create a new Audit for each of the inputs, if the
142
+ # input is not already an Audit. Returns an array of Audits.
143
+ def register(*inputs)
144
+ inputs.collect {|input| input.kind_of?(Audit) ? input : Audit.new(input) }
145
+ end
146
+
147
+ # Creates a new Audit from the inputs. The value of the new Audit will be the inputs
148
+ # array where any Audits are replaced by their _current value. The source of the
149
+ # new Audit will be a corresponding array of nils, or Audits if provided.
150
+ #
151
+ # a = Audit.new(1)
152
+ # b = Audit.merge(a, 2)
153
+ # b._values # => [[1, 2]]
154
+ # b._sources # => [[a, nil]]
155
+ #
156
+ # If no inputs are provided, then merge a new Audit with an initial value of nil.
157
+ # If only one input is provided, then merge returns a new Audit initialized to
158
+ # the input, or a _fork of the input if it is already an Audit.
159
+ def merge(*inputs)
160
+ case inputs.length
161
+ when 0 then Audit.new
162
+ when 1
163
+ input = inputs.first
164
+ input.kind_of?(Audit) ? input._fork : Audit.new(input)
165
+ else
166
+ values = inputs.collect {|input| input.kind_of?(Audit) ? input._current : input}
167
+ sources = inputs.collect {|input| input.kind_of?(Audit) ? input : nil}
168
+ Audit.new(values, sources)
169
+ end
170
+ end
171
+
172
+ end
173
+
174
+ attr_reader :_sources, :_values
175
+
176
+ # A new audit takes a value and/or source. A nil source is typically given
177
+ # for the original value.
178
+ def initialize(value=nil, source=nil)
179
+ @_sources = []
180
+ @_values = []
181
+
182
+ _record(source, value)
183
+ end
184
+
185
+ # Records the next value produced by the source. When an audit is
186
+ # passed as a value, record will record the current value of the audit.
187
+ # Record will similarly resolve every audit in an array containing audits.
188
+ #
189
+ # Example:
190
+ #
191
+ # a = Audit.new(1)
192
+ # b = Audit.new(2)
193
+ # c = Audit.new(3)
194
+ #
195
+ # c.record(:a, a)
196
+ # c.sources # => [:a]
197
+ # c.values # => [1]
198
+ #
199
+ # c.record(:ab, [a,b])
200
+ # c.sources # => [:a, :ab]
201
+ # c.values # => [1, [1, 2]]
202
+ def _record(source, value)
203
+ _sources << source
204
+ _values << value
205
+ self
206
+ end
207
+
208
+ # The original value used to initialize the Audit
209
+ def _original
210
+ _values.first
211
+ end
212
+
213
+ # The current (ie last) value recorded in the Audit
214
+ def _current
215
+ _values.last
216
+ end
217
+
218
+ # The original source used to initialize the Audit
219
+ def _original_source
220
+ _sources.first
221
+ end
222
+
223
+ # The current (ie last) source recorded in the Audit
224
+ def _current_source
225
+ _sources.last
226
+ end
227
+
228
+ # The index of the last occurence of source in the audit (Note
229
+ # equality is based on the object id of the specified source)
230
+ def _index_last(source)
231
+ _sources.rindex(source)
232
+ end
233
+
234
+ # The value recorded with the last occurence of source in the audit. (Note
235
+ # equality is based on the object id of the specified source)
236
+ def _last(source)
237
+ index = _index_last(source)
238
+ index.nil? ? nil : _values[index]
239
+ end
240
+
241
+ # Returns the value at the specified index.
242
+ def _input(index)
243
+ _values[index]
244
+ end
245
+
246
+ # Returns the input to the last occurence of source in the audit (ie
247
+ # the value prior to this source). Example:
248
+ #
249
+ # a = Audit.new
250
+ # a.record(:a, 'a')
251
+ # a.record(:b, 'b')
252
+ #
253
+ # a.input_last(:a) # => nil
254
+ # a.input_last(:b) # => 'a'
255
+ def _input_last(source)
256
+ index = _index_last(source)
257
+ _input(index-1)
258
+ end
259
+
260
+ # Returns the value after the specfied index
261
+ def _output(index)
262
+ _values[index+1]
263
+ end
264
+
265
+ # Returns the output of the last occurence of source in the audit (ie
266
+ # the value at this source). Example:
267
+ #
268
+ # a = Audit.new
269
+ # a.record(:a, 'a')
270
+ # a.record(:b, 'b')
271
+ #
272
+ # a.output_last(:a) # => 'a'
273
+ # a.output_last(:b) # => 'b'
274
+ def _output_last(source)
275
+ index = _index_last(source)
276
+ _output(index-1)
277
+ end
278
+
279
+ # Searches back and recursively (if the source is an audit) collects all sources
280
+ # for the current value.
281
+ def _source_trail
282
+ _sources.collect do |source|
283
+ source_trail(source)
284
+ end
285
+ end
286
+
287
+ # Searches back and recursively (if the source is an audit) collects all values
288
+ # leading to the current value.
289
+ def _value_trail
290
+ trail = []
291
+ 0.upto(_sources.length-1) do |index|
292
+ trail << value_trail(_sources[index], _values[index])
293
+ end
294
+ trail
295
+ end
296
+
297
+ # Produces a new Audit suitable for development along a separate path, by merging
298
+ # the input sources with self. The value of the new audit will be an array of the
299
+ # current values of the input sources and self.
300
+ #
301
+ # If no sources are provided, then _merge returns _fork.
302
+ def _merge(*sources)
303
+ sources.unshift(self)
304
+ Audit.merge(*sources)
305
+ end
306
+
307
+ # Produces a new Audit with duplicate sources and values, suitable for
308
+ # separate development along a separate path.
309
+ def _fork
310
+ a = Audit.new
311
+ a._sources = _sources.dup
312
+ a._values = _values.dup
313
+ a
314
+ end
315
+
316
+ # Produces a new Audit from self and records the next value as
317
+ # the return value of the block. The block itself will be recored
318
+ # as the value's source.
319
+ def _split(&block)
320
+ sp = Audit.new(nil, self)
321
+ sp._record(block, yield(_current))
322
+ sp
323
+ end
324
+
325
+ alias _eql ==
326
+
327
+ # Compares _current with another using ==
328
+ def ==(another)
329
+ _current == another
330
+ end
331
+
332
+ alias _match =~
333
+
334
+ # The method =~ does NOT work properly with an audit. As with other
335
+ # methods, =~ is aliased to work on _current. However, variables like
336
+ # $1, $2, etc cannot be passed back. As a result:
337
+ #
338
+ # a = Audit.new "abcd"
339
+ # a =~ /ab(\w)/ # => true
340
+ # $1 # => nil (should be 'c')
341
+ #
342
+ # # instead use _current directly...
343
+ # a._current =~ /ab(\w)/ # => true
344
+ # $1 # => 'c'
345
+ #
346
+ # Note the same applies to !~, as it executes through =~
347
+ def =~(regexp)
348
+ # note: this is not ideal... the variables $1, $2,
349
+ # etc are not sent back to the executing context (binding)
350
+ _current =~ regexp
351
+ end
352
+
353
+ # this shouldn't be necessary as Comparable feeds all it's methods
354
+ #include Comparable
355
+
356
+ # Compares _current with another using <=> if <=> is defined
357
+ # for _current. Otherwise returns 0.
358
+ #def <=>(another)
359
+ # _current.respond_to?(:<=>) ? _current <=> another : 0
360
+ #end
361
+
362
+ # CONSIDER FORWARDING ALL OF THESE!
363
+ #
364
+ #[:eql?, :equal?, :is_a?, :kind_of?, :nil?, :respond_to?, :tainted?, :to_str].each do |method|
365
+ # alias_name = "_#{method}".to_sym
366
+ # alias alias_name method
367
+ # define_method(method) do |*args|
368
+ # _current.send(method, *args)
369
+ # end
370
+ #end
371
+
372
+ #alias _cmp ===
373
+ #def ===(another) _current.send('===', another) end
374
+
375
+ protected
376
+
377
+ attr_writer :_sources, :_values
378
+
379
+ # Forwards all missing methods to _current
380
+ def method_missing(sym, *args, &block)
381
+ _current.send(sym, *args, &block)
382
+ end
383
+
384
+ private
385
+
386
+ # helper method to recursively collect the source trail for a given source
387
+ def source_trail(source)
388
+ case source
389
+ when Array
390
+ source.collect {|s| source_trail(s)}
391
+ when Audit
392
+ source._source_trail
393
+ else
394
+ source
395
+ end
396
+ end
397
+
398
+ # helper method to recursively collect the value trail for a given source
399
+ def value_trail(source, value)
400
+ case source
401
+ when Array
402
+ trail = []
403
+ 0.upto(source.length-1) do |index|
404
+ trail << value_trail(source[index], value[index])
405
+ end
406
+ trail
407
+ when Audit
408
+ source._value_trail
409
+ else
410
+ value
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end