rspec-path_matchers 0.1.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 +7 -0
- data/.commitlintrc.yml +37 -0
- data/.husky/commit-msg +1 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +15 -0
- data/.vscode/settings.json +2 -0
- data/CHANGELOG.md +43 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +409 -0
- data/Rakefile +73 -0
- data/design.rb +76 -0
- data/lib/rspec/path_matchers/matchers/base.rb +197 -0
- data/lib/rspec/path_matchers/matchers/directory_contents_inspector.rb +57 -0
- data/lib/rspec/path_matchers/matchers/have_directory.rb +126 -0
- data/lib/rspec/path_matchers/matchers/have_file.rb +45 -0
- data/lib/rspec/path_matchers/matchers/have_no_entry.rb +49 -0
- data/lib/rspec/path_matchers/matchers/have_symlink.rb +43 -0
- data/lib/rspec/path_matchers/options/atime.rb +51 -0
- data/lib/rspec/path_matchers/options/birthtime.rb +60 -0
- data/lib/rspec/path_matchers/options/content.rb +59 -0
- data/lib/rspec/path_matchers/options/ctime.rb +51 -0
- data/lib/rspec/path_matchers/options/group.rb +63 -0
- data/lib/rspec/path_matchers/options/json_content.rb +43 -0
- data/lib/rspec/path_matchers/options/mode.rb +51 -0
- data/lib/rspec/path_matchers/options/mtime.rb +51 -0
- data/lib/rspec/path_matchers/options/owner.rb +63 -0
- data/lib/rspec/path_matchers/options/size.rb +51 -0
- data/lib/rspec/path_matchers/options/symlink_atime.rb +51 -0
- data/lib/rspec/path_matchers/options/symlink_birthtime.rb +60 -0
- data/lib/rspec/path_matchers/options/symlink_ctime.rb +51 -0
- data/lib/rspec/path_matchers/options/symlink_group.rb +63 -0
- data/lib/rspec/path_matchers/options/symlink_mtime.rb +51 -0
- data/lib/rspec/path_matchers/options/symlink_owner.rb +63 -0
- data/lib/rspec/path_matchers/options/symlink_target.rb +54 -0
- data/lib/rspec/path_matchers/options/symlink_target_exist.rb +54 -0
- data/lib/rspec/path_matchers/options/symlink_target_type.rb +59 -0
- data/lib/rspec/path_matchers/options/yaml_content.rb +44 -0
- data/lib/rspec/path_matchers/options.rb +33 -0
- data/lib/rspec/path_matchers/version.rb +7 -0
- data/lib/rspec/path_matchers.rb +33 -0
- data/package.json +11 -0
- data/release-please-config.json +36 -0
- metadata +280 -0
data/Rakefile
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Run the same tasks that the CI build will run'
|
4
|
+
task default: %w[spec rubocop yard bundle:audit build]
|
5
|
+
|
6
|
+
# Bundler Audit
|
7
|
+
|
8
|
+
require 'bundler/audit/task'
|
9
|
+
Bundler::Audit::Task.new
|
10
|
+
|
11
|
+
# Bundler Gem Build
|
12
|
+
|
13
|
+
require 'bundler'
|
14
|
+
require 'bundler/gem_tasks'
|
15
|
+
|
16
|
+
# Make it so that calling `rake release` just calls `rake release:rubygems_push` to
|
17
|
+
# avoid creating and pushing a new tag.
|
18
|
+
|
19
|
+
Rake::Task['release'].clear
|
20
|
+
desc 'Customized release task to avoid creating a new tag'
|
21
|
+
task release: 'release:rubygem_push'
|
22
|
+
|
23
|
+
# RSpec
|
24
|
+
|
25
|
+
require 'rspec/core/rake_task'
|
26
|
+
|
27
|
+
RSpec::Core::RakeTask.new
|
28
|
+
|
29
|
+
CLEAN << 'coverage'
|
30
|
+
CLEAN << '.rspec_status'
|
31
|
+
CLEAN << 'rspec-report.xml'
|
32
|
+
|
33
|
+
# Rubocop
|
34
|
+
|
35
|
+
require 'rubocop/rake_task'
|
36
|
+
|
37
|
+
RuboCop::RakeTask.new
|
38
|
+
|
39
|
+
# YARD
|
40
|
+
|
41
|
+
# yard:build
|
42
|
+
|
43
|
+
require 'yard'
|
44
|
+
|
45
|
+
YARD::Rake::YardocTask.new('yard:build') do |t|
|
46
|
+
t.files = %w[lib/**/*.rb examples/**/*]
|
47
|
+
t.options = %w[--no-private]
|
48
|
+
t.stats_options = %w[--list-undoc]
|
49
|
+
end
|
50
|
+
|
51
|
+
CLEAN << '.yardoc'
|
52
|
+
CLEAN << 'doc'
|
53
|
+
|
54
|
+
# yard:audit
|
55
|
+
|
56
|
+
desc 'Run yardstick to show missing YARD doc elements'
|
57
|
+
task :'yard:audit' do
|
58
|
+
sh "yardstick 'lib/**/*.rb'"
|
59
|
+
end
|
60
|
+
|
61
|
+
# yard:coverage
|
62
|
+
|
63
|
+
require 'yardstick/rake/verify'
|
64
|
+
|
65
|
+
Yardstick::Rake::Verify.new(:'yard:coverage') do |verify|
|
66
|
+
verify.threshold = 100
|
67
|
+
end
|
68
|
+
|
69
|
+
# yard
|
70
|
+
|
71
|
+
desc 'Run all YARD tasks'
|
72
|
+
# task yard: %i[yard:build yard:audit yard:coverage]
|
73
|
+
task yard: %i[yard:build]
|
data/design.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
########################################
|
4
|
+
# Matchers for directory entries
|
5
|
+
expect(path).to have_file(
|
6
|
+
name, content:, json_content:, yaml_content:, size:, mode:, owner:, group:, ctime:, mtime:
|
7
|
+
)
|
8
|
+
|
9
|
+
expect(path).to have_dir(
|
10
|
+
name, entry_count:, mode:, owner:, group:, ctime:, mtime:, exact:
|
11
|
+
)
|
12
|
+
|
13
|
+
expect(path).to have_symlink(
|
14
|
+
name, mode:, owner:, group:, ctime:, mtime:, target:, target_type:, dangling:
|
15
|
+
)
|
16
|
+
|
17
|
+
########################################
|
18
|
+
# have_file matcher
|
19
|
+
expect(path).to have_file(name)
|
20
|
+
|
21
|
+
# Content checks
|
22
|
+
expect(path).to have_file(name, content: matcher | String | Regexp) # Matcher is compared to file content as a String
|
23
|
+
expect(path).to have_file(name, json_content: matcher | true) # ...the parsed file content as a Hash/Array/Object
|
24
|
+
expect(path).to have_file(name, yaml_content: matcher | true) # ...the parsed file content as a Hash/Array/Object
|
25
|
+
expect(path).to have_file(name, size: matcher) # ...the file size as an Integer
|
26
|
+
|
27
|
+
# Attribute checks
|
28
|
+
expect(path).to have_file(name, mode: matcher | String) # ...the file mode as a String (e.g. '0644')
|
29
|
+
expect(path).to have_file(name, owner: matcher | String) # ...the file owner as a String
|
30
|
+
expect(path).to have_file(name, group: matcher | String) # ...the file group as a String
|
31
|
+
expect(path).to have_file(name, ctime: matcher | Time) # ...the file creation time as a Time
|
32
|
+
expect(path).to have_file(name, mtime: matcher | Time) # ...the file modification time as a Time
|
33
|
+
|
34
|
+
########################################
|
35
|
+
# have_dir matcher
|
36
|
+
expect(path).to have_dir(name)
|
37
|
+
|
38
|
+
# Content checks
|
39
|
+
expect(path).to have_dir(name, entry_count: matcher) # Matcher is compared to the number of entries in the directory
|
40
|
+
|
41
|
+
# Attribute checks
|
42
|
+
expect(path).to have_dir(name, mode: matcher) # ...the directory mode as a String (e.g. '0755')
|
43
|
+
expect(path).to have_dir(name, owner: matcher) # ...the directory owner as a String
|
44
|
+
expect(path).to have_dir(name, group: matcher) # ...the directory group as a String
|
45
|
+
expect(path).to have_dir(name, ctime: matcher) # ...the directory creation time as a Time
|
46
|
+
expect(path).to have_dir(name, mtime: matcher) # ...the directory modification time as a Time
|
47
|
+
|
48
|
+
# Nested directory checks ()
|
49
|
+
expect(path).to(
|
50
|
+
have_dir(name) do
|
51
|
+
file('nested_file.txt', content: 'expected content')
|
52
|
+
dir('nested_dir') do
|
53
|
+
file('deeply_nested_file.txt', content: 'deeply expected content')
|
54
|
+
end
|
55
|
+
symlink('nested_symlink', target: 'expected_target')
|
56
|
+
no_file('non_existent_file.txt')
|
57
|
+
no_dir('non_existent_dir')
|
58
|
+
no_symlink('non_existent_symlink')
|
59
|
+
no_entry('non_existent_entry') # This checks for the absence of any type of entry with the given name
|
60
|
+
end
|
61
|
+
)
|
62
|
+
|
63
|
+
########################################
|
64
|
+
# have_symlink matcher
|
65
|
+
expect(path).to have_symlink(name)
|
66
|
+
|
67
|
+
# Attribute checks
|
68
|
+
expect(path).to have_symlink(name, mode: mode_matcher) # Matcher is compared to symlink mode as a String (e.g. '0777')
|
69
|
+
expect(path).to have_symlink(name, owner: owner_matcher) # ...the symlink owner as a String
|
70
|
+
expect(path).to have_symlink(name, group: group_matcher) # ...the symlink group as a String
|
71
|
+
expect(path).to have_symlink(name, ctime: timestamp_matcher) # ...the symlink creation time as a Time
|
72
|
+
expect(path).to have_symlink(name, mtime: timestamp_matcher) # ...the symlink modification time as a Time
|
73
|
+
|
74
|
+
expect(path).to have_symlink(name, target: target_matcher) # ...the symlink target as a String
|
75
|
+
expect(path).to have_symlink(name, target_type: target_type)
|
76
|
+
expect(path).to have_symlink(name, target_exist?: boolean) # Assert whether the symlink is dangling or not
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module PathMatchers
|
5
|
+
module Matchers
|
6
|
+
# The base class for matchers
|
7
|
+
class Base # rubocop:disable Metrics/ClassLength
|
8
|
+
def initialize(name, **options_hash)
|
9
|
+
super()
|
10
|
+
@name = name.to_s
|
11
|
+
@options = options_factory(*option_keys, **options_hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name, :options, :base_path, :path
|
15
|
+
|
16
|
+
# A human-readable description of the matcher's expectation
|
17
|
+
#
|
18
|
+
# This is used by RSpec to build the failure message when an `expect(...).to`
|
19
|
+
# expectation is not met. For example, if a test asserts `expect(path).to
|
20
|
+
# have_file("foo")` and the file does not exist, the failure message will
|
21
|
+
# include the output of this method: "expected to have file \"foo\"".
|
22
|
+
#
|
23
|
+
# @return [String] A description of the matcher
|
24
|
+
#
|
25
|
+
def description
|
26
|
+
desc = "have #{entry_type} #{name.inspect}"
|
27
|
+
options_description = build_options_description
|
28
|
+
desc += " with #{options_description}" unless options_description.empty?
|
29
|
+
desc
|
30
|
+
end
|
31
|
+
|
32
|
+
def failure_messages
|
33
|
+
@failure_messages ||= []
|
34
|
+
end
|
35
|
+
|
36
|
+
def matches?(base_path)
|
37
|
+
# It is important to reset failure_messages in case this matcher instance
|
38
|
+
# is reused
|
39
|
+
@failure_messages = []
|
40
|
+
|
41
|
+
# Phase 1: Validate all options recursively for syntax errors
|
42
|
+
validation_errors = []
|
43
|
+
collect_validation_errors(validation_errors)
|
44
|
+
raise ArgumentError, validation_errors.join(', ') if validation_errors.any?
|
45
|
+
|
46
|
+
# Phase 2: Execute the actual match logic
|
47
|
+
execute_match(base_path)
|
48
|
+
end
|
49
|
+
|
50
|
+
def collect_negative_validation_errors(errors)
|
51
|
+
errors << "The matcher `not_to #{matcher_name}(...)` cannot be given options" if options.any_given?
|
52
|
+
end
|
53
|
+
|
54
|
+
# This method is called by RSpec for `expect(...).not_to have_...`
|
55
|
+
def does_not_match?(base_path) # rubocop:disable Naming/PredicatePrefix
|
56
|
+
# 1. Validate that no options were passed to the negative matcher.
|
57
|
+
validation_errors = []
|
58
|
+
collect_negative_validation_errors(validation_errors)
|
59
|
+
raise ArgumentError, validation_errors.join(', ') if validation_errors.any?
|
60
|
+
|
61
|
+
@base_path = base_path.to_s
|
62
|
+
@path = File.join(base_path, name)
|
63
|
+
|
64
|
+
# 2. A negative match SUCCEEDS if the entry of the specified type does NOT exist.
|
65
|
+
# We delegate the type-specific check to the subclass.
|
66
|
+
# The method returns `true` if the entry is NOT of the correct type (pass),
|
67
|
+
# and `false` if it IS of the correct type (fail).
|
68
|
+
!correct_type?
|
69
|
+
end
|
70
|
+
|
71
|
+
# This is the message RSpec will display if `does_not_match?` returns `false`.
|
72
|
+
def failure_message_when_negated
|
73
|
+
"expected it not to be a #{entry_type}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Recursively gathers all syntax/validation errors
|
77
|
+
#
|
78
|
+
# Subclasses (like HaveDirectory) may extend this to recurse into nested
|
79
|
+
# matchers.
|
80
|
+
#
|
81
|
+
# @param errors [Array<String>] An array to append validation error messages to.
|
82
|
+
#
|
83
|
+
# @return [void]
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
#
|
87
|
+
def collect_validation_errors(errors)
|
88
|
+
validate_option_values(errors)
|
89
|
+
end
|
90
|
+
|
91
|
+
def failure_message
|
92
|
+
header = "the entry '#{name}' at '#{base_path}' was expected to satisfy the following but did not:"
|
93
|
+
# Format single- and multi-line nested messages with proper indentation.
|
94
|
+
messages = failure_messages.map do |msg|
|
95
|
+
msg.lines.map.with_index do |line, i|
|
96
|
+
i.zero? ? " - #{line.chomp}" : " #{line.chomp}"
|
97
|
+
end.join("\n")
|
98
|
+
end.join("\n")
|
99
|
+
"#{header}\n#{messages}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def correct_type?
|
103
|
+
raise NotImplementedError, 'This method should be implemented in a subclass'
|
104
|
+
end
|
105
|
+
|
106
|
+
def matcher_name
|
107
|
+
raise NotImplementedError, 'This method should be implemented in a subclass'
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
def entry_type
|
113
|
+
self.class.name.split('::').last.sub(/^Have/, '').downcase
|
114
|
+
end
|
115
|
+
|
116
|
+
# Performs the actual matching against the directory entry
|
117
|
+
#
|
118
|
+
# This method assumes that collect_validation_errors has already been called
|
119
|
+
# and passed. This method is protected so that container matchers (like
|
120
|
+
# HaveDirectory) can call it on nested matchers without using .send.
|
121
|
+
#
|
122
|
+
def execute_match(base_path) # rubocop:disable Naming/PredicateMethod
|
123
|
+
@base_path = base_path.to_s
|
124
|
+
@path = File.join(base_path, name)
|
125
|
+
|
126
|
+
# Validate existence and type.
|
127
|
+
validate_existance(failure_messages)
|
128
|
+
return false if failure_messages.any?
|
129
|
+
|
130
|
+
# Validate specific options and nested expectations.
|
131
|
+
validate_options
|
132
|
+
failure_messages.empty?
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def build_options_description
|
138
|
+
descriptions = options.to_h.filter_map do |key, value|
|
139
|
+
next if value == RSpec::PathMatchers::Options::NOT_GIVEN
|
140
|
+
|
141
|
+
"#{key.to_s.chomp('?')} #{option_definition(key).description(value)}"
|
142
|
+
end
|
143
|
+
descriptions.join(' and ')
|
144
|
+
end
|
145
|
+
|
146
|
+
def options_factory(*members, **options_hash)
|
147
|
+
Data.define(*members) do
|
148
|
+
def initialize(**kwargs)
|
149
|
+
# Default every member to the NOT_GIVEN sentinel
|
150
|
+
defaults = self.class.members.to_h { |member| [member, RSpec::PathMatchers::Options::NOT_GIVEN] }
|
151
|
+
final_args = defaults.merge(kwargs)
|
152
|
+
super(**final_args)
|
153
|
+
end
|
154
|
+
|
155
|
+
def any_given?
|
156
|
+
to_h.values.any? { |v| v != RSpec::PathMatchers::Options::NOT_GIVEN }
|
157
|
+
end
|
158
|
+
end.new(**options_hash)
|
159
|
+
end
|
160
|
+
|
161
|
+
def option_definition(key)
|
162
|
+
option_definitions.find { |definition| definition.key == key }
|
163
|
+
end
|
164
|
+
|
165
|
+
def option_keys
|
166
|
+
option_definitions.map(&:key)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Ensure the user provided option-values are of the expected type and format
|
170
|
+
def validate_option_values(errors)
|
171
|
+
options.members.filter_map do |key|
|
172
|
+
expected = options.send(key)
|
173
|
+
next if expected == RSpec::PathMatchers::Options::NOT_GIVEN
|
174
|
+
|
175
|
+
option_definition(key).validate_expected(expected, errors)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def validate_existance(_failure_messages)
|
180
|
+
raise NotImplementedError, 'This method should be implemented in a subclass'
|
181
|
+
end
|
182
|
+
|
183
|
+
# Validate the options for the current matcher
|
184
|
+
#
|
185
|
+
# Subclasses will override this to add nested execution.
|
186
|
+
def validate_options
|
187
|
+
options.members.each do |key|
|
188
|
+
expected = options.send(key)
|
189
|
+
next if expected == RSpec::PathMatchers::Options::NOT_GIVEN
|
190
|
+
|
191
|
+
option_definition(key).match(path, expected, failure_messages)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module PathMatchers
|
5
|
+
module Matchers
|
6
|
+
# Provides the DSL for the `have_dir` matcher's block
|
7
|
+
#
|
8
|
+
# It is responsible for creating and collecting the nested matchers
|
9
|
+
# without immediately executing them.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
#
|
13
|
+
class DirectoryContentsInspector
|
14
|
+
# By including RSpec::Matchers, we make methods like `be`, `eq`, `include`,
|
15
|
+
# `an_instance_of`, etc., available within the `have_dir` block
|
16
|
+
#
|
17
|
+
include RSpec::Matchers
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@nested_matchers = []
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :nested_matchers
|
24
|
+
|
25
|
+
# Defines an expectation for a file within the directory.
|
26
|
+
def file(name, **)
|
27
|
+
nested_matchers << RSpec::PathMatchers::Matchers::HaveFile.new(name, **)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Defines an expectation for a nested directory.
|
31
|
+
def dir(name, ...)
|
32
|
+
nested_matchers << RSpec::PathMatchers::Matchers::HaveDirectory.new(name, ...)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Defines an expectation for a symlink within the directory.
|
36
|
+
def symlink(name, **)
|
37
|
+
nested_matchers << RSpec::PathMatchers::Matchers::HaveSymlink.new(name, **)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Defines an expectation that a file does NOT exist within the directory.
|
41
|
+
def no_file(name)
|
42
|
+
nested_matchers << RSpec::PathMatchers::Matchers::HaveNoEntry.new(name, type: :file)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Defines an expectation that a directory does NOT exist within the directory.
|
46
|
+
def no_dir(name)
|
47
|
+
nested_matchers << RSpec::PathMatchers::Matchers::HaveNoEntry.new(name, type: :directory)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Defines an expectation that a symlink does NOT exist within the directory.
|
51
|
+
def no_symlink(name)
|
52
|
+
nested_matchers << RSpec::PathMatchers::Matchers::HaveNoEntry.new(name, type: :symlink)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative 'directory_contents_inspector'
|
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 HaveDirectory < 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
|
+
attr_reader :nested_matchers, :exact
|
23
|
+
|
24
|
+
# Initializes the matcher with the directory name and options
|
25
|
+
#
|
26
|
+
# @param name [String] The name of the directory
|
27
|
+
#
|
28
|
+
# @param exact [Boolean] The directory must contain only entries declared in the specification block
|
29
|
+
#
|
30
|
+
# @param options_hash [Hash] A hash of attribute matchers (e.g., mode:, owner:)
|
31
|
+
#
|
32
|
+
# @param specification_block [Proc] A specification block that defines the expected directory contents
|
33
|
+
#
|
34
|
+
def initialize(name, exact: false, **options_hash, &specification_block)
|
35
|
+
super(name, **options_hash)
|
36
|
+
|
37
|
+
@exact = exact
|
38
|
+
@nested_matchers = []
|
39
|
+
return unless specification_block
|
40
|
+
|
41
|
+
inspector = DirectoryContentsInspector.new
|
42
|
+
inspector.instance_eval(&specification_block)
|
43
|
+
@nested_matchers = inspector.nested_matchers
|
44
|
+
end
|
45
|
+
|
46
|
+
def description
|
47
|
+
desc = super
|
48
|
+
desc += ' exactly' if exact
|
49
|
+
return desc if nested_matchers.empty?
|
50
|
+
|
51
|
+
nested_descriptions = nested_matchers.map do |matcher|
|
52
|
+
matcher.description.lines.map.with_index do |line, i|
|
53
|
+
i.zero? ? "- #{line.chomp}" : " #{line.chomp}"
|
54
|
+
end.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
"#{desc} containing:\n #{nested_descriptions.join("\n ")}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def collect_negative_validation_errors(errors)
|
61
|
+
super
|
62
|
+
return unless nested_matchers.any?
|
63
|
+
|
64
|
+
errors << "The matcher `not_to #{matcher_name}(...)` cannot be given a specification block"
|
65
|
+
end
|
66
|
+
|
67
|
+
def collect_validation_errors(errors)
|
68
|
+
super
|
69
|
+
|
70
|
+
errors << "`exact:` must be true or false, but was #{exact.inspect}" unless [true, false].include?(exact)
|
71
|
+
|
72
|
+
# Recursively validate nested matchers.
|
73
|
+
nested_matchers.each { |matcher| matcher.collect_validation_errors(errors) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def option_definitions = OPTIONS
|
77
|
+
def correct_type? = File.directory?(path)
|
78
|
+
def matcher_name = 'have_dir'
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
# Overrides Base to add the exactness check after other validations.
|
83
|
+
def validate_options
|
84
|
+
super # Validate this directory's own options first.
|
85
|
+
|
86
|
+
nested_matchers.each do |matcher|
|
87
|
+
failure_messages << matcher.failure_message unless matcher.execute_match(path)
|
88
|
+
end
|
89
|
+
|
90
|
+
# If any of the declared expectations failed, we stop here.
|
91
|
+
# The user needs to fix those first.
|
92
|
+
return unless failure_messages.empty?
|
93
|
+
|
94
|
+
check_for_unexpected_entries if exact
|
95
|
+
end
|
96
|
+
|
97
|
+
def validate_existance(failure_messages)
|
98
|
+
return if File.directory?(path)
|
99
|
+
|
100
|
+
failure_messages << (File.exist?(path) ? 'expected it to be a directory' : 'expected it to exist')
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Checks for any files/directories on disk that were not declared in the block.
|
106
|
+
def check_for_unexpected_entries
|
107
|
+
positively_declared_entries = nested_matchers.reject do |m|
|
108
|
+
m.is_a?(RSpec::PathMatchers::Matchers::HaveNoEntry)
|
109
|
+
end.map(&:name)
|
110
|
+
|
111
|
+
actual_entries = Dir.children(path)
|
112
|
+
unexpected_entries = actual_entries - positively_declared_entries
|
113
|
+
|
114
|
+
return if unexpected_entries.empty?
|
115
|
+
|
116
|
+
message = build_unexpected_entries_message(unexpected_entries)
|
117
|
+
failure_messages << message
|
118
|
+
end
|
119
|
+
|
120
|
+
def build_unexpected_entries_message(unexpected_entries)
|
121
|
+
"did not expect entries #{unexpected_entries.inspect} to be present"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module PathMatchers
|
7
|
+
module Matchers
|
8
|
+
# An RSpec matcher checks for the existence and properties of a file
|
9
|
+
class HaveFile < Base
|
10
|
+
OPTIONS = [
|
11
|
+
RSpec::PathMatchers::Options::Atime,
|
12
|
+
RSpec::PathMatchers::Options::Birthtime,
|
13
|
+
RSpec::PathMatchers::Options::Content,
|
14
|
+
RSpec::PathMatchers::Options::Ctime,
|
15
|
+
RSpec::PathMatchers::Options::Group,
|
16
|
+
RSpec::PathMatchers::Options::JsonContent,
|
17
|
+
RSpec::PathMatchers::Options::Mode,
|
18
|
+
RSpec::PathMatchers::Options::Mtime,
|
19
|
+
RSpec::PathMatchers::Options::Owner,
|
20
|
+
RSpec::PathMatchers::Options::Size,
|
21
|
+
RSpec::PathMatchers::Options::YamlContent
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
def option_definitions = OPTIONS
|
25
|
+
|
26
|
+
def correct_type? = File.file?(path)
|
27
|
+
|
28
|
+
def matcher_name = 'have_file'
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def validate_existance(failure_messages)
|
33
|
+
return nil if File.file?(path)
|
34
|
+
|
35
|
+
failure_messages <<
|
36
|
+
if File.exist?(path)
|
37
|
+
'expected it to be a regular file'
|
38
|
+
else
|
39
|
+
'expected it to exist'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# In lib/rspec/path_matchers/matchers/have_no_entry.rb
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RSpec
|
5
|
+
module PathMatchers
|
6
|
+
module Matchers
|
7
|
+
# Asserts that a directory entry of a specific type does NOT exist.
|
8
|
+
# This is a simple, internal matcher-like object.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class HaveNoEntry
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
def initialize(name, type:)
|
15
|
+
@name = name
|
16
|
+
@type = type
|
17
|
+
@base_path = nil
|
18
|
+
@path = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
# The core logic. Returns `true` if the expectation is met (the entry is absent).
|
22
|
+
def execute_match(base_path) # rubocop:disable Naming/PredicateMethod
|
23
|
+
@base_path = base_path
|
24
|
+
@path = File.join(base_path, @name)
|
25
|
+
|
26
|
+
case @type
|
27
|
+
when :file then !File.file?(@path)
|
28
|
+
when :directory then !File.directory?(@path)
|
29
|
+
else !File.symlink?(@path)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# The failure message if `execute_match` returns `false`.
|
34
|
+
def failure_message
|
35
|
+
"expected #{@type} '#{@name}' not to be found at '#{@base_path}', but it exists"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Provide a description for the `have_dir` block's output.
|
39
|
+
def description
|
40
|
+
"not have #{@type} #{@name.inspect}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Provide a stub for this method, which is called by HaveDirectory
|
44
|
+
# but is not needed for this simple matcher.
|
45
|
+
def collect_validation_errors(_errors); end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module PathMatchers
|
7
|
+
module Matchers
|
8
|
+
# An RSpec matcher checks for the existence and properties of a file
|
9
|
+
class HaveSymlink < Base
|
10
|
+
OPTIONS = [
|
11
|
+
RSpec::PathMatchers::Options::SymlinkAtime,
|
12
|
+
RSpec::PathMatchers::Options::SymlinkBirthtime,
|
13
|
+
RSpec::PathMatchers::Options::SymlinkCtime,
|
14
|
+
RSpec::PathMatchers::Options::SymlinkGroup,
|
15
|
+
RSpec::PathMatchers::Options::SymlinkMtime,
|
16
|
+
RSpec::PathMatchers::Options::SymlinkOwner,
|
17
|
+
RSpec::PathMatchers::Options::SymlinkTarget,
|
18
|
+
RSpec::PathMatchers::Options::SymlinkTargetExist,
|
19
|
+
RSpec::PathMatchers::Options::SymlinkTargetType
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
def option_definitions = OPTIONS
|
23
|
+
|
24
|
+
def correct_type? = File.symlink?(path)
|
25
|
+
|
26
|
+
def matcher_name = 'have_symlink'
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def validate_existance(failure_messages)
|
31
|
+
return nil if File.symlink?(path)
|
32
|
+
|
33
|
+
failure_messages <<
|
34
|
+
if File.exist?(path)
|
35
|
+
'expected it to be a symlink'
|
36
|
+
else
|
37
|
+
'expected it to exist'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|