rspec-path_matchers 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +33 -0
- data/README.md +161 -260
- data/design.rb +12 -12
- data/lib/rspec/path_matchers/matchers/base.rb +210 -56
- data/lib/rspec/path_matchers/matchers/directory_matcher.rb +172 -0
- data/lib/rspec/path_matchers/matchers/{have_file.rb → file_matcher.rb} +8 -5
- data/lib/rspec/path_matchers/matchers/no_entry_matcher.rb +64 -0
- data/lib/rspec/path_matchers/matchers/{have_symlink.rb → symlink_matcher.rb} +9 -6
- data/lib/rspec/path_matchers/options/atime.rb +6 -40
- data/lib/rspec/path_matchers/options/base.rb +296 -0
- data/lib/rspec/path_matchers/options/birthtime.rb +6 -49
- data/lib/rspec/path_matchers/options/content.rb +13 -42
- data/lib/rspec/path_matchers/options/ctime.rb +6 -40
- data/lib/rspec/path_matchers/options/etc_base.rb +42 -0
- data/lib/rspec/path_matchers/options/file_stat_base.rb +47 -0
- data/lib/rspec/path_matchers/options/group.rb +5 -52
- data/lib/rspec/path_matchers/options/json_content.rb +6 -30
- data/lib/rspec/path_matchers/options/mode.rb +6 -40
- data/lib/rspec/path_matchers/options/mtime.rb +7 -41
- data/lib/rspec/path_matchers/options/owner.rb +6 -53
- data/lib/rspec/path_matchers/options/parsed_content_base.rb +67 -0
- data/lib/rspec/path_matchers/options/size.rb +5 -40
- data/lib/rspec/path_matchers/options/symlink_atime.rb +7 -40
- data/lib/rspec/path_matchers/options/symlink_birthtime.rb +7 -49
- data/lib/rspec/path_matchers/options/symlink_ctime.rb +7 -40
- data/lib/rspec/path_matchers/options/symlink_group.rb +6 -52
- data/lib/rspec/path_matchers/options/symlink_mtime.rb +7 -40
- data/lib/rspec/path_matchers/options/symlink_owner.rb +7 -53
- data/lib/rspec/path_matchers/options/symlink_target.rb +6 -43
- data/lib/rspec/path_matchers/options/symlink_target_exist.rb +7 -42
- data/lib/rspec/path_matchers/options/symlink_target_type.rb +20 -43
- data/lib/rspec/path_matchers/options/yaml_content.rb +6 -31
- data/lib/rspec/path_matchers/options.rb +1 -0
- data/lib/rspec/path_matchers/refinements.rb +79 -0
- data/lib/rspec/path_matchers/version.rb +1 -1
- data/lib/rspec/path_matchers.rb +185 -16
- metadata +12 -8
- data/lib/rspec/path_matchers/matchers/directory_contents_inspector.rb +0 -57
- data/lib/rspec/path_matchers/matchers/have_directory.rb +0 -126
- data/lib/rspec/path_matchers/matchers/have_no_entry.rb +0 -49
@@ -1,50 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'file_stat_base'
|
4
|
+
|
3
5
|
module RSpec
|
4
6
|
module PathMatchers
|
5
7
|
module Options
|
6
8
|
# atime: <expected>
|
7
|
-
class Atime
|
9
|
+
class Atime < FileStatBase
|
8
10
|
def self.key = :atime
|
9
|
-
|
10
|
-
def self.
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.validate_expected(expected, failure_messages)
|
15
|
-
return if expected == NOT_GIVEN ||
|
16
|
-
expected.is_a?(Time) || expected.is_a?(DateTime) ||
|
17
|
-
RSpec::PathMatchers.matcher?(expected)
|
18
|
-
|
19
|
-
failure_messages <<
|
20
|
-
"expected `#{key}:` to be a Matcher, Time, or DateTime, but was #{expected.inspect}"
|
21
|
-
end
|
22
|
-
|
23
|
-
# Returns nil if the expected value matches the actual value
|
24
|
-
# @param path [String] the path of the entry to check
|
25
|
-
# @return [String, nil]
|
26
|
-
#
|
27
|
-
def self.match(path, expected, failure_messages)
|
28
|
-
actual = File.stat(path).atime
|
29
|
-
case expected
|
30
|
-
when Time, DateTime then match_time(actual, expected, failure_messages)
|
31
|
-
else match_matcher(actual, expected, failure_messages)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
# private methods
|
36
|
-
|
37
|
-
private_class_method def self.match_time(actual, expected, failure_messages)
|
38
|
-
return if expected.to_time == actual
|
39
|
-
|
40
|
-
failure_messages << "expected #{key} to be #{expected.to_time.inspect}, but was #{actual.inspect}"
|
41
|
-
end
|
42
|
-
|
43
|
-
private_class_method def self.match_matcher(actual, expected, failure_messages)
|
44
|
-
return if expected.matches?(actual)
|
45
|
-
|
46
|
-
failure_messages << "expected #{key} to #{expected.description}, but was #{actual.inspect}"
|
47
|
-
end
|
11
|
+
def self.stat_attribute = :atime
|
12
|
+
def self.valid_expected_types = [Time, DateTime]
|
13
|
+
def self.normalize_expected_literal(expected) = expected.to_time
|
48
14
|
end
|
49
15
|
end
|
50
16
|
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/path_matchers/refinements'
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module PathMatchers
|
7
|
+
module Options
|
8
|
+
# Abstract base class for all option matchers
|
9
|
+
#
|
10
|
+
# @ api public
|
11
|
+
#
|
12
|
+
class Base
|
13
|
+
using RSpec::PathMatchers::Refinements::ArrayRefinements
|
14
|
+
|
15
|
+
# The option key
|
16
|
+
#
|
17
|
+
# For example, if the option key is `:owner`, then it could be used like this:
|
18
|
+
#
|
19
|
+
# ```ruby
|
20
|
+
# expect(path).to be_file(owner: 'alice')
|
21
|
+
# ```
|
22
|
+
#
|
23
|
+
# @abstract
|
24
|
+
#
|
25
|
+
# @return [Symbol] the key for this option matcher
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
#
|
29
|
+
def self.key
|
30
|
+
raise NotImplementedError, 'Subclasses must implement Base.key'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Adds to `failures` if the entry at path does not match the expectation
|
34
|
+
#
|
35
|
+
# Entry is a file, directory, or symlink.
|
36
|
+
#
|
37
|
+
# You can assume that entry at path exists and is the expected type (file,
|
38
|
+
# directory, or symlink).
|
39
|
+
#
|
40
|
+
# This is the main method that the matcher (such as be_dir, be_file, or
|
41
|
+
# be_symlink) calls to run its check for an option.
|
42
|
+
#
|
43
|
+
# @param path [String] the path of the entry to check
|
44
|
+
#
|
45
|
+
# @param expected [Object] the expected value to match against the entry
|
46
|
+
#
|
47
|
+
# @param failures [Array<RSpec::PathMatchers::Failure>] the array to append
|
48
|
+
# failure objects to (if any)
|
49
|
+
#
|
50
|
+
# @return [void]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
#
|
54
|
+
def self.match(path, expected, failures)
|
55
|
+
actual = fetch_actual(path, failures)
|
56
|
+
return if actual == FETCH_ERROR
|
57
|
+
rescue NotImplementedError
|
58
|
+
RSpec.configuration.reporter.message(not_supported_message(path))
|
59
|
+
else
|
60
|
+
if RSpec::PathMatchers.matcher?(expected)
|
61
|
+
match_matcher(actual, expected, failures)
|
62
|
+
else
|
63
|
+
match_literal(actual, expected, failures)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# The description of the expectation for this option
|
68
|
+
#
|
69
|
+
# This is used by RSpec when describing the matcher when tests are run in
|
70
|
+
# documentation format or when generating failure messages.
|
71
|
+
#
|
72
|
+
# @param expected [Object] the expected value to match against the entry
|
73
|
+
#
|
74
|
+
# @return [String] the description of the expectation
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
#
|
78
|
+
def self.description(expected)
|
79
|
+
RSpec::PathMatchers.matcher?(expected) ? expected.description : expected.inspect
|
80
|
+
end
|
81
|
+
|
82
|
+
# Adds to `errors` if the value of `expected` is not valid for this option type
|
83
|
+
#
|
84
|
+
# The matcher (such as `be_dir`, `be_file`, or `be_symlink`) calls this
|
85
|
+
# method to validate the expected value before running the matcher.
|
86
|
+
#
|
87
|
+
# It checks that the expected value is a RSpec matcher or one of the types
|
88
|
+
# listed in {valid_expected_types}.
|
89
|
+
#
|
90
|
+
# @param expected [Object] the expected value to validate
|
91
|
+
#
|
92
|
+
# @param errors [Array<String>] the array to append validation errors to
|
93
|
+
#
|
94
|
+
# @return [void]
|
95
|
+
#
|
96
|
+
# @api public
|
97
|
+
#
|
98
|
+
def self.validate_expected(expected, errors)
|
99
|
+
return if expected == NOT_GIVEN ||
|
100
|
+
RSpec::PathMatchers.matcher?(expected) ||
|
101
|
+
valid_expected_types.any? { |type| expected.is_a?(type) }
|
102
|
+
|
103
|
+
types = ['Matcher', *valid_expected_types.map(&:name)].to_sentence(conjunction: 'or')
|
104
|
+
|
105
|
+
errors << "expected `#{key}:` to be a #{types}, but was #{expected.inspect}"
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
# The actual value the expectation will be compared with
|
111
|
+
#
|
112
|
+
# Depending on what is being checked, this could be a file's owner, group,
|
113
|
+
# permissions, content, etc.
|
114
|
+
#
|
115
|
+
# Return `FETCH_ERROR` if the value could not be fetched, which will
|
116
|
+
# cause the matcher to fail.
|
117
|
+
#
|
118
|
+
# @param path [String] the path of the entry to check
|
119
|
+
#
|
120
|
+
# @param failures [Array<RSpec::PathMatchers::Failure>] the array to append
|
121
|
+
# failure objects to (if any)
|
122
|
+
#
|
123
|
+
# @return [Object, FETCH_ERROR] the actual value of the entry at path or FETCH_ERROR
|
124
|
+
#
|
125
|
+
# @api protected
|
126
|
+
#
|
127
|
+
private_class_method def self.fetch_actual(path, failures)
|
128
|
+
raise NotImplementedError, 'Subclasses must implement Base.fetch_actual'
|
129
|
+
end
|
130
|
+
|
131
|
+
# The valid types (in addition to an RSpec matcher) for the option value
|
132
|
+
#
|
133
|
+
# For instance, if the option key is `:owner`, then the option value could be
|
134
|
+
# a `String` specifying the owner name as in `be_file(owner: 'alice')`.
|
135
|
+
#
|
136
|
+
# @example specify that only an RSpec matcher is allowed
|
137
|
+
# def self.valid_expected_types = []
|
138
|
+
#
|
139
|
+
# @example specify that the option value must be an RSpec matcher or a String
|
140
|
+
# def self.valid_expected_types = [String]
|
141
|
+
#
|
142
|
+
# @example specify that the option value can be a matcher, String, or Regexp
|
143
|
+
# def self.valid_expected_types = [String, Regexp]
|
144
|
+
#
|
145
|
+
# @return [Array<Class>] an array of valid types for the option value
|
146
|
+
#
|
147
|
+
# @api protected
|
148
|
+
#
|
149
|
+
private_class_method def self.valid_expected_types = []
|
150
|
+
|
151
|
+
# Converts the expected value to a normalized form for comparison
|
152
|
+
#
|
153
|
+
# This is used to ensure that the expected value is in a consistent format
|
154
|
+
# for comparison, such as converting a DateTime object to a Time object.
|
155
|
+
#
|
156
|
+
# This is NOT called if expected is an RSpec matcher.
|
157
|
+
#
|
158
|
+
# @example normalize the expected value to a Time object
|
159
|
+
# def self.normalize_expected_literal(expected) = expected.to_time
|
160
|
+
#
|
161
|
+
# @param expected [Object] the expected value to normalize
|
162
|
+
#
|
163
|
+
# @return [Object] the normalized expected value
|
164
|
+
#
|
165
|
+
# @api protected
|
166
|
+
#
|
167
|
+
private_class_method def self.normalize_expected_literal(expected) = expected
|
168
|
+
|
169
|
+
# Checks if the actual value matches the expected value
|
170
|
+
#
|
171
|
+
# This is called whenever expected is not an RSpec matcher. By default,
|
172
|
+
# it does a simple equality check using `==`.
|
173
|
+
#
|
174
|
+
# Option subclasses should override this method to provide custom matching
|
175
|
+
# logic, such as when `expected` is a Regexp.
|
176
|
+
#
|
177
|
+
# @example check if the actual value matches a Regexp or a String
|
178
|
+
# def self.literal_match?(actual, expected)
|
179
|
+
# expected.is_a?(Regexp) ? expected.match?(actual) : expected == actual
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# @param actual [Object] the actual value fetched from the file system
|
183
|
+
#
|
184
|
+
# @param expected [Object] the expected literal value to match against
|
185
|
+
#
|
186
|
+
# @return [Boolean] true if they match, false otherwise
|
187
|
+
#
|
188
|
+
# @api protected
|
189
|
+
#
|
190
|
+
private_class_method def self.literal_match?(actual, expected) = actual == expected
|
191
|
+
|
192
|
+
# Generates a failure message for a literal match failure
|
193
|
+
#
|
194
|
+
# This is used when the actual value does not match the expected value.
|
195
|
+
# It provides a clear message indicating what was expected and what was
|
196
|
+
# actually found.
|
197
|
+
#
|
198
|
+
# Option subclasses should override this method to provide custom failure
|
199
|
+
# messages for specific types of options.
|
200
|
+
#
|
201
|
+
# @example generate a failure message for a literal match failure
|
202
|
+
# def self.literal_failure_message(actual, expected)
|
203
|
+
# if expected.is_a?(Regexp)
|
204
|
+
# "expected #{key} to match #{expected.inspect}, but was #{actual.inspect}"
|
205
|
+
# else
|
206
|
+
# "expected #{key} to be #{expected.inspect}, but was #{actual.inspect}"
|
207
|
+
# end
|
208
|
+
# end
|
209
|
+
#
|
210
|
+
# @param actual [Object] the actual value fetched from the file system
|
211
|
+
#
|
212
|
+
# @param expected [Object] the expected literal value to match against
|
213
|
+
#
|
214
|
+
# @return [String] the failure message
|
215
|
+
#
|
216
|
+
# @api protected
|
217
|
+
#
|
218
|
+
private_class_method def self.literal_failure_message(actual, expected)
|
219
|
+
"expected #{key} to be #{expected.inspect}, but was #{actual.inspect}"
|
220
|
+
end
|
221
|
+
|
222
|
+
# Add to `failures` if actual value matches the normalized expected value
|
223
|
+
#
|
224
|
+
# This is called when expected is not an RSpec matcher.
|
225
|
+
#
|
226
|
+
# Option subclasses should override this method to provide custom matching
|
227
|
+
# logic or custom failure messages.
|
228
|
+
#
|
229
|
+
# @param actual [Object] the actual value fetched from the file system
|
230
|
+
#
|
231
|
+
# @param expected [Object] the expected literal value to match against
|
232
|
+
#
|
233
|
+
# @param failures [Array<RSpec::PathMatchers::Failure>] the array to append
|
234
|
+
# failure objects to (if any)
|
235
|
+
#
|
236
|
+
# @return [void]
|
237
|
+
#
|
238
|
+
# @api protected
|
239
|
+
#
|
240
|
+
private_class_method def self.match_literal(actual, expected, failures)
|
241
|
+
expected = normalize_expected_literal(expected)
|
242
|
+
|
243
|
+
return if literal_match?(actual, expected)
|
244
|
+
|
245
|
+
message = literal_failure_message(actual, expected)
|
246
|
+
add_failure(message, failures)
|
247
|
+
end
|
248
|
+
|
249
|
+
private_class_method def self.add_failure(message, failures)
|
250
|
+
failures << RSpec::PathMatchers::Failure.new('.', message)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Add to `failures` if actual value matches the normalized expected value
|
254
|
+
#
|
255
|
+
# This is called when expected is an RSpec matcher.
|
256
|
+
#
|
257
|
+
# Option subclasses should override this method to provide custom matching
|
258
|
+
# logic or custom failure messages.
|
259
|
+
#
|
260
|
+
# @param actual [Object] the actual value fetched from the file system
|
261
|
+
#
|
262
|
+
# @param expected [RSpec::Matchers::Matcher] the expected matcher to match against
|
263
|
+
#
|
264
|
+
# @param failures [Array<RSpec::PathMatchers::Failure>] the array to append
|
265
|
+
# failure objects to (if any)
|
266
|
+
#
|
267
|
+
# @return [void]
|
268
|
+
#
|
269
|
+
# @api protected
|
270
|
+
#
|
271
|
+
private_class_method def self.match_matcher(actual, expected, failures)
|
272
|
+
return if expected.matches?(actual)
|
273
|
+
|
274
|
+
message = "expected #{key} to #{expected.description}, but was #{actual.inspect}"
|
275
|
+
add_failure(message, failures)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Warning message for unsupported expectations
|
279
|
+
#
|
280
|
+
# This is used when the platform or file system does not support the
|
281
|
+
# expectation, such as when trying to check file ownership on a platform that
|
282
|
+
# does not support it.
|
283
|
+
#
|
284
|
+
# @param path [String] the path of the entry that the expectation was attempted on
|
285
|
+
#
|
286
|
+
# @return [String] a warning message indicating that the expectation is not supported
|
287
|
+
#
|
288
|
+
# @api private
|
289
|
+
#
|
290
|
+
private_class_method def self.not_supported_message(path)
|
291
|
+
"WARNING: #{key} expectations are not supported for #{path} and will be skipped"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
@@ -1,59 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'file_stat_base'
|
4
|
+
|
3
5
|
module RSpec
|
4
6
|
module PathMatchers
|
5
7
|
module Options
|
6
8
|
# birthtime: <expected>
|
7
|
-
class Birthtime
|
9
|
+
class Birthtime < FileStatBase
|
8
10
|
def self.key = :birthtime
|
9
|
-
|
10
|
-
def self.
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.validate_expected(expected, failure_messages)
|
15
|
-
return if expected == NOT_GIVEN ||
|
16
|
-
expected.is_a?(Time) || expected.is_a?(DateTime) ||
|
17
|
-
RSpec::PathMatchers.matcher?(expected)
|
18
|
-
|
19
|
-
failure_messages <<
|
20
|
-
"expected `#{key}:` to be a Matcher, Time, or DateTime, but was #{expected.inspect}"
|
21
|
-
end
|
22
|
-
|
23
|
-
# Returns nil if the expected birthtime matches the actual birthtime
|
24
|
-
# @param path [String] the path of the entry to check
|
25
|
-
# @param expected [Object] the expected value to match against the actual value
|
26
|
-
# @param failure_messages [Array<String>] the array to append failure messages to
|
27
|
-
# @return [Void]
|
28
|
-
#
|
29
|
-
def self.match(path, expected, failure_messages) # rubocop:disable Metrics/MethodLength
|
30
|
-
begin
|
31
|
-
actual = File.stat(path).birthtime
|
32
|
-
rescue NotImplementedError
|
33
|
-
message = "WARNING: #{key} expectations are not supported for #{path} and will be skipped"
|
34
|
-
RSpec.configuration.reporter.message(message)
|
35
|
-
return
|
36
|
-
end
|
37
|
-
|
38
|
-
case expected
|
39
|
-
when Time, DateTime then match_time(actual, expected, failure_messages)
|
40
|
-
else match_matcher(actual, expected, failure_messages)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
# private methods
|
45
|
-
|
46
|
-
private_class_method def self.match_time(actual, expected, failure_messages)
|
47
|
-
return if expected.to_time == actual
|
48
|
-
|
49
|
-
failure_messages << "expected #{key} to be #{expected.to_time.inspect}, but was #{actual.inspect}"
|
50
|
-
end
|
51
|
-
|
52
|
-
private_class_method def self.match_matcher(actual, expected, failure_messages)
|
53
|
-
return if expected.matches?(actual)
|
54
|
-
|
55
|
-
failure_messages << "expected #{key} to #{expected.description}, but was #{actual.inspect}"
|
56
|
-
end
|
11
|
+
def self.stat_attribute = :birthtime
|
12
|
+
def self.valid_expected_types = [Time, DateTime]
|
13
|
+
def self.normalize_expected_literal(expected) = expected.to_time
|
57
14
|
end
|
58
15
|
end
|
59
16
|
end
|
@@ -1,58 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'base'
|
4
|
+
|
3
5
|
module RSpec
|
4
6
|
module PathMatchers
|
5
7
|
module Options
|
6
8
|
# content: <expected>
|
7
|
-
class Content
|
9
|
+
class Content < Base
|
8
10
|
def self.key = :content
|
11
|
+
def self.fetch_actual(path, _failures) = File.read(path)
|
12
|
+
def self.valid_expected_types = [String, Regexp]
|
9
13
|
|
10
|
-
def self.
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.validate_expected(expected, failure_messages)
|
15
|
-
return if expected == NOT_GIVEN ||
|
16
|
-
expected.is_a?(String) ||
|
17
|
-
expected.is_a?(Regexp) ||
|
18
|
-
RSpec::PathMatchers.matcher?(expected)
|
14
|
+
def self.literal_match?(actual, expected)
|
15
|
+
return expected.match?(actual) if expected.is_a?(Regexp)
|
19
16
|
|
20
|
-
|
21
|
-
"expected `#{key}:` to be a String, Regexp, or Matcher, but was #{expected.inspect}"
|
17
|
+
super
|
22
18
|
end
|
23
19
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
actual = File.read(path)
|
30
|
-
case expected
|
31
|
-
when String then match_string(actual, expected, failure_messages)
|
32
|
-
when Regexp then match_regexp(actual, expected, failure_messages)
|
33
|
-
else match_matcher(actual, expected, failure_messages)
|
20
|
+
def self.literal_failure_message(actual, expected)
|
21
|
+
if expected.is_a?(Regexp)
|
22
|
+
"expected content to match #{expected.inspect}, but got #{actual.inspect}"
|
23
|
+
else
|
24
|
+
super
|
34
25
|
end
|
35
26
|
end
|
36
|
-
|
37
|
-
# private methods
|
38
|
-
|
39
|
-
private_class_method def self.match_string(actual, expected, failure_messages)
|
40
|
-
return if expected == actual
|
41
|
-
|
42
|
-
failure_messages << "expected content to be #{expected.inspect}"
|
43
|
-
end
|
44
|
-
|
45
|
-
private_class_method def self.match_regexp(actual, expected, failure_messages)
|
46
|
-
return if expected.match?(actual)
|
47
|
-
|
48
|
-
failure_messages << "expected content to match #{expected.inspect}"
|
49
|
-
end
|
50
|
-
|
51
|
-
private_class_method def self.match_matcher(actual, expected, failure_messages)
|
52
|
-
return if expected.matches?(actual)
|
53
|
-
|
54
|
-
failure_messages << "expected content to #{expected.description}"
|
55
|
-
end
|
56
27
|
end
|
57
28
|
end
|
58
29
|
end
|
@@ -1,50 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'file_stat_base'
|
4
|
+
|
3
5
|
module RSpec
|
4
6
|
module PathMatchers
|
5
7
|
module Options
|
6
8
|
# ctime: <expected>
|
7
|
-
class Ctime
|
9
|
+
class Ctime < FileStatBase
|
8
10
|
def self.key = :ctime
|
9
|
-
|
10
|
-
def self.
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.validate_expected(expected, failure_messages)
|
15
|
-
return if expected == NOT_GIVEN ||
|
16
|
-
expected.is_a?(Time) || expected.is_a?(DateTime) ||
|
17
|
-
RSpec::PathMatchers.matcher?(expected)
|
18
|
-
|
19
|
-
failure_messages <<
|
20
|
-
"expected `#{key}:` to be a Matcher, Time, or DateTime, but was #{expected.inspect}"
|
21
|
-
end
|
22
|
-
|
23
|
-
# Returns nil if the path matches the expected size
|
24
|
-
# @param path [String] the path of the entry to check
|
25
|
-
# @return [String, nil]
|
26
|
-
#
|
27
|
-
def self.match(path, expected, failure_messages)
|
28
|
-
actual = File.stat(path).ctime
|
29
|
-
case expected
|
30
|
-
when Time, DateTime then match_time(actual, expected, failure_messages)
|
31
|
-
else match_matcher(actual, expected, failure_messages)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
# private methods
|
36
|
-
|
37
|
-
private_class_method def self.match_time(actual, expected, failure_messages)
|
38
|
-
return if expected.to_time == actual
|
39
|
-
|
40
|
-
failure_messages << "expected ctime to be #{expected.to_time.inspect}, but was #{actual.inspect}"
|
41
|
-
end
|
42
|
-
|
43
|
-
private_class_method def self.match_matcher(actual, expected, failure_messages)
|
44
|
-
return if expected.matches?(actual)
|
45
|
-
|
46
|
-
failure_messages << "expected ctime to #{expected.description}, but was #{actual.inspect}"
|
47
|
-
end
|
11
|
+
def self.stat_attribute = :ctime
|
12
|
+
def self.valid_expected_types = [Time, DateTime]
|
13
|
+
def self.normalize_expected_literal(expected) = expected.to_time
|
48
14
|
end
|
49
15
|
end
|
50
16
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'file_stat_base'
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module PathMatchers
|
7
|
+
module Options
|
8
|
+
# Base class for options that use the Etc module (owner, group)
|
9
|
+
class EtcBase < FileStatBase
|
10
|
+
def self.valid_expected_types = [String]
|
11
|
+
|
12
|
+
# Overrides the base match method to first check if the platform
|
13
|
+
# supports Etc lookups before proceeding
|
14
|
+
def self.match(path, expected, failures)
|
15
|
+
# Skip the check entirely if the platform doesn't support it
|
16
|
+
return unless supported_platform?
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
private_class_method def self.supported_platform?
|
22
|
+
return true if Etc.respond_to?(etc_method)
|
23
|
+
|
24
|
+
RSpec.configuration.reporter.message(
|
25
|
+
"WARNING: #{key} expectations are not supported on this platform and will be skipped."
|
26
|
+
)
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Fetches the UID/GID from stat and looks up the name via Etc
|
31
|
+
private_class_method def self.fetch_actual(path, _failures)
|
32
|
+
Etc.public_send(etc_method, super).name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Abstract method for subclasses to define :getpwuid or :getgrgid
|
36
|
+
private_class_method def self.etc_method
|
37
|
+
raise NotImplementedError, 'Subclasses must implement EtcBase.etc_method'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module PathMatchers
|
7
|
+
module Options
|
8
|
+
# Base class for options whose actual value comes from File::Stat
|
9
|
+
class FileStatBase < Base
|
10
|
+
# Implements fetch_actual by calling a specified method on a File::Stat object.
|
11
|
+
#
|
12
|
+
def self.fetch_actual(path, _failures)
|
13
|
+
File.public_send(stat_source_method, path).public_send(stat_attribute)
|
14
|
+
end
|
15
|
+
|
16
|
+
# The method used on a File object to get the stat information
|
17
|
+
#
|
18
|
+
# This should be `:stat` to follow symlinks and `:lstat` for symbolic links.
|
19
|
+
#
|
20
|
+
# The default is `:stat`, which means it will follow symbolic links.
|
21
|
+
#
|
22
|
+
# @return [Symbol]
|
23
|
+
#
|
24
|
+
# @api protected
|
25
|
+
#
|
26
|
+
private_class_method def self.stat_source_method = :stat
|
27
|
+
|
28
|
+
# The name of the File::Stat attribute used to get the actual value
|
29
|
+
#
|
30
|
+
# @example getting file size
|
31
|
+
# def self.stat_attribute = :size
|
32
|
+
#
|
33
|
+
# @return [Symbol]
|
34
|
+
#
|
35
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
36
|
+
#
|
37
|
+
# @abstract
|
38
|
+
#
|
39
|
+
# @api protected
|
40
|
+
#
|
41
|
+
private_class_method def self.stat_attribute
|
42
|
+
raise NotImplementedError, 'Subclasses must implement FileStatBase.stat_attribute'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|