tap 0.7.9 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. data/History +28 -0
  2. data/MIT-LICENSE +1 -1
  3. data/README +71 -43
  4. data/Rakefile +81 -64
  5. data/Tutorial +235 -0
  6. data/bin/tap +80 -44
  7. data/lib/tap.rb +41 -12
  8. data/lib/tap/app.rb +243 -246
  9. data/lib/tap/file_task.rb +357 -118
  10. data/lib/tap/generator.rb +88 -29
  11. data/lib/tap/generator/generators/config/config_generator.rb +4 -2
  12. data/lib/tap/generator/generators/config/templates/config.erb +1 -2
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +3 -18
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +22 -15
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +13 -2
  16. data/{test/test/inference_methods/test_assert_files_exist/input/input_1.txt → lib/tap/generator/generators/generator/USAGE} +0 -0
  17. data/lib/tap/generator/generators/generator/generator_generator.rb +21 -0
  18. data/lib/tap/generator/generators/generator/templates/generator.erb +23 -0
  19. data/lib/tap/generator/generators/generator/templates/usage.erb +1 -0
  20. data/{test/test/inference_methods/test_assert_files_exist/input/input_2.txt → lib/tap/generator/generators/package/USAGE} +0 -0
  21. data/lib/tap/generator/generators/package/package_generator.rb +38 -0
  22. data/lib/tap/generator/generators/package/templates/package.erb +186 -0
  23. data/lib/tap/generator/generators/root/root_generator.rb +14 -9
  24. data/lib/tap/generator/generators/root/templates/Rakefile +20 -14
  25. data/{test/test/inference_methods/test_infer_glob/expected/file.yml → lib/tap/generator/generators/root/templates/ReadMe.txt} +0 -0
  26. data/lib/tap/generator/generators/root/templates/tap.yml +82 -0
  27. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -1
  28. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +2 -1
  29. data/{test/test/inference_methods/test_infer_glob/expected/file_1.txt → lib/tap/generator/generators/script/USAGE} +0 -0
  30. data/lib/tap/generator/generators/script/script_generator.rb +17 -0
  31. data/lib/tap/generator/generators/script/templates/script.erb +42 -0
  32. data/lib/tap/generator/generators/task/task_generator.rb +1 -1
  33. data/lib/tap/generator/generators/task/templates/task.erb +24 -16
  34. data/lib/tap/generator/generators/task/templates/test.erb +13 -17
  35. data/lib/tap/generator/generators/workflow/templates/task.erb +10 -10
  36. data/lib/tap/generator/generators/workflow/templates/test.erb +1 -1
  37. data/lib/tap/generator/generators/workflow/workflow_generator.rb +3 -18
  38. data/lib/tap/root.rb +108 -146
  39. data/lib/tap/script.rb +362 -0
  40. data/lib/tap/script/console.rb +28 -0
  41. data/lib/tap/script/destroy.rb +13 -1
  42. data/lib/tap/script/generate.rb +13 -1
  43. data/lib/tap/script/run.rb +100 -57
  44. data/lib/tap/support/batch_queue.rb +0 -3
  45. data/lib/tap/support/logger.rb +6 -3
  46. data/lib/tap/support/rake.rb +54 -0
  47. data/lib/tap/support/task_configuration.rb +169 -0
  48. data/lib/tap/support/tdoc.rb +198 -0
  49. data/lib/tap/support/tdoc/config_attr.rb +338 -0
  50. data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
  51. data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
  52. data/lib/tap/support/versions.rb +33 -1
  53. data/lib/tap/task.rb +339 -227
  54. data/lib/tap/test.rb +86 -128
  55. data/lib/tap/test/env_vars.rb +16 -5
  56. data/lib/tap/test/file_methods.rb +373 -0
  57. data/lib/tap/test/subset_methods.rb +299 -180
  58. data/lib/tap/version.rb +2 -1
  59. data/lib/tap/workflow.rb +2 -0
  60. data/test/app/lib/app_test_task.rb +1 -0
  61. data/test/app_test.rb +327 -83
  62. data/test/check/binding_eval.rb +23 -0
  63. data/test/check/define_method_check.rb +22 -0
  64. data/test/check/dependencies_check.rb +175 -0
  65. data/test/check/inheritance_check.rb +22 -0
  66. data/test/file_task_test.rb +524 -291
  67. data/test/{test/inference_methods/test_infer_glob/expected/file_2.txt → root/glob/one.txt} +0 -0
  68. data/test/root/glob/two.txt +0 -0
  69. data/test/root_test.rb +330 -262
  70. data/test/script_test.rb +194 -0
  71. data/test/support/audit_test.rb +5 -2
  72. data/test/support/combinator_test.rb +10 -10
  73. data/test/support/rake_test.rb +35 -0
  74. data/test/support/task_configuration_test.rb +272 -0
  75. data/test/support/tdoc_test.rb +363 -0
  76. data/test/support/templater_test.rb +2 -2
  77. data/test/support/versions_test.rb +32 -0
  78. data/test/tap_test_helper.rb +39 -0
  79. data/test/task_base_test.rb +115 -0
  80. data/test/task_class_test.rb +56 -4
  81. data/test/task_execute_test.rb +29 -0
  82. data/test/task_test.rb +89 -70
  83. data/test/test/env_vars_test.rb +48 -0
  84. data/test/test/{inference_methods → file_methods}/test_assert_expected/expected/file.txt +0 -0
  85. data/test/test/{inference_methods → file_methods}/test_assert_expected/expected/folder/file.txt +0 -0
  86. data/test/test/{inference_methods → file_methods}/test_assert_expected/input/file.txt +0 -0
  87. data/test/test/{inference_methods → file_methods}/test_assert_expected/input/folder/file.txt +0 -0
  88. data/test/test/file_methods/test_assert_files_exist/input/input_1.txt +0 -0
  89. data/test/test/file_methods/test_assert_files_exist/input/input_2.txt +0 -0
  90. data/test/test/file_methods/test_assert_output_files_equal/expected/one.txt +1 -0
  91. data/test/test/file_methods/test_assert_output_files_equal/expected/two.txt +1 -0
  92. data/test/test/file_methods/test_assert_output_files_equal/input/one.txt +1 -0
  93. data/test/test/file_methods/test_assert_output_files_equal/input/two.txt +1 -0
  94. data/test/test/{inference_methods → file_methods}/test_file_compare/expected/output_1.txt +0 -0
  95. data/test/test/{inference_methods → file_methods}/test_file_compare/expected/output_2.txt +0 -0
  96. data/test/test/{inference_methods → file_methods}/test_file_compare/input/input_1.txt +0 -0
  97. data/test/test/{inference_methods → file_methods}/test_file_compare/input/input_2.txt +0 -0
  98. data/test/test/file_methods/test_infer_glob/expected/file.yml +0 -0
  99. data/test/test/file_methods/test_infer_glob/expected/file_1.txt +0 -0
  100. data/test/test/file_methods/test_infer_glob/expected/file_2.txt +0 -0
  101. data/test/test/file_methods/test_method_glob/expected/file.yml +0 -0
  102. data/test/test/file_methods/test_method_glob/expected/file_1.txt +0 -0
  103. data/test/test/file_methods/test_method_glob/expected/file_2.txt +0 -0
  104. data/test/test/{inference_methods → file_methods}/test_yml_compare/expected/output_1.yml +0 -0
  105. data/test/test/{inference_methods → file_methods}/test_yml_compare/expected/output_2.yml +0 -0
  106. data/test/test/{inference_methods → file_methods}/test_yml_compare/input/input_1.yml +0 -0
  107. data/test/test/{inference_methods → file_methods}/test_yml_compare/input/input_2.yml +0 -0
  108. data/test/test/file_methods_test.rb +204 -0
  109. data/test/test/subset_methods_test.rb +93 -33
  110. data/test/test/test_assert_expected_result_files/expected/task/name/a.txt +1 -0
  111. data/test/test/test_assert_expected_result_files/expected/task/name/b.txt +1 -0
  112. data/test/test/test_assert_expected_result_files/input/a.txt +1 -0
  113. data/test/test/test_assert_expected_result_files/input/b.txt +1 -0
  114. data/test/test/test_file_task_test/expected/one.txt +1 -0
  115. data/test/test/test_file_task_test/expected/two.txt +1 -0
  116. data/test/test/test_file_task_test/input/one.txt +1 -0
  117. data/test/test/test_file_task_test/input/two.txt +1 -0
  118. data/test/test_test.rb +143 -3
  119. data/test/workflow_test.rb +2 -0
  120. data/vendor/rails_generator.rb +56 -0
  121. data/vendor/rails_generator/base.rb +263 -0
  122. data/vendor/rails_generator/commands.rb +581 -0
  123. data/vendor/rails_generator/generated_attribute.rb +42 -0
  124. data/vendor/rails_generator/lookup.rb +209 -0
  125. data/vendor/rails_generator/manifest.rb +53 -0
  126. data/vendor/rails_generator/options.rb +143 -0
  127. data/vendor/rails_generator/scripts.rb +83 -0
  128. data/vendor/rails_generator/scripts/destroy.rb +7 -0
  129. data/vendor/rails_generator/scripts/generate.rb +7 -0
  130. data/vendor/rails_generator/scripts/update.rb +12 -0
  131. data/vendor/rails_generator/simple_logger.rb +46 -0
  132. data/vendor/rails_generator/spec.rb +44 -0
  133. metadata +180 -196
  134. data/lib/tap/generator/generators/root/templates/app.yml +0 -19
  135. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +0 -4
  136. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +0 -26
  137. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  138. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +0 -57
  139. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +0 -108
  140. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +0 -40
  141. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +0 -21
  142. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +0 -60
  143. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +0 -5
  144. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +0 -53
  145. data/lib/tap/script/server.rb +0 -12
  146. data/lib/tap/support/rap.rb +0 -38
  147. data/lib/tap/test/inference_methods.rb +0 -298
  148. data/test/task/config/task_with_config.yml +0 -1
  149. data/test/test/inference_methods_test.rb +0 -311
data/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