tap 0.7.9 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. data/History +28 -0
  2. data/MIT-LICENSE +1 -1
  3. data/README +71 -43
  4. data/Rakefile +81 -64
  5. data/Tutorial +235 -0
  6. data/bin/tap +80 -44
  7. data/lib/tap.rb +41 -12
  8. data/lib/tap/app.rb +243 -246
  9. data/lib/tap/file_task.rb +357 -118
  10. data/lib/tap/generator.rb +88 -29
  11. data/lib/tap/generator/generators/config/config_generator.rb +4 -2
  12. data/lib/tap/generator/generators/config/templates/config.erb +1 -2
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +3 -18
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +22 -15
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +13 -2
  16. data/{test/test/inference_methods/test_assert_files_exist/input/input_1.txt → lib/tap/generator/generators/generator/USAGE} +0 -0
  17. data/lib/tap/generator/generators/generator/generator_generator.rb +21 -0
  18. data/lib/tap/generator/generators/generator/templates/generator.erb +23 -0
  19. data/lib/tap/generator/generators/generator/templates/usage.erb +1 -0
  20. data/{test/test/inference_methods/test_assert_files_exist/input/input_2.txt → lib/tap/generator/generators/package/USAGE} +0 -0
  21. data/lib/tap/generator/generators/package/package_generator.rb +38 -0
  22. data/lib/tap/generator/generators/package/templates/package.erb +186 -0
  23. data/lib/tap/generator/generators/root/root_generator.rb +14 -9
  24. data/lib/tap/generator/generators/root/templates/Rakefile +20 -14
  25. data/{test/test/inference_methods/test_infer_glob/expected/file.yml → lib/tap/generator/generators/root/templates/ReadMe.txt} +0 -0
  26. data/lib/tap/generator/generators/root/templates/tap.yml +82 -0
  27. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -1
  28. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +2 -1
  29. data/{test/test/inference_methods/test_infer_glob/expected/file_1.txt → lib/tap/generator/generators/script/USAGE} +0 -0
  30. data/lib/tap/generator/generators/script/script_generator.rb +17 -0
  31. data/lib/tap/generator/generators/script/templates/script.erb +42 -0
  32. data/lib/tap/generator/generators/task/task_generator.rb +1 -1
  33. data/lib/tap/generator/generators/task/templates/task.erb +24 -16
  34. data/lib/tap/generator/generators/task/templates/test.erb +13 -17
  35. data/lib/tap/generator/generators/workflow/templates/task.erb +10 -10
  36. data/lib/tap/generator/generators/workflow/templates/test.erb +1 -1
  37. data/lib/tap/generator/generators/workflow/workflow_generator.rb +3 -18
  38. data/lib/tap/root.rb +108 -146
  39. data/lib/tap/script.rb +362 -0
  40. data/lib/tap/script/console.rb +28 -0
  41. data/lib/tap/script/destroy.rb +13 -1
  42. data/lib/tap/script/generate.rb +13 -1
  43. data/lib/tap/script/run.rb +100 -57
  44. data/lib/tap/support/batch_queue.rb +0 -3
  45. data/lib/tap/support/logger.rb +6 -3
  46. data/lib/tap/support/rake.rb +54 -0
  47. data/lib/tap/support/task_configuration.rb +169 -0
  48. data/lib/tap/support/tdoc.rb +198 -0
  49. data/lib/tap/support/tdoc/config_attr.rb +338 -0
  50. data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
  51. data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
  52. data/lib/tap/support/versions.rb +33 -1
  53. data/lib/tap/task.rb +339 -227
  54. data/lib/tap/test.rb +86 -128
  55. data/lib/tap/test/env_vars.rb +16 -5
  56. data/lib/tap/test/file_methods.rb +373 -0
  57. data/lib/tap/test/subset_methods.rb +299 -180
  58. data/lib/tap/version.rb +2 -1
  59. data/lib/tap/workflow.rb +2 -0
  60. data/test/app/lib/app_test_task.rb +1 -0
  61. data/test/app_test.rb +327 -83
  62. data/test/check/binding_eval.rb +23 -0
  63. data/test/check/define_method_check.rb +22 -0
  64. data/test/check/dependencies_check.rb +175 -0
  65. data/test/check/inheritance_check.rb +22 -0
  66. data/test/file_task_test.rb +524 -291
  67. data/test/{test/inference_methods/test_infer_glob/expected/file_2.txt → root/glob/one.txt} +0 -0
  68. data/test/root/glob/two.txt +0 -0
  69. data/test/root_test.rb +330 -262
  70. data/test/script_test.rb +194 -0
  71. data/test/support/audit_test.rb +5 -2
  72. data/test/support/combinator_test.rb +10 -10
  73. data/test/support/rake_test.rb +35 -0
  74. data/test/support/task_configuration_test.rb +272 -0
  75. data/test/support/tdoc_test.rb +363 -0
  76. data/test/support/templater_test.rb +2 -2
  77. data/test/support/versions_test.rb +32 -0
  78. data/test/tap_test_helper.rb +39 -0
  79. data/test/task_base_test.rb +115 -0
  80. data/test/task_class_test.rb +56 -4
  81. data/test/task_execute_test.rb +29 -0
  82. data/test/task_test.rb +89 -70
  83. data/test/test/env_vars_test.rb +48 -0
  84. data/test/test/{inference_methods → file_methods}/test_assert_expected/expected/file.txt +0 -0
  85. data/test/test/{inference_methods → file_methods}/test_assert_expected/expected/folder/file.txt +0 -0
  86. data/test/test/{inference_methods → file_methods}/test_assert_expected/input/file.txt +0 -0
  87. data/test/test/{inference_methods → file_methods}/test_assert_expected/input/folder/file.txt +0 -0
  88. data/test/test/file_methods/test_assert_files_exist/input/input_1.txt +0 -0
  89. data/test/test/file_methods/test_assert_files_exist/input/input_2.txt +0 -0
  90. data/test/test/file_methods/test_assert_output_files_equal/expected/one.txt +1 -0
  91. data/test/test/file_methods/test_assert_output_files_equal/expected/two.txt +1 -0
  92. data/test/test/file_methods/test_assert_output_files_equal/input/one.txt +1 -0
  93. data/test/test/file_methods/test_assert_output_files_equal/input/two.txt +1 -0
  94. data/test/test/{inference_methods → file_methods}/test_file_compare/expected/output_1.txt +0 -0
  95. data/test/test/{inference_methods → file_methods}/test_file_compare/expected/output_2.txt +0 -0
  96. data/test/test/{inference_methods → file_methods}/test_file_compare/input/input_1.txt +0 -0
  97. data/test/test/{inference_methods → file_methods}/test_file_compare/input/input_2.txt +0 -0
  98. data/test/test/file_methods/test_infer_glob/expected/file.yml +0 -0
  99. data/test/test/file_methods/test_infer_glob/expected/file_1.txt +0 -0
  100. data/test/test/file_methods/test_infer_glob/expected/file_2.txt +0 -0
  101. data/test/test/file_methods/test_method_glob/expected/file.yml +0 -0
  102. data/test/test/file_methods/test_method_glob/expected/file_1.txt +0 -0
  103. data/test/test/file_methods/test_method_glob/expected/file_2.txt +0 -0
  104. data/test/test/{inference_methods → file_methods}/test_yml_compare/expected/output_1.yml +0 -0
  105. data/test/test/{inference_methods → file_methods}/test_yml_compare/expected/output_2.yml +0 -0
  106. data/test/test/{inference_methods → file_methods}/test_yml_compare/input/input_1.yml +0 -0
  107. data/test/test/{inference_methods → file_methods}/test_yml_compare/input/input_2.yml +0 -0
  108. data/test/test/file_methods_test.rb +204 -0
  109. data/test/test/subset_methods_test.rb +93 -33
  110. data/test/test/test_assert_expected_result_files/expected/task/name/a.txt +1 -0
  111. data/test/test/test_assert_expected_result_files/expected/task/name/b.txt +1 -0
  112. data/test/test/test_assert_expected_result_files/input/a.txt +1 -0
  113. data/test/test/test_assert_expected_result_files/input/b.txt +1 -0
  114. data/test/test/test_file_task_test/expected/one.txt +1 -0
  115. data/test/test/test_file_task_test/expected/two.txt +1 -0
  116. data/test/test/test_file_task_test/input/one.txt +1 -0
  117. data/test/test/test_file_task_test/input/two.txt +1 -0
  118. data/test/test_test.rb +143 -3
  119. data/test/workflow_test.rb +2 -0
  120. data/vendor/rails_generator.rb +56 -0
  121. data/vendor/rails_generator/base.rb +263 -0
  122. data/vendor/rails_generator/commands.rb +581 -0
  123. data/vendor/rails_generator/generated_attribute.rb +42 -0
  124. data/vendor/rails_generator/lookup.rb +209 -0
  125. data/vendor/rails_generator/manifest.rb +53 -0
  126. data/vendor/rails_generator/options.rb +143 -0
  127. data/vendor/rails_generator/scripts.rb +83 -0
  128. data/vendor/rails_generator/scripts/destroy.rb +7 -0
  129. data/vendor/rails_generator/scripts/generate.rb +7 -0
  130. data/vendor/rails_generator/scripts/update.rb +12 -0
  131. data/vendor/rails_generator/simple_logger.rb +46 -0
  132. data/vendor/rails_generator/spec.rb +44 -0
  133. metadata +180 -196
  134. data/lib/tap/generator/generators/root/templates/app.yml +0 -19
  135. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +0 -4
  136. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +0 -26
  137. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  138. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +0 -57
  139. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +0 -108
  140. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +0 -40
  141. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +0 -21
  142. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +0 -60
  143. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +0 -5
  144. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +0 -53
  145. data/lib/tap/script/server.rb +0 -12
  146. data/lib/tap/support/rap.rb +0 -38
  147. data/lib/tap/test/inference_methods.rb +0 -298
  148. data/test/task/config/task_with_config.yml +0 -1
  149. data/test/test/inference_methods_test.rb +0 -311
data/bin/tap CHANGED
@@ -1,63 +1,99 @@
1
- #require 'rdoc/usage'
2
- #require 'getoptlong'
3
-
4
- # add options as needed
5
- #opts = GetoptLong.new(
6
- # [ '--help', '-h', GetoptLong::NO_ARGUMENT ])
7
- # opts.each do |opt, arg|
8
- # case opt
9
- # when '--help' then nil
10
- # end
11
- # end
12
-
13
- lib_dir = File.join( File.dirname(__FILE__), "../lib/")
14
- require lib_dir + "tap.rb"
15
-
16
- # configure the app to app.yml if it exists
17
- if File.exists?("app.yml")
18
- config = ERB.new( File.read("app.yml") ).result
19
- config = YAML.load(config)
20
- Tap::App.instance.reconfigure(config) if config
1
+ # usage: tap <command> {options} [args]
2
+ #
3
+ # examples:
4
+ # tap generate root /path/to/root # generates a root dir
5
+ # tap run taskname --option input # runs the 'taskname' task
6
+ #
7
+ # help:
8
+ # tap help # prints this help
9
+ # tap command --help # prints help for 'command'
10
+
11
+ require File.join( File.dirname(__FILE__), "../lib/tap.rb")
12
+ require "tap/script"
13
+
14
+ app = Tap::App.instance
15
+ script = Tap::Script.instance
16
+
17
+ begin
18
+ # configure the app to tap.yml if it exists
19
+ config_file = Tap::Script.config_filepath(Dir.pwd)
20
+ config = Tap::Script.read_config(config_file)
21
+ script.configure_app(config)
22
+ rescue(Exception)
23
+ # catch errors and exit gracefully
24
+ # (errors usu from gem loading errors)
25
+ puts "Configuration error: #{$!.message}"
26
+ puts "Check #{config_file} configurations"
27
+ exit(1)
28
+ end
29
+
30
+ # alert the user to the root directory if it's not Dir.pwd
31
+ unless app.options.quiet || app.root == File.expand_path(Dir.pwd)
32
+ puts "root: #{app.root}"
33
+ end
34
+
35
+ #
36
+ # run before script
37
+ #
38
+ begin
39
+ eval(script.config.before.to_s)
40
+ rescue
41
+ puts "Error in before script."
42
+ if app.options.debug
43
+ raise
44
+ else
45
+ puts $!.message
46
+ exit(1)
47
+ end
21
48
  end
22
49
 
23
50
  begin
51
+ available_commands = script.config.scripts
24
52
  command = ARGV.shift
53
+
25
54
  case command
26
- when "help", "?", nil
27
- puts "usage: tap <command> [options] [args]"
55
+ when "--help", "-h", "help", "?", nil
56
+ # give some help
57
+ puts Tap::Script.usage(__FILE__)
58
+ puts
28
59
  puts "available commands:"
29
-
30
- commands = Tap::App.instance.glob(:script).collect do |script|
31
- next unless File.extname(script) == ".rb"
32
- Tap::App.instance.relative_filepath(:script, script).chomp(".rb")
33
- end
34
- Tap::App.glob(lib_dir + "tap/script/**/*").each do |script|
35
- next unless File.extname(script) == ".rb"
36
- command = Tap::App.relative_filepath(lib_dir + "tap/script", script).chomp(".rb")
37
- commands << command unless commands.include?(command)
38
- end
60
+
61
+ commands = available_commands.keys
62
+ commands.unshift("help")
39
63
 
40
64
  print " "
41
65
  puts commands.sort.join("\n ")
42
66
  puts
43
-
44
- else
45
- app_script_filepath = Tap::App.instance.filepath(:script, command + ".rb")
46
- tap_script_filepath = lib_dir + "tap/script/#{command}.rb"
47
-
48
- if File.exists?(app_script_filepath)
49
- load app_script_filepath
50
- elsif File.exists?(tap_script_filepath)
51
- load tap_script_filepath
67
+ puts "version #{Tap::VERSION} -- #{Tap::HOMEPAGE}"
68
+ else
69
+ if available_commands.has_key?(command)
70
+ # run the script, if it exists
71
+ load available_commands[command]
52
72
  else
53
- raise "Unknown command: '#{command}'"
73
+ puts "Unknown command: '#{command}'"
74
+ puts "Type 'tap help' for usage information."
54
75
  end
55
76
  end
56
77
  rescue
57
- if Tap::App.instance.options.debug
78
+ if app.options.debug
79
+ raise
80
+ else
81
+ puts $!.message
82
+ puts "Type 'tap #{command} --help' for usage information."
83
+ end
84
+ end
85
+
86
+ #
87
+ # run after script
88
+ #
89
+ begin
90
+ eval(script.config.after.to_s)
91
+ rescue
92
+ puts "Error in after script."
93
+ if app.options.debug
58
94
  raise
59
95
  else
60
96
  puts $!.message
61
- puts "Type 'tap help' for usage information."
97
+ exit(1)
62
98
  end
63
99
  end
data/lib/tap.rb CHANGED
@@ -1,15 +1,44 @@
1
- require 'active_support'
1
+ # gem_original_require (set in rubygems) is used here to speed up
2
+ # requires when possible... ok since I'm manually activating the
3
+ # neeeded gems.
4
+ require 'rubygems'
5
+
6
+ gem_original_require 'yaml' # expensive to load
7
+ gem_original_require 'logger'
8
+ gem_original_require 'ostruct'
9
+ gem_original_require 'thread'
10
+ gem_original_require 'monitor'
11
+ gem_original_require 'erb'
12
+
13
+ # Loading activesupport piecemeal like this cuts the tap load time in half.
14
+ gem 'activesupport'
15
+
16
+ gem_original_require 'active_support/core_ext/array/extract_options.rb'
17
+ class Array #:nodoc:
18
+ include ActiveSupport::CoreExtensions::Array::ExtractOptions
19
+ end
20
+ gem_original_require 'active_support/core_ext/class.rb'
21
+ gem_original_require 'active_support/core_ext/module.rb'
22
+ gem_original_require 'active_support/core_ext/symbol.rb'
23
+ gem_original_require 'active_support/core_ext/string.rb'
24
+ gem_original_require 'active_support/core_ext/blank.rb'
25
+ gem_original_require 'active_support/core_ext/hash/keys.rb'
26
+ gem_original_require 'active_support/dependencies'
27
+ class Hash #:nodoc:
28
+ include ActiveSupport::CoreExtensions::Hash::Keys
29
+ end
2
30
 
3
31
  $:.unshift File.dirname(__FILE__)
4
32
 
5
- require 'tap/version'
6
- require 'tap/root'
7
- require 'tap/app'
8
- require 'tap/task'
9
- require 'tap/workflow'
10
- require 'tap/file_task'
11
- require 'tap/support/audit'
12
- require 'tap/support/logger'
13
- require 'tap/support/templater'
14
- require 'tap/support/batch_queue'
15
- require 'tap/support/run_error'
33
+ gem_original_require 'tap/support/audit'
34
+ gem_original_require 'tap/support/task_configuration'
35
+ gem_original_require 'tap/support/logger'
36
+ gem_original_require 'tap/support/templater'
37
+ gem_original_require 'tap/support/batch_queue'
38
+ gem_original_require 'tap/support/run_error'
39
+ gem_original_require 'tap/version'
40
+ gem_original_require 'tap/root'
41
+ gem_original_require 'tap/app'
42
+ gem_original_require 'tap/task'
43
+ gem_original_require 'tap/workflow'
44
+ gem_original_require 'tap/file_task'
data/lib/tap/app.rb CHANGED
@@ -1,33 +1,23 @@
1
- require 'yaml'
2
- require 'logger'
3
- require 'ostruct'
4
- require 'getoptlong'
5
- require 'monitor'
6
- require 'thread'
7
- require 'thwait'
8
- require 'erb'
9
-
10
1
  module Tap
11
2
 
12
3
  # == Overview
13
4
  #
14
5
  # App coordinates the setup and running of tasks, and provides an interface
15
6
  # to the application directory structure. App is convenient for use within
16
- # scripts, and can be extended with various modules to provide command line
17
- # utilites or to be integrated within a tap server.
7
+ # scripts, and provides the basis for the 'tap' command line application.
18
8
  #
19
- # All tasks have an App (by default App.instance) which helps set the task
20
- # up by loading configuration templates from the application directory, and
21
- # to which all queue commands are issued. Tasks also access application-
9
+ # All tasks have an App (by default App.instance) which helps initialize the
10
+ # task by loading configuration templates from the application directory.
11
+ # Task queue commands are passed to app, and tasks access application-
22
12
  # wide resources like the logger and various options through App.
23
13
  #
24
14
  # task = Task.new {|task, input| input += 1 }
25
15
  # task.app # => App.instance
26
- # task.queue 1,2,3
16
+ # task.enq 1,2,3
27
17
  #
28
- # app.run
18
+ # task.app.run
29
19
  #
30
- # task.outputs.collect do |audit|
20
+ # task.results.collect do |audit|
31
21
  # audit._current
32
22
  # end # => [2,3,4]
33
23
  #
@@ -38,29 +28,32 @@ module Tap
38
28
  # Multithreaded tasks execute cosynchronously, each on their own thread.
39
29
  #
40
30
  # Workflows can be achieved by setting the condition and on_complete blocks
41
- # for a task. Tasks are skipped in the queue until the condition block is
42
- # met with the currently queued inputs. When a task finishes, it will
43
- # execute the on_complete block where results can be dispacted to other
44
- # tasks, and these tasks queued for execution.
31
+ # for a task. Tasks are skipped until their condition block is met by the
32
+ # queued inputs. When a task finishes, it executes its on_complete block,
33
+ # thereby allowing results to be dispatched to other tasks.
45
34
  #
46
35
  # In this system, the workflow logic exists among the tasks. App keeps on
47
- # running as long as it finds executable tasks in the queue.
36
+ # running as long as it finds executable tasks in the queue, or until it
37
+ # is stopped or terminated.
48
38
  #
49
39
  # === Error Handling
50
40
  #
51
41
  # When unhandled errors arise during a run, App enters a termination (rescue)
52
- # routine. During termination a TerminationError is raised for each executing
53
- # task so that the task immediately exits, or begins executing its internal
54
- # error handling code (perhaps performing rollbacks).
42
+ # routine. During termination a TerminationError is raised in each executing
43
+ # task so that the task exits, or begins executing its internal error handling
44
+ # code (perhaps performing rollbacks).
55
45
  #
56
46
  # Additional errors that arise during termination are collected and packaged
57
47
  # with the orignal error into a RunError. By default all errors are logged
58
- # and the run exits. If options.debug == true, then the RunError will be raised
59
- # for further handling.
48
+ # and the run exits. If options.debug == true, then the RunError will be
49
+ # raised for further handling.
60
50
  #
61
51
  # Note: the task that caused the original unhandled error is no longer executing
62
52
  # when termination begins and thus will not recieve a TerminationError.
63
53
  #
54
+ #--
55
+ # === Auditing
56
+ #++
64
57
  class App < Root
65
58
  include MonitorMixin
66
59
 
@@ -68,8 +61,7 @@ module Tap
68
61
  attr_writer :instance
69
62
 
70
63
  # Returns the current instance of App. If no instance has been set,
71
- # then a new App with the default configuration will be initialized
72
- # to instance.
64
+ # then a new App with the default configuration will be initialized.
73
65
  def instance
74
66
  @instance ||= App.new
75
67
  end
@@ -79,29 +71,31 @@ module Tap
79
71
  instance.run(task, *inputs)
80
72
  end
81
73
 
82
- # Parses the input argument as YAML, if arg is a string that matches
83
- # the YAML document specifier (ie it begins with "---\n"). Otherwise
84
- # returns the argument.
85
- def parse_yaml(arg)
86
- arg =~ /^---\s*\n/ ? YAML.load(arg) : arg
74
+ # Parses the input string as YAML, if the string matches the YAML document
75
+ # specifier (ie it begins with "---\s*\n"). Otherwise returns the string.
76
+ #
77
+ # str = {'key' => 'value'}.to_yaml # => "--- \nkey: value\n"
78
+ # Tap::App.parse_yaml(str) # => {'key' => 'value'}
79
+ # Tap::App.parse_yaml("str") # => "str"
80
+ def parse_yaml(str)
81
+ str =~ /^---\s*\n/ ? YAML.load(str) : str
87
82
  end
88
83
 
84
+ # Read the contents of filepath, templates these using ERB, and loads
85
+ # the result as YAML. Returns nil if filepath does not exist.
89
86
  def read_erb_yaml(filepath)
90
- return nil unless File.exists?(filepath)
87
+ return nil if !File.exists?(filepath) || File.directory?(filepath)
91
88
 
92
89
  input = File.read(filepath)
93
90
  input = ERB.new(input).result
94
91
  YAML.load(input)
95
92
  end
96
-
97
- def make_config(task, config, version=nil)
98
-
99
- end
100
93
  end
101
94
 
102
95
  attr_reader :options, :logger, :queue, :state
103
96
  attr_accessor :map
104
97
 
98
+ # The constants defining the possible App states.
105
99
  module State
106
100
  READY = 0
107
101
  RUN = 1
@@ -111,26 +105,26 @@ module Tap
111
105
 
112
106
  DEFAULT_MAX_THREADS = 10
113
107
 
114
- # Creates a new App with the given configuration. See reconfigure for
115
- # configuration options.
108
+ # Creates a new App with the given configuration.
109
+ # See reconfigure for configuration options.
116
110
  def initialize(config={})
117
111
  super()
118
112
 
119
113
  @queue = Support::BatchQueue.new
120
- @threads = []
114
+ @threads = [].extend(MonitorMixin)
121
115
  @thread_queue = nil
122
116
  @main_thread = nil
123
117
  @state = State::READY
124
-
118
+
125
119
  # defaults must be provided for options and logging to ensure
126
120
  # that they will be initialized by reconfigure
127
121
  self.reconfigure( {
128
- :options => {}, :logger => {}, :map => {},
122
+ :options => {}, :logger => {}, :map => {}
129
123
  }.merge(config) )
130
124
  end
131
125
 
132
- # Returns the current configuration for the App.
133
- def config
126
+ # Returns the configuration of self.
127
+ def config
134
128
  {:root => self.root,
135
129
  :directories => self.directories,
136
130
  :absolute_paths => self.absolute_paths,
@@ -138,18 +132,26 @@ module Tap
138
132
  :logger => {
139
133
  :device => self.logger.logdev.dev,
140
134
  :level => self.logger.level,
141
- :datetime_format => self.logger.datetime_format},
142
- :load_paths => self.load_paths}
135
+ :datetime_format => self.logger.datetime_format}}
143
136
  end
144
137
 
145
- # Reconfigures App with the input configurations; other configurations are not affected.
138
+ # Reconfigures self with the input configurations; other configurations are not affected.
139
+ #
140
+ # app = Tap::App.new :root => "/root", :directories => {:dir => 'path/to/dir'}
141
+ # app.reconfigure(
142
+ # :root => "./new/root",
143
+ # :logger => {:level => Logger::DEBUG})
144
+ #
145
+ # app.root # => File.expand_path("./new/root")
146
+ # app[:dir] # => File.expand_path("./new/root/path/to/dir")
147
+ # app.logger.level # => Logger::DEBUG
146
148
  #
147
149
  # Available configurations:
148
- # root:: resets the root directory of the app using root=
149
- # directories:: resets the app directories using directories= (note ALL direcotries
150
- # are reset. use app[dir]= to set a single directory)
151
- # options:: resets the application options (note ALL options are reset. use
152
- # app.options.opt= to set a single option)
150
+ # root:: resets the root directory of self using root=
151
+ # directories:: resets directory aliases using directories= (note ALL
152
+ # aliases are reset. use app[da]= to set a single alias)
153
+ # options:: resets the application options (note ALL options are reset.
154
+ # use app.options.opt= to set a single option)
153
155
  # logger:: creates and sets a new logger from the configuration
154
156
  #
155
157
  # Available logger configurations and defaults:
@@ -162,9 +164,9 @@ module Tap
162
164
  # - Additional configurations may be added in subclasses using handle_configuration
163
165
  def reconfigure(config={})
164
166
  config = config.symbolize_keys
165
-
167
+
166
168
  # ensure critical keys are evaluated in the proper order
167
- keys = [:root, :directories, :absolute_paths, :options, :load_paths, :loadable]
169
+ keys = [:root, :directories, :absolute_paths, :options]
168
170
  config.keys.each do |key|
169
171
  keys << key unless keys.include?(key)
170
172
  end
@@ -196,33 +198,18 @@ module Tap
196
198
  self.logger = logger
197
199
  when :map
198
200
  self.map = value
199
- when :load_paths
200
- self.load_paths = value
201
- when :loadable
202
- value.each {|dir| self.load_paths << self[dir]}
203
201
  else
204
- raise "Unknown configuration: #{key}" unless handle_configuation(key, value)
202
+ unless handle_configuation(key, value)
203
+ if block_given?
204
+ yield(key, value)
205
+ else
206
+ raise ArgumentError.new("Unknown configuration: #{key}")
207
+ end
208
+ end
205
209
  end
206
210
  end
207
- end
208
-
209
- #
210
- # Dependencies and reloading
211
- #
212
-
213
- def load_paths
214
- Dependencies.load_paths
215
- end
216
-
217
- def load_paths=(paths)
218
- Dependencies.load_paths = paths
219
- end
220
-
221
- def reload
222
- # TODO - logging, maybe through Dependencies as
223
- # RAILS_DEFAULT_LOGGER = logger (problem with this during server?)
224
- # Dependencies.log_activity = true
225
- Dependencies.clear
211
+
212
+ self
226
213
  end
227
214
 
228
215
  #
@@ -243,81 +230,95 @@ module Tap
243
230
  logger.add(level, msg, action.to_s) unless options.quiet
244
231
  end
245
232
 
246
- def monitor(action="beginning", msg="", &block)
247
- if options.quiet
248
- yield
249
- nil
250
- else
251
- @monitoring = true
252
- result = logger.monitor(action, msg, &block)
253
- @monitoring = false
254
- result
255
- end
256
- end
257
-
258
- def tick_monitor(action=nil, msg="", level=Logger::INFO)
259
- unless options.quiet || !@monitoring
260
- action.nil? ?
261
- logger.tick :
262
- logger.format_add(level, msg, action.to_s) {|format| "\n" + format}
233
+ # Formatted log. Works like log, but passes the current log format to the
234
+ # block and uses whatever format the block returns. The format recieves
235
+ # the following arguments like so:
236
+ #
237
+ # format % [severity, timestamp, (action || '--'), msg]
238
+ #
239
+ # By default, if you don't specify a block, flog just chomps a newline off
240
+ # the format, so your log will be inline.
241
+ def flog(action="", msg="", level=Logger::INFO) # :yields: format
242
+ unless options.quiet
243
+ logger.format_add(level, msg, action) do |format|
244
+ block_given? ? yield(format) : format.chomp("\n")
245
+ end
263
246
  end
264
247
  end
265
248
 
266
- def monitor_stats(hash)
267
- unless options.quiet || !@monitoring
268
- logger << "\n"
269
- logger << hash.stringify_keys.to_yaml
270
- end
271
- end
249
+ # def monitor(action="beginning", msg="", &block)
250
+ # if options.quiet
251
+ # yield
252
+ # nil
253
+ # else
254
+ # @monitoring = true
255
+ # result = logger.monitor(action, msg, &block)
256
+ # @monitoring = false
257
+ # result
258
+ # end
259
+ # end
260
+ #
261
+ # def tick_monitor(action=nil, msg="", level=Logger::INFO)
262
+ # unless options.quiet || !@monitoring
263
+ # action.nil? ?
264
+ # logger.tick :
265
+ # logger.format_add(level, msg, action.to_s) {|format| "\n" + format}
266
+ # end
267
+ # end
268
+ #
269
+ # def monitor_stats(hash)
270
+ # unless options.quiet || !@monitoring
271
+ # logger << "\n"
272
+ # logger << hash.stringify_keys.to_yaml
273
+ # end
274
+ # end
272
275
 
273
276
  #
274
277
  # Task methods
275
278
  #
276
279
 
277
- # Returns the specifed task, reconfigured with config (if provided).
278
- #
279
- # t = Task.new :key => 'value'
280
- # t = app.task(t, :key => 'another')
281
- # t.config[:key] # => 'another'
282
- #
283
- # If a task name is given, then the appropriate class will be loaded and
284
- # instantiated into a task with given name. The class is the de-versioned
285
- # and camelized name, or the class as specified in map.
280
+ # Instantiates the specifed task with config (if provided).
286
281
  #
287
- # app.map # => {"mapped-task" => "Task"}
282
+ # app.map = {"mapped-task" => "AnotherTask"}
288
283
  #
289
284
  # t = app.task('mapped-task-1.0', :key => 'value')
290
- # t.class # => Task
285
+ # t.class # => AnotherTask
291
286
  # t.name # => "mapped-task-1.0"
292
287
  # t.config[:key] # => 'value'
293
288
  #
294
- def task(td, config=nil)
295
- t = if td.kind_of?(Tap::Task)
296
- td
297
- else
289
+ # A new task is instantiated for each call to task; tasks may share the
290
+ # same name. The task class is the de-versioned and camelized task name,
291
+ # or the class specified in map. The task class will be auto-loaded using
292
+ # Dependencies, if needed.
293
+ #
294
+ # A LookupError is raised if the task class cannot be found.
295
+ def task(task_name, config=nil)
296
+ begin
298
297
  # lookup the corresponding class and instantiate
299
- constants = task_class_name(td).split('::')
298
+ constants = task_class_name(task_name).split('::')
300
299
  task_class = constants.inject(Object) do |klass, const|
301
- begin
302
- klass.const_get(const)
303
- rescue(NameError)
304
- raise "unknown task '#{td}'"
305
- end
300
+ klass.const_get(const)
306
301
  end
307
- task_class.new(td)
302
+ task_class.new(task_name, config)
303
+ rescue(NameError)
304
+ raise LookupError.new("unknown task '#{task_name}'")
308
305
  end
309
-
310
- # reconfigure if a config is provided
311
- t.config = config unless config.nil?
312
- t
313
306
  end
314
307
 
308
+ # Returns the class name of the specified task. If a task descriptor
309
+ # is given, task_class_name returns the de-versioned, camelized
310
+ # descriptor, or the class name as specified in map.
311
+ #
312
+ # t = Task.new
313
+ # app.map = {"mapped-task" => "AnotherTask"}
314
+ # app.task_class_name(t) # => "Tap::Task"
315
+ # app.task_class_name('mapped-task-1.0') # => "AnotherTask"
315
316
  def task_class_name(td)
316
317
  case td
317
- when Tap::Task then td.class.to_s
318
+ when Tap::Task::Base then td.class.to_s
318
319
  else
319
320
  # de-version and resolve using map
320
- name, version = deversion(td)
321
+ name, version = deversion(td.to_s)
321
322
  map.has_key?(name) ? map[name] : name.camelize
322
323
  end
323
324
  end
@@ -352,55 +353,55 @@ module Tap
352
353
  #
353
354
  # Execution methods
354
355
  #
355
-
356
+
356
357
  # Dequeues the task and inputs and executes the task with the current task inputs.
357
- # Only the provided task will be executed (ie the task batch will not be executed)
358
- # and execution is on the current thread even if the task is multithreaded.
358
+ #
359
+ # Execute differs from run in several significant ways:
360
+ # - Only the provided task will be executed (ie the task batch will not be executed)
361
+ # - Execution is on the current thread even if the task is multithreaded.
362
+ # - No error handling is performed
359
363
  #
360
- # execute can only run if the application state is READY or RUN, and does not
361
- # perform any error handling
362
- def execute(task)
363
- synchronize do
364
- #log(:execute, task.to_dir, Logger::DEBUG) if options.debug
365
-
366
- unless state == State::READY || state == State::RUN
367
- raise "cannot execute unless application state is READY or RUN"
368
- end
369
-
370
- if deq = queue.deq(task)
371
- task, inputs = deq
372
- task.execute(*inputs)
373
- end
374
- end
375
- end
376
-
377
- # Enqueues the task and inputs (if provided), then iterates through the queue
378
- # executing tasks using this execution cycle:
379
- #
380
- # for each enqueued task:
381
- # next unless executable?(task)
364
+ # Execute can only run if the application state is READY or RUN.
365
+ # def execute(task)
366
+ # synchronize do
367
+ # #log(:execute, task.to_dir, Logger::DEBUG) if options.debug
382
368
  #
383
- # dequeue task and inputs
384
- # if multithreading is allowed and task.multithread?
385
- # execute task on a thread
386
- # else
387
- # execute task
388
- #
389
- # At the end of run, the queue may still contain unexecuted tasks. These
390
- # tasks require additional inputs or conditions in order to execute. Tasks
391
- # may be specified as a descriptor (ex: "some/task" => SomeTask.new) or
392
- # as the task itself; run returns the specified task on completion.
369
+ # unless state == State::READY || state == State::RUN
370
+ # raise "cannot execute unless application state is READY or RUN"
371
+ # end
372
+ #
373
+ # if deq = queue.deq(task)
374
+ # task, inputs = deq
375
+ # task.execute(*inputs)
376
+ # end
377
+ # end
378
+ # end
379
+
380
+ # Enqueues the task and inputs (if provided), then begins the run cycle.
381
+ #
382
+ # During the run cycle, the app will iterate through the queue and execute
383
+ # tasks. Tasks will be executed ONLY if task.executable? returns true with
384
+ # the currently enqueued inputs. Tasks are executed on their own thread if
385
+ # task.multithread? is true.
386
+ #
387
+ # As a result of these rules, the queue may still contain unexecuted tasks
388
+ # a the end of run. These tasks require additional inputs or conditions
389
+ # in order to execute. Execution errors are handled as described above.
390
+ #
391
+ # Tasks may be specified as a task descriptor (ex: "sample/task") or provided
392
+ # directly. Task descriptors are resolved using task.
393
+ #
394
+ # run returns the specified task on completion.
393
395
  def run(td=nil, *inputs)
394
396
  synchronize do
395
397
  # TODO: log starting run
396
398
 
397
399
  # lookup and enqueue the task, if provided
398
- specified_task = task(td) if td
400
+ specified_task = (td == nil || td.kind_of?(Tap::Task::Base) ? td : task(td))
399
401
  queue.enq(specified_task, *inputs) if specified_task
400
402
 
401
403
  # generate threading variables
402
404
  self.state = State::RUN
403
- self.main_thread = Thread.current
404
405
  max_threads = options.max_threads || DEFAULT_MAX_THREADS
405
406
  self.thread_queue = max_threads > 0 ? Queue.new : nil
406
407
 
@@ -461,8 +462,8 @@ module Tap
461
462
  # executed. collect any errors that arise during
462
463
  # termination.
463
464
  clear_thread_queue
464
- errors = terminate_threads
465
-
465
+ errors = clear_threads(false)
466
+
466
467
  # handle the errors accordingly
467
468
  if options.debug
468
469
  raise Tap::Support::RunError.new($!, errors)
@@ -477,7 +478,6 @@ module Tap
477
478
 
478
479
  # reset run variables
479
480
  self.thread_queue = nil
480
- self.main_thread = nil
481
481
  self.state = State::READY
482
482
 
483
483
  # TODO: log run complete
@@ -495,26 +495,20 @@ module Tap
495
495
  self.state = State::STOP if state == State::RUN
496
496
  end
497
497
 
498
- # Terminates a running application by raising an error on the
499
- # main execution thread which enters the application into a
500
- # terminate (rescue) mode. By default the error will be a
501
- # TerminateError, but another error can be provided as cause
502
- # for the termination. The application state is set to
503
- # State::TERMINATE.
504
- #
505
- # Termination in this way allows executing tasks to invoke
506
- # their specific error handling code, perhaps performing
507
- # rollbacks. (ie terminate should be used as a graceful
508
- # exit)
498
+ # Signals a running application to terminate executing tasks
499
+ # by setting state = State::TERMINATE. When running tasks
500
+ # reach a termination check, the task will raise a Termination
501
+ # error, thus allowing executing tasks to invoke their specific
502
+ # error handling code, perhaps performing rollbacks.
503
+ #
504
+ # Termination checks can be manually specified in a task
505
+ # using the check_terminate method. Termination checks
506
+ # automatically occur before each task execution.
509
507
  #
510
508
  # Does nothing unless state is State::RUN or State::STOP.
511
- def terminate(error=TerminateError.new)
509
+ def terminate#(error=TerminateError.new)
512
510
  if state == State::RUN || state == State::STOP
513
511
  self.state = State::TERMINATE
514
-
515
- # the terminate error must be raised on the main thread
516
- # so that the App will enter the termination (rescue) block
517
- main_thread.raise(error)
518
512
  end
519
513
  end
520
514
 
@@ -542,7 +536,7 @@ module Tap
542
536
 
543
537
  # Sets the condition block for the task. If the task is batched,
544
538
  # then the condition will be set for each task in the batch.
545
- def condition(task, &block) # :yields: task, inputs
539
+ def condition(task, &block) # :yields: task, audited_inputs
546
540
  task.batch.each {|t| t.condition(&block)}
547
541
  end
548
542
 
@@ -565,8 +559,7 @@ module Tap
565
559
  # The condition block between these tasks will be set to the
566
560
  # input block, if provided. Batched tasks will have the pattern
567
561
  # set for each task in the batch.
568
- def sequence(*tasks, &block) # :yields: task, inputs
569
- tasks.collect! {|td| task(td)}
562
+ def sequence(*tasks, &block) # :yields: task, audited_inputs
570
563
  current_task = tasks.shift
571
564
 
572
565
  tasks.each do |next_task|
@@ -582,7 +575,7 @@ module Tap
582
575
  # source completes. The condition block for the targets will
583
576
  # be set to the input block, if provided. Batched tasks will have
584
577
  # the pattern set for each task in the batch.
585
- def fork(source, *targets, &block) # :yields: task, inputs
578
+ def fork(source, *targets, &block) # :yields: task, audited_inputs
586
579
  on_complete(source) do |results|
587
580
  targets.each do |target|
588
581
  # forking requires new audit trails because the same
@@ -601,7 +594,7 @@ module Tap
601
594
  # completes. The condition block for the target will be set to the
602
595
  # input block, if provided. Batched tasks will have the pattern set
603
596
  # for each task in the batch.
604
- def merge(target, *sources, &block) # :yields: task, inputs
597
+ def merge(target, *sources, &block) # :yields: task, audited_inputs
605
598
  sources.each do |source|
606
599
  # merging can use the existing audit trails... each distinct
607
600
  # input is getting sent to one place (the target)
@@ -620,7 +613,7 @@ module Tap
620
613
  end
621
614
 
622
615
  attr_writer :state
623
- attr_accessor :thread_queue, :main_thread, :threads
616
+ attr_accessor :thread_queue, :threads
624
617
 
625
618
  private
626
619
 
@@ -635,6 +628,11 @@ module Tap
635
628
  # terminated but still running. Raise another
636
629
  # termination error to enter the termination
637
630
  # (rescue) code.
631
+
632
+ # BUG - if things just work out that way, terminate can
633
+ # be set from a thread that had an error, then control
634
+ # switches such that this is raised on the main thread.
635
+ # in this case it looks like THIS is the original error.
638
636
  raise TerminateError.new
639
637
  end
640
638
 
@@ -662,75 +660,74 @@ module Tap
662
660
  queue.priority_enq(task, *inputs) unless task.nil?
663
661
  end
664
662
  end
665
-
666
- def clear_threads
667
- return if threads.empty?
663
+
664
+ def clear_threads(raise_errors=true)
665
+ threads.synchronize do
666
+ errors = []
667
+ return errors if threads.empty?
668
668
 
669
- # clears threads gracefully by enqueuing nils, to break
670
- # the threads out of their loops, then waiting for the
671
- # threads to work through the queue to the nils
672
- threads.size.times { thread_queue.enq nil }
673
- ThreadsWait.all_waits(*threads) do |thread|
674
- # TODO - log thread clear
669
+ # clears threads gracefully by enqueuing nils, to break
670
+ # the threads out of their loops, then waiting for the
671
+ # threads to work through the queue to the nils
672
+ #
673
+ threads.size.times { thread_queue.enq nil }
674
+ while true
675
+ # TODO -- add a time out?
676
+
677
+ threads.dup.each do |thread|
678
+ next if thread.alive?
679
+ threads.delete(thread)
680
+ error = thread["error"]
681
+
682
+ next if error.nil?
683
+ raise error if raise_errors
684
+
685
+ errors << error
686
+ end
687
+
688
+ break if threads.empty?
689
+ Thread.pass
690
+ end
691
+
692
+ errors
675
693
  end
676
- threads.clear
677
694
  end
678
695
 
679
696
  def start_thread
680
- # start a new thread and add it to threads.
681
- # threads simply loop and wait for a task to
682
- # be queued. the thread will block until a
683
- # task is available (due to thread_queue.deq)
684
- threads << Thread.new do
685
- # TODO - log thread start
697
+ threads.synchronize do
698
+ # start a new thread and add it to threads.
699
+ # threads simply loop and wait for a task to
700
+ # be queued. the thread will block until a
701
+ # task is available (due to thread_queue.deq)
702
+ #
703
+ # TODO -- track thread index like?
704
+ # thread["index"] = threads.length
705
+ threads << Thread.new do
706
+ # TODO - log thread start
686
707
 
687
- execution_loop do
688
- task, inputs = thread_queue.deq
689
- break if task.nil?
690
-
691
- # TODO: log execute task on thread #
692
708
  begin
693
- task.execute(*inputs)
709
+ execution_loop do
710
+ task, inputs = thread_queue.deq
711
+ break if task.nil?
712
+
713
+ # TODO: log execute task on thread #
714
+ task.execute(*inputs)
715
+ end
694
716
  rescue
695
717
  # unless you're already terminating,
696
718
  # an unhandled error should immediately
697
719
  # terminate all threads
698
- if state == State::TERMINATE
699
- raise $!
700
- else
701
- terminate($!)
702
- end
720
+ terminate unless state == State::TERMINATE
721
+ Thread.current["error"] = $! unless $!.kind_of?(TerminateError)
703
722
  end
704
- end
705
- end
706
- end
707
-
708
- def terminate_threads
709
- # terminate each thread by raising an exception
710
- # wait for the thread to exit and gather any
711
- # errors that arise that are not from the
712
- # termination exception.
713
- errors = []
714
- threads.each_with_index do |thread, index|
715
- next unless thread.alive?
716
-
717
- begin
718
- thread.raise TerminateError.new
719
- thread.join
720
- rescue TerminateError
721
- # do not track these as errors
722
- # arising from termination
723
- rescue
724
- # track the thread of the error
725
- # with the error itself
726
- errors[index] = $!
727
723
  end
728
724
  end
729
- threads.clear
730
-
731
- errors
732
725
  end
733
726
 
727
+ # LookupErrors are raised for errors during dependency lookup
728
+ class LookupError < RuntimeError # :nodoc:
729
+ end
730
+
734
731
  # TerminateErrors are raised to kill executing tasks when terminate
735
732
  # is called on an running App.
736
733
  class TerminateError < RuntimeError