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.
- 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
|