tap 0.7.9

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 (146) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README +71 -0
  3. data/Rakefile +117 -0
  4. data/bin/tap +63 -0
  5. data/lib/tap.rb +15 -0
  6. data/lib/tap/app.rb +739 -0
  7. data/lib/tap/file_task.rb +354 -0
  8. data/lib/tap/generator.rb +29 -0
  9. data/lib/tap/generator/generators/config/USAGE +0 -0
  10. data/lib/tap/generator/generators/config/config_generator.rb +23 -0
  11. data/lib/tap/generator/generators/config/templates/config.erb +2 -0
  12. data/lib/tap/generator/generators/file_task/USAGE +0 -0
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
  16. data/lib/tap/generator/generators/root/USAGE +0 -0
  17. data/lib/tap/generator/generators/root/root_generator.rb +36 -0
  18. data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
  19. data/lib/tap/generator/generators/root/templates/app.yml +19 -0
  20. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
  21. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
  22. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  23. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
  24. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
  25. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
  26. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
  27. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
  28. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
  29. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
  30. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  31. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
  32. data/lib/tap/generator/generators/task/USAGE +0 -0
  33. data/lib/tap/generator/generators/task/task_generator.rb +21 -0
  34. data/lib/tap/generator/generators/task/templates/task.erb +21 -0
  35. data/lib/tap/generator/generators/task/templates/test.erb +29 -0
  36. data/lib/tap/generator/generators/workflow/USAGE +0 -0
  37. data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
  38. data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
  39. data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
  40. data/lib/tap/generator/options.rb +26 -0
  41. data/lib/tap/generator/usage.rb +26 -0
  42. data/lib/tap/root.rb +275 -0
  43. data/lib/tap/script/console.rb +7 -0
  44. data/lib/tap/script/destroy.rb +8 -0
  45. data/lib/tap/script/generate.rb +8 -0
  46. data/lib/tap/script/run.rb +111 -0
  47. data/lib/tap/script/server.rb +12 -0
  48. data/lib/tap/support/audit.rb +415 -0
  49. data/lib/tap/support/batch_queue.rb +165 -0
  50. data/lib/tap/support/combinator.rb +114 -0
  51. data/lib/tap/support/logger.rb +91 -0
  52. data/lib/tap/support/rap.rb +38 -0
  53. data/lib/tap/support/run_error.rb +20 -0
  54. data/lib/tap/support/template.rb +81 -0
  55. data/lib/tap/support/templater.rb +155 -0
  56. data/lib/tap/support/versions.rb +63 -0
  57. data/lib/tap/task.rb +448 -0
  58. data/lib/tap/test.rb +320 -0
  59. data/lib/tap/test/env_vars.rb +16 -0
  60. data/lib/tap/test/inference_methods.rb +298 -0
  61. data/lib/tap/test/subset_methods.rb +260 -0
  62. data/lib/tap/version.rb +3 -0
  63. data/lib/tap/workflow.rb +73 -0
  64. data/test/app/config/addition_template.yml +6 -0
  65. data/test/app/config/batch.yml +2 -0
  66. data/test/app/config/empty.yml +0 -0
  67. data/test/app/config/erb.yml +1 -0
  68. data/test/app/config/template.yml +6 -0
  69. data/test/app/config/version-0.1.yml +1 -0
  70. data/test/app/config/version.yml +1 -0
  71. data/test/app/lib/app_test_task.rb +2 -0
  72. data/test/app_class_test.rb +33 -0
  73. data/test/app_test.rb +1372 -0
  74. data/test/file_task/config/batch.yml +2 -0
  75. data/test/file_task/config/configured.yml +1 -0
  76. data/test/file_task/old_file_one.txt +0 -0
  77. data/test/file_task/old_file_two.txt +0 -0
  78. data/test/file_task_test.rb +1041 -0
  79. data/test/root/alt_lib/alt_module.rb +4 -0
  80. data/test/root/lib/absolute_alt_filepath.rb +2 -0
  81. data/test/root/lib/alternative_filepath.rb +2 -0
  82. data/test/root/lib/another_module.rb +2 -0
  83. data/test/root/lib/nested/some_module.rb +4 -0
  84. data/test/root/lib/no_module_included.rb +0 -0
  85. data/test/root/lib/some/module.rb +4 -0
  86. data/test/root/lib/some_class.rb +2 -0
  87. data/test/root/lib/some_module.rb +3 -0
  88. data/test/root/load_path/load_path_module.rb +2 -0
  89. data/test/root/load_path/skip_module.rb +2 -0
  90. data/test/root/mtime/older.txt +0 -0
  91. data/test/root/unload/full_path.rb +2 -0
  92. data/test/root/unload/loaded_by_nested.rb +2 -0
  93. data/test/root/unload/nested/nested_load.rb +6 -0
  94. data/test/root/unload/nested/nested_with_ext.rb +4 -0
  95. data/test/root/unload/nested/relative_path.rb +4 -0
  96. data/test/root/unload/older.rb +2 -0
  97. data/test/root/unload/unload_base.rb +9 -0
  98. data/test/root/versions/another.yml +0 -0
  99. data/test/root/versions/file-0.1.2.yml +0 -0
  100. data/test/root/versions/file-0.1.yml +0 -0
  101. data/test/root/versions/file.yml +0 -0
  102. data/test/root_test.rb +483 -0
  103. data/test/support/audit_test.rb +449 -0
  104. data/test/support/batch_queue_test.rb +320 -0
  105. data/test/support/combinator_test.rb +249 -0
  106. data/test/support/logger_test.rb +31 -0
  107. data/test/support/template_test.rb +122 -0
  108. data/test/support/templater/erb.txt +2 -0
  109. data/test/support/templater/erb.yml +2 -0
  110. data/test/support/templater/somefile.txt +2 -0
  111. data/test/support/templater_test.rb +192 -0
  112. data/test/support/versions_test.rb +71 -0
  113. data/test/tap_test_helper.rb +4 -0
  114. data/test/tap_test_suite.rb +4 -0
  115. data/test/task/config/batch.yml +2 -0
  116. data/test/task/config/batched.yml +2 -0
  117. data/test/task/config/configured.yml +1 -0
  118. data/test/task/config/example.yml +1 -0
  119. data/test/task/config/overriding.yml +2 -0
  120. data/test/task/config/task_with_config.yml +1 -0
  121. data/test/task/config/template.yml +4 -0
  122. data/test/task_class_test.rb +118 -0
  123. data/test/task_execute_test.rb +233 -0
  124. data/test/task_test.rb +424 -0
  125. data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
  126. data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
  127. data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
  128. data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
  129. data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
  130. data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
  131. data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
  132. data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
  133. data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
  134. data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
  135. data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
  136. data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
  137. data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
  138. data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
  139. data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
  140. data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
  141. data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
  142. data/test/test/inference_methods_test.rb +311 -0
  143. data/test/test/subset_methods_test.rb +115 -0
  144. data/test/test_test.rb +233 -0
  145. data/test/workflow_test.rb +108 -0
  146. metadata +274 -0
data/lib/tap/test.rb ADDED
@@ -0,0 +1,320 @@
1
+ require 'test/unit'
2
+ require 'tap/test/inference_methods'
3
+ require 'tap/test/subset_methods'
4
+
5
+ module Test # :nodoc:
6
+ module Unit # :nodoc:
7
+ class TestCase
8
+ class << self
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
13
+ #
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.
18
+ def acts_as_tap_test(options={})
19
+ options = {:root => infer_root}.merge(options.symbolize_keys)
20
+ acts_as_inference_test(options)
21
+
22
+ include Tap::Test::SubsetMethods
23
+ include Tap::Test::InstanceMethods
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ module Tap
31
+
32
+ #
33
+ module Test
34
+
35
+ # Used during check_audit to hold the sources and values of an audit
36
+ # in the correct order. Oriented so that the next value to be checked
37
+ # is at the top of the stack.
38
+ class AuditStack
39
+ attr_reader :test
40
+
41
+ def initialize(test)
42
+ @test = test
43
+ @stack = []
44
+ end
45
+
46
+ def load_audit(values)
47
+ [values._sources, values._values].transpose.reverse_each do |sv|
48
+ load(*sv)
49
+ end
50
+ end
51
+
52
+ def load(source, value)
53
+ @stack.unshift [source, value]
54
+ end
55
+
56
+ def next
57
+ @stack.shift
58
+ end
59
+ end
60
+
61
+ module InstanceMethods
62
+ attr_accessor :runlist
63
+
64
+ # Setup clears the test using clear_tasks and assures that Tap::App.instance
65
+ # is the test-specific application.
66
+ def setup
67
+ super
68
+ Tap::App.instance = app
69
+ clear_runlist
70
+ end
71
+
72
+ # Returns the test-specific application.
73
+ 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
90
+ end
91
+
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
+ # Recieves a hash of task => expected pairs. Asserts that the last inputs for
126
+ # each task are equal to the expected inputs.
127
+ def assert_inputs(hash)
128
+ hash.each_pair do |task, expected|
129
+ inputs = task.results.collect { |r| r._input_last(task) }
130
+ assert_equal expected, inputs
131
+ end
132
+ end
133
+
134
+ # Recieves a hash of results => [index, expected] pairs. Asserts that the task result
135
+ # inputs at the specified index are equal to the expected inputs.
136
+ def assert_inputs_by_index(results, hash)
137
+ hash.each_pair do |index, expected|
138
+ assert_equal expected, results.collect {|r| r._input(index)}
139
+ end
140
+ end
141
+
142
+ # Recieves a hash of task => expected pairs. Asserts that the last outputs for
143
+ # each task are equal to the expected outputs.
144
+ def assert_outputs(hash)
145
+ hash.each_pair do |task, expected|
146
+ outputs = task.results.collect { |r| r._output_last(task) }
147
+ assert_equal expected, outputs
148
+ end
149
+ end
150
+
151
+ # Recieves a hash of results => [index, expected] pairs. Asserts that the result
152
+ # outputs at the specified index are equal to the expected outputs.
153
+ def assert_outputs_by_index(results, hash)
154
+ hash.each_pair do |index, expected|
155
+ assert_equal expected, results.collect {|r| r._output(index)}
156
+ end
157
+ end
158
+
159
+ # Recieves an array of audits and a hash of [index, expected_audit] pairs. Asserts that
160
+ # the collection of all [source, value] pairs in the indexed audit matches the corresponding
161
+ # pairs provided in expected_audit.
162
+ #
163
+ # Example:
164
+ # a0 = Audit.new('')
165
+ # a1 = Audit.new('')
166
+ # a1.record(:a, 'a')
167
+ # a1.record(:b, 'b')
168
+ #
169
+ # assert_audits([a0, a1], 1 => [[nil, ''], [:a, 'a'], [:b, 'b']]) # => true
170
+ #
171
+ # Note that since task results are an array of audits:
172
+ # t.results # if => [a0, a1]
173
+ # assert_audits(t1.results, 1 => [[nil, ''], [:a, 'a'], [:b, 'b']]) # then => true
174
+ #
175
+ def assert_audits(audits, hash)
176
+ hash.each_pair do |i, expected|
177
+ audit = audits[i]
178
+ raise ArgumentError.new("No audit provided at index: #{i}") if audit.nil?
179
+
180
+ actual = [collect_object_ids(audit._source_trail), audit._values].transpose
181
+ expected = expected.transpose
182
+ expected[0] = collect_object_ids(expected[0])
183
+ expected = expected.transpose
184
+ assert_equal expected, actual, "Unexpected audit for result #{i}.\nAudits displayed as: [[source.object_id, value]]"
185
+ end
186
+ end
187
+
188
+ def check_audit(audit, &block)
189
+ raise "Cannot check audit. Public method ':_from' already defined for Object." if Object.public_method_defined?(:_from)
190
+
191
+ begin
192
+ audit_stack = AuditStack.new(self)
193
+ audit_stack.load_audit(audit)
194
+
195
+ # Define the :from testing method. Defining this method on Object is good for syntax,
196
+ # but generally a bad practice. Hence, the method is undefined immediately after the
197
+ # test block completes.
198
+ #
199
+ # The from method assumes the object itself is the expected value, and that the expected
200
+ # source is given as an argument. If a block is given to from, that indicates that the value
201
+ # is the result of a merge -- the block must contain the checks to assert the origins of the
202
+ # merge value.
203
+ Object.class_eval %Q{
204
+ def _from(expected_source=nil, &block)
205
+ audit_stack = ObjectSpace._id2ref(#{audit_stack.object_id})
206
+ source, value = audit_stack.next
207
+
208
+ if block_given?
209
+ audit_stack.test.assert_equal Array, source.class, "Expected source from merge."
210
+ [source, value].transpose.reverse_each do |source, value|
211
+ source.kind_of?(Tap::Support::Audit) ?
212
+ audit_stack.load_audit(source) :
213
+ audit_stack.load(source, value)
214
+ end
215
+ yield
216
+ source, value = audit_stack.next
217
+ end
218
+
219
+ audit_stack.test.assert_equal self, value, "Wrong audit value (be sure your checks are in the correct order)"
220
+ audit_stack.test.assert_equal expected_source.object_id, source.object_id, PP.singleline_pp(value, "Wrong source id for value: ")
221
+ end}
222
+
223
+ yield
224
+ ensure
225
+ Object.class_eval %Q{remove_method(:_from)} if Object.public_method_defined?(:_from)
226
+ end
227
+ end
228
+
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 = {}
234
+ begin
235
+ options.each_pair do |op, val|
236
+ hold[op] = app.options.send(op)
237
+ app.options.send("#{op}=", val)
238
+ end
239
+
240
+ yield block if block_given?
241
+ ensure
242
+ hold.each_pair { |op, val| app.options.send("#{op}=", val) }
243
+ end
244
+ 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
+
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?
286
+
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
297
+
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
303
+ end
304
+ end
305
+ end
306
+
307
+ private
308
+
309
+ def collect_object_ids(array)
310
+ array.collect do |s|
311
+ s.kind_of?(Array) ? collect_object_ids(s) : s.object_id
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+
319
+
320
+
@@ -0,0 +1,16 @@
1
+ module Tap
2
+ module Test
3
+ module EnvVars
4
+
5
+ # Access to the case-insensitive ENV variables
6
+ def env(type)
7
+ type = type.downcase
8
+ ENV.each_pair do |key, value|
9
+ return value if key.downcase == type
10
+ end
11
+ nil
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,298 @@
1
+ require 'tap/root'
2
+ require 'test/unit'
3
+ require 'active_support'
4
+ require 'tap/test/env_vars'
5
+
6
+ module Test # :nodoc:
7
+ module Unit # :nodoc:
8
+ class TestCase
9
+ class << self
10
+
11
+ # Causes a unit test to act as an io test -- resulting in the following:
12
+ # - Definition of an Tap::Root object
13
+ # - Tap::Test::InferenceMethods are included, providing methods to easily access
14
+ # files according to a standard file structure.
15
+ #
16
+ # Note: Unless otherwise specified, +acts_as_io_test+ infers a root directory
17
+ # based on the calling file. Therefore, to keep your directory structure from
18
+ # referencing an unexpected locations, be sure to specify the root directory
19
+ # explicitly if you call +acts_as_io_test+ from a file that is NOT your test file.
20
+ def acts_as_inference_test(options={})
21
+ options = {
22
+ :root => infer_root,
23
+ :directories => {},
24
+ :keep_outputs => false,
25
+ :keep_failures => false}.merge(options.symbolize_keys)
26
+
27
+ # declare the testing directory structure
28
+ directories = default_directories.merge(options[:directories])
29
+ ifs = Tap::Root.new(options[:root], directories)
30
+
31
+ write_inheritable_attribute(:ifs, ifs)
32
+ class_inheritable_reader :ifs
33
+
34
+ write_inheritable_attribute(:options, options)
35
+ class_inheritable_reader :options
36
+
37
+ include Tap::Test::InferenceMethods
38
+ end
39
+
40
+ # The default directories for an io test:
41
+ #
42
+ # :input => 'input'
43
+ # :output => 'output'
44
+ # :expected => 'expected'
45
+ # :fail => 'fail'
46
+ def default_directories
47
+ {:input => 'input', :output => 'output', :expected => 'expected', :fail => 'fail'}
48
+ end
49
+
50
+ # Infers the root directory from the calling file by chomping the extension and '_test'. Ex:
51
+ # 'some_class.rb' => 'some_class'
52
+ # 'some_class_test.rb' => 'some_class'
53
+ def infer_root
54
+ # the calling file is not the direct caller of +infer_root+... this method is
55
+ # only accessed from within another method call, hence the target caller is caller[1]
56
+ # rather than caller[0].
57
+
58
+ # caller[1] is considered the calling file (which should be the test case)
59
+ # note that the output of calller.first is like:
60
+ # ./path/to/file.rb:10
61
+ # ./path/to/file.rb:10:in 'method'
62
+ calling_file = caller[1].gsub(/:\d+[:in .*]?$/, "" )
63
+ calling_file.chomp!("#{File.extname(calling_file)}")
64
+ calling_file.chomp("_test")
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ module Tap
72
+ module Test
73
+ module InferenceMethods
74
+ include Tap::Test::EnvVars
75
+
76
+ def method_name
77
+ @method_name
78
+ end
79
+
80
+ # Convenience accessor for the inference structure
81
+ def ifs
82
+ self.class.ifs
83
+ end
84
+
85
+ # Convenience accessor for the class options
86
+ def options
87
+ self.class.options
88
+ end
89
+
90
+ def make_directory_structure
91
+ ifs.directories.values.each do |dir|
92
+ FileUtils.mkdir_p(File.join(ifs.root, method_name, dir))
93
+ end
94
+ end
95
+
96
+ # Teardown deletes the output and fail directories unless flagged otherwise. Note that teardown
97
+ # also checks the environment variables for flags. To keep all outputs (or failures) for all tests,
98
+ # flag keep outputs from the command line like:
99
+ #
100
+ # %rake test KEEP_OUTPUTS=true
101
+ # %rake test KEEP_FAILURES=true
102
+ def teardown
103
+ # clear out the output folder if it exists, unless flagged otherwise
104
+ output_dir = infer_dir(:output, method_name)
105
+ unless !File.exists?(output_dir) || options[:keep_outputs] || env("KEEP_OUTPUTS")
106
+ begin
107
+ FileUtils.rm_r output_dir
108
+ rescue
109
+ raise("teardown failure: could not remove output files")
110
+ end
111
+ end
112
+
113
+ # clear out the fail folder if it exists, unless flagged otherwise
114
+ fail_dir = infer_dir(:fail, method_name)
115
+ unless !File.exists?(fail_dir) || options[:keep_failures] || env("KEEP_FAILURES")
116
+ begin
117
+ FileUtils.rm_r fail_dir
118
+ rescue
119
+ raise("teardown failure: could not remove fail files")
120
+ end
121
+ end
122
+
123
+ # Remove the directory if possible
124
+ begin
125
+ dir = ifs.filepath(method_name)
126
+ if File.exists?(dir) && Dir.glob(File.join(dir, "*")).empty?
127
+ FileUtils.rmdir dir
128
+ end
129
+ rescue
130
+ # rescue cases where there is a hidden file, for example .svn
131
+ end
132
+ end
133
+
134
+ def infer_root(method=method_name)
135
+ ifs.filepath(method)
136
+ end
137
+
138
+ # The method directory is defined as 'dir/method', where method is the calling method
139
+ # by default. infer_dir returns the method directory if it exists, otherwise it returns
140
+ # ifs[dir].
141
+ def infer_dir(dir, method=method_name)
142
+ File.join(infer_root(method), ifs.directories[dir] || dir.to_s)
143
+ end
144
+
145
+ # Returns a glob of files matching the input pattern, underneath the method directory
146
+ # if it exists, otherwise the <tt>ifs[dir]</tt> directory.
147
+ def infer_glob(dir, *patterns)
148
+ dir = ifs.relative_filepath(:root, infer_dir(dir))
149
+ ifs.glob(dir, *patterns)
150
+ end
151
+
152
+ # Returns a filepath constructed from the method directory if it exists,
153
+ # otherwise the filepath will be constructed from <tt>ifs[dir]</tt>.
154
+ def infer_filepath(dir, *filenames)
155
+ File.join(infer_dir(dir), *filenames)
156
+ end
157
+
158
+ # Removes the method directory from the input filepath, returning the resuting filename.
159
+ # If the method directory does not exist, <tt>ifs[dir]</tt> will be removed.
160
+ def infer_relative_filepath(dir, filepath)
161
+ dir = ifs.relative_filepath(:root, infer_dir(dir))
162
+ ifs.relative_filepath(dir, filepath)
163
+ end
164
+
165
+ # Returns an output file corresponding to the input file, translated from the
166
+ # input directory to the output directory.
167
+ #
168
+ # If the input method directory exists, it will be removed from the filepath.
169
+ # If the output method directory exists, it will be inserted in the filepath.
170
+ def infer_translate(filepath, input_dir, output_dir)
171
+ input_dir = ifs.relative_filepath(:root, infer_dir(input_dir))
172
+ output_dir = ifs.relative_filepath(:root, infer_dir(output_dir))
173
+ ifs.translate(filepath, input_dir, output_dir)
174
+ end
175
+
176
+ # Generates a temporary filepath formatted like "output_dir\filename.pid.n.ext" where n
177
+ # is a counter that will be incremented from until a non-existant filepath is achieved.
178
+ #
179
+ # Notes:
180
+ # - By default filename is the calling method
181
+ # - The extension is chomped off the end of the filename
182
+ # - If the directory for the filepath does not exist, the directory will be created
183
+ # - Like all files in the output directory, tempfiles will be deleted by the default
184
+ # +teardown+ method
185
+ def tempfile(filename=method_name)
186
+ n = 0
187
+ ext = File.extname(filename)
188
+ basename = filename.chomp(ext)
189
+ filepath = make_tmpname(basename, n, ext)
190
+ while File.exists?(filepath)
191
+ n += 1
192
+ filepath = make_tmpname(basename, n, ext)
193
+ end
194
+
195
+ dirname = File.dirname(filepath)
196
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
197
+ filepath
198
+ end
199
+
200
+ # Copies the output file to the :fail directory if no block is given, or if the
201
+ # given block returns true or raises an error. Segregate file assumes the output
202
+ # file is in the :output directory and determines the final filepath by using
203
+ # the +translate+ method.
204
+ #
205
+ # If the block raises an error, then the error will be re-raised after the
206
+ # segregation.
207
+ def segregate_file(output_file, &block)
208
+ begin
209
+ segregate = block_given? ? yield(output_file) : true
210
+ rescue
211
+ segregate = true
212
+ error = $!
213
+ ensure
214
+ if segregate
215
+ target = infer_translate(output_file, :output, :fail)
216
+
217
+ dirname = File.dirname(target)
218
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
219
+ FileUtils.cp(output_file, target)
220
+
221
+ # re-raise the error for further handling
222
+ raise $! if error
223
+ end
224
+ end
225
+ end
226
+
227
+ #
228
+ def assert_expected(*expected_files, &block)
229
+ errors = []
230
+
231
+ check_all_files = expected_files.empty?
232
+ if check_all_files
233
+ expected_files = infer_glob(:expected)
234
+ begin
235
+ output_files = infer_glob(:output)
236
+
237
+ efl = expected_files.collect { |file| infer_relative_filepath(:expected, file) }
238
+ ofl = output_files.collect { |file| infer_relative_filepath(:output, file) }
239
+
240
+ unless efl == ofl
241
+ raise "Inconsistent globs.\nexpected: #{PP.singleline_pp(efl, '')}\nbut was: #{PP.singleline_pp(ofl, '')}\n"
242
+ end
243
+ rescue
244
+ errors << $!.message
245
+ end
246
+ end
247
+
248
+ expected_files.each do |expected_file|
249
+ begin
250
+ output_file = infer_translate(expected_file, :expected, :output)
251
+
252
+ raise "Expected output file does not exist: #{output_file}" unless File.exists?(output_file)
253
+
254
+ segregate_file(output_file) do |output_file|
255
+ pass = block_given? ? yield(expected_file, output_file) : files_equal?(expected_file, output_file)
256
+ !pass
257
+ end unless File.directory?(output_file)
258
+ rescue
259
+ errors << $!.message
260
+ end
261
+ end
262
+
263
+ unless errors.empty?
264
+ flunk errors.join("\n")
265
+ end
266
+ end
267
+
268
+ # Asserts that each pair of input files are equal, using FileUtils.cmp.
269
+ def files_equal?(a, b)
270
+ FileUtils.cmp(a, b)
271
+ end
272
+
273
+ # Asserts that each pair of input files produce an equivalent object when
274
+ # loaded as a yaml file.
275
+ def yml_equal?(a, b)
276
+ YAML.load_file(a) == YAML.load_file(b)
277
+ end
278
+
279
+ # Yields to the input block for each pair of input files. An error is raised
280
+ # if the input arrays do not have equal numbers of entries.
281
+ def each_pair(a, b, &block)
282
+ a = [a] unless a.kind_of?(Array)
283
+ b = [b] unless b.kind_of?(Array)
284
+
285
+ raise ArgumentError, "The input arrays must have an equal number of entries." unless a.length == b.length
286
+ a.each_index do |index|
287
+ yield(a[index], b[index])
288
+ end
289
+ end
290
+
291
+ private
292
+
293
+ def make_tmpname(basename, n, ext="")
294
+ infer_filepath(:output, sprintf('%s%d.%d%s', basename, $$, n, ext))
295
+ end
296
+ end
297
+ end
298
+ end