tap 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. data/Basic Overview +151 -0
  2. data/Command Reference +99 -0
  3. data/History +24 -0
  4. data/MIT-LICENSE +1 -1
  5. data/README +29 -57
  6. data/Rakefile +30 -37
  7. data/Tutorial +243 -191
  8. data/bin/tap +66 -35
  9. data/lib/tap.rb +47 -29
  10. data/lib/tap/app.rb +700 -342
  11. data/lib/tap/{script → cmd}/console.rb +0 -0
  12. data/lib/tap/{script → cmd}/destroy.rb +0 -0
  13. data/lib/tap/{script → cmd}/generate.rb +0 -0
  14. data/lib/tap/cmd/run.rb +156 -0
  15. data/lib/tap/constants.rb +4 -0
  16. data/lib/tap/dump.rb +57 -0
  17. data/lib/tap/env.rb +316 -0
  18. data/lib/tap/file_task.rb +106 -109
  19. data/lib/tap/generator.rb +4 -1
  20. data/lib/tap/generator/generators/command/USAGE +6 -0
  21. data/lib/tap/generator/generators/command/command_generator.rb +17 -0
  22. data/lib/tap/generator/generators/{script/templates/script.erb → command/templates/command.erb} +10 -10
  23. data/lib/tap/generator/generators/config/USAGE +21 -0
  24. data/lib/tap/generator/generators/config/config_generator.rb +17 -7
  25. data/lib/tap/generator/generators/file_task/USAGE +3 -0
  26. data/lib/tap/generator/generators/file_task/file_task_generator.rb +16 -0
  27. data/lib/tap/generator/generators/file_task/templates/file.txt +2 -0
  28. data/lib/tap/generator/generators/file_task/templates/file.yml +3 -0
  29. data/lib/tap/generator/generators/file_task/templates/task.erb +26 -20
  30. data/lib/tap/generator/generators/file_task/templates/test.erb +20 -10
  31. data/lib/tap/generator/generators/generator/generator_generator.rb +1 -1
  32. data/lib/tap/generator/generators/generator/templates/generator.erb +21 -12
  33. data/lib/tap/generator/generators/root/templates/Rakefile +33 -24
  34. data/lib/tap/generator/generators/root/templates/tap.yml +28 -31
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +1 -0
  36. data/lib/tap/generator/generators/task/USAGE +3 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +18 -5
  38. data/lib/tap/generator/generators/task/templates/task.erb +7 -12
  39. data/lib/tap/generator/generators/task/templates/test.erb +10 -11
  40. data/lib/tap/generator/generators/workflow/templates/task.erb +1 -1
  41. data/lib/tap/generator/generators/workflow/templates/test.erb +1 -1
  42. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  43. data/lib/tap/patches/rake/testtask.rb +55 -0
  44. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  45. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  46. data/lib/tap/root.rb +172 -67
  47. data/lib/tap/script.rb +70 -336
  48. data/lib/tap/support/aggregator.rb +55 -0
  49. data/lib/tap/support/audit.rb +281 -280
  50. data/lib/tap/support/batchable.rb +59 -0
  51. data/lib/tap/support/class_configuration.rb +279 -0
  52. data/lib/tap/support/configurable.rb +92 -0
  53. data/lib/tap/support/configurable_methods.rb +296 -0
  54. data/lib/tap/support/executable.rb +98 -0
  55. data/lib/tap/support/executable_queue.rb +82 -0
  56. data/lib/tap/support/logger.rb +9 -15
  57. data/lib/tap/support/rake.rb +43 -54
  58. data/lib/tap/support/run_error.rb +32 -13
  59. data/lib/tap/support/shell_utils.rb +47 -0
  60. data/lib/tap/support/tdoc.rb +9 -8
  61. data/lib/tap/support/tdoc/config_attr.rb +40 -16
  62. data/lib/tap/support/validation.rb +77 -0
  63. data/lib/tap/support/versions.rb +36 -36
  64. data/lib/tap/task.rb +276 -482
  65. data/lib/tap/test.rb +20 -261
  66. data/lib/tap/test/env_vars.rb +7 -5
  67. data/lib/tap/test/file_methods.rb +126 -121
  68. data/lib/tap/test/subset_methods.rb +86 -45
  69. data/lib/tap/test/tap_methods.rb +271 -0
  70. data/lib/tap/workflow.rb +174 -46
  71. data/test/app/config/another/task.yml +1 -0
  72. data/test/app/config/erb.yml +2 -1
  73. data/test/app/config/some/task.yml +1 -0
  74. data/test/app/config/template.yml +2 -6
  75. data/test/app_test.rb +1241 -1008
  76. data/test/env/test_configure/recurse_a.yml +2 -0
  77. data/test/env/test_configure/recurse_b.yml +2 -0
  78. data/test/env/test_configure/tap.yml +23 -0
  79. data/test/env/test_load_env_config/dir/tap.yml +3 -0
  80. data/test/env/test_load_env_config/recurse_a.yml +2 -0
  81. data/test/env/test_load_env_config/recurse_b.yml +2 -0
  82. data/test/env/test_load_env_config/tap.yml +3 -0
  83. data/test/env_test.rb +198 -0
  84. data/test/file_task_test.rb +70 -53
  85. data/{lib/tap/generator/generators/package/USAGE → test/root/file.txt} +0 -0
  86. data/test/root_test.rb +621 -454
  87. data/test/script_test.rb +38 -174
  88. data/test/support/aggregator_test.rb +99 -0
  89. data/test/support/audit_test.rb +409 -416
  90. data/test/support/batchable_test.rb +74 -0
  91. data/test/support/{task_configuration_test.rb → class_configuration_test.rb} +106 -47
  92. data/test/{task/config/overriding.yml → support/configurable/config/configured.yml} +0 -0
  93. data/test/support/configurable_test.rb +295 -0
  94. data/test/support/executable_queue_test.rb +103 -0
  95. data/test/support/executable_test.rb +38 -0
  96. data/test/support/logger_test.rb +17 -17
  97. data/test/support/rake_test.rb +4 -2
  98. data/test/support/shell_utils_test.rb +24 -0
  99. data/test/support/tdoc_test.rb +265 -258
  100. data/test/support/validation_test.rb +54 -0
  101. data/test/support/versions_test.rb +38 -38
  102. data/test/tap_test_helper.rb +19 -5
  103. data/test/tap_test_suite.rb +5 -2
  104. data/test/task_base_test.rb +13 -104
  105. data/test/task_syntax_test.rb +300 -0
  106. data/test/task_test.rb +258 -381
  107. data/test/test/env_vars_test.rb +40 -40
  108. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/expected/one.txt +0 -0
  109. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/expected/two.txt +0 -0
  110. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/input/one.txt +0 -0
  111. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/input/two.txt +0 -0
  112. data/test/test/{test_file_task_test → file_methods/test_assert_files_can_have_no_expected_files_if_specified}/input/one.txt +0 -0
  113. data/test/test/{test_file_task_test → file_methods/test_assert_files_can_have_no_expected_files_if_specified}/input/two.txt +0 -0
  114. data/test/test/file_methods/test_assert_files_fails_for_different_content/expected/one.txt +1 -0
  115. data/test/test/{test_file_task_test → file_methods/test_assert_files_fails_for_different_content}/expected/two.txt +0 -0
  116. data/test/test/file_methods/test_assert_files_fails_for_different_content/input/one.txt +1 -0
  117. data/test/test/file_methods/test_assert_files_fails_for_different_content/input/two.txt +1 -0
  118. data/test/test/{test_file_task_test → file_methods/test_assert_files_fails_for_missing_expected_file}/expected/one.txt +0 -0
  119. data/test/test/file_methods/test_assert_files_fails_for_missing_expected_file/input/one.txt +1 -0
  120. data/test/test/file_methods/test_assert_files_fails_for_missing_expected_file/input/two.txt +1 -0
  121. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/expected/one.txt +1 -0
  122. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/expected/two.txt +1 -0
  123. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/input/one.txt +1 -0
  124. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/input/two.txt +1 -0
  125. data/test/test/file_methods/test_assert_files_fails_for_no_expected_files/input/one.txt +1 -0
  126. data/test/test/file_methods/test_assert_files_fails_for_no_expected_files/input/two.txt +1 -0
  127. data/test/test/file_methods_doc/test_sub/expected/one.txt +1 -0
  128. data/test/test/file_methods_doc/test_sub/expected/two.txt +1 -0
  129. data/test/test/file_methods_doc/test_sub/input/one.txt +1 -0
  130. data/test/test/file_methods_doc/test_sub/input/two.txt +1 -0
  131. data/test/test/file_methods_doc_test.rb +29 -0
  132. data/test/test/file_methods_test.rb +214 -143
  133. data/test/test/subset_methods_test.rb +111 -115
  134. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/expected/task/name/a.txt +0 -0
  135. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/expected/task/name/b.txt +0 -0
  136. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/input/a.txt +0 -0
  137. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/input/b.txt +0 -0
  138. data/test/test/tap_methods_test.rb +399 -0
  139. data/test/workflow_test.rb +101 -91
  140. metadata +86 -70
  141. data/lib/tap/generator/generators/package/package_generator.rb +0 -38
  142. data/lib/tap/generator/generators/package/templates/package.erb +0 -186
  143. data/lib/tap/generator/generators/script/USAGE +0 -0
  144. data/lib/tap/generator/generators/script/script_generator.rb +0 -17
  145. data/lib/tap/script/run.rb +0 -154
  146. data/lib/tap/support/batch_queue.rb +0 -162
  147. data/lib/tap/support/combinator.rb +0 -114
  148. data/lib/tap/support/task_configuration.rb +0 -169
  149. data/lib/tap/support/template.rb +0 -81
  150. data/lib/tap/support/templater.rb +0 -155
  151. data/lib/tap/version.rb +0 -4
  152. data/test/app/config/addition_template.yml +0 -6
  153. data/test/app_class_test.rb +0 -33
  154. data/test/check/binding_eval.rb +0 -23
  155. data/test/check/define_method_check.rb +0 -22
  156. data/test/check/dependencies_check.rb +0 -175
  157. data/test/check/inheritance_check.rb +0 -22
  158. data/test/support/batch_queue_test.rb +0 -320
  159. data/test/support/combinator_test.rb +0 -249
  160. data/test/support/template_test.rb +0 -122
  161. data/test/support/templater/erb.txt +0 -2
  162. data/test/support/templater/erb.yml +0 -2
  163. data/test/support/templater/somefile.txt +0 -2
  164. data/test/support/templater_test.rb +0 -192
  165. data/test/task/config/template.yml +0 -4
  166. data/test/task_class_test.rb +0 -170
  167. data/test/task_execute_test.rb +0 -262
  168. data/test/test/file_methods/test_assert_expected/expected/file.txt +0 -1
  169. data/test/test/file_methods/test_assert_expected/expected/folder/file.txt +0 -1
  170. data/test/test/file_methods/test_assert_expected/input/file.txt +0 -1
  171. data/test/test/file_methods/test_assert_expected/input/folder/file.txt +0 -1
  172. data/test/test/file_methods/test_assert_files_exist/input/input_1.txt +0 -0
  173. data/test/test/file_methods/test_assert_files_exist/input/input_2.txt +0 -0
  174. data/test/test/file_methods/test_file_compare/expected/output_1.txt +0 -3
  175. data/test/test/file_methods/test_file_compare/expected/output_2.txt +0 -1
  176. data/test/test/file_methods/test_file_compare/input/input_1.txt +0 -3
  177. data/test/test/file_methods/test_file_compare/input/input_2.txt +0 -3
  178. data/test/test/file_methods/test_infer_glob/expected/file.yml +0 -0
  179. data/test/test/file_methods/test_infer_glob/expected/file_1.txt +0 -0
  180. data/test/test/file_methods/test_infer_glob/expected/file_2.txt +0 -0
  181. data/test/test/file_methods/test_yml_compare/expected/output_1.yml +0 -6
  182. data/test/test/file_methods/test_yml_compare/expected/output_2.yml +0 -6
  183. data/test/test/file_methods/test_yml_compare/input/input_1.yml +0 -4
  184. data/test/test/file_methods/test_yml_compare/input/input_2.yml +0 -4
  185. data/test/test_test.rb +0 -373
@@ -12,7 +12,7 @@
12
12
  # unaffected as the constants are reset after RDoc loads.
13
13
  #
14
14
  if Object.const_defined?(:RubyToken) || Object.const_defined?(:RubyLex)
15
- class Object # :nodoc:
15
+ class Object
16
16
  old_ruby_token = const_defined?(:RubyToken) ? remove_const(:RubyToken) : nil
17
17
  old_ruby_lex = const_defined?(:RubyLex) ? remove_const(:RubyLex) : nil
18
18
 
@@ -34,13 +34,13 @@ else
34
34
  require 'rdoc/rdoc'
35
35
 
36
36
  if Object.const_defined?(:RubyToken) && !RDoc.const_defined?(:RubyToken)
37
- class Object # :nodoc:
37
+ class Object
38
38
  RDoc.const_set(:RubyToken, remove_const(:RubyToken))
39
39
  end
40
40
  end
41
41
 
42
42
  if Object.const_defined?(:RubyLex) && !RDoc.const_defined?(:RubyLex)
43
- class Object # :nodoc:
43
+ class Object
44
44
  RDoc.const_set(:RubyLex, remove_const(:RubyLex))
45
45
  RDoc::RubyLex.const_set(:RubyLex, RDoc::RubyLex)
46
46
  end
@@ -113,8 +113,8 @@ module Tap
113
113
  lines = []
114
114
  else
115
115
  if normalize_comments
116
- line =~ /^\s*#(.*)/
117
- line = $1.to_s.strip
116
+ line =~ /^\s*#\s?(.*)/
117
+ line = $1.to_s
118
118
  end
119
119
 
120
120
  lines << line
@@ -129,7 +129,8 @@ module Tap
129
129
  def find_class_or_module_named(name)
130
130
  return self if full_name == name
131
131
  (@classes.values + @modules.values).each do |c|
132
- return c if c.find_class_or_module_named(name)
132
+ res = c.find_class_or_module_named(name)
133
+ return res if res
133
134
  end
134
135
  nil
135
136
  end
@@ -206,7 +207,25 @@ module Tap
206
207
  # (see 'rdoc/parsers/parse_rb' line 2509)
207
208
  def parse_config(context, single, tk, comment)
208
209
  tks = get_tk_to_nl
210
+
211
+ key_tk = nil
212
+ value_tk = nil
213
+
214
+ tks.each do |token|
215
+ next if token.kind_of?(TkSPACE)
209
216
 
217
+ if key_tk == nil
218
+ key_tk = token if token.kind_of?(TkSYMBOL)
219
+ else
220
+ case token
221
+ when TkCOMMA then value_tk = token
222
+ else
223
+ value_tk = token if value_tk.kind_of?(TkCOMMA)
224
+ break
225
+ end
226
+ end
227
+ end
228
+
210
229
  text = ""
211
230
  if tks.last.kind_of?(TkCOMMENT)
212
231
  text = tks.last.text.chomp("\n").chomp("\r")
@@ -216,18 +235,23 @@ module Tap
216
235
 
217
236
  tmp = RDoc::CodeObject.new
218
237
  read_documentation_modifiers(tmp, RDoc::ATTR_MODIFIERS)
219
- return unless tmp.document_self
238
+ text = nil unless tmp.document_self
220
239
  end
221
-
222
- key_tk = tks.select {|tk| tk.kind_of?(TkSYMBOL)}.first
223
- return if key_tk.nil?
240
+
241
+ tks.reverse_each {|token| unget_tk(token) }
242
+ return if key_tk == nil || text == nil
224
243
 
225
244
  arg = key_tk.text[1..-1]
245
+ if value_tk
246
+ if text =~ /(.*):no_default:(.*)/
247
+ text = $1 + $2
248
+ else
249
+ text += " (#{value_tk.text})"
250
+ end
251
+ end
226
252
  att = TDoc::ConfigAttr.new(text, arg, config_rw, comment)
227
253
  att.config_declaration = get_tkread
228
-
229
- # TODO -- it would be nice to read the default value here...
230
-
254
+
231
255
  context.add_attribute(att)
232
256
  end
233
257
 
@@ -241,14 +265,14 @@ module Tap
241
265
  self.config_mode = tk.name
242
266
 
243
267
  tks = get_tk_to_nl
244
- is_config_mode_flag = tks.select do |tk|
245
- !tk.kind_of?(TkSPACE) && !tk.kind_of?(TkCOMMENT)
268
+ is_config_mode_flag = tks.select do |token|
269
+ !token.kind_of?(TkSPACE) && !token.kind_of?(TkCOMMENT)
246
270
  end.empty?
247
271
 
248
272
  # If no args are given, take this as a flag for c
249
273
  return if is_config_mode_flag
250
274
 
251
- tks.reverse_each {|tk| unget_tk(tk) }
275
+ tks.reverse_each {|token| unget_tk(token) }
252
276
  args = parse_symbol_arg
253
277
  read = get_tkread
254
278
  rw = "?"
@@ -0,0 +1,77 @@
1
+ autoload(:PP, 'pp')
2
+
3
+ module Tap
4
+ module Support
5
+
6
+ # Validation generates blocks for common validations/processing of
7
+ # configurations set through Configurable. These blocks can be passed
8
+ # to the config declarations using an ampersand (&).
9
+ #
10
+ # See the 'Configuration' section in the Tap::Task documentation for
11
+ # more details on how Validation works in practice.
12
+ module Validation
13
+
14
+ # Raised when Validation blocks fail.
15
+ class ValidationError < ArgumentError
16
+ def initialize(input, validations)
17
+ validation_str = PP.singleline_pp(validations, "")
18
+ super PP.singleline_pp(input, "expected #{validation_str} but was: ")
19
+ end
20
+ end
21
+
22
+ module_function
23
+
24
+ # Yaml conversion and checker. Valid if any of the validations
25
+ # match in a case statement. Otherwise raises an error.
26
+
27
+ # Returns input if any of the validations match the input, as
28
+ # in a case statement. Raises a ValidationError otherwise.
29
+ #
30
+ # For example:
31
+ #
32
+ # validate(10, [Integer, nil])
33
+ #
34
+ # Does the same as:
35
+ #
36
+ # case 10
37
+ # when Integer, nil then input
38
+ # else raise ValidationError.new(...)
39
+ # end
40
+ #
41
+ def validate(input, validations)
42
+ case input
43
+ when *validations then input
44
+ else
45
+ raise ValidationError.new(input, validations)
46
+ end
47
+ end
48
+
49
+ # Returns a block that calls validate using the block input
50
+ # and the input validations.
51
+ def check(*validations)
52
+ lambda {|input| validate(input, validations) }
53
+ end
54
+
55
+ # Returns a block that loads input strings as YAML, then
56
+ # calls validate with the result and the input validations.
57
+ # If the block input is not a string, the block input is
58
+ # validated.
59
+ #
60
+ # b = yaml(Integer, nil)
61
+ # b.class # => Proc
62
+ # b.call(1) # => 1
63
+ # b.call("1") # => 1
64
+ # b.call(nil) # => nil
65
+ # b.call("str") # => ValidationError
66
+ #
67
+ # Note: yaml is especially useful for validating configs
68
+ # that may be specified as strings or as an actual object.
69
+ def yaml(*validations)
70
+ lambda do |input|
71
+ res = input.kind_of?(String) ? YAML.load(input) : input
72
+ validate(res, validations)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -11,7 +11,7 @@ module Tap
11
11
  # 'path-version.extension'. If no version is specified, then the filepath
12
12
  # is returned.
13
13
  #
14
- # version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
14
+ # version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
15
15
  #
16
16
  def version(path, version)
17
17
  version = version.to_s.strip
@@ -26,7 +26,7 @@ module Tap
26
26
  # Increments the version of the filepath by the specified increment.
27
27
  #
28
28
  # increment("path/to/file-1.0.txt", "0.0.1") # => "path/to/file-1.0.1.txt"
29
- # increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
29
+ # increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
30
30
  #
31
31
  def increment(path, increment)
32
32
  path, version = deversion(path)
@@ -52,43 +52,43 @@ module Tap
52
52
  # Splits the version from the input path, then returns the path and version.
53
53
  # If no version is specified, then the returned version will be nil.
54
54
  #
55
- # deversion("path/to/file-1.0.txt") # => ["path/to/file.txt", "1.0"]
56
- # deversion("path/to/file.txt") # => ["path/to/file.txt", nil]
55
+ # deversion("path/to/file-1.0.txt") # => ["path/to/file.txt", "1.0"]
56
+ # deversion("path/to/file.txt") # => ["path/to/file.txt", nil]
57
57
  #
58
58
  def deversion(path)
59
59
  path =~ /^(.*)-(\d(\.?\d)*)(.*)?/ ? [$1 + $4, $2] : [path, nil]
60
- end
61
-
62
- # A <=> comparison for versions. compare_versions can take strings,
63
- # integers, or even arrays representing the parts of a version.
64
- #
65
- # compare_versions("1.0.0", "0.9.9") # => 1
66
- # compare_versions(1.1, 1.1) # => 0
67
- # compare_versions([0,9], [0,9,1]) # => -1
68
- def compare_versions(a,b)
69
- a, b = [a,b].collect {|item| to_integer_array(item) }
70
-
71
- # equalize the lengths of the integer arrays
72
- d = b.length - a.length
73
- case
74
- when d < 0 then b.concat Array.new(-d, 0)
75
- when d > 0 then a.concat Array.new(d, 0)
76
- end
77
-
78
- a <=> b
79
- end
80
-
81
- private
82
-
83
- # Converts an input argument (typically a string or an array)
84
- # to an array of integers. Splits version string on "."
85
- def to_integer_array(arg)
86
- arr = case arg
87
- when Array then arg
88
- else arg.to_s.split('.')
89
- end
90
- arr.collect {|i| i.to_i}
91
- end
60
+ end
61
+
62
+ # A <=> comparison for versions. compare_versions can take strings,
63
+ # integers, or even arrays representing the parts of a version.
64
+ #
65
+ # compare_versions("1.0.0", "0.9.9") # => 1
66
+ # compare_versions(1.1, 1.1) # => 0
67
+ # compare_versions([0,9], [0,9,1]) # => -1
68
+ def compare_versions(a,b)
69
+ a, b = [a,b].collect {|item| to_integer_array(item) }
70
+
71
+ # equalize the lengths of the integer arrays
72
+ d = b.length - a.length
73
+ case
74
+ when d < 0 then b.concat Array.new(-d, 0)
75
+ when d > 0 then a.concat Array.new(d, 0)
76
+ end
77
+
78
+ a <=> b
79
+ end
80
+
81
+ private
82
+
83
+ # Converts an input argument (typically a string or an array)
84
+ # to an array of integers. Splits version string on "."
85
+ def to_integer_array(arg)
86
+ arr = case arg
87
+ when Array then arg
88
+ else arg.to_s.split('.')
89
+ end
90
+ arr.collect {|i| i.to_i}
91
+ end
92
92
 
93
93
  end
94
94
  end
@@ -1,14 +1,58 @@
1
1
  module Tap
2
-
3
- # == Overview
4
- #
5
- # Tasks are the basic executable unit of Tap. When an App executes
6
- # a Task, the Task will be passed inputs which it then processes into
7
- # results. Tasks are joined into workflows using condition blocks
8
- # defining when a set of inputs are ready for execution, and where to
9
- # pass results when the Task completes. Tasks fundamentally consist
10
- # of a condition_block, a process method, and an on_complete_block.
11
- #
2
+ # = Overview
3
+ #
4
+ # Tasks are the basic organizational unit of Tap. Tasks provide
5
+ # a standard backbone for creating the working parts of an application
6
+ # by facilitating configuration, batched execution of methods, and
7
+ # interaction with the command line.
8
+ #
9
+ # The functionality of Task is built from several base modules:
10
+ # - Tap::Support::Batchable
11
+ # - Tap::Support::Configurable
12
+ # - Tap::Support::Executable
13
+ #
14
+ # Tap::Workflow is built on the same foundations; the sectons on
15
+ # configuration and batching apply equally to Workflows as Tasks.
16
+ #
17
+ # === Task Definition
18
+ #
19
+ # Tasks are instantiated with a task block; when the task is run
20
+ # the block gets called with the enqued inputs. As such, the block
21
+ # should specify the same number of inputs as you enque (plus the
22
+ # task itself, which is a standard input).
23
+ #
24
+ # no_inputs = Task.new {|task| }
25
+ # one_input = Task.new {|task, input| }
26
+ # mixed_inputs = Task.new {|task, a, b, *args| }
27
+ #
28
+ # no_inputs.enq
29
+ # one_input.enq(:a)
30
+ # mixed_inputs.enq(:a, :b)
31
+ # mixed_inputs.enq(:a, :b, 1, 2, 3)
32
+ #
33
+ # Subclasses of Task specify executable code by overridding the process
34
+ # method. In this case the number of enqued inputs should correspond to
35
+ # process (passing the task would be redundant).
36
+ #
37
+ # class NoInput < Tap::Task
38
+ # def process() end
39
+ # end
40
+ #
41
+ # class OneInput < Tap::Task
42
+ # def process(input) end
43
+ # end
44
+ #
45
+ # class MixedInputs < Tap::Task
46
+ # def process(a, b, *args) end
47
+ # end
48
+ #
49
+ # NoInput.new.enq
50
+ # OneInput.new.enq(:a)
51
+ # MixedInputs.new.enq(:a, :b)
52
+ # MixedInputs.new.enq(:a, :b, 1, 2, 3)
53
+ #
54
+ # === Configuration
55
+ #
12
56
  # Tasks are configurable. By default each task will be configured
13
57
  # with the default class configurations, which can be set when the
14
58
  # class is defined.
@@ -19,516 +63,262 @@ module Tap
19
63
  # end
20
64
  #
21
65
  # t = ConfiguredTask.new
22
- # t.config # => {:one => 'one', :two => 'two'}
23
- #
24
- # Tasks also have a name (based on the class name by default). The
25
- # task name is used as a relative filepath from application directories
26
- # to assoicated files. During initialization, the task name is used
27
- # to lookup configurations from config_file. Additional and overriding
28
- # configurations may be provided during initialization.
29
- #
30
- # t.name # => "configured_task"
31
- # t.app[:config] # => "/path/to/app/config"
32
- # t.config_file # => "/path/to/app/config/configured_task.yml"
33
- #
34
- # # [/path/to/app/config/example.yml]
35
- # # one: ONE
36
- #
37
- # t = ConfiguredTask.new "example", :three => 'three'
38
- # t.name # => "example"
39
- # t.config_file # => "/path/to/app/config/example.yml"
40
- # t.config # => {:one => 'ONE', :two => 'two', :three => 'three'}
66
+ # t.name # => "configured_task"
67
+ # t.config # => {:one => 'one', :two => 'two'}
41
68
  #
42
- # === Batches
69
+ # Configurations can be validated or processed using an optional
70
+ # block. Tap::Support::Validation pre-packages several common
71
+ # validation/processing blocks, and can be accessed through the
72
+ # class method 'c':
43
73
  #
44
- # Tasks are designed to facilitate batch processing of inputs. Each
45
- # time a task queues inputs to itself, the inputs are collected into a
46
- # batch. Upon execution, all inputs are passed to the task at once.
47
- # If the task is iterative (the default), then each of these inputs is
48
- # processed individually. Otherwise, all inputs are processed as a single
49
- # group:
74
+ # class ValidatingTask < Tap::Task
75
+ # # string config validated to be a string
76
+ # config :string, 'str', &c.check(String)
50
77
  #
51
- # runlist = []
52
- # t1 = Task.new {|task, input| runlist << input}
53
- # t1.enq 1
54
- # t1.enq 2,3
55
- # t1.app.run
56
- # runlist # => [1,2,3]
78
+ # # integer config; string inputs are converted using YAML
79
+ # config :integer, 1, &c.yaml(Integer)
80
+ # end
57
81
  #
58
- # runlist = []
59
- # t1.iterate = false
60
- # t1.enq 1
61
- # t1.enq 2,3
62
- # t1.app.run
63
- # runlist # => [[1,2,3]]
82
+ # t = ValidatingTask.new
83
+ # t.string = 1 # => ValidationError
84
+ # t.integer = 1.1 # => ValidationError
64
85
  #
65
- # Additionally, tasks are designed to facilitate processing using a batch
66
- # of related tasks. Often these batches consist of the same task class,
67
- # but with a variety of configurations, but they can be assembled in other
68
- # ways. When a task is queued, each task in the batch will be queued:
86
+ # t.integer = "1"
87
+ # t.integer == 1 # => true
69
88
  #
70
- # runlist = []
71
- # t1 = Task.new {|task, input| runlist << input}
72
- # t1.batch # => [t1]
73
- # t2 = t1.create_batch_task
74
- # t1.batch # => [t1, t2]
75
- #
76
- # t1.enq 1
77
- # t2.enq 2,3
78
- # t1.app.run
79
- # runlist # => [1,2,3, 1,2,3]
80
- #
81
- # Here runlist reflects that t1 and t2 were run in succession with the same [1,2,3]
82
- # inputs. Configuration batches are automatically generated when task.config_file
83
- # specifies an array of configurations; each configuration is translated into a
84
- # batched task.
89
+ # Tasks have a name that gets used as a relative filepath to find
90
+ # associated files (for instance config_file). By default the task
91
+ # name is based on the task class, such that Tap::Task corresponds
92
+ # to 'tap/task'. Custom names can be provided when a task is
93
+ # initialized, as can additional and/or overriding configurations.
94
+ #
95
+ # # [/path/to/app/config/example.yml]
96
+ # # one: ONE
97
+ #
98
+ # t = ConfiguredTask.new "example", :three => 'three'
99
+ # t.name # => "example"
100
+ # t.app[:config] # => "/path/to/app/config"
101
+ # t.config_file # => "/path/to/app/config/example.yml"
102
+ # t.config # => {:one => 'ONE', :two => 'two', :three => 'three'}
103
+ #
104
+ # Tasks can be assembled into batches that enque and execute
105
+ # collectively. Batched tasks are automatically generated when a
106
+ # config_file specifies an array of configurations.
85
107
  #
86
108
  # # [/path/to/app/config/batch.yml]
87
109
  # # - one: ONE
88
110
  # # - one: ANOTHER ONE
89
111
  #
90
112
  # t = ConfiguredTask.new "batch"
91
- # t.batch.size # => 2
113
+ # t.batch.size # => 2
92
114
  # t1, t2 = t.batch
93
115
  #
94
- # t1.name # => "batch"
95
- # t1.config_template # => {:one => 'ONE'}
96
- # t1.config # => {:one => 'ONE', :two => 'two'}
116
+ # t1.name # => "batch"
117
+ # t1.config # => {:one => 'ONE', :two => 'two'}
97
118
  #
98
- # t2.name # => "batch"
99
- # t2.config_template # => {:one => 'ANOTHER ONE'}
100
- # t2.config # => {:one => 'ANOTHER ONE', :two => 'two'}
119
+ # t2.name # => "batch"
120
+ # t2.config # => {:one => 'ANOTHER ONE', :two => 'two'}
101
121
  #
102
- # Task processes configuration files to make the definition of configuration
103
- # templates more DRY and powerful (see Tap::Support::Templater for more
104
- # details). Here the default ConfiguredTask configurations are overridden
105
- # by variations created by the variations! tag:
106
- #
107
- # # [/path/to/app/config/template.yml]
108
- # # one: ONE
109
- # # variations!:
110
- # # - two: TWO
111
- # # - three: THREE
122
+ # === Batches
112
123
  #
113
- # t = ConfiguredTask.new "template"
114
- # t.batch.size # => 2
115
- # t1, t2 = t.batch
124
+ # Tasks facilitate batch processing of inputs using batched tasks. Often
125
+ # a batch consists of the the same task class instantiated with a variety
126
+ # of configurations. Once batched, tasks enque together; when any one of
127
+ # the tasks is enqued, the entire batch is enqued.
116
128
  #
117
- # t1.config # => {:one => 'ONE', :two => 'TWO'}
118
- # t2.config # => {:one => 'ONE', :two => 'two', :three => 'THREE'}
129
+ # runlist = []
130
+ # t1 = Task.new {|task, input| runlist << input}
131
+ # t1.batch # => [t1]
119
132
  #
120
- # All together, these features make it easy to process a batch of inputs
121
- # using a batch of configurations -- all encapsulated in a workflow-ready
122
- # architecture.
133
+ # t2 = t1.initialize_batch_obj
134
+ # t1.batch # => [t1, t2]
135
+ # t2.batch # => [t1, t2]
136
+ #
137
+ # t1.enq 1
138
+ # t2.enq 2
139
+ # t1.app.run
123
140
  #
124
- # == Non-Task Tasks!
141
+ # runlist # => [1,1,2,2]
125
142
  #
126
- # The methods definining the essential behavior of a Task are encapsulated in
127
- # the Tap::Task::Base module. Using this module, Non-Task classes can be defined,
128
- # and Non-Task objects extended to behave like Tasks; ie they can be enqued, run,
129
- # and incorporated into workflows.
143
+ # Here runlist reflects that t1 and t2 were run in succession with the 1
144
+ # input, and then the 2 input.
130
145
  #
131
- # See Tap::Task::Base for more details (esp for objects with a process method),
132
- # as well as Tap::Support::Rake, which makes Rake tasks behave like Tap tasks.
146
+ # === Non-Task Tasks
133
147
  #
134
- class Task < Monitor
135
- write_inheritable_attribute(:configurations, Support::TaskConfiguration.new)
136
- class_inheritable_reader(:configurations)
137
-
138
- write_inheritable_attribute(:iterative, true)
139
- class_inheritable_reader(:iterative)
140
-
141
- write_inheritable_attribute(:source_files, [])
142
- class_inheritable_reader(:source_files)
143
-
144
- class << self
145
-
146
- # Currently an experimental way of identifying source files for TDoc
147
- # documentation.
148
- def source_file(arg) # :nodoc:
149
- source_files << arg
150
- end
151
-
152
- # Declares a configuration without any accessors.
153
- #
154
- # With no keys specified, sets config to make no accessors
155
- # for each new configuration.
156
- def declare_config(*keys)
157
- if keys.empty?
158
- self.config_mode = :none
159
- else
160
- keys.each do |key|
161
- configurations.declare(key, self)
162
- end
163
- end
164
- end
165
-
166
- # Creates a configuration writer for the input keys. Works like
167
- # attr_writer, except the value is written to config, rather than
168
- # a local variable. In addition, the config will be validated
169
- # using validate_config upon setting the value.
170
- #
171
- # With no keys specified, sets config to create config_writer
172
- # for each new configuration.
173
- def config_writer(*keys)
174
- if keys.empty?
175
- self.config_mode = :config_writer
176
- else
177
- keys.each do |key|
178
- configurations.declare(key, self)
179
- define_config_writer(key)
180
- end
181
- end
182
- end
183
-
184
- # Creates a configuration reader for the input keys. Works like
185
- # attr_reader, except the value is read from config, rather than
186
- # a local variable.
187
- #
188
- # With no keys specified, sets config to create a config_reader
189
- # for each new configuration.
190
- def config_reader(*keys)
191
- if keys.empty?
192
- self.config_mode = :config_reader
193
- else
194
- keys.each do |key|
195
- configurations.declare(key, self)
196
- define_config_reader(key)
197
- end
198
- end
199
- end
200
-
201
- # Creates configuration accessors for the input keys. Works like
202
- # attr_accessor, except the value is read from and written to config,
203
- # rather than a local variable.
204
- #
205
- # With no keys specified, sets config to create a config_accessor
206
- # for each new configuration.
207
- def config_accessor(*keys)
208
- if keys.empty?
209
- self.config_mode = :config_accessor
210
- else
211
- keys.each do |key|
212
- configurations.declare(key, self)
213
- define_config_reader(key)
214
- define_config_writer(key)
215
- end
216
- end
217
- end
148
+ # The essential behavior of a Task is expressed in the Tap::Task::Base
149
+ # module. Using this module, non-task classes can be made to behave like
150
+ # tasks. An even more fundamental module, Tap::Executable, allows any
151
+ # method to behave in this manner.
152
+ #
153
+ # Configurations are specific to Task but batches are not. Non-task
154
+ # tasks can be batched. Executable methods cannot be batched.
155
+ #
156
+ # See Tap::Task::Base and Tap::Support::Executable for more details as
157
+ # well as Tap::Support::Rake::Task, which makes Rake[http://rake.rubyforge.org/]
158
+ # tasks behave like Tap tasks.
159
+ class Task
160
+
161
+ # Defines the essential behavior of a Task. Using this module,
162
+ # non-task classes can be made to behave like tasks; ie they can
163
+ # be enqued, batched, and incorporated into workflows.
164
+ module Base
165
+ include Support::Executable
218
166
 
219
- # Sets a class configuration. Configurations are inherited, but can
220
- # be overridden or added in subclasses. Accessors are created by
221
- # default, but this behavior can be modified by use of the other
222
- # config methods.
223
- #
224
- # class SampleTask < Tap::Task
225
- # config :key, 'value'
226
- #
227
- # config_reader
228
- # config :reader_only
229
- # end
230
- #
231
- # t = SampleTask.new
232
- # t.respond_to?(:reader_only) # => true
233
- # t.respond_to?(:reader_only=) # => false
234
- #
235
- # t.config # => {:key => 'value', :reader_only => nil}
236
- # t.key # => 'value'
237
- # t.key = 'another'
238
- # t.config # => {:key => 'another', :reader_only => nil}
239
- def config(key, value=nil, attributes={})
240
- declare_config(key)
241
- configurations.set(key, value, attributes)
242
-
243
- case config_mode
244
- when nil, :config_accessor then config_accessor(key)
245
- when :config_writer then config_writer(key)
246
- when :config_reader then config_reader(key)
247
- end
248
- end
167
+ attr_reader :app
249
168
 
250
- # Sets the default iteration behavior for a task class
251
- # to iterate inputs during task execution. As a result,
252
- # tasks will NOT execute without inputs.
253
- def iterate
254
- self.write_inheritable_attribute(:iterative, true)
169
+ # Initializes obj to behave like a Task. The input method will be
170
+ # called when obj is run by Tap::App.
171
+ def self.initialize(obj, method_name, app=App.instance)
172
+ obj.extend Base
173
+ obj.extend Support::Batchable
174
+ obj.instance_variable_set(:@app, app)
175
+ obj.instance_variable_set(:@batch, [])
176
+ obj.instance_variable_set(:@multithread, false)
177
+ obj.instance_variable_set(:@on_complete_block, nil)
178
+ obj.instance_variable_set(:@_method_name, method_name)
179
+ obj.initialize_batch_obj
180
+ obj
255
181
  end
256
182
 
257
- # Sets the default iteration behavior for a task class
258
- # to not iterate inputs during task execution. As a result,
259
- # tasks can execute without inputs.
260
- def do_not_iterate
261
- self.write_inheritable_attribute(:iterative, false)
183
+ # Enqueues self and self.batch to app with the inputs.
184
+ # The number of inputs provided should match the number
185
+ # of inputs specified by the arity of the _method_name method.
186
+ def enq(*inputs)
187
+ batch.each {|t| t.unbatched_enq(*inputs) }
188
+ self
262
189
  end
263
-
264
- protected
265
-
266
- attr_accessor :config_mode
267
190
 
268
- private
269
-
270
- def define_config_reader(key) # :nodoc:
271
- define_method(key) do
272
- config[key]
273
- end
191
+ # Like enq, but only enques self and not self.batch.
192
+ def unbatched_enq(*inputs)
193
+ app.queue.enq(self, inputs)
274
194
  end
275
195
 
276
- def define_config_writer(key) # :nodoc:
277
- define_method("#{key}=") do |value|
278
- config[key] = value
279
- validate_config(key, value)
280
- value
281
- end
282
- end
283
- end
284
-
285
- # Tap::Task::Base encapsulates the methods definining the essential
286
- # behavior of a Task.
287
- module Base
288
- attr_reader :app, :batch, :condition_block, :on_complete_block, :results
289
- attr_accessor :multithread, :iterate
290
-
291
- # Creates a new Task with the specified attributes.
292
- def self.extended(base)
293
- base.extend MonitorMixin
196
+ alias :unbatched_on_complete :on_complete
294
197
 
295
- base.instance_variable_set("@app", App.instance) if base.app.nil?
296
- base.instance_variable_set("@batch", [base]) if base.batch.nil?
297
- base.instance_variable_set("@results", []) if base.results.nil?
298
- base.instance_variable_set("@condition_block", nil) if base.condition_block.nil?
299
- base.instance_variable_set("@on_complete_block", nil) if base.on_complete_block.nil?
300
- base.instance_variable_set("@multithread", false) if base.multithread.nil?
301
- base.instance_variable_set("@iterate", false) if base.iterate.nil?
198
+ # Sets the on_complete_block for self and self.batch.
199
+ # Use unbatched_on_complete to set the on_complete_block
200
+ # for just self.
201
+ def on_complete(override=false, &block)
202
+ batch.each {|t| t.unbatched_on_complete(override, &block)}
203
+ self
302
204
  end
303
205
 
304
- # Returns true if the batch size is greater than one
305
- # (the one being the current task itself).
306
- def batched?
307
- batch.length > 1
308
- end
309
-
310
- # Returns the index of the current task in batch.
311
- def batch_index
312
- batch.index(self)
313
- end
314
-
315
- # Returns true if multithread is true.
316
- #
317
- # (NOTE -- multithreading is currently experimental!)
318
- def multithread?
319
- multithread
320
- end
321
-
322
- # Returns true if iterate is true. If iterate has not been set for the task,
323
- # then iterate? returns the value of the class iterative variable.
324
- def iterate?
325
- iterate
326
- end
327
-
328
- # Sets a condition block for the task. Raises an error if condition_block is
329
- # already set, unless override = true.
330
- #
331
- # Note that the block will recieve the audited_inputs of the task
332
- # (see Audit for more information).
333
- def condition(override=false, &block) # :yields: self, audited_inputs
334
- raise "Condition for task already set: #{self}" unless condition_block.nil? || override
335
- self.condition_block = block
336
- end
337
-
338
- # Sets a block to execute when the task completes execution. Raises an error
339
- # if on_complete_block is already set, unless override = true.
340
- #
341
- # Note that the block will recieve the results of the task, ie an array of
342
- # audited values and not values themselves (see Audit for more information).
343
- def on_complete(override=false, &block) # :yields: results
344
- raise "On complete for task already set: #{self}" unless on_complete_block.nil? || override
345
- self.on_complete_block = block
346
- end
347
-
348
- # Returns true if no condition block is specified, or the condition
349
- # block evaluates to true with the given inputs.
350
- def executable?(inputs)
351
- condition_block ? condition_block.call(self, inputs) : true
352
- end
353
-
354
- # Execute the actions associated with this task. Returns an array of Audits;
355
- # one for each input value. Raises an error if the task is not executable with
356
- # the inputs, and executes the on_complete block when finished.
357
- #
358
- # Execute is synchronized such that a task can only execute on one thread at a
359
- # time.
360
- def execute(*inputs)
361
- synchronize do
362
- check_terminate
363
-
364
- audited_inputs = if iterate?
365
- Support::Audit.register(*inputs)
366
- else
367
- Support::Audit.merge(*inputs)
368
- end
369
-
370
- self.results = nil
371
- raise "Condition not met." unless executable?(audited_inputs)
372
-
373
- before_execute
374
- begin
375
- self.results = if iterate?
376
- audited_inputs.collect do |audited_input|
377
- on_execute(audited_input)
378
- end
379
- else
380
- [on_execute(audited_inputs)]
381
- end
382
- rescue
383
- on_execute_error($!)
384
- end
385
- after_execute
386
-
387
- on_complete_block.call(results) if on_complete_block
388
- results
389
- end
206
+ alias :unbatched_multithread= :multithread=
207
+
208
+ # Sets the multithread for self and self.batch. Use
209
+ # unbatched_multithread= to set multithread for just self.
210
+ def multithread=(value)
211
+ batch.each {|t| t.unbatched_multithread = value }
212
+ self
390
213
  end
391
214
 
392
- # Raises a TerminateError if the application is in state
393
- # TERMINATE, as a mechanism to gracefully terminate threads.
394
- #--
395
- # Only occurs if the task is multithreaded.
396
- #++
397
- #
398
- # check_terminate is called before each execution, but can
399
- # be manually called at any time to provide a breakpoint
400
- # in long executions.
215
+ # Raises a TerminateError if app.state == State::TERMINATE.
216
+ # check_terminate may be called at any time to provide a
217
+ # breakpoint in long-running processes.
401
218
  def check_terminate
402
- #if multithread? && app.state == App::State::TERMINATE
403
219
  if app.state == App::State::TERMINATE
404
220
  raise App::TerminateError.new
405
221
  end
406
- end
407
-
408
- protected
409
-
410
- attr_writer :app, :batch, :condition_block, :on_complete_block, :results
411
-
412
- # Hook to execute code before inputs are processed.
413
- def before_execute() end
414
-
415
- # Hook for overridding the execution code that generates the
416
- # task results. The input will be an audited version of the
417
- # inputs array provided to execute, or, if iterate? is true,
418
- # an audited version of each individual input provided to
419
- # execute. on_execute should return the audited_inputs.
420
- #
421
- # By default, on_execute sends the current value of the audited
422
- # input process and then records the output with self as the source.
423
- #
424
- # It is not advised to override the default on_execute unless
425
- # absolutely necessary. Consider adjusting the behavior of
426
- # process first.
427
- def on_execute(audited_inputs)
428
- output = process(audited_inputs._current)
429
- audited_inputs._record(self, output)
430
- end
431
-
432
- # Hook to execute code after inputs are processed, but before the on_complete block.
433
- def after_execute() end
434
-
435
- # Hook to handle unhandled errors from processing inputs on a task level.
436
- # By default on_execute_error simply re-raises the unhandled error.
437
- def on_execute_error(err)
438
- raise err
439
- end
222
+ end
440
223
  end
441
224
 
442
225
  include Base
443
-
444
- attr_reader :name, :config, :config_template, :task_block
445
-
446
- # Creates a new Task with the specified attributes.
447
- def initialize(name=nil, config=nil, app=App.instance, &task_block)
448
- super() # required for Monitor
449
-
450
- self.name = name.nil? ? self.class.to_s.underscore : name
451
- self.app = app
452
-
453
- self.condition_block = nil
454
- self.task_block = task_block
455
- self.on_complete_block = nil
456
- self.results = []
457
- self.multithread = false
458
- self.iterate = self.class.iterative
459
- self.batch = []
460
-
461
- # collect all configuration templates from the app
462
- templates = []
463
- app.config_templates(self.config_file).each do |template|
464
- templates.concat make_config_templates(template)
465
- end
466
-
467
- # be sure there is at least one template and
468
- # add a task to batch for every template
469
- templates << {} if templates.empty?
470
- templates.each do |template|
471
- self.create_batch_task(template, config)
472
- end
473
- end
226
+ include Support::Configurable
474
227
 
475
- # Creates a batched task based on the config_template and overrides,
476
- # and adds the task to batch. The batched task will be a duplicate
477
- # of the current task but with new configurations.
478
- #
479
- # This method can be overridden to initialize variables that are
480
- # specific to a batch task, for instance variables depending on
481
- # batch_index.
482
- def create_batch_task(config_template={}, overrides={})
483
- task = (self.batch.empty? ? self : self.dup)
484
- task.config_template = config_template.symbolize_keys
485
- task.batch << task
486
- task.config = overrides
487
- task
488
- end
228
+ attr_reader :task_block
489
229
 
490
- # Returns the path to the configuration file used to make the task configuration
491
- # templates. By default this is the YAML file for the task name relative to the
492
- # application config directory:
230
+ # Creates a new Task with the specified attributes.
493
231
  #
494
- # t.app['config'] # => '/path/to/config'
495
- # t.name # => 'some/task'
496
- # t.config_file # => '/path/to/config/some/task.yml'
232
+ # === Subclassing
233
+ # Batched tasks are generated by duplicating an existing instance, hence
234
+ # it is a good idea to set shared instance variables BEFORE calling super
235
+ # in a subclass initialize method. Non-shared instance variables can be
236
+ # set by overriding the initialize_batch_obj method:
497
237
  #
498
- def config_file
499
- # since this is called during initialization before they
500
- # are set, take care that this does not depend on batch,
501
- # config, or config_template
502
- app.filepath('config', self.name + ".yml")
503
- end
504
-
505
- # Configures the task with the given configuration overrides. A Task has three
506
- # configuration sources: the default class configurations, the config_template
507
- # loaded from config_file, and the inputs to this method. Configurations from these
508
- # sources are merged as: default_config.merge(config_template).merge(overrides)
238
+ # class SubclassTask < Tap::Task
239
+ # attr_accessor :shared_variable, :instance_specific_variable
240
+ #
241
+ # def initialize(*args)
242
+ # @shared_variable = Object.new
243
+ # super
244
+ # end
245
+ #
246
+ # def initialize_batch_obj(*args)
247
+ # task = super
248
+ # task.instance_specific_variable = Object.new
249
+ # task
250
+ # end
251
+ # end
509
252
  #
510
- # Configurations are symbolized before they are merged. If overrides is nil, then
511
- # they will be treated as an empty hash. All configurations are validated using
512
- # validate_config.
513
- def config=(overrides)
514
- overrides = overrides.nil? ? {} : overrides.symbolize_keys
515
- @config = self.class.configurations.default.merge(config_template).merge(overrides)
516
- self.config.each_pair {|key, value| validate_config(key, value) }
517
- self.config
253
+ # t1 = SubclassTask.new
254
+ # t2 = t1.initialize_batch_obj
255
+ # t1.shared_variable == t2.shared_variable # => true
256
+ # t1.instance_specific_variable == t2.instance_specific_variable # => false
257
+ #
258
+ def initialize(name=nil, config={}, app=App.instance, &task_block)
259
+ @task_block = (task_block == nil ? default_task_block : task_block)
260
+ @multithread = false
261
+ @on_complete_block = nil
262
+ @_method_name = :execute
263
+ super(name, config, app)
518
264
  end
519
265
 
520
- # The method for processing inputs into outputs. Override this method in
521
- # subclasses to provide class-specific process logic. By default the
522
- # input is passed to the task_block (provided during initialization).
523
- # Simply returns the input if no task_block is set.
524
- def process(input)
525
- check_terminate
526
- task_block.nil? ? input : task_block.call(self, input)
266
+ # Executes self with the given inputs. Execute provides hooks for subclasses
267
+ # to insert standard execution code: before_execute, on_execute_error,
268
+ # and after_execute. Override any/all of these methods as needed.
269
+ #
270
+ # Execute passes the inputs to process and returns the result.
271
+ def execute(*inputs)
272
+ before_execute
273
+ begin
274
+ result = process(*inputs)
275
+ rescue
276
+ on_execute_error($!)
277
+ end
278
+ after_execute
279
+
280
+ result
527
281
  end
528
282
 
529
- # Enqueues self to the app queue with the inputs.
530
- def enq(*inputs)
531
- app.queue.enq(self, *inputs)
283
+ # The method for processing inputs into outputs. Override this method in
284
+ # subclasses to provide class-specific process logic. The number of
285
+ # arguments specified by process corresponds to the number of arguments
286
+ # the task should have when enqued.
287
+ #
288
+ # class TaskWithTwoInputs < Tap::Task
289
+ # def process(a, b)
290
+ # [b,a]
291
+ # end
292
+ # end
293
+ #
294
+ # t = TaskWithTwoInputs.new
295
+ # t.enq(1,2).enq(3,4)
296
+ # t.app.run
297
+ # t.app.results(t) # => [[2,1], [4,3]]
298
+ #
299
+ # By default process passes self and the input(s) to the task_block
300
+ # provided during initialization. In this case the task block dictates
301
+ # the number of arguments enq should receive. Simply returns the inputs
302
+ # if no task_block is set.
303
+ #
304
+ # # two arguments in addition to task are specified
305
+ # # so this Task must be enqued with two inputs...
306
+ # t = Task.new {|task, a, b| [b,a] }
307
+ # t.enq(1,2).enq(3,4)
308
+ # t.app.run
309
+ # t.app.results(t) # => [[2,1], [4,3]]
310
+ #
311
+ def process(*inputs)
312
+ return inputs if task_block == nil
313
+ inputs.unshift(self)
314
+
315
+ arity = task_block.arity
316
+ n = inputs.length
317
+ unless n == arity || (arity < 0 && (-1-n) <= arity)
318
+ raise ArgumentError.new("wrong number of arguments (#{n} for #{arity})")
319
+ end
320
+
321
+ task_block.call(*inputs)
532
322
  end
533
323
 
534
324
  # Logs the inputs to the application logger (via app.log)
@@ -537,24 +327,28 @@ module Tap
537
327
  app.log(action, msg, level)
538
328
  end
539
329
 
330
+ # Returns self.name
331
+ def to_s
332
+ name
333
+ end
334
+
540
335
  protected
541
-
542
- attr_writer :name, :config_template, :task_block
543
336
 
544
- # Hook to validate configurations.
545
- def validate_config(key, value)
337
+ # Hook to set a default task block. By default, nil.
338
+ def default_task_block
339
+ nil
546
340
  end
547
341
 
548
- # Hook to provide class-specific processing of the config_file template
549
- # (ie app.config_template(config_file)). By default the template is
550
- # processed using Support::Templater.make_templates.
551
- def make_config_templates(template)
552
- Support::Templater.make_templates(template)
553
- # this would be a security hole... can't do it.
554
- # if you can set :data then you could read
555
- # any accessible file.
556
- #{ |filename| app.read(:data, filename) }
557
- end
342
+ # Hook to execute code before inputs are processed.
343
+ def before_execute() end
344
+
345
+ # Hook to execute code after inputs are processed.
346
+ def after_execute() end
558
347
 
348
+ # Hook to handle unhandled errors from processing inputs on a task level.
349
+ # By default on_execute_error simply re-raises the unhandled error.
350
+ def on_execute_error(err)
351
+ raise err
352
+ end
559
353
  end
560
354
  end