tap 0.7.9
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/README +71 -0
- data/Rakefile +117 -0
- data/bin/tap +63 -0
- data/lib/tap.rb +15 -0
- data/lib/tap/app.rb +739 -0
- data/lib/tap/file_task.rb +354 -0
- data/lib/tap/generator.rb +29 -0
- data/lib/tap/generator/generators/config/USAGE +0 -0
- data/lib/tap/generator/generators/config/config_generator.rb +23 -0
- data/lib/tap/generator/generators/config/templates/config.erb +2 -0
- data/lib/tap/generator/generators/file_task/USAGE +0 -0
- data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
- data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
- data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
- data/lib/tap/generator/generators/root/USAGE +0 -0
- data/lib/tap/generator/generators/root/root_generator.rb +36 -0
- data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
- data/lib/tap/generator/generators/root/templates/app.yml +19 -0
- data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
- data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
- data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
- data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
- data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
- data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
- data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
- data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
- data/lib/tap/generator/generators/task/USAGE +0 -0
- data/lib/tap/generator/generators/task/task_generator.rb +21 -0
- data/lib/tap/generator/generators/task/templates/task.erb +21 -0
- data/lib/tap/generator/generators/task/templates/test.erb +29 -0
- data/lib/tap/generator/generators/workflow/USAGE +0 -0
- data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
- data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
- data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
- data/lib/tap/generator/options.rb +26 -0
- data/lib/tap/generator/usage.rb +26 -0
- data/lib/tap/root.rb +275 -0
- data/lib/tap/script/console.rb +7 -0
- data/lib/tap/script/destroy.rb +8 -0
- data/lib/tap/script/generate.rb +8 -0
- data/lib/tap/script/run.rb +111 -0
- data/lib/tap/script/server.rb +12 -0
- data/lib/tap/support/audit.rb +415 -0
- data/lib/tap/support/batch_queue.rb +165 -0
- data/lib/tap/support/combinator.rb +114 -0
- data/lib/tap/support/logger.rb +91 -0
- data/lib/tap/support/rap.rb +38 -0
- data/lib/tap/support/run_error.rb +20 -0
- data/lib/tap/support/template.rb +81 -0
- data/lib/tap/support/templater.rb +155 -0
- data/lib/tap/support/versions.rb +63 -0
- data/lib/tap/task.rb +448 -0
- data/lib/tap/test.rb +320 -0
- data/lib/tap/test/env_vars.rb +16 -0
- data/lib/tap/test/inference_methods.rb +298 -0
- data/lib/tap/test/subset_methods.rb +260 -0
- data/lib/tap/version.rb +3 -0
- data/lib/tap/workflow.rb +73 -0
- data/test/app/config/addition_template.yml +6 -0
- data/test/app/config/batch.yml +2 -0
- data/test/app/config/empty.yml +0 -0
- data/test/app/config/erb.yml +1 -0
- data/test/app/config/template.yml +6 -0
- data/test/app/config/version-0.1.yml +1 -0
- data/test/app/config/version.yml +1 -0
- data/test/app/lib/app_test_task.rb +2 -0
- data/test/app_class_test.rb +33 -0
- data/test/app_test.rb +1372 -0
- data/test/file_task/config/batch.yml +2 -0
- data/test/file_task/config/configured.yml +1 -0
- data/test/file_task/old_file_one.txt +0 -0
- data/test/file_task/old_file_two.txt +0 -0
- data/test/file_task_test.rb +1041 -0
- data/test/root/alt_lib/alt_module.rb +4 -0
- data/test/root/lib/absolute_alt_filepath.rb +2 -0
- data/test/root/lib/alternative_filepath.rb +2 -0
- data/test/root/lib/another_module.rb +2 -0
- data/test/root/lib/nested/some_module.rb +4 -0
- data/test/root/lib/no_module_included.rb +0 -0
- data/test/root/lib/some/module.rb +4 -0
- data/test/root/lib/some_class.rb +2 -0
- data/test/root/lib/some_module.rb +3 -0
- data/test/root/load_path/load_path_module.rb +2 -0
- data/test/root/load_path/skip_module.rb +2 -0
- data/test/root/mtime/older.txt +0 -0
- data/test/root/unload/full_path.rb +2 -0
- data/test/root/unload/loaded_by_nested.rb +2 -0
- data/test/root/unload/nested/nested_load.rb +6 -0
- data/test/root/unload/nested/nested_with_ext.rb +4 -0
- data/test/root/unload/nested/relative_path.rb +4 -0
- data/test/root/unload/older.rb +2 -0
- data/test/root/unload/unload_base.rb +9 -0
- data/test/root/versions/another.yml +0 -0
- data/test/root/versions/file-0.1.2.yml +0 -0
- data/test/root/versions/file-0.1.yml +0 -0
- data/test/root/versions/file.yml +0 -0
- data/test/root_test.rb +483 -0
- data/test/support/audit_test.rb +449 -0
- data/test/support/batch_queue_test.rb +320 -0
- data/test/support/combinator_test.rb +249 -0
- data/test/support/logger_test.rb +31 -0
- data/test/support/template_test.rb +122 -0
- data/test/support/templater/erb.txt +2 -0
- data/test/support/templater/erb.yml +2 -0
- data/test/support/templater/somefile.txt +2 -0
- data/test/support/templater_test.rb +192 -0
- data/test/support/versions_test.rb +71 -0
- data/test/tap_test_helper.rb +4 -0
- data/test/tap_test_suite.rb +4 -0
- data/test/task/config/batch.yml +2 -0
- data/test/task/config/batched.yml +2 -0
- data/test/task/config/configured.yml +1 -0
- data/test/task/config/example.yml +1 -0
- data/test/task/config/overriding.yml +2 -0
- data/test/task/config/task_with_config.yml +1 -0
- data/test/task/config/template.yml +4 -0
- data/test/task_class_test.rb +118 -0
- data/test/task_execute_test.rb +233 -0
- data/test/task_test.rb +424 -0
- data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
- data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
- data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
- data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
- data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
- data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
- data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
- data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
- data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
- data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
- data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
- data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
- data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
- data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
- data/test/test/inference_methods_test.rb +311 -0
- data/test/test/subset_methods_test.rb +115 -0
- data/test/test_test.rb +233 -0
- data/test/workflow_test.rb +108 -0
- 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,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
|