rspec-command 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +12 -0
- data/.yardopts +5 -0
- data/Gemfile +19 -0
- data/LICENSE +202 -0
- data/README.md +181 -0
- data/Rakefile +32 -0
- data/lib/rspec-command.rb +17 -0
- data/lib/rspec_command.rb +361 -0
- data/lib/rspec_command/match_fixture.rb +165 -0
- data/lib/rspec_command/rake.rb +120 -0
- data/lib/rspec_command/version.rb +21 -0
- data/rspec-command.gemspec +48 -0
- data/spec/command_spec.rb +301 -0
- data/spec/file_list_spec.rb +106 -0
- data/spec/fixtures/data.txt +1 -0
- data/spec/fixtures/sub/sub1.txt +1 -0
- data/spec/fixtures/sub/sub2.txt +1 -0
- data/spec/fixtures/sub_nested/sub_inner/sub_inner1.txt +1 -0
- data/spec/fixtures/sub_nested/sub_inner/sub_inner2.txt +1 -0
- data/spec/fixtures/sub_nested/sub_nested.txt +1 -0
- data/spec/match_fixture_spec.rb +186 -0
- data/spec/rake_spec.rb +117 -0
- data/spec/spec_helper.rb +58 -0
- metadata +240 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require 'rspec_command'
|
@@ -0,0 +1,361 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require 'fileutils'
|
18
|
+
require 'tempfile'
|
19
|
+
|
20
|
+
require 'rspec'
|
21
|
+
require 'rspec/its'
|
22
|
+
require 'mixlib/shellout'
|
23
|
+
|
24
|
+
|
25
|
+
# An RSpec helper module for testing command-line tools.
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
# @since 1.0.0
|
29
|
+
# @example Enable globally
|
30
|
+
# RSpec.configure do |config|
|
31
|
+
# config.include RSpecCommand
|
32
|
+
# end
|
33
|
+
# @example Enable for a single example group
|
34
|
+
# describe 'myapp' do
|
35
|
+
# command 'myapp --version'
|
36
|
+
# its(:stdout) { it_expected.to include('1.0.0') }
|
37
|
+
# end
|
38
|
+
module RSpecCommand
|
39
|
+
extend RSpec::SharedContext
|
40
|
+
|
41
|
+
autoload :MatchFixture, 'rspec_command/match_fixture'
|
42
|
+
autoload :Rake, 'rspec_command/rake'
|
43
|
+
|
44
|
+
around do |example|
|
45
|
+
Dir.mktmpdir('rspec_command') do |path|
|
46
|
+
example.metadata[:rspec_command_temp_path] = path
|
47
|
+
example.run
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @!attribute [r] temp_path
|
52
|
+
# Path to the temporary directory created for the current example.
|
53
|
+
# @return [String]
|
54
|
+
let(:temp_path) do |example|
|
55
|
+
example.metadata[:rspec_command_temp_path]
|
56
|
+
end
|
57
|
+
|
58
|
+
# @!attribute [r] fixture_root
|
59
|
+
# Base path for the fixtures directory. Default value is 'fixtures'.
|
60
|
+
# @return [String]
|
61
|
+
# @example
|
62
|
+
# let(:fixture_root) { 'data' }
|
63
|
+
let(:fixture_root) { 'fixtures' }
|
64
|
+
|
65
|
+
# @!attribute [r] _environment
|
66
|
+
# @!visibility private
|
67
|
+
# @api private
|
68
|
+
# Accumulator for environment variables.
|
69
|
+
# @see RSpecCommand.environment
|
70
|
+
let(:_environment) { Hash.new }
|
71
|
+
|
72
|
+
# Run a command.
|
73
|
+
#
|
74
|
+
# @see .command
|
75
|
+
# @param cmd [String, Array] Command to run. If passed as an array, no shell
|
76
|
+
# expansion will be performed.
|
77
|
+
# @param options [Hash<Symbol, Object>] Options to pass to
|
78
|
+
# Mixlib::ShellOut.new.
|
79
|
+
# @option options [Boolean] allow_error If true, don't raise an error on
|
80
|
+
# failed commands.
|
81
|
+
# @return [Mixlib::ShellOut]
|
82
|
+
# @example
|
83
|
+
# before do
|
84
|
+
# command('git init')
|
85
|
+
# end
|
86
|
+
def command(cmd, options={})
|
87
|
+
# Try to find a Gemfile
|
88
|
+
gemfile_path = find_file(self.class.file_path, 'Gemfile')
|
89
|
+
gemfile_environment = gemfile_path ? {'BUNDLE_GEMFILE' => gemfile_path} : {}
|
90
|
+
# Create the command
|
91
|
+
allow_error = options.delete(:allow_error)
|
92
|
+
full_cmd = if gemfile_path
|
93
|
+
if cmd.is_a?(Array)
|
94
|
+
%w{bundle exec} + cmd
|
95
|
+
else
|
96
|
+
"bundle exec #{cmd}"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
cmd
|
100
|
+
end
|
101
|
+
Mixlib::ShellOut.new(
|
102
|
+
full_cmd,
|
103
|
+
{
|
104
|
+
cwd: temp_path,
|
105
|
+
environment: gemfile_environment.merge(_environment),
|
106
|
+
}.merge(options),
|
107
|
+
).tap do |cmd|
|
108
|
+
# Run the command
|
109
|
+
cmd.run_command
|
110
|
+
cmd.error! unless allow_error
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Matcher to compare files or folders from the temporary directory to a
|
115
|
+
# fixture.
|
116
|
+
#
|
117
|
+
# @example
|
118
|
+
# describe 'myapp' do
|
119
|
+
# command 'myapp write'
|
120
|
+
# it { is_expected.to match_fixture('write_data') }
|
121
|
+
# end
|
122
|
+
def match_fixture(fixture_path, local_path=nil)
|
123
|
+
MatchFixture.new(find_fixture(self.class.file_path), temp_path, fixture_path, local_path)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Run a local block with $stdout and $stderr redirected to a strings. Useful
|
127
|
+
# for running CLI code in unit tests. The returned string has `#stdout`,
|
128
|
+
# `#stderr` and `#exitstatus` attributes to emulate the output from {.command}.
|
129
|
+
#
|
130
|
+
# @param block [Proc] Code to run.
|
131
|
+
# @return [String]
|
132
|
+
# @example
|
133
|
+
# describe 'my rake task' do
|
134
|
+
# subject do
|
135
|
+
# capture_output do
|
136
|
+
# Rake::Task['mytask'].invoke
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
# end
|
140
|
+
def capture_output(&block)
|
141
|
+
old_stdout = $stdout.dup
|
142
|
+
old_stderr = $stderr.dup
|
143
|
+
# Potential future improvement is to use IO.pipe instead of temp files, but
|
144
|
+
# that would require threads or something to read contiuously since the
|
145
|
+
# buffer is only 64k on the kernel side.
|
146
|
+
Tempfile.open('capture_stdout') do |tmp_stdout|
|
147
|
+
Tempfile.open('capture_stderr') do |tmp_stderr|
|
148
|
+
$stdout.reopen(tmp_stdout)
|
149
|
+
$stdout.sync = true
|
150
|
+
$stderr.reopen(tmp_stderr)
|
151
|
+
$stderr.sync = true
|
152
|
+
output = nil
|
153
|
+
begin
|
154
|
+
# Inner block to make sure the ensure happens first.
|
155
|
+
begin
|
156
|
+
block.call
|
157
|
+
ensure
|
158
|
+
# Rewind.
|
159
|
+
tmp_stdout.seek(0, 0)
|
160
|
+
tmp_stderr.seek(0, 0)
|
161
|
+
# Read in the output.
|
162
|
+
output = OutputString.new(tmp_stdout.read, tmp_stderr.read)
|
163
|
+
end
|
164
|
+
rescue Exception => e
|
165
|
+
if output
|
166
|
+
# Try to add the output so far as an attribute on the exception via
|
167
|
+
# a closure.
|
168
|
+
e.define_singleton_method(:output_so_far) do
|
169
|
+
output
|
170
|
+
end
|
171
|
+
end
|
172
|
+
raise
|
173
|
+
else
|
174
|
+
output
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
ensure
|
179
|
+
$stdout.reopen(old_stdout)
|
180
|
+
$stderr.reopen(old_stderr)
|
181
|
+
end
|
182
|
+
|
183
|
+
# String subclass to make string output look kind of like Mixlib::ShellOut.
|
184
|
+
#
|
185
|
+
# @api private
|
186
|
+
# @see capture_stdout
|
187
|
+
class OutputString < String
|
188
|
+
def initialize(stdout, stderr)
|
189
|
+
super(stdout)
|
190
|
+
@stderr = stderr
|
191
|
+
end
|
192
|
+
|
193
|
+
def stdout
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
def stderr
|
198
|
+
@stderr
|
199
|
+
end
|
200
|
+
|
201
|
+
def exitstatus
|
202
|
+
0
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# Search backwards along the working directory looking for a file, a la .git.
|
209
|
+
# Either file or block must be given.
|
210
|
+
#
|
211
|
+
# @param example_path [String] Path of the current example file. Find via
|
212
|
+
# example.file_path.
|
213
|
+
# @param file [String] Relative path to search for.
|
214
|
+
# @param backstop [String] Path to not search past.
|
215
|
+
# @param block [Proc] Block to use as a filter.
|
216
|
+
# @return [String, nil]
|
217
|
+
def find_file(example_path, file=nil, backstop=nil, &block)
|
218
|
+
path = File.dirname(File.expand_path(example_path))
|
219
|
+
last_path = nil
|
220
|
+
while path != last_path && path != backstop
|
221
|
+
if block
|
222
|
+
block_val = block.call(path)
|
223
|
+
return block_val if block_val
|
224
|
+
else
|
225
|
+
file_path = File.join(path, file)
|
226
|
+
return file_path if File.exists?(file_path)
|
227
|
+
end
|
228
|
+
last_path = path
|
229
|
+
path = File.dirname(path)
|
230
|
+
end
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
|
234
|
+
# Find the base folder of the current gem.
|
235
|
+
def find_gem_base(example_path)
|
236
|
+
@gem_base ||= begin
|
237
|
+
paths = []
|
238
|
+
paths << find_file(example_path) do |path|
|
239
|
+
spec_path = Dir.entries(path).find do |ent|
|
240
|
+
ent.end_with?('.gemspec')
|
241
|
+
end
|
242
|
+
spec_path = File.join(path, spec_path) if spec_path
|
243
|
+
spec_path
|
244
|
+
end
|
245
|
+
paths << find_file(example_path, 'Gemfile')
|
246
|
+
File.dirname(paths.find {|v| v })
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Find a fixture file or the fixture base folder.
|
251
|
+
def find_fixture(example_path, path=nil)
|
252
|
+
@fixture_base ||= find_file(example_path, fixture_root, find_gem_base(example_path))
|
253
|
+
path ? File.join(@fixture_base, path) : @fixture_base
|
254
|
+
end
|
255
|
+
|
256
|
+
# @!classmethods
|
257
|
+
module ClassMethods
|
258
|
+
# Run a command as the subject of this example. The command can be passed in
|
259
|
+
# as a string, array, or block. The subject will be a Mixlib::ShellOut
|
260
|
+
# object, all attributes from there will work with rspec-its.
|
261
|
+
#
|
262
|
+
# @see #command
|
263
|
+
# @param cmd [String, Array] Command to run. If passed as an array, no shell
|
264
|
+
# expansion will be performed.
|
265
|
+
# @param options [Hash<Symbol, Object>] Options to pass to
|
266
|
+
# Mixlib::ShellOut.new.
|
267
|
+
# @param block [Proc] Optional block to return a command to run.
|
268
|
+
# @option options [Boolean] allow_error If true, don't raise an error on
|
269
|
+
# failed commands.
|
270
|
+
# @example
|
271
|
+
# describe 'myapp' do
|
272
|
+
# command 'myapp show'
|
273
|
+
# its(:stdout) { is_expected.to match(/a thing/) }
|
274
|
+
# end
|
275
|
+
def command(cmd=nil, options={}, &block)
|
276
|
+
metadata[:command] = true
|
277
|
+
subject do |example|
|
278
|
+
# If a block is given, use it to get the command.
|
279
|
+
cmd = instance_eval(&block) if block
|
280
|
+
command(cmd, options)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Create a file in the temporary directory for this example.
|
285
|
+
#
|
286
|
+
# @param path [String] Path within the temporary directory to write to.
|
287
|
+
# @param content [String] File data to write.
|
288
|
+
# @param block [Proc] Optional block to return file data to write.
|
289
|
+
# @example
|
290
|
+
# describe 'myapp' do
|
291
|
+
# command 'myapp read data.txt'
|
292
|
+
# file 'data.txt', <<-EOH
|
293
|
+
# a thing
|
294
|
+
# EOH
|
295
|
+
# its(:exitstatus) { is_expected.to eq 0 }
|
296
|
+
# end
|
297
|
+
def file(path, content=nil, &block)
|
298
|
+
raise "file path should be relative the the temporary directory." if path == File.expand_path(path)
|
299
|
+
before do
|
300
|
+
content = instance_eval(&block) if block
|
301
|
+
dest_path = File.join(temp_path, path)
|
302
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
303
|
+
IO.write(dest_path, content)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Copy fixture data from the spec folder to the temporary directory for this
|
308
|
+
# example.
|
309
|
+
#
|
310
|
+
# @param path [String] Path of the fixture to copy.
|
311
|
+
# @param dest [String] Optional destination path. By default the destination
|
312
|
+
# is the same as path.
|
313
|
+
# @example
|
314
|
+
# describe 'myapp' do
|
315
|
+
# command 'myapp run test/'
|
316
|
+
# fixture_file 'test'
|
317
|
+
# its(:exitstatus) { is_expected.to eq 0 }
|
318
|
+
# end
|
319
|
+
def fixture_file(path, dest=nil)
|
320
|
+
raise "file path should be relative the the temporary directory." if path == File.expand_path(path)
|
321
|
+
before do |example|
|
322
|
+
fixture_path = find_fixture(example.file_path, path)
|
323
|
+
dest_path = dest ? File.join(temp_path, dest) : temp_path
|
324
|
+
FileUtils.mkdir_p(dest_path)
|
325
|
+
file_list = MatchFixture::FileList.new(fixture_path)
|
326
|
+
file_list.files.each do |file|
|
327
|
+
abs = file_list.absolute(file)
|
328
|
+
if File.directory?(abs)
|
329
|
+
FileUtils.mkdir_p(File.join(dest_path, file))
|
330
|
+
else
|
331
|
+
FileUtils.copy(abs , File.join(dest_path, file), preserve: true)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Set an environment variable for this example.
|
338
|
+
#
|
339
|
+
# @param variables [Hash] Key/value pairs to set.
|
340
|
+
# @example
|
341
|
+
# describe 'myapp' do
|
342
|
+
# command 'myapp show'
|
343
|
+
# environment DEBUG: true
|
344
|
+
# its(:stderr) { is_expected.to include('[debug]') }
|
345
|
+
# end
|
346
|
+
def environment(variables)
|
347
|
+
before do
|
348
|
+
variables.each do |key, value|
|
349
|
+
_environment[key.to_s] = value.to_s
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def included(klass)
|
355
|
+
super
|
356
|
+
klass.extend ClassMethods
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
extend ClassMethods
|
361
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
|
18
|
+
module RSpecCommand
|
19
|
+
# @api private
|
20
|
+
# @since 1.0.0
|
21
|
+
class MatchFixture
|
22
|
+
# Create a new matcher for a fixture.
|
23
|
+
#
|
24
|
+
# @param fixture_root [String] Absolute path to the fixture folder.
|
25
|
+
# @param local_root [String] Absolute path to test folder to compare against.
|
26
|
+
# @param fixture_path [String] Relative path to the fixture to compare against.
|
27
|
+
# @param local_path [String] Optional relative path to the test data to compare against.
|
28
|
+
def initialize(fixture_root, local_root, fixture_path, local_path=nil)
|
29
|
+
@fixture = FileList.new(fixture_root, fixture_path)
|
30
|
+
@local = FileList.new(local_root, local_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Primary callback for RSpec matcher API.
|
34
|
+
#
|
35
|
+
# @param cmd Ignored.
|
36
|
+
# @return [Boolean]
|
37
|
+
def matches?(cmd)
|
38
|
+
files_match? && file_content_match?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Callback for RSpec. Returns a human-readable description for the matcher.
|
42
|
+
#
|
43
|
+
# @return [String]
|
44
|
+
def description
|
45
|
+
"match fixture #{@fixture.path}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Callback fro RSpec. Returns a human-readable failure message.
|
49
|
+
#
|
50
|
+
# @return [String]
|
51
|
+
def failure_message
|
52
|
+
matching_files = @fixture.files & @local.files
|
53
|
+
fixture_only_files = @fixture.files - @local.files
|
54
|
+
local_only_files = @local.files - @fixture.files
|
55
|
+
buf = "expected fixture #{@fixture.path} to match files:\n"
|
56
|
+
(@fixture.files | @local.files).sort.each do |file|
|
57
|
+
if matching_files.include?(file)
|
58
|
+
local_file = @local.absolute(file)
|
59
|
+
fixture_file = @fixture.absolute(file)
|
60
|
+
if File.directory?(local_file) && File.directory?(fixture_file)
|
61
|
+
# Do nothing
|
62
|
+
elsif File.directory?(fixture_file)
|
63
|
+
buf << " #{file} should be a directory\n"
|
64
|
+
elsif File.directory?(local_file)
|
65
|
+
buf << " #{file} should not be a directory"
|
66
|
+
else
|
67
|
+
actual = IO.read(local_file)
|
68
|
+
expected = IO.read(fixture_file)
|
69
|
+
if actual != expected
|
70
|
+
# Show a diff
|
71
|
+
buf << " #{file} does not match fixture:"
|
72
|
+
buf << differ.diff(actual, expected).split(/\n/).map {|line| ' '+line }.join("\n")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
elsif fixture_only_files.include?(file)
|
76
|
+
buf << " #{file} is not found\n"
|
77
|
+
elsif local_only_files.include?(file)
|
78
|
+
buf << " #{file} should not exist\n"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
buf
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Do the file entries match? Doesn't check content.
|
87
|
+
#
|
88
|
+
# @return [Boolean]
|
89
|
+
def files_match?
|
90
|
+
@fixture.files == @local.files
|
91
|
+
end
|
92
|
+
|
93
|
+
# Do the file contents match?
|
94
|
+
#
|
95
|
+
# @return [Boolean]
|
96
|
+
def file_content_match?
|
97
|
+
@fixture.full_files.zip(@local.full_files).all? do |fixture_file, local_file|
|
98
|
+
if File.directory?(fixture_file)
|
99
|
+
File.directory?(local_file)
|
100
|
+
else
|
101
|
+
!File.directory?(local_file) && IO.read(fixture_file) == IO.read(local_file)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return a Differ object to make diffs.
|
107
|
+
#
|
108
|
+
# @note This is using a nominally private API. It could break in the future.
|
109
|
+
# @return [RSpec::Support::Differ]
|
110
|
+
# @example
|
111
|
+
# differ.diff(actual, expected)
|
112
|
+
def differ
|
113
|
+
RSpec::Expectations.differ
|
114
|
+
end
|
115
|
+
|
116
|
+
class FileList
|
117
|
+
attr_reader :root, :path
|
118
|
+
|
119
|
+
# @param root [String] Absolute path to the root of the files.
|
120
|
+
# @param path [String] Relative path to the specific files.
|
121
|
+
def initialize(root, path=nil)
|
122
|
+
@root = root
|
123
|
+
@path = path
|
124
|
+
end
|
125
|
+
|
126
|
+
# Absolute path to the target.
|
127
|
+
def full_path
|
128
|
+
@full_path ||= path ? File.join(root, path) : root
|
129
|
+
end
|
130
|
+
|
131
|
+
# Absolute paths to target files that exist.
|
132
|
+
def full_files
|
133
|
+
@full_files ||= if File.directory?(full_path)
|
134
|
+
Dir[File.join(full_path, '**', '*')].sort
|
135
|
+
else
|
136
|
+
[full_path].select {|path| File.exist?(path) }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Relative paths to the target files that exist.
|
141
|
+
def files
|
142
|
+
@files ||= full_files.map {|file| relative(file) }
|
143
|
+
end
|
144
|
+
|
145
|
+
# Convert an absolute path to a relative one
|
146
|
+
def relative(file)
|
147
|
+
if File.directory?(full_path)
|
148
|
+
file[full_path.length+1..-1]
|
149
|
+
else
|
150
|
+
File.basename(file)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Convert a relative path to an absolute one.
|
155
|
+
def absolute(file)
|
156
|
+
if File.directory?(full_path)
|
157
|
+
File.join(full_path, file)
|
158
|
+
else
|
159
|
+
full_path
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|