rspec-path_matchers 0.1.1 → 0.2.1

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.rubocop.yml +5 -0
  4. data/.yardopts +7 -0
  5. data/CHANGELOG.md +34 -1
  6. data/README.md +164 -262
  7. data/design.rb +10 -10
  8. data/lib/rspec/path_matchers/matchers/base.rb +210 -56
  9. data/lib/rspec/path_matchers/matchers/directory_matcher.rb +172 -0
  10. data/lib/rspec/path_matchers/matchers/{have_file.rb → file_matcher.rb} +8 -5
  11. data/lib/rspec/path_matchers/matchers/no_entry_matcher.rb +64 -0
  12. data/lib/rspec/path_matchers/matchers/{have_symlink.rb → symlink_matcher.rb} +9 -6
  13. data/lib/rspec/path_matchers/options/atime.rb +6 -40
  14. data/lib/rspec/path_matchers/options/base.rb +315 -0
  15. data/lib/rspec/path_matchers/options/birthtime.rb +6 -49
  16. data/lib/rspec/path_matchers/options/content.rb +21 -40
  17. data/lib/rspec/path_matchers/options/ctime.rb +6 -40
  18. data/lib/rspec/path_matchers/options/etc_base.rb +42 -0
  19. data/lib/rspec/path_matchers/options/file_stat_base.rb +47 -0
  20. data/lib/rspec/path_matchers/options/group.rb +5 -52
  21. data/lib/rspec/path_matchers/options/json_content.rb +6 -30
  22. data/lib/rspec/path_matchers/options/mode.rb +6 -40
  23. data/lib/rspec/path_matchers/options/mtime.rb +7 -41
  24. data/lib/rspec/path_matchers/options/owner.rb +6 -53
  25. data/lib/rspec/path_matchers/options/parsed_content_base.rb +66 -0
  26. data/lib/rspec/path_matchers/options/size.rb +5 -40
  27. data/lib/rspec/path_matchers/options/symlink_atime.rb +7 -40
  28. data/lib/rspec/path_matchers/options/symlink_birthtime.rb +7 -49
  29. data/lib/rspec/path_matchers/options/symlink_ctime.rb +7 -40
  30. data/lib/rspec/path_matchers/options/symlink_group.rb +6 -52
  31. data/lib/rspec/path_matchers/options/symlink_mtime.rb +7 -40
  32. data/lib/rspec/path_matchers/options/symlink_owner.rb +7 -53
  33. data/lib/rspec/path_matchers/options/symlink_target.rb +6 -43
  34. data/lib/rspec/path_matchers/options/symlink_target_exist.rb +6 -41
  35. data/lib/rspec/path_matchers/options/symlink_target_type.rb +20 -43
  36. data/lib/rspec/path_matchers/options/yaml_content.rb +6 -31
  37. data/lib/rspec/path_matchers/options.rb +1 -0
  38. data/lib/rspec/path_matchers/refinements.rb +79 -0
  39. data/lib/rspec/path_matchers/version.rb +1 -1
  40. data/lib/rspec/path_matchers.rb +185 -16
  41. metadata +13 -8
  42. data/lib/rspec/path_matchers/matchers/directory_contents_inspector.rb +0 -57
  43. data/lib/rspec/path_matchers/matchers/have_directory.rb +0 -126
  44. 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.description(expected)
11
- RSpec::PathMatchers.matcher?(expected) ? expected.description : expected.inspect
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,315 @@
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 it 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
+ # Add to `failures` if actual value matches the normalized expected value
193
+ #
194
+ # This is called when expected is not an RSpec matcher.
195
+ #
196
+ # Option subclasses should override this method to provide custom matching
197
+ # logic or custom failure messages.
198
+ #
199
+ # @param actual [Object] the actual value fetched from the file system
200
+ #
201
+ # @param expected [Object] the expected literal value to match against
202
+ #
203
+ # @param failures [Array<RSpec::PathMatchers::Failure>] the array to append
204
+ # failure objects to (if any)
205
+ #
206
+ # @return [void]
207
+ #
208
+ # @api protected
209
+ #
210
+ private_class_method def self.match_literal(actual, expected, failures)
211
+ expected = normalize_expected_literal(expected)
212
+
213
+ return if literal_match?(actual, expected)
214
+
215
+ add_failure(literal_failure_message(actual, expected), failures)
216
+ end
217
+
218
+ private_class_method def self.add_failure(message, failures)
219
+ failures << RSpec::PathMatchers::Failure.new('.', message)
220
+ end
221
+
222
+ # Generates a failure message for a literal match failure
223
+ #
224
+ # This is used when the actual value does not match the expected value.
225
+ # It provides a clear message indicating what was expected and what was
226
+ # actually found.
227
+ #
228
+ # Option subclasses should override this method to provide custom failure
229
+ # messages for specific types of options.
230
+ #
231
+ # @example generate a failure message for a literal match failure
232
+ # def self.literal_failure_message(actual, expected)
233
+ # if expected.is_a?(Regexp)
234
+ # "expected #{key} to match #{expected.inspect}, but it was #{actual.inspect}"
235
+ # else
236
+ # "expected #{key} to be #{expected.inspect}, but it was #{actual.inspect}"
237
+ # end
238
+ # end
239
+ #
240
+ # @param actual [Object] the actual value fetched from the file system
241
+ #
242
+ # @param expected [Object] the expected literal value to match against
243
+ #
244
+ # @return [String] the failure message
245
+ #
246
+ # @api protected
247
+ #
248
+ private_class_method def self.literal_failure_message(actual, expected)
249
+ "expected #{key} to be #{expected.inspect}, but it was #{actual.inspect}"
250
+ end
251
+
252
+ # Add to `failures` if actual value matches the normalized expected value
253
+ #
254
+ # This is called when expected is an RSpec matcher.
255
+ #
256
+ # Option subclasses should override this method to provide custom matching
257
+ # logic or custom failure messages.
258
+ #
259
+ # @param actual [Object] the actual value fetched from the file system
260
+ #
261
+ # @param expected [RSpec::Matchers::Matcher] the expected matcher to match against
262
+ #
263
+ # @param failures [Array<RSpec::PathMatchers::Failure>] the array to append
264
+ # failure objects to (if any)
265
+ #
266
+ # @return [void]
267
+ #
268
+ # @api protected
269
+ #
270
+ private_class_method def self.match_matcher(actual, expected, failures)
271
+ return if expected.matches?(actual)
272
+
273
+ add_failure(matcher_failure_message(actual, expected), failures)
274
+ end
275
+
276
+ # Generates a failure message for a matcher match failure
277
+ #
278
+ # This is used when the actual value does not match the expected value.
279
+ # It provides a clear message indicating what was expected and what was
280
+ # actually found.
281
+ #
282
+ # Option subclasses should override this method to provide custom failure
283
+ # messages for specific types of options.
284
+ #
285
+ # @param actual [Object] the actual value fetched from the file system
286
+ #
287
+ # @param expected [Object] the expected literal value to match against
288
+ #
289
+ # @return [String] the failure message
290
+ #
291
+ # @api protected
292
+ #
293
+ private_class_method def self.matcher_failure_message(actual, expected)
294
+ "expected #{key} to #{expected.description}, but it was #{actual.inspect}"
295
+ end
296
+
297
+ # Warning message for unsupported expectations
298
+ #
299
+ # This is used when the platform or file system does not support the
300
+ # expectation, such as when trying to check file ownership on a platform that
301
+ # does not support it.
302
+ #
303
+ # @param path [String] the path of the entry that the expectation was attempted on
304
+ #
305
+ # @return [String] a warning message indicating that the expectation is not supported
306
+ #
307
+ # @api private
308
+ #
309
+ private_class_method def self.not_supported_message(path)
310
+ "WARNING: #{key} expectations are not supported for #{path} and will be skipped"
311
+ end
312
+ end
313
+ end
314
+ end
315
+ 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.description(expected)
11
- RSpec::PathMatchers.matcher?(expected) ? expected.description : expected.inspect
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,57 +1,38 @@
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.description(expected)
11
- RSpec::PathMatchers.matcher?(expected) ? expected.description : expected.inspect
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)
19
-
20
- failure_messages <<
21
- "expected `#{key}:` to be a String, Regexp, or Matcher, but was #{expected.inspect}"
22
- end
23
-
24
- # Returns nil if the path matches the expected content
25
- # @param path [String] the path of the entry to check
26
- # @return [String, nil]
27
- #
28
- def self.match(path, expected, failure_messages)
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)
34
- end
14
+ # Override to provide custom matching logic for regexp literals
15
+ def self.literal_match?(actual, expected)
16
+ expected.is_a?(Regexp) ? expected.match?(actual) : super
35
17
  end
36
18
 
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}"
19
+ # Handles failures when a matcher is used (e.g., content: include('...'))
20
+ def self.matcher_failure_message(actual, expected)
21
+ actual_summary = actual.length > 100 ? 'did not' : "was #{actual.inspect}"
22
+ "expected content to #{expected.description}, but it #{actual_summary}"
43
23
  end
44
24
 
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
25
+ def self.literal_failure_message(actual, expected)
26
+ verb = expected.is_a?(Regexp) ? 'match' : 'be'
50
27
 
51
- private_class_method def self.match_matcher(actual, expected, failure_messages)
52
- return if expected.matches?(actual)
28
+ actual_summary =
29
+ if actual.length > 100
30
+ verb == 'match' ? 'did not' : 'was not'
31
+ else
32
+ "was #{actual.inspect}"
33
+ end
53
34
 
54
- failure_messages << "expected content to #{expected.description}"
35
+ "expected content to #{verb} #{expected.inspect}, but it #{actual_summary}"
55
36
  end
56
37
  end
57
38
  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.description(expected)
11
- RSpec::PathMatchers.matcher?(expected) ? expected.description : expected.inspect
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