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/lib/tap/test.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'test/unit'
2
- require 'tap/test/inference_methods'
2
+ require 'tap/test/file_methods'
3
3
  require 'tap/test/subset_methods'
4
4
 
5
5
  module Test # :nodoc:
@@ -7,21 +7,21 @@ module Test # :nodoc:
7
7
  class TestCase
8
8
  class << self
9
9
  # Causes a unit test to act as a tap test -- resulting in the following:
10
- # - setup of the test as an io_test (see documentation)
11
- # - TapTest::InstanceMethods are included, providing methods to easily run
12
- # tap tasks and test their outputs
10
+ # - setup using acts_as_file_test
11
+ # - inclusion of Tap::Test::SubsetMethods
12
+ # - inclusion of Tap::Test::InstanceMethods
13
13
  #
14
14
  # Note: Unless otherwise specified, +acts_as_tap_test+ infers a root directory
15
- # based on the calling file. Therefore, to keep your directory structure from
16
- # referencing an unexpected locations, be sure to specify the root directory
17
- # explicitly if you call +acts_as_tap_test+ from a file that is NOT your test file.
15
+ # based on the calling file. Be sure to specify the root directory explicitly
16
+ # if you call acts_as_file_test from a file that is NOT meant to be test file.
18
17
  def acts_as_tap_test(options={})
19
- options = {:root => infer_root}.merge(options.symbolize_keys)
20
- acts_as_inference_test(options)
18
+ options = {:root => file_test_root}.merge(options.symbolize_keys)
19
+ acts_as_file_test(options)
21
20
 
22
21
  include Tap::Test::SubsetMethods
23
22
  include Tap::Test::InstanceMethods
24
23
  end
24
+
25
25
  end
26
26
  end
27
27
  end
@@ -29,13 +29,13 @@ end
29
29
 
30
30
  module Tap
31
31
 
32
- #
32
+ # == UNDER CONSTRUCTION
33
33
  module Test
34
34
 
35
35
  # Used during check_audit to hold the sources and values of an audit
36
36
  # in the correct order. Oriented so that the next value to be checked
37
37
  # is at the top of the stack.
38
- class AuditStack
38
+ class AuditStack # :nodoc:
39
39
  attr_reader :test
40
40
 
41
41
  def initialize(test)
@@ -58,70 +58,21 @@ module Tap
58
58
  end
59
59
  end
60
60
 
61
- module InstanceMethods
62
- attr_accessor :runlist
61
+ module InstanceMethods # :nodoc:
63
62
 
64
63
  # Setup clears the test using clear_tasks and assures that Tap::App.instance
65
64
  # is the test-specific application.
66
65
  def setup
67
66
  super
68
67
  Tap::App.instance = app
69
- clear_runlist
68
+ app.queue.clear
70
69
  end
71
70
 
72
71
  # Returns the test-specific application.
73
72
  def app
74
- @app ||= Tap::App.new(:root => ifs.root, :directories => ifs.directories)
75
- end
76
-
77
- # Clears all declared tasks, sets the application trace option to false, makes directories (if flagged
78
- # and as needed), and clears the runlist.
79
- def clear_runlist
80
- # clear the attributes
81
- @runlist = []
82
- end
83
-
84
- # A tracing procedure. echo adds input to runlist then returns input.
85
- def echo
86
- lambda do |task, input|
87
- @runlist << input
88
- input
89
- end
73
+ @app ||= Tap::App.new(:root => trs.root, :directories => trs.directories)
90
74
  end
91
75
 
92
- # A tracing procedure for numeric inputs. add_one adds the input to
93
- # runlist then returns input + 1.
94
- def add_one
95
- lambda do |task, input|
96
- @runlist << input
97
- input += 1
98
- end
99
- end
100
-
101
- # A convenience testing method asserting that runlist array is equal to the inputs
102
- # def assert_runlist(*expected)
103
- # assert_equal expected, runlist
104
- # end
105
-
106
- # A convenience testing method asserting that sorted runlist array is equal to the
107
- # sorted inputs. Sorting the runlist is unpreferred, because you lose information about
108
- # the order of items added to the runlist.
109
- #
110
- # However, at times there is no good alternative because the order of execution
111
- # may be determined by a hash.
112
- # def assert_sorted_runlist(*expected)
113
- # two-step in case the elements cannot be sorted (as in the case of hashes)
114
- # found_all = (expected.length == runlist.length)
115
- # expected.each do |e|
116
- # break unless found_all
117
-
118
- # next if runlist.include?(e)
119
- # found_all = false
120
- # end
121
-
122
- # assert found_all, "Expected: #{PP.pp(expected, '')}Was: #{PP.pp(runlist, '')}"
123
- # end
124
-
125
76
  # Recieves a hash of task => expected pairs. Asserts that the last inputs for
126
77
  # each task are equal to the expected inputs.
127
78
  def assert_inputs(hash)
@@ -226,80 +177,87 @@ module Tap
226
177
  end
227
178
  end
228
179
 
229
- # Executes the block having applied the specified options to app.
230
- # Ensures that current options values are restored to app after
231
- # the block completes or raises an error.
232
- def with_options(options, app=self.app, &block)
233
- hold = {}
180
+ # Applies the input options to the specified app for the duration
181
+ # of the block. Unless merge_with_existing is false, the input
182
+ # options will be merged with the existing options; otherwise
183
+ # the app options will be reconfigured to just the inputs.
184
+ #
185
+ # app = Tap::App.new(:options => {:one => 1, :two => 2})
186
+ #
187
+ # with_options({:one => 'one'}, app) do
188
+ # app.options.marshal_dump # => {:one => 'one', :two => 2}
189
+ # end
190
+ # app.options.marshal_dump # => {:one => 1, :two => 2}
191
+ #
192
+ def with_options(options, app=self.app, merge_with_existing=true, &block)
193
+ app_config = {:options => options}
194
+ with_config(app_config, app, merge_with_existing, &block)
195
+ end
196
+
197
+ # Applies the input configurations to the specified app for the
198
+ # duration of the block. Unless merge_with_existing is false,
199
+ # the input configurations will be merged with the existing
200
+ # configurations; otherwise the app will be reconfigured to
201
+ # using the inputs as specified.
202
+ #
203
+ # app = Tap::App.new(:directories => {:dir => 'dir', :alt => 'alt_dir'})
204
+ # tmp_config = {
205
+ # :directories => {:alt => 'another', :new => 'new_dir'},
206
+ # :options => {:one => 1}}
207
+ #
208
+ # with_config(tmp_config, app) do
209
+ # app.directories # => {:dir => 'dir', :alt => 'another', :new => 'new_dir'}
210
+ # app.options.marshal_dump # => {:one => 1}
211
+ # end
212
+ # app.directories # => {:dir => 'dir', :alt => 'alt_dir'}
213
+ # app.options.marshal_dump # => {}
214
+ #
215
+ def with_config(app_config, app=self.app, merge_with_existing=true, &block)
234
216
  begin
235
- options.each_pair do |op, val|
236
- hold[op] = app.options.send(op)
237
- app.options.send("#{op}=", val)
217
+ hold = app.config
218
+ if merge_with_existing
219
+ hold.each_pair do |key, value|
220
+ next unless app_config.has_key?(key)
221
+ next unless value.kind_of?(Hash)
222
+
223
+ app_config[key] = value.merge(app_config[key])
224
+ end
238
225
  end
239
-
226
+ app.reconfigure(app_config)
227
+
240
228
  yield block if block_given?
241
229
  ensure
242
- hold.each_pair { |op, val| app.options.send("#{op}=", val) }
230
+ app.reconfigure(hold)
243
231
  end
244
232
  end
245
-
246
- def file_task_test(task, options={})
247
- make_directory_structure
248
-
249
- raise "Not a FileTask" unless task.kind_of?(Tap::FileTask)
250
-
251
- options = {
252
- :input_files => nil,
253
- :expected_files => nil,
254
- :strict => true#,
255
- #:set_dirname => true
256
- }.merge(options)
257
-
258
- input_files = options.delete(:input_files)
259
- input_files = [input_files] unless input_files.nil? || input_files.kind_of?(Array)
260
-
261
- expected_files = options.delete(:expected_files)
262
- expected_files = [expected_files] unless expected_files.nil? || expected_files.kind_of?(Array)
263
233
 
264
- strict = options.delete(:strict)
265
- # set_dirname = options.delete(:set_dirname)
266
-
267
- # redirect to infer_dir(:output)
268
-
269
- current_block = task.inference_block
270
- task.inference(true) do |root, dir, path|
271
- if current_block
272
- current_block.call(infer_dir(:output), dir, path)
273
- else
274
- infer_filepath(:output, path)
275
- end
276
- end
277
-
278
- with_options(options, task.app) do
279
- # task.dirname = infer_dir(:output) if set_dirname
280
-
281
- input_files = infer_glob(:input) if input_files.nil?
282
- flunk "No input files specified." if input_files.empty?
283
-
284
- output_files = task.execute(*input_files).collect {|a| a._current}
285
- expected_files = infer_glob(:expected) if expected_files.nil?
234
+ # assert_expected_result_files runs the input task using the
235
+ # input app config, submitting all method input files for
236
+ # processing. the assertions returns true only if all the
237
+ # expected and output files are equal. As such it is very
238
+ # convenient for file transform tasks.
239
+ #
240
+ # The default application config sets the app root to
241
+ # method_root, silences execution and directs app[:data] to
242
+ # the method output directory.
243
+ #
244
+ # assert_expected_result_files makes the input/expected
245
+ # directories if they do not exist. By default all the
246
+ # output files will be cleaned up during teardown. See
247
+ # FileMethods for more information.
248
+ def assert_expected_result_files(task, app_config={})
249
+ make_test_directories
286
250
 
287
- if strict
288
- # assure there are no missing or extra output files
289
- assert_equal infer_glob(:output), output_files, "Missing or extra output files"
290
-
291
- # check that the relative filepaths are the same
292
- translated_files = output_files.collect {|file| infer_translate(file, :output, :expected)}
293
- assert_equal expected_files, translated_files, "Missing or extra expected files"
294
- else
295
- assert_equal expected_files.length, output_files.length, "Missing or extra expected files"
296
- end
251
+ app_config = {
252
+ :root => method_root,
253
+ :options => {:quiet => true},
254
+ :absolute_paths => {:data => method_filepath(:output)}
255
+ }.merge(app_config)
297
256
 
298
- # check that the expected and output files are equal
299
- 0.upto(expected_files.length-1) do |i|
300
- unless FileUtils.compare_file(expected_files[i], output_files[i])
301
- flunk "File compare failed:\n<#{expected_files[i]}> not equal to\n<#{output_files[i]}>"
302
- end
257
+ with_config(app_config, task.app) do
258
+ assert_output_files_equal do |input_files|
259
+ output_files = task.execute(*input_files).collect {|a| a._current}
260
+ block_given? ? yield(output_files) : output_files
303
261
  end
304
262
  end
305
263
  end
@@ -1,16 +1,27 @@
1
1
  module Tap
2
2
  module Test
3
+
4
+ # Provides a method for case-insensitive access to the ENV variables
3
5
  module EnvVars
4
6
 
5
- # Access to the case-insensitive ENV variables
7
+ # Access to the case-insensitive ENV variables. Raises an error
8
+ # if multiple case-insensitive values are defined in ENV.
6
9
  def env(type)
7
10
  type = type.downcase
8
- ENV.each_pair do |key, value|
9
- return value if key.downcase == type
11
+ selected = ENV.select {|key, value| key.downcase == type}
12
+
13
+ case selected.length
14
+ when 0 then nil
15
+ when 1 then selected[0][1]
16
+ else
17
+ raise "Multiple env values for '#{type}'"
10
18
  end
11
- nil
12
19
  end
13
-
20
+
21
+ # Returns true if the env_var(var) is set and matches /^true%/i
22
+ def env_true?(var)
23
+ env(var) && env(var) =~ /^true$/i
24
+ end
14
25
  end
15
26
  end
16
27
  end
@@ -0,0 +1,373 @@
1
+ require 'tap/root'
2
+ require 'tap/test/env_vars'
3
+ require 'test/unit'
4
+ require 'fileutils'
5
+
6
+ module Test # :nodoc:
7
+ module Unit # :nodoc:
8
+ # Methods extending TestCase. See the TestTutorial for more information.
9
+ class TestCase
10
+ class << self
11
+
12
+ # Causes a TestCase to act as a file test, by instantiating a class Tap::Root
13
+ # (trs), and including the FileMethods. The root and directories used to
14
+ # instantiate trs can be specified as options. By default file_test_root
15
+ # and default_directories will be used.
16
+ #
17
+ # class FileDependentTest < Test::Unit::TestCase
18
+ # acts_as_file_test(:root => file_test_root, :directories => default_directories)
19
+ # end
20
+ #
21
+ # Note: file_test_root determines a root directory +based on the calling file+.
22
+ # Be sure to specify the root directory explicitly if you call acts_as_file_test
23
+ # from a file that is NOT meant to be test file.
24
+ def acts_as_file_test(options={})
25
+ options = {
26
+ :root => file_test_root,
27
+ :directories => {}
28
+ }.merge(options.symbolize_keys)
29
+
30
+ directories = default_directories.merge(options[:directories])
31
+ trs = Tap::Root.new(options[:root], directories)
32
+
33
+ write_inheritable_attribute(:trs, trs)
34
+ class_inheritable_reader :trs
35
+
36
+ include Tap::Test::FileMethods
37
+ end
38
+
39
+ # The default directories for a file test:
40
+ #
41
+ # :input => 'input'
42
+ # :output => 'output'
43
+ # :expected => 'expected'
44
+ def default_directories
45
+ {:input => 'input', :output => 'output', :expected => 'expected'}
46
+ end
47
+
48
+ # Infers the test root directory from the calling file. Ex:
49
+ # 'some_class.rb' => 'some_class'
50
+ # 'some_class_test.rb' => 'some_class'
51
+ def file_test_root
52
+ # the calling file is not the direct caller of +method_root+... this method is
53
+ # only accessed from within another method call, hence the target caller is caller[1]
54
+ # rather than caller[0].
55
+
56
+ # caller[1] is considered the calling file (which should be the test case)
57
+ # note that the output of calller.first is like:
58
+ # ./path/to/file.rb:10
59
+ # ./path/to/file.rb:10:in 'method'
60
+ calling_file = caller[1].gsub(/:\d+[:in .*]?$/, "" )
61
+ calling_file.chomp!("#{File.extname(calling_file)}")
62
+ calling_file.chomp("_test")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ module Tap
70
+ module Test
71
+
72
+ # == Under Construction
73
+ #
74
+ # FileMethods sets up a TestCase with methods for accessing and utilizing
75
+ # test-specific files and directories. Each class including FileMethods
76
+ # is set up with a Tap::Root structure (trs) that mediates the creation of
77
+ # test method filepaths. FileMethods extends TestCase with the
78
+ # acts_as_file_test to include the FileMethods and initialize the trs.
79
+ #
80
+ # class FileDependentTest < Test::Unit::TestCase
81
+ # acts_as_file_test
82
+ #
83
+ # def test_file_transform
84
+ # trs.root # => "."
85
+ # method_root # => "./file_dependent_test/test_file_transform"
86
+ # method_dir(:input) # => "./file_dependent_test/test_file_transform/input"
87
+ # end
88
+ # end
89
+ #
90
+ # == File Transform Tests
91
+ #
92
+ # FileMethods is specifically designed for tests that transform a set of input
93
+ # files into output files. For this type of test, input and expected files can
94
+ # placed into their respective directories then used within the context of
95
+ # assert_output_files_equal to ensure the output files are equal to the expected
96
+ # files.
97
+ #
98
+ # For example, lets define a test that transforms input files into output files
99
+ # in a trivial way, simply by replacing 'input' with 'output' in the file.
100
+ #
101
+ # class FileTest < Test::Unit::TestCase
102
+ # acts_as_file_test
103
+ #
104
+ # def test_transform
105
+ # assert_output_files_equal do |input_files|
106
+ # input_files.collect do |filepath|
107
+ # input = File.read(filepath)
108
+ # output_file = method_filepath(:ouput_files, File.basename(filepath))
109
+ #
110
+ # File.open(output_file, "w") do |f|
111
+ # f << input.gsub(/input/, "output")
112
+ # end
113
+ # end
114
+ # end
115
+ # end
116
+ # end
117
+ #
118
+ # Now make some input and expected files for the 'test_transform' method:
119
+ #
120
+ # FileUtils.mkdir_p("file_test/test_transform/input_files")
121
+ # FileUtils.mkdir_p("file_test/test_transform/expected_files")
122
+ #
123
+ # [file_one.txt", file_two.txt"].each_with_index do |basename, i|
124
+ # input = "file_test/test_transform/input_files/" + basename
125
+ # File.open(input, "w") {|f| f << "test input #{i}"}
126
+ #
127
+ # expected = "file_test/test_transform/expected_files/" + basename
128
+ # File.open(expected, "w") {|f| f << "test output #{i}"}
129
+ # end
130
+ #
131
+ # When you run the FileTest tests, the test_transform test will pass
132
+ # the existing input_files to the assert_output_files_equal block. The
133
+ # block makes output files in "file_test/test_transform/output_files",
134
+ # and then assert_output_files_equal compares the files and finds they
135
+ # are equal. As a result the test passes.
136
+ #
137
+ # The test fails if you alter the expected file contents, add/remove
138
+ # expected files, or in some other way make these files unequal.
139
+ #
140
+ # When the test completes, the default teardown method will clean up the
141
+ # output_files directory. For ease in debugging, ENV variable flags can
142
+ # be specified to keep all output files (KEEP_OUTPUTS) or to keep the
143
+ # output files for just the tests that fail (KEEP_FAILURES). These flags
144
+ # can easily be specified from the command line when running a rake task:
145
+ #
146
+ # % tap run test keep_failures=true
147
+ #
148
+ module FileMethods
149
+ include Tap::Test::EnvVars
150
+
151
+ # Convenience accessor for the test root structure
152
+ def trs
153
+ self.class.trs
154
+ end
155
+
156
+ # Creates the trs.directories, specific to the method calling make_test_directories
157
+ def make_test_directories
158
+ trs.directories.values.each do |dir|
159
+ FileUtils.mkdir_p( File.join(trs.root, method_name, dir) )
160
+ end
161
+ end
162
+
163
+ # Setup deletes the the output directory if it exists, and tries to remove the
164
+ # method root directory so the directory structure is reset before running the
165
+ # test, even if outputs were left over from previous tests.
166
+ def setup
167
+ super
168
+ clear_method_dir(:output)
169
+ try_remove_dir(method_root)
170
+ end
171
+
172
+ # Teardown deletes the the output directories unless flagged otherwise. Note
173
+ # that teardown also checks the environment variables for flags. To keep all outputs
174
+ # (or failures) for all tests, flag keep outputs from the command line like:
175
+ #
176
+ # % tap run test KEEP_OUTPUTS=true
177
+ # % tap run test KEEP_FAILURES=true
178
+ def teardown
179
+ # clear out the output folder if it exists, unless flagged otherwise
180
+ unless env("KEEP_OUTPUTS") || (!@test_passed && env("KEEP_FAILURES"))
181
+ begin
182
+ clear_method_dir(:output)
183
+ rescue
184
+ raise("teardown failure: could not remove output files")
185
+ end
186
+ end
187
+
188
+ try_remove_dir(method_root)
189
+ try_remove_dir(trs.root)
190
+ end
191
+
192
+ # The method_root directory is defined as trs.filepath(method_name)
193
+ def method_root(method=method_name)
194
+ trs.filepath(method)
195
+ end
196
+
197
+ # The method directory is defined as 'dir/method', where method is the calling method
198
+ # by default. method_dir returns the method directory if it exists, otherwise it returns
199
+ # trs[dir].
200
+ def method_dir(dir, method=method_name)
201
+ File.join(method_root(method), trs.directories[dir] || dir.to_s)
202
+ end
203
+
204
+ # Returns a glob of files matching the input pattern, underneath the method directory
205
+ # if it exists, otherwise the <tt>trs[dir]</tt> directory.
206
+ def method_glob(dir, *patterns)
207
+ dir = trs.relative_filepath(:root, method_dir(dir))
208
+ trs.glob(dir, *patterns)
209
+ end
210
+
211
+ # Returns a filepath constructed from the method directory if it exists,
212
+ # otherwise the filepath will be constructed from <tt>trs[dir]</tt>.
213
+ def method_filepath(dir, *filenames)
214
+ File.join(method_dir(dir), *filenames)
215
+ end
216
+
217
+ # Removes the method directory from the input filepath, returning the resuting filename.
218
+ # If the method directory does not exist, <tt>trs[dir]</tt> will be removed.
219
+ def method_relative_filepath(dir, filepath)
220
+ dir = trs.relative_filepath(:root, method_dir(dir))
221
+ trs.relative_filepath(dir, filepath)
222
+ end
223
+
224
+ # Returns an output file corresponding to the input file, translated from the
225
+ # input directory to the output directory.
226
+ #
227
+ # If the input method directory exists, it will be removed from the filepath.
228
+ # If the output method directory exists, it will be inserted in the filepath.
229
+ def method_translate(filepath, input_dir, output_dir)
230
+ input_dir = trs.relative_filepath(:root, method_dir(input_dir))
231
+ output_dir = trs.relative_filepath(:root, method_dir(output_dir))
232
+ trs.translate(filepath, input_dir, output_dir)
233
+ end
234
+
235
+ # Attempts to recursively remove the specified method directory and all
236
+ # files within it. Raises an error if the removal does not succeed.
237
+ def clear_method_dir(dir)
238
+ # clear out the folder if it exists
239
+ dir_path = method_dir(dir, method_name)
240
+ FileUtils.rm_r(dir_path) if File.exists?(dir_path)
241
+ end
242
+
243
+ # Attempts to remove the specified directory. The root
244
+ # will not be removed if the directory does not exist, or
245
+ # is not empty.
246
+ def try_remove_dir(dir)
247
+ # Remove the directory if possible
248
+ begin
249
+ FileUtils.rmdir(dir) if File.exists?(dir) && Dir.glob(File.join(dir, "*")).empty?
250
+ rescue
251
+ # rescue cases where there is a hidden file, for example .svn
252
+ end
253
+ end
254
+
255
+ # Generates a temporary filepath formatted like "output_dir\filename.pid.n.ext" where n
256
+ # is a counter that will be incremented from until a non-existant filepath is achieved.
257
+ #
258
+ # Notes:
259
+ # - By default filename is the calling method
260
+ # - The extension is chomped off the end of the filename
261
+ # - If the directory for the filepath does not exist, the directory will be created
262
+ # - Like all files in the output directory, tempfiles will be deleted by the default
263
+ # +teardown+ method
264
+ def tempfile(filename=method_name)
265
+ n = 0
266
+ ext = File.extname(filename)
267
+ basename = filename.chomp(ext)
268
+ filepath = make_tmpname(basename, n, ext)
269
+ while File.exists?(filepath)
270
+ n += 1
271
+ filepath = make_tmpname(basename, n, ext)
272
+ end
273
+
274
+ dirname = File.dirname(filepath)
275
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
276
+ filepath
277
+ end
278
+
279
+ # Asserts that each pair of input files are equal, using FileUtils.cmp.
280
+ def files_equal?(a, b)
281
+ FileUtils.cmp(a, b)
282
+ end
283
+
284
+ # Asserts that each pair of input files produce an equivalent object when
285
+ # loaded as a yaml file.
286
+ def yml_equal?(a, b)
287
+ YAML.load_file(a) == YAML.load_file(b)
288
+ end
289
+
290
+ # Yields to the input block for each pair of input files. An error is raised
291
+ # if the input arrays do not have equal numbers of entries.
292
+ def each_pair(a, b, &block)
293
+ a = [a] unless a.kind_of?(Array)
294
+ b = [b] unless b.kind_of?(Array)
295
+
296
+ raise ArgumentError, "The input arrays must have an equal number of entries." unless a.length == b.length
297
+ a.each_index do |index|
298
+ yield(a[index], b[index])
299
+ end
300
+ end
301
+
302
+ def assert_output_files_equal(options={}) # :yields: input_files
303
+ make_test_directories
304
+
305
+ options = {
306
+ :input_files => nil,
307
+ :expected_files => nil,
308
+ :require_expected_files => true,
309
+ :check_for_missing_outputs => true,
310
+ :iterate_inputs => false,
311
+ :include_directories => false
312
+ }.merge(options)
313
+
314
+ # set or search for input and expected files, and arrayify
315
+ input_files, expected_files = [:input, :expected].collect do |key|
316
+ options_key = "#{key}_files".to_sym
317
+
318
+ files = options.delete(options_key)
319
+ files = method_glob(key) if files.nil?
320
+ files = (files.kind_of?(Array) ? files : [files])
321
+ files.collect! {|file| File.expand_path(file) }
322
+ unless options[:include_directories]
323
+ files.delete_if {|file| File.directory?(file)}
324
+ end
325
+ files
326
+ end
327
+
328
+ flunk "No expected files were specfied." if options[:require_expected_files] && expected_files.empty?
329
+
330
+ # create output files
331
+ output_files = if options[:iterate_inputs]
332
+ input_files.collect do |input_file|
333
+ yield(input_file)
334
+ end
335
+ else
336
+ yield(input_files)
337
+ end
338
+ output_files = output_files.flatten.collect {|file| File.expand_path(file) }
339
+
340
+ if options[:check_for_missing_outputs]
341
+ expected_output_files = method_glob(:output)
342
+ unless options[:include_directories]
343
+ expected_output_files.delete_if {|file| File.directory?(file)}
344
+ end
345
+ expected_output_files.collect! {|file| File.expand_path(file) }
346
+
347
+ # assure there are no missing or extra output files
348
+ assert_equal expected_output_files, output_files, "Missing or extra output files"
349
+ end
350
+
351
+ # check that the expected and output filepaths are the same
352
+ translated_files = output_files.collect {|file| method_translate(file, :output, :expected)}
353
+ translated_files.collect! {|file| File.expand_path(file) }
354
+ assert_equal expected_files, translated_files, "Missing or extra expected files"
355
+
356
+ # check that the expected and output file contents are equal
357
+ errors = []
358
+ each_pair(expected_files, output_files) do |expected_file, output_file|
359
+ unless files_equal?(expected_file, output_file)
360
+ errors << "<#{expected_file}> not equal to\n<#{output_file}>"
361
+ end
362
+ end
363
+ flunk "File compare failed:\n" + errors.join("\n") unless errors.empty?
364
+ end
365
+
366
+ private
367
+
368
+ def make_tmpname(basename, n, ext="")
369
+ method_filepath(:output, sprintf('%s%d.%d%s', basename, $$, n, ext))
370
+ end
371
+ end
372
+ end
373
+ end