tap 0.7.9

Sign up to get free protection for your applications and to get access to all the features.
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