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
@@ -1,24 +1,23 @@
1
1
  require File.join(File.dirname(__FILE__), '<%= '../' * class_nesting_depth %>tap_test_helper.rb')
2
2
  require '<%= class_path.empty? ? file_name : File.join(class_path, file_name) %>'
3
3
 
4
- class <%= class_name_without_nesting %>Test < Test::Unit::TestCase
4
+ class <%= class_name %>Test < Test::Unit::TestCase
5
5
  acts_as_tap_test
6
6
 
7
7
  def test_<%= file_name %>
8
- t = <%= class_name %>.new nil, :label => 'key'
8
+ t = <%= class_name %>.new nil, :key => 'value'
9
9
 
10
10
  # specify application options
11
- with_options(:quiet => true) do
11
+ with_options(:quiet => true, :debug => true) do
12
12
 
13
13
  # run the task with some inputs
14
- app.run(t, "one", "two")
15
-
16
- # check the configuration and outputs
17
- assert_equal({:label => 'key'}, t.config)
18
- assert_outputs(t => [
19
- "key: one was processed",
20
- "key: two was processed"])
21
-
14
+ t.enq("one")
15
+ app.run
16
+
17
+ # check the configuration and outputs
18
+ assert_equal({:key => 'value'}, t.config)
19
+ assert_audit_equal ExpAudit[[nil, "one"], [t, "one was processed with value"]], app._results(t).first
20
+
22
21
  end
23
22
  end
24
23
 
@@ -9,7 +9,7 @@ class <%= class_name_without_nesting %> < Tap::Workflow
9
9
  def workflow
10
10
  # Define the workflow entry and exit points,
11
11
  # as well as the workflow logic.
12
- self.entry_point = Task.new
12
+ self.entry_point = Tap::Task.new
13
13
 
14
14
  # app.sequence(entry_point, 'another/task')
15
15
  end
@@ -1,7 +1,7 @@
1
1
  require File.join(File.dirname(__FILE__), '<%= '../' * class_nesting_depth %>tap_test_helper.rb')
2
2
  require '<%= class_path.empty? ? file_name : File.join(class_path, file_name) %>'
3
3
 
4
- class <%= class_name_without_nesting %>Test < Test::Unit::TestCase
4
+ class <%= class_name %>Test < Test::Unit::TestCase
5
5
  acts_as_tap_test
6
6
 
7
7
  end
@@ -0,0 +1,8 @@
1
+ # this is the same code as in rake/rake_test_loader.rb
2
+ # except it duplicates ARGV before iterating over it.
3
+ #
4
+ # This prevents an error in Ruby 1.9 when one of the
5
+ # loaded files attempts to modify ARGV. In that case
6
+ # you get an error like: 'can't modify array during
7
+ # iteration (RuntimeError)'
8
+ ARGV.dup.each { |f| load f unless f =~ /^-/ }
@@ -0,0 +1,55 @@
1
+ # NO idea why this prevents an error with @ruby_opts=nil,
2
+ # or even how @ruby_opts could be nil, on ruby 1.9 with
3
+ # rake test and tdoc. It does, though.
4
+ require 'pp'
5
+
6
+ module Rake # :nodoc:
7
+
8
+ class TestTask < TaskLib # :nodoc:
9
+
10
+ # Patch for TestTask#define in 'rake\testtask.rb'
11
+ #
12
+ # This patch lets you specify Windows-style paths in lib
13
+ # (ie with spaces and slashes) and to do something like:
14
+ #
15
+ # Rake::TestTask.new(:test) do |t|
16
+ # t.libs = $: << 'lib'
17
+ # end
18
+ #
19
+ # Using this patch you can specify additional load paths
20
+ # for the test from the command line using --lib-dir
21
+ #
22
+ def define
23
+ lib_opt = @libs.collect {|f| "-I\"#{File.expand_path(f)}\""}.join(' ')
24
+ desc "Run tests" + (@name==:test ? "" : " for #{@name}")
25
+ task @name do
26
+ run_code = ''
27
+ RakeFileUtils.verbose(@verbose) do
28
+ run_code =
29
+ case @loader
30
+ when :direct
31
+ "-e 'ARGV.each{|f| load f}'"
32
+ when :testrb
33
+ "-S testrb #{fix}"
34
+ when :rake
35
+ rake_loader
36
+ end
37
+ @ruby_opts.unshift(lib_opt)
38
+ @ruby_opts.unshift( "-w" ) if @warning
39
+ ruby @ruby_opts.join(" ") +
40
+ " \"#{run_code}\" " +
41
+ file_list.collect { |fn| "\"#{fn}\"" }.join(' ') +
42
+ " #{option_list}"
43
+ end
44
+ end
45
+ self
46
+ end
47
+
48
+ # Loads in the patched rake_test_loader to avoid the ARGV
49
+ # modification error, which arises within TDoc.
50
+ def rake_loader # :nodoc:
51
+ File.expand_path(File.join(File.dirname(__FILE__), 'rake_test_loader.rb'))
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,51 @@
1
+ module Test
2
+ module Unit
3
+ module Util # :nodoc:
4
+ module BacktraceFilter # :nodoc:
5
+
6
+ if method_defined?(:filter_backtrace)
7
+ alias :tap_original_filter_backtrace :filter_backtrace
8
+ end
9
+
10
+ # This is a slightly-modified version of the default BacktraceFilter
11
+ # provided in the Ruby 1.9 distribution. It solves the issue documented
12
+ # below, and hopefully will not be necessary when Ruby 1.9 is stable.
13
+ #
14
+ def filter_backtrace(backtrace, prefix=nil)
15
+ return ["No backtrace"] unless(backtrace)
16
+ split_p = if(prefix)
17
+ prefix.split(TESTUNIT_FILE_SEPARATORS)
18
+ else
19
+ TESTUNIT_PREFIX
20
+ end
21
+ match = proc do |e|
22
+ split_e = e.split(TESTUNIT_FILE_SEPARATORS)[0, split_p.size]
23
+ next false unless(split_e[0..-2] == split_p[0..-2])
24
+ split_e[-1].sub(TESTUNIT_RB_FILE, '') == split_p[-1]
25
+ end
26
+
27
+ # The Ruby 1.9 issue is that sometimes backtrace is a String
28
+ # and String is no longer Enumerable (hence it doesn't respond
29
+ # respond to detect). Arrayify to solve.
30
+ backtrace = [backtrace] unless backtrace.kind_of?(Array)
31
+
32
+ return backtrace unless(backtrace.detect(&match))
33
+ found_prefix = false
34
+ new_backtrace = backtrace.reverse.reject do |e|
35
+ if(match[e])
36
+ found_prefix = true
37
+ true
38
+ elsif(found_prefix)
39
+ false
40
+ else
41
+ true
42
+ end
43
+ end.reverse
44
+ new_backtrace = (new_backtrace.empty? ? backtrace : new_backtrace)
45
+ new_backtrace = new_backtrace.reject(&match)
46
+ new_backtrace.empty? ? backtrace : new_backtrace
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ require 'date/format'
2
+
3
+ # Taken directly from the Ruby 1.8.6 standard library. Ruby 1.9 has apparently
4
+ # eliminated this class, but it is currently (2008-02-08) required by ActiveSupport.
5
+ #
6
+ # Making this file available to ActiveSupport allows testing under Ruby 1.9,
7
+ # but this patch should be unnecessary when ActiveSupport upgrades and becomes
8
+ # compatible with Ruby 1.9
9
+ module ParseDate # :nodoc:
10
+ def parsedate(str, comp=false)
11
+ Date._parse(str, comp).
12
+ values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :wday)
13
+ end
14
+
15
+ module_function :parsedate
16
+ end
@@ -1,4 +1,5 @@
1
1
  require 'tap/support/versions'
2
+ autoload(:FileUtils, 'fileutils')
2
3
 
3
4
  module Tap
4
5
 
@@ -10,14 +11,17 @@ module Tap
10
11
  # r = Root.new '/root_dir', :input => 'in', :output => 'out'
11
12
  #
12
13
  # # work with directories
13
- # r[:in] # => '/root_dir/in'
14
- # r[:out] # => '/root_dir/out'
15
- # r[:implicit] # => '/root_dir/implicit'
14
+ # r[:input] # => '/root_dir/in'
15
+ # r[:output] # => '/root_dir/out'
16
+ # r['implicit'] # => '/root_dir/implicit'
17
+ #
18
+ # # expanded paths are returned unchanged
19
+ # r[File.expand_path('expanded')] # => File.expand_path('expanded')
16
20
  #
17
21
  # # work with filepaths
18
- # fp = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
19
- # r.relative_filepath(:in, fp) # => 'path/to/file.txt'
20
- # r.translate(fp, :in, :out) # => '/root_dir/out/path/to/file.txt'
22
+ # fp = r.filepath(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
23
+ # r.relative_filepath(:input, fp) # => 'path/to/file.txt'
24
+ # r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
21
25
  #
22
26
  # # version filepaths
23
27
  # r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
@@ -29,50 +33,52 @@ module Tap
29
33
  # r.filepath(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
30
34
  #
31
35
  # By default, Roots are initialized to the present working directory (Dir.pwd).
32
- # As in the ':implicit' example, whenever Root needs to resolve an alias that is
33
- # not explicitly set, Root infers a path relative to the root directory.
36
+ # As in the 'implicit' example, Root infers a path relative to the root directory
37
+ # whenever it needs to resolve an alias that is not explicitly set. The only
38
+ # exceptions to this are fully expanded paths. These are returned unchanged.
34
39
  #
35
40
  # === Implementation Notes
36
41
  #
37
- # Internally Root stores expanded paths for both subdirectories and absolute paths in a
38
- # hash, 'paths', for quick lookup using the da (directory alias) keys. Expanding paths
39
- # ensures they remain constant even when the present working directory (Dir.pwd) changes.
42
+ # Internally Root stores expanded paths all aliased paths in the 'paths' hash.
43
+ # Expanding paths ensures they remain constant even when the present working
44
+ # directory (Dir.pwd) changes.
40
45
  #
41
- # Root keeps a separate hash mapping aliases to the subdirectory paths, 'directories'.
42
- # These relative filepaths allow reassignment of the expanded paths if and when the
43
- # root directory changes. By contrast, there is no separate data structure storing the
44
- # absolute paths. An absolute path can be distinguished from a subdirectory path by
45
- # aliases present in 'paths' but not 'directories'.
46
+ # Root keeps a separate 'directories' hash mapping aliases to their subdirectory paths.
47
+ # This hash allow reassignment if and when the root directory changes. By contrast,
48
+ # there is no separate data structure storing the absolute paths. An absolute path
49
+ # thus has an alias in 'paths' but not 'directories', whereas subdirectory paths
50
+ # have aliases in both.
46
51
  #
47
52
  # These features may be important to note when subclassing Root:
48
53
  # - root and all filepaths in 'paths' are expanded
49
- # - directory aliases and subdirectory paths are stored in 'directories'
54
+ # - subdirectory paths are stored in 'directories'
50
55
  # - absolute paths are present in 'paths' but not in 'directories'
51
56
  #
52
- class Root
57
+ class Root
58
+ # Regexp to match a windows-style root filepath.
59
+ WIN_ROOT_PATTERN = /^[A-z]:\//
60
+
53
61
  class << self
54
62
  include Support::Versions
55
63
 
56
64
  # Returns the filepath of path relative to dir. Both dir and path are
57
- # expanded before the relative filepath is determined. If strict=true
58
- # (the default), then relative_filepath raises an error if the path is
59
- # not relative to dir. Otherwise, the expanded path will be returned.
65
+ # expanded before the relative filepath is determined. An error is
66
+ # raised if the path is not relative to dir.
60
67
  #
61
68
  # Root.relative_filepath('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
62
69
  #
63
- def relative_filepath(dir, path, strict=true)
64
- dir = File.expand_path(dir)
65
- expanded = File.expand_path(path)
70
+ def relative_filepath(dir, path, dir_string=Dir.pwd)
71
+ expanded_dir = File.expand_path(dir, dir_string)
72
+ expanded_path = File.expand_path(path, dir_string)
66
73
 
67
- unless expanded[0...dir.length] == dir
68
- raise "\n#{expanded}\nis not relative to:\n#{dir}" if strict
69
- return expanded
74
+ unless expanded_path.index(expanded_dir) == 0
75
+ raise "\n#{expanded_path}\nis not relative to:\n#{expanded_dir}"
70
76
  end
71
77
 
72
78
  # use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
73
79
  # as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
74
80
  # empty string is returned
75
- expanded[(dir.length+1)..-1] || ""
81
+ expanded_path[( expanded_dir.chomp("/").length + 1)..-1] || ""
76
82
  end
77
83
 
78
84
  # Lists all unique paths matching the input glob patterns.
@@ -94,14 +100,86 @@ module Tap
94
100
  results
95
101
  end.flatten.uniq
96
102
  end
103
+
104
+ # Executes the block in the specified directory. Makes the directory, if
105
+ # necessary when mkdir is specified. Otherwise, indir raises an error
106
+ # for non-existant directories, as well as non-directory inputs.
107
+ def indir(dir, mkdir=false)
108
+ unless File.directory?(dir)
109
+ if !File.exists?(dir) && mkdir
110
+ FileUtils.mkdir_p(dir)
111
+ else
112
+ raise "non a directory: #{dir}"
113
+ end
114
+ end
115
+
116
+ pwd = Dir.pwd
117
+ begin
118
+ Dir.chdir(dir)
119
+ yield
120
+ ensure
121
+ Dir.chdir(pwd)
122
+ end
123
+ end
124
+
125
+ # The path root type indicating windows, *nix, or some unknown
126
+ # style of filepaths (:win, :nix, :unknown).
127
+ def path_root_type
128
+ @path_root_type ||= case
129
+ when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
130
+ when File.expand_path(".")[0] == ?/ then :nix
131
+ else :unknown
132
+ end
133
+ end
134
+
135
+ # Returns true if the input path appears to be an expanded path,
136
+ # based on Root.path_root_type.
137
+ #
138
+ # If root_type == :win returns true if the path matches
139
+ # WIN_ROOT_PATTERN.
140
+ #
141
+ # Root.expanded_path?('C:/path') # => true
142
+ # Root.expanded_path?('c:/path') # => true
143
+ # Root.expanded_path?('D:/path') # => true
144
+ # Root.expanded_path?('path') # => false
145
+ #
146
+ # If root_type == :nix, then expanded? returns true if
147
+ # the path begins with '/'.
148
+ #
149
+ # Root.expanded_path?('/path') # => true
150
+ # Root.expanded_path?('path') # => false
151
+ #
152
+ # Otherwise expanded_path? always returns nil.
153
+ def expanded_path?(path, root_type=path_root_type)
154
+ case root_type
155
+ when :win
156
+ path =~ WIN_ROOT_PATTERN ? true : false
157
+ when :nix
158
+ path[0] == ?/
159
+ else
160
+ nil
161
+ end
162
+ end
97
163
  end
98
164
 
99
165
  include Support::Versions
100
- attr_reader :root, :directories, :paths
101
166
 
102
- # Creates a new Root with the given root, directories, and absolute
103
- # paths. By default root is the current working directory and no
104
- # directories or absolute paths are specified.
167
+ # The root directory
168
+ attr_reader :root
169
+
170
+ # A hash of (alias, relative path) pairs for aliased subdirectories
171
+ attr_reader :directories
172
+
173
+ # A hash of (alias, expanded path) pairs for aliased subdirectories and absolute paths.
174
+ attr_reader :paths
175
+
176
+ # The filesystem root, inferred from self.root
177
+ # (ex '/' on *nix or something like 'C:/' on Windows).
178
+ attr_reader :path_root
179
+
180
+ # Creates a new Root with the given root directory, aliased directories
181
+ # and absolute paths. By default root is the present working directory
182
+ # and no aliased directories or absolute paths are specified.
105
183
  def initialize(root=Dir.pwd, directories={}, absolute_paths={})
106
184
  assign_paths(root, directories, absolute_paths)
107
185
  end
@@ -112,13 +190,21 @@ module Tap
112
190
  end
113
191
 
114
192
  # Sets the directories to those provided. 'root' and :root are reserved
115
- # directory keys and cannot be set using this method (use root= instead).
193
+ # and cannot be set using this method (use root= instead).
194
+ #
195
+ # r['alt'] # => File.join(r.root, 'alt')
196
+ # r.directories = {'alt' => 'dir'}
197
+ # r['alt'] # => File.join(r.root, 'dir')
116
198
  def directories=(dirs)
117
199
  assign_paths(root, dirs, absolute_paths)
118
200
  end
119
201
 
120
202
  # Sets the absolute paths to those provided. 'root' and :root are reserved
121
203
  # directory keys and cannot be set using this method (use root= instead).
204
+ #
205
+ # r['abs'] # => File.join(r.root, 'abs')
206
+ # r.absolute_paths = {'abs' => '/path/to/dir'}
207
+ # r['abs'] # => '/path/to/dir'
122
208
  def absolute_paths=(paths)
123
209
  assign_paths(root, directories, paths)
124
210
  end
@@ -132,9 +218,10 @@ module Tap
132
218
  abs_paths
133
219
  end
134
220
 
135
- # Sets a directory alias (da) for the path relative to the root directory.
136
- # The root directory cannot be set with this method (use root= instead).
137
- # Absolute filepaths can be set using the second syntax.
221
+ # Sets an alias for the subdirectory relative to the root directory.
222
+ # The aliases 'root' and :root cannot be set with this method
223
+ # (use root= instead). Absolute filepaths can be set using the
224
+ # second syntax.
138
225
  #
139
226
  # r = Root.new '/root_dir'
140
227
  # r[:dir] = 'path/to/dir'
@@ -144,13 +231,14 @@ module Tap
144
231
  # r[:abs] # => '/abs/path/to/dir'
145
232
  #
146
233
  #--
234
+ # Implementation Notes:
147
235
  # The syntax for setting an absolute filepath requires an odd use []=.
148
236
  # In fact the method recieves the arguments (:dir, true, '/abs/path/to/dir')
149
237
  # rather than (:dir, '/abs/path/to/dir', true), meaning that internally path
150
238
  # and absolute are switched when setting an absolute filepath.
151
239
  #++
152
- def []=(da, path, absolute=false)
153
- raise ArgumentError, "The directory key '#{da}' is reserved." if da.to_s == 'root'
240
+ def []=(dir, path, absolute=false)
241
+ raise ArgumentError, "The directory key '#{dir}' is reserved." if dir.to_s == 'root'
154
242
 
155
243
  # switch the paths if absolute was provided
156
244
  unless absolute == false
@@ -158,42 +246,49 @@ module Tap
158
246
  path = absolute
159
247
  absolute = switch
160
248
  end
161
-
249
+
162
250
  case
163
251
  when path.nil?
164
- @directories.delete(da)
165
- @paths.delete(da)
252
+ @directories.delete(dir)
253
+ @paths.delete(dir)
166
254
  when absolute
167
- @directories.delete(da)
168
- @paths[da] = File.expand_path(path)
255
+ @directories.delete(dir)
256
+ @paths[dir] = File.expand_path(path)
169
257
  else
170
- @directories[da] = path
171
- @paths[da] = File.expand_path(File.join(root, path))
172
- end
258
+ @directories[dir] = path
259
+ @paths[dir] = File.expand_path(File.join(root, path))
260
+ end
173
261
  end
174
262
 
175
263
  # Returns the expanded path for the specified alias. If the alias
176
- # has not been set, then the path is inferred to be 'root/da'.
264
+ # has not been set, then the path is inferred to be 'root/dir' unless
265
+ # the path is relative to path_root. These paths are returned
266
+ # directly.
177
267
  #
178
- # r = Root.new '/root_dir'
179
- # r[:dir] = 'path/to/dir'
180
- # r[:dir] # => '/root_dir/path/to/dir'
268
+ # r = Root.new '/root_dir', :dir => 'path/to/dir'
269
+ # r[:dir] # => '/root_dir/path/to/dir'
181
270
  #
182
- # r[:unset] = nil
183
- # r[:unset] # => '/root_dir/unset'
271
+ # r.path_root # => '/'
272
+ # r['relative/path'] # => '/root_dir/relative/path'
273
+ # r['/expanded/path'] # => '/expanded/path'
184
274
  #
185
- def [](da)
186
- self.paths[da] || File.expand_path(File.join(root, da.to_s))
275
+ def [](dir)
276
+ path = self.paths[dir]
277
+ return path unless path == nil
278
+
279
+ dir = dir.to_s
280
+ Root.expanded_path?(dir) ? dir : File.expand_path(File.join(root, dir))
187
281
  end
188
282
 
189
283
  # Constructs expanded filepaths relative to the path of the specified alias.
190
- def filepath(da, *filename)
191
- File.expand_path(File.join(self[da], *filename))
284
+ def filepath(dir, *filename)
285
+ # TODO - consider filename.compact so nils will not raise errors
286
+ File.expand_path(File.join(self[dir], *filename))
192
287
  end
193
288
 
194
289
  # Retrieves the filepath relative to the path of the specified alias.
195
- def relative_filepath(da, filepath, strict=true)
196
- Root.relative_filepath(self[da], filepath, strict)
290
+ def relative_filepath(dir, filepath)
291
+ Root.relative_filepath(self[dir], filepath)
197
292
  end
198
293
 
199
294
  # Generates a target filepath translated from the aliased input dir to
@@ -202,23 +297,28 @@ module Tap
202
297
  #
203
298
  # fp = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
204
299
  # r.translate(fp, :in, :out) # => '/root_dir/out/path/to/file.txt'
205
- def translate(filepath, input_da, output_da)
206
- filepath(output_da, relative_filepath(input_da, filepath))
300
+ def translate(filepath, input_dir, output_dir)
301
+ filepath(output_dir, relative_filepath(input_dir, filepath))
207
302
  end
208
303
 
209
304
  # Lists all files in the aliased dir matching the input patterns. Patterns
210
305
  # should be valid inputs for +Dir.glob+. If no patterns are specified, lists
211
306
  # all files/folders matching '**/*'.
212
- def glob(da, *patterns)
307
+ def glob(dir, *patterns)
213
308
  patterns << "**/*" if patterns.empty?
214
- patterns.collect! {|pattern| filepath(da, pattern)}
309
+ patterns.collect! {|pattern| filepath(dir, pattern)}
215
310
  Root.glob(*patterns)
216
311
  end
217
312
 
218
313
  # Lists all versions of filename in the aliased dir matching the version patterns.
219
314
  # If no patterns are specified, then all versions of filename will be returned.
220
- def vglob(da, filename, *vpatterns)
221
- Root.vglob(filepath(da, filename), *vpatterns)
315
+ def vglob(dir, filename, *vpatterns)
316
+ Root.vglob(filepath(dir, filename), *vpatterns)
317
+ end
318
+
319
+ # Executes the provided block in the specified directory using Root.indir.
320
+ def indir(dir, mkdir=false)
321
+ Root.indir(self[dir], mkdir) { yield }
222
322
  end
223
323
 
224
324
  private
@@ -228,9 +328,14 @@ module Tap
228
328
  @root = File.expand_path(root)
229
329
  @directories = {}
230
330
  @paths = {'root' => @root, :root => @root}
231
-
232
- directories.each_pair {|da, path| self[da] = path }
233
- absolute_paths.each_pair {|da, path| self[da, true] = path }
331
+
332
+ @path_root = File.dirname(@root)
333
+ while @path_root != (parent = File.dirname(@path_root))
334
+ @path_root = parent
335
+ end
336
+
337
+ directories.each_pair {|dir, path| self[dir] = path }
338
+ absolute_paths.each_pair {|dir, path| self[dir, true] = path }
234
339
  end
235
340
 
236
341
  end