rspec-path_matchers 0.1.1 → 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 +21 -0
- data/README.md +161 -260
- data/design.rb +10 -10
- 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 +6 -41
- 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
@@ -4,41 +4,134 @@ module RSpec
|
|
4
4
|
module PathMatchers
|
5
5
|
module Matchers
|
6
6
|
# The base class for matchers
|
7
|
+
#
|
8
|
+
# Implements the [RSpec matcher
|
9
|
+
# protocol](https://rspec.info/documentation/3.13/rspec-expectations/RSpec/Matchers/MatcherProtocol.html)
|
10
|
+
# for value expectations including:
|
11
|
+
#
|
12
|
+
# - `#matches?(base_path)` - checks if the matcher matches the given base_path
|
13
|
+
# - `#failure_message` - returns a human-readable failure message
|
14
|
+
# - `#actual` - returns the actual value that was matched against
|
15
|
+
# - `#expected` - returns the expected value that was matched against
|
16
|
+
# - `#description` - returns a human-readable description of the matcher
|
17
|
+
# - `#does_not_match?(base_path)` - checks if the matcher does not match the given base_path
|
18
|
+
# - `#failure_message_when_negated` - returns a human-readable failure message for negative matches
|
19
|
+
#
|
7
20
|
class Base # rubocop:disable Metrics/ClassLength
|
8
|
-
|
21
|
+
# Create a new matcher instance
|
22
|
+
#
|
23
|
+
# Subclasses may override this to provide additional arguments or options.
|
24
|
+
# They should call `super` to ensure the base class is initialized correctly.
|
25
|
+
#
|
26
|
+
# @param entry_name [String] The name of the entry to match against
|
27
|
+
#
|
28
|
+
# - If entry_name is empty, the matcher will match against the base_path
|
29
|
+
# directly (this is the path passed to `matches?` or `does_not_match?`).
|
30
|
+
# - If entry_name is NOT empty, the matcher will match against
|
31
|
+
# File.join(base_path, entry_name)
|
32
|
+
#
|
33
|
+
# @param matcher_name [Symbol] The matcher name (e.g. :have_dir, :have_file,
|
34
|
+
# etc.) to use in descriptions and messages
|
35
|
+
#
|
36
|
+
# @param options_hash [Hash] Options for the matcher passed to the options
|
37
|
+
# factory to get an options object
|
38
|
+
#
|
39
|
+
def initialize(entry_name, matcher_name:, **options_hash)
|
9
40
|
super()
|
10
|
-
@
|
41
|
+
@entry_name = entry_name.to_s
|
42
|
+
@matcher_name = matcher_name
|
11
43
|
@options = options_factory(*option_keys, **options_hash)
|
44
|
+
@failures = []
|
12
45
|
end
|
13
46
|
|
14
|
-
|
47
|
+
# @attribute [r] options
|
48
|
+
#
|
49
|
+
# The matcher options loaded from the options_hash passed to the initializer
|
50
|
+
#
|
51
|
+
# @return [Object] A Data object containing the options
|
52
|
+
#
|
53
|
+
attr_reader :options
|
54
|
+
|
55
|
+
# @attribute [r] base_path
|
56
|
+
#
|
57
|
+
# The base path against which the matcher is applied
|
58
|
+
#
|
59
|
+
# @return [String]
|
60
|
+
#
|
61
|
+
attr_reader :base_path
|
62
|
+
|
63
|
+
# @attribute [r] path
|
64
|
+
#
|
65
|
+
# The full path to the entry being matched against
|
66
|
+
#
|
67
|
+
# @return [String]
|
68
|
+
#
|
69
|
+
attr_reader :path
|
70
|
+
|
71
|
+
# @attribute [r] matcher_name
|
72
|
+
#
|
73
|
+
# The name of this matcher, used for descriptions and messages
|
74
|
+
#
|
75
|
+
# @return [Symbol] The matcher name (e.g. :have_dir, :have_file, etc.)
|
76
|
+
#
|
77
|
+
attr_reader :matcher_name
|
78
|
+
|
79
|
+
# @attribute [r] failures
|
80
|
+
#
|
81
|
+
# An array of failures that describe why the matcher did not match the actual
|
82
|
+
# value
|
83
|
+
#
|
84
|
+
# Only populated after `matches?` or `execute_match` is called
|
85
|
+
#
|
86
|
+
# @return [Array<RSpec::PathMatchers::Failure>]
|
87
|
+
#
|
88
|
+
attr_reader :failures
|
89
|
+
|
90
|
+
# @attribute [r] entry_name
|
91
|
+
#
|
92
|
+
# The name of the entry being matched against or an empty String
|
93
|
+
#
|
94
|
+
# If `entry_name` is empty, the matcher will match against the base_path
|
95
|
+
# directly. If `entry_name` is not empty, the matcher will match against
|
96
|
+
# `File.join(base_path, entry_name)`.
|
97
|
+
#
|
98
|
+
# @return [String]
|
99
|
+
#
|
100
|
+
def entry_name
|
101
|
+
@entry_name || base_path.basename
|
102
|
+
end
|
15
103
|
|
16
104
|
# A human-readable description of the matcher's expectation
|
17
105
|
#
|
18
|
-
# This is used by RSpec
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
106
|
+
# This description is used by RSpec formatters (e.g., `--format
|
107
|
+
# documentation`) to provide output about the specification itself. It is NOT
|
108
|
+
# used to build the detailed failure message when an expectation fails.
|
109
|
+
#
|
110
|
+
# Subclasses can override this method to add to or provide a more specific
|
111
|
+
# description based on the entry type, options, or other factors.
|
22
112
|
#
|
23
|
-
# @return [String]
|
113
|
+
# @return [String]
|
24
114
|
#
|
25
115
|
def description
|
26
|
-
desc = "have #{entry_type} #{
|
116
|
+
desc = (@entry_name.empty? ? "be a #{entry_type}" : "have #{entry_type} #{entry_name.inspect}")
|
27
117
|
options_description = build_options_description
|
28
118
|
desc += " with #{options_description}" unless options_description.empty?
|
29
119
|
desc
|
30
120
|
end
|
31
121
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
122
|
+
# Returns `true` if the matcher matches the actual value
|
123
|
+
#
|
124
|
+
# If `false` is returned, the `failures` array will be populated with
|
125
|
+
# human-readable error messages that describe why the match failed.
|
126
|
+
#
|
127
|
+
# @param base_path [Object] The base_path together with entry_name determine the actual value
|
128
|
+
#
|
129
|
+
# @return [Boolean] `true` if the matcher matches, `false` otherwise
|
130
|
+
#
|
131
|
+
# @raise [ArgumentError] if there are errors in the matcher or its options
|
132
|
+
#
|
36
133
|
def matches?(base_path)
|
37
|
-
#
|
38
|
-
# is reused
|
39
|
-
@failure_messages = []
|
40
|
-
|
41
|
-
# Phase 1: Validate all options recursively for syntax errors
|
134
|
+
# Phase 1: Validate all options for syntax errors
|
42
135
|
validation_errors = []
|
43
136
|
collect_validation_errors(validation_errors)
|
44
137
|
raise ArgumentError, validation_errors.join(', ') if validation_errors.any?
|
@@ -47,24 +140,31 @@ module RSpec
|
|
47
140
|
execute_match(base_path)
|
48
141
|
end
|
49
142
|
|
50
|
-
def
|
51
|
-
|
143
|
+
def failure_message
|
144
|
+
header = "#{path} was not as expected:"
|
145
|
+
grouped_failures = failures.group_by(&:relative_path)
|
146
|
+
|
147
|
+
messages = grouped_failures.map do |relative_path, failures_for_path|
|
148
|
+
format_failure_group(relative_path, failures_for_path)
|
149
|
+
end
|
150
|
+
|
151
|
+
"#{header}\n#{messages.join("\n")}"
|
52
152
|
end
|
53
153
|
|
54
154
|
# This method is called by RSpec for `expect(...).not_to have_...`
|
55
155
|
def does_not_match?(base_path) # rubocop:disable Naming/PredicatePrefix
|
56
|
-
# 1
|
156
|
+
# Phase 1: Validate all options for syntax errors (in this case options are not allowed)
|
57
157
|
validation_errors = []
|
58
158
|
collect_negative_validation_errors(validation_errors)
|
59
159
|
raise ArgumentError, validation_errors.join(', ') if validation_errors.any?
|
60
160
|
|
61
161
|
@base_path = base_path.to_s
|
62
|
-
@path = File.join(base_path,
|
162
|
+
@path = @entry_name.empty? ? base_path : File.join(base_path, entry_name)
|
63
163
|
|
64
|
-
# 2
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
164
|
+
# Phase 2: Execute the actual match logic
|
165
|
+
#
|
166
|
+
# A negative match SUCCEEDS if the entry of the specified type does NOT
|
167
|
+
# exist. We delegate the type-specific check to the subclass.
|
68
168
|
!correct_type?
|
69
169
|
end
|
70
170
|
|
@@ -73,12 +173,19 @@ module RSpec
|
|
73
173
|
"expected it not to be a #{entry_type}"
|
74
174
|
end
|
75
175
|
|
76
|
-
|
176
|
+
protected
|
177
|
+
|
178
|
+
# Add to `errors` if the matcher is not defined correctly
|
179
|
+
#
|
180
|
+
# Subclasses may override this method to provide additional checking. For
|
181
|
+
# instance, BeDirectory extends this to add validation for nested matchers.
|
77
182
|
#
|
78
|
-
#
|
79
|
-
# matchers
|
183
|
+
# A nesting matcher (such as BeDirectory) may call this method on the
|
184
|
+
# nested matchers to collect all validation errors before raising an
|
185
|
+
# ArgumentError.
|
80
186
|
#
|
81
|
-
# @param errors [Array<String>] An array to
|
187
|
+
# @param errors [Array<String>] An array to populate with validation error
|
188
|
+
# messages
|
82
189
|
#
|
83
190
|
# @return [void]
|
84
191
|
#
|
@@ -88,52 +195,93 @@ module RSpec
|
|
88
195
|
validate_option_values(errors)
|
89
196
|
end
|
90
197
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
198
|
+
# Returns `true` if `path` exists and is of the correct type for this matcher
|
199
|
+
#
|
200
|
+
# Subclasses must implement this method to check the type of the entry. For
|
201
|
+
# example, BeFile checks if the path is a regular file; BeDirectory, a
|
202
|
+
# directory; and BeSymlink, a symlink.
|
203
|
+
#
|
204
|
+
# @return [Boolean]
|
205
|
+
#
|
206
|
+
# @abstract
|
207
|
+
#
|
102
208
|
def correct_type?
|
103
|
-
raise NotImplementedError, '
|
209
|
+
raise NotImplementedError, 'Subclasses must implement Base#correct_type?'
|
104
210
|
end
|
105
211
|
|
106
|
-
|
107
|
-
|
212
|
+
# Add to errors if options were passed to the negative matcher
|
213
|
+
#
|
214
|
+
# This method is called by RSpec for `expect(...).not_to <matcher>...`
|
215
|
+
#
|
216
|
+
# Subclasses can override this to add additional validation.
|
217
|
+
#
|
218
|
+
# @param errors [Array<String>] An array to append validation error messages to
|
219
|
+
#
|
220
|
+
# @return [void]
|
221
|
+
#
|
222
|
+
def collect_negative_validation_errors(errors)
|
223
|
+
errors << "The matcher `not_to #{matcher_name}(...)` cannot be given options" if options.any_given?
|
108
224
|
end
|
109
225
|
|
110
|
-
|
226
|
+
# Subclasses should define their own option definitions
|
227
|
+
#
|
228
|
+
# @return [Array<RSpec::PathMatchers::Options::Base>] An array of option definitions
|
229
|
+
#
|
230
|
+
def option_definitions = []
|
111
231
|
|
232
|
+
# The type of entry this matcher is checking for
|
233
|
+
#
|
234
|
+
# @return [Symbol] The entry type (e.g. :file, :directory, :symlink, ...)
|
235
|
+
#
|
112
236
|
def entry_type
|
113
|
-
|
237
|
+
raise NotImplementedError, 'Subclasses must implement Base#entry_type'
|
114
238
|
end
|
115
239
|
|
116
240
|
# Performs the actual matching against the directory entry
|
117
241
|
#
|
118
242
|
# This method assumes that collect_validation_errors has already been called
|
119
|
-
# and passed.
|
120
|
-
#
|
243
|
+
# and passed.
|
244
|
+
#
|
245
|
+
# This method is protected so that container matchers (like BeDirectory)
|
246
|
+
# can call it on nested matchers without using .send.
|
121
247
|
#
|
122
248
|
def execute_match(base_path) # rubocop:disable Naming/PredicateMethod
|
249
|
+
# It is important to reset failures in case this matcher is reused
|
250
|
+
@failures = []
|
251
|
+
|
123
252
|
@base_path = base_path.to_s
|
124
|
-
@path = File.join(base_path,
|
253
|
+
@path = @entry_name.empty? ? base_path : File.join(base_path, entry_name)
|
125
254
|
|
126
255
|
# Validate existence and type.
|
127
|
-
validate_existance
|
128
|
-
return false if
|
256
|
+
validate_existance
|
257
|
+
return false if failures.any?
|
129
258
|
|
130
259
|
# Validate specific options and nested expectations.
|
131
260
|
validate_options
|
132
|
-
|
261
|
+
failures.empty?
|
133
262
|
end
|
134
263
|
|
135
264
|
private
|
136
265
|
|
266
|
+
# Formats a group of failures for a single relative path.
|
267
|
+
#
|
268
|
+
# @param relative_path [String] The path of the failure relative to the subject.
|
269
|
+
# @param failures_for_path [Array<Failure>] A list of failures for that path.
|
270
|
+
# @return [String] A formatted string block for this group of failures.
|
271
|
+
#
|
272
|
+
def format_failure_group(relative_path, failures_for_path)
|
273
|
+
indented_errors = failures_for_path.map { |f| " #{f.message}" }.join("\n")
|
274
|
+
|
275
|
+
if relative_path == '.'
|
276
|
+
# For failures on the subject itself, just show the indented errors.
|
277
|
+
indented_errors
|
278
|
+
else
|
279
|
+
# For failures on a child, show the path and then the indented errors.
|
280
|
+
path_line = " - #{relative_path}"
|
281
|
+
"#{path_line}\n#{indented_errors}"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
137
285
|
def build_options_description
|
138
286
|
descriptions = options.to_h.filter_map do |key, value|
|
139
287
|
next if value == RSpec::PathMatchers::Options::NOT_GIVEN
|
@@ -176,21 +324,27 @@ module RSpec
|
|
176
324
|
end
|
177
325
|
end
|
178
326
|
|
179
|
-
def validate_existance
|
180
|
-
raise NotImplementedError, '
|
327
|
+
def validate_existance
|
328
|
+
raise NotImplementedError, 'Subclasses must implement Base#validate_existance'
|
181
329
|
end
|
182
330
|
|
183
331
|
# Validate the options for the current matcher
|
184
332
|
#
|
185
|
-
# Subclasses
|
333
|
+
# Subclasses may override this to add additional validation logic. For
|
334
|
+
# instance, BeDirectory extends this to check nested matchers.
|
335
|
+
#
|
186
336
|
def validate_options
|
187
337
|
options.members.each do |key|
|
188
338
|
expected = options.send(key)
|
189
339
|
next if expected == RSpec::PathMatchers::Options::NOT_GIVEN
|
190
340
|
|
191
|
-
option_definition(key).match(path, expected,
|
341
|
+
option_definition(key).match(path, expected, failures)
|
192
342
|
end
|
193
343
|
end
|
344
|
+
|
345
|
+
def add_failure(message, failures)
|
346
|
+
failures << RSpec::PathMatchers::Failure.new('.', message)
|
347
|
+
end
|
194
348
|
end
|
195
349
|
end
|
196
350
|
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module RSpec
|
7
|
+
module PathMatchers
|
8
|
+
module Matchers
|
9
|
+
# An RSpec matcher that checks for the existence and properties of a directory
|
10
|
+
#
|
11
|
+
class DirectoryMatcher < Base
|
12
|
+
OPTIONS = [
|
13
|
+
RSpec::PathMatchers::Options::Atime,
|
14
|
+
RSpec::PathMatchers::Options::Birthtime,
|
15
|
+
RSpec::PathMatchers::Options::Ctime,
|
16
|
+
RSpec::PathMatchers::Options::Group,
|
17
|
+
RSpec::PathMatchers::Options::Mode,
|
18
|
+
RSpec::PathMatchers::Options::Mtime,
|
19
|
+
RSpec::PathMatchers::Options::Owner
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
# @attribute [r] exact
|
23
|
+
#
|
24
|
+
# If true, the dir must contain only entries given in `containing_exactly`
|
25
|
+
#
|
26
|
+
# The default is false, meaning the directory can contain additional entries.
|
27
|
+
#
|
28
|
+
# @return [Boolean]
|
29
|
+
#
|
30
|
+
attr_reader :exact
|
31
|
+
|
32
|
+
# Initializes the matcher with the directory name and options
|
33
|
+
#
|
34
|
+
# @param entry_name [String] The name of the directory relative to the subject or empty
|
35
|
+
#
|
36
|
+
# @param matcher_name [Symbol] The name of the DSL method used to create this matcher
|
37
|
+
#
|
38
|
+
# @param options_hash [Hash] A hash of attribute matchers (e.g., mode:, owner:)
|
39
|
+
#
|
40
|
+
def initialize(entry_name, matcher_name:, **options_hash)
|
41
|
+
super
|
42
|
+
|
43
|
+
@exact = false
|
44
|
+
@nested_matchers = []
|
45
|
+
end
|
46
|
+
|
47
|
+
def containing(*matchers)
|
48
|
+
@nested_matchers << matchers
|
49
|
+
@exact = false
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def containing_exactly(*matchers)
|
54
|
+
@nested_matchers << matchers
|
55
|
+
@exact = true
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def description
|
60
|
+
desc = super
|
61
|
+
return desc if nested_matchers.empty?
|
62
|
+
|
63
|
+
nested_descriptions = nested_matchers.map do |matcher|
|
64
|
+
matcher.description.lines.map.with_index do |line, i|
|
65
|
+
i.zero? ? "- #{line.chomp}" : " #{line.chomp}"
|
66
|
+
end.join("\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
"#{desc} containing#{' exactly' if exact}:\n #{nested_descriptions.join("\n ")}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def entry_type = :directory
|
73
|
+
def option_definitions = OPTIONS
|
74
|
+
def correct_type? = File.directory?(path)
|
75
|
+
|
76
|
+
def collect_negative_validation_errors(errors)
|
77
|
+
super
|
78
|
+
return unless nested_matchers.any?
|
79
|
+
|
80
|
+
errors << "The matcher `not_to #{matcher_name}(...)` cannot have expectations on its contents"
|
81
|
+
end
|
82
|
+
|
83
|
+
def collect_validation_errors(errors)
|
84
|
+
super
|
85
|
+
|
86
|
+
if @nested_matchers.size > 1
|
87
|
+
errors << 'Collectively, `#containing` and `#containing_exactly` may be called only once'
|
88
|
+
end
|
89
|
+
|
90
|
+
# Recursively validate nested matchers.
|
91
|
+
nested_matchers.each { |matcher| matcher.collect_validation_errors(errors) }
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
# Overrides Base to add the exactness check after other validations.
|
97
|
+
def validate_options
|
98
|
+
# Validate this directory's own options (mode, owner, etc.)
|
99
|
+
super
|
100
|
+
|
101
|
+
# Validate the nested entries described in `containing`
|
102
|
+
validate_nested_matchers
|
103
|
+
|
104
|
+
return unless failures.empty?
|
105
|
+
|
106
|
+
# If using `containing_exactly`, check for unexpected entries
|
107
|
+
check_for_unexpected_entries if exact
|
108
|
+
end
|
109
|
+
|
110
|
+
def validate_existance
|
111
|
+
return if File.directory?(path)
|
112
|
+
|
113
|
+
message = (File.exist?(path) ? 'expected it to be a directory' : 'expected it to exist')
|
114
|
+
add_failure(message, failures)
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# This new private method encapsulates the logic for checking children.
|
120
|
+
def validate_nested_matchers
|
121
|
+
nested_matchers.each do |matcher|
|
122
|
+
next if matcher.execute_match(path)
|
123
|
+
|
124
|
+
matcher.failures.each do |failure|
|
125
|
+
new_relative_path = (Pathname.new(matcher.entry_name) + Pathname.new(failure.relative_path)).to_s
|
126
|
+
failures << RSpec::PathMatchers::Failure.new(new_relative_path, failure.message)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# An array of nested matchers that define the expected contents of the
|
132
|
+
# directory
|
133
|
+
#
|
134
|
+
# One element is added to the @nested_matchers array for each call to
|
135
|
+
# `containing` or `containing_exactly`. Since `containing` or
|
136
|
+
# `containing_exactly` are allowed only once per matcher, an error will be
|
137
|
+
# logged during validation if the size of this array is greater than one.
|
138
|
+
#
|
139
|
+
# Each element in @nested_matchers is itself an array of the matchers given
|
140
|
+
# in each `containing` or `containing_exactly` call.
|
141
|
+
#
|
142
|
+
# This method returns the first element of @nested_matchers (or an empty
|
143
|
+
# array of @nested_matchers is itself empty).
|
144
|
+
#
|
145
|
+
# @return [Array<RSpec::PathMatchers::Matchers::Base>]
|
146
|
+
#
|
147
|
+
def nested_matchers
|
148
|
+
@nested_matchers.first || []
|
149
|
+
end
|
150
|
+
|
151
|
+
# Checks for any files/directories on disk that were not declared in the block.
|
152
|
+
def check_for_unexpected_entries
|
153
|
+
positively_declared_entries = nested_matchers.reject do |m|
|
154
|
+
m.is_a?(RSpec::PathMatchers::Matchers::NoEntryMatcher)
|
155
|
+
end.map(&:entry_name)
|
156
|
+
|
157
|
+
actual_entries = Dir.children(path)
|
158
|
+
unexpected_entries = actual_entries - positively_declared_entries
|
159
|
+
|
160
|
+
return if unexpected_entries.empty?
|
161
|
+
|
162
|
+
message = build_unexpected_entries_message(unexpected_entries)
|
163
|
+
add_failure(message, failures)
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_unexpected_entries_message(unexpected_entries)
|
167
|
+
"contained unexpected entries #{unexpected_entries.sort.inspect}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -6,7 +6,8 @@ module RSpec
|
|
6
6
|
module PathMatchers
|
7
7
|
module Matchers
|
8
8
|
# An RSpec matcher checks for the existence and properties of a file
|
9
|
-
|
9
|
+
#
|
10
|
+
class FileMatcher < Base
|
10
11
|
OPTIONS = [
|
11
12
|
RSpec::PathMatchers::Options::Atime,
|
12
13
|
RSpec::PathMatchers::Options::Birthtime,
|
@@ -21,23 +22,25 @@ module RSpec
|
|
21
22
|
RSpec::PathMatchers::Options::YamlContent
|
22
23
|
].freeze
|
23
24
|
|
25
|
+
def entry_type = :file
|
26
|
+
|
24
27
|
def option_definitions = OPTIONS
|
25
28
|
|
26
29
|
def correct_type? = File.file?(path)
|
27
30
|
|
28
|
-
def matcher_name = 'have_file'
|
29
|
-
|
30
31
|
protected
|
31
32
|
|
32
|
-
def validate_existance
|
33
|
+
def validate_existance
|
33
34
|
return nil if File.file?(path)
|
34
35
|
|
35
|
-
|
36
|
+
message =
|
36
37
|
if File.exist?(path)
|
37
38
|
'expected it to be a regular file'
|
38
39
|
else
|
39
40
|
'expected it to exist'
|
40
41
|
end
|
42
|
+
|
43
|
+
add_failure(message, failures)
|
41
44
|
end
|
42
45
|
end
|
43
46
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module PathMatchers
|
5
|
+
module Matchers
|
6
|
+
# Asserts that a directory entry of a specific entry_type does NOT exist.
|
7
|
+
# This is a simple, internal matcher-like object.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class NoEntryMatcher
|
11
|
+
attr_reader :entry_name, :entry_type, :base_path, :path
|
12
|
+
|
13
|
+
# Initializes the matcher with the entry name and type
|
14
|
+
#
|
15
|
+
# @param entry_name [String] The name of the entry to check
|
16
|
+
#
|
17
|
+
# @param matcher_name [Symbol] The name of the DSL method used to create this matcher
|
18
|
+
#
|
19
|
+
# @param entry_type [Symbol] The type of the entry (:file, :directory, or :symlink)
|
20
|
+
#
|
21
|
+
def initialize(entry_name, matcher_name:, entry_type:)
|
22
|
+
@entry_name = entry_name
|
23
|
+
@matcher_name = matcher_name
|
24
|
+
@entry_type = entry_type
|
25
|
+
@base_path = nil
|
26
|
+
@path = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# The core logic. Returns `true` if the expectation is met (the entry is absent).
|
30
|
+
def execute_match(base_path) # rubocop:disable Naming/PredicateMethod
|
31
|
+
@base_path = base_path
|
32
|
+
@path = File.join(base_path, entry_name)
|
33
|
+
|
34
|
+
case entry_type
|
35
|
+
when :file then !File.file?(path)
|
36
|
+
when :directory then !File.directory?(path)
|
37
|
+
else !File.symlink?(path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# The failure message if `execute_match` returns `false`.
|
42
|
+
def failure_message
|
43
|
+
"expected #{entry_type} '#{entry_name}' not to be found at '#{base_path}', but it exists"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the failure message in an array as expected by the DirectoryMatcher
|
47
|
+
#
|
48
|
+
# @return [Array<String>]
|
49
|
+
#
|
50
|
+
def failures
|
51
|
+
[RSpec::PathMatchers::Failure.new('.', failure_message)]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Provides a human-readable description for use in test output.
|
55
|
+
def description
|
56
|
+
"not have #{entry_type} #{entry_name.inspect}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Called by DirectoryMatcher but is not needed for this simple matcher
|
60
|
+
def collect_validation_errors(_errors); end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -5,8 +5,9 @@ require_relative 'base'
|
|
5
5
|
module RSpec
|
6
6
|
module PathMatchers
|
7
7
|
module Matchers
|
8
|
-
# An RSpec matcher checks for the existence and properties of a
|
9
|
-
|
8
|
+
# An RSpec matcher that checks for the existence and properties of a symlink
|
9
|
+
#
|
10
|
+
class SymlinkMatcher < Base
|
10
11
|
OPTIONS = [
|
11
12
|
RSpec::PathMatchers::Options::SymlinkAtime,
|
12
13
|
RSpec::PathMatchers::Options::SymlinkBirthtime,
|
@@ -19,23 +20,25 @@ module RSpec
|
|
19
20
|
RSpec::PathMatchers::Options::SymlinkTargetType
|
20
21
|
].freeze
|
21
22
|
|
23
|
+
def entry_type = :symlink
|
24
|
+
|
22
25
|
def option_definitions = OPTIONS
|
23
26
|
|
24
27
|
def correct_type? = File.symlink?(path)
|
25
28
|
|
26
|
-
def matcher_name = 'have_symlink'
|
27
|
-
|
28
29
|
protected
|
29
30
|
|
30
|
-
def validate_existance
|
31
|
+
def validate_existance
|
31
32
|
return nil if File.symlink?(path)
|
32
33
|
|
33
|
-
|
34
|
+
message =
|
34
35
|
if File.exist?(path)
|
35
36
|
'expected it to be a symlink'
|
36
37
|
else
|
37
38
|
'expected it to exist'
|
38
39
|
end
|
40
|
+
|
41
|
+
add_failure(message, failures)
|
39
42
|
end
|
40
43
|
end
|
41
44
|
end
|