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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.commitlintrc.yml +37 -0
  3. data/.husky/commit-msg +1 -0
  4. data/.release-please-manifest.json +3 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +15 -0
  7. data/.vscode/settings.json +2 -0
  8. data/CHANGELOG.md +43 -0
  9. data/CODE_OF_CONDUCT.md +132 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +409 -0
  12. data/Rakefile +73 -0
  13. data/design.rb +76 -0
  14. data/lib/rspec/path_matchers/matchers/base.rb +197 -0
  15. data/lib/rspec/path_matchers/matchers/directory_contents_inspector.rb +57 -0
  16. data/lib/rspec/path_matchers/matchers/have_directory.rb +126 -0
  17. data/lib/rspec/path_matchers/matchers/have_file.rb +45 -0
  18. data/lib/rspec/path_matchers/matchers/have_no_entry.rb +49 -0
  19. data/lib/rspec/path_matchers/matchers/have_symlink.rb +43 -0
  20. data/lib/rspec/path_matchers/options/atime.rb +51 -0
  21. data/lib/rspec/path_matchers/options/birthtime.rb +60 -0
  22. data/lib/rspec/path_matchers/options/content.rb +59 -0
  23. data/lib/rspec/path_matchers/options/ctime.rb +51 -0
  24. data/lib/rspec/path_matchers/options/group.rb +63 -0
  25. data/lib/rspec/path_matchers/options/json_content.rb +43 -0
  26. data/lib/rspec/path_matchers/options/mode.rb +51 -0
  27. data/lib/rspec/path_matchers/options/mtime.rb +51 -0
  28. data/lib/rspec/path_matchers/options/owner.rb +63 -0
  29. data/lib/rspec/path_matchers/options/size.rb +51 -0
  30. data/lib/rspec/path_matchers/options/symlink_atime.rb +51 -0
  31. data/lib/rspec/path_matchers/options/symlink_birthtime.rb +60 -0
  32. data/lib/rspec/path_matchers/options/symlink_ctime.rb +51 -0
  33. data/lib/rspec/path_matchers/options/symlink_group.rb +63 -0
  34. data/lib/rspec/path_matchers/options/symlink_mtime.rb +51 -0
  35. data/lib/rspec/path_matchers/options/symlink_owner.rb +63 -0
  36. data/lib/rspec/path_matchers/options/symlink_target.rb +54 -0
  37. data/lib/rspec/path_matchers/options/symlink_target_exist.rb +54 -0
  38. data/lib/rspec/path_matchers/options/symlink_target_type.rb +59 -0
  39. data/lib/rspec/path_matchers/options/yaml_content.rb +44 -0
  40. data/lib/rspec/path_matchers/options.rb +33 -0
  41. data/lib/rspec/path_matchers/version.rb +7 -0
  42. data/lib/rspec/path_matchers.rb +33 -0
  43. data/package.json +11 -0
  44. data/release-please-config.json +36 -0
  45. 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