tap 0.7.9 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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