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,17 @@
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
  # mtime: <expected>
7
- class SymlinkMtime
9
+ class SymlinkMtime < FileStatBase
8
10
  def self.key = :mtime
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.lstat(path).mtime
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 mtime 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 mtime to #{expected.description}, but was #{actual.inspect}"
47
- end
11
+ def self.stat_attribute = :mtime
12
+ def self.valid_expected_types = [Time, DateTime]
13
+ def self.normalize_expected_literal(expected) = expected.to_time
14
+ def self.stat_source_method = :lstat
48
15
  end
49
16
  end
50
17
  end
@@ -1,62 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'etc_base'
4
+
3
5
  module RSpec
4
6
  module PathMatchers
5
7
  module Options
6
- # owner: <expected>
7
- class SymlinkOwner
8
+ # group: <expected>
9
+ class SymlinkOwner < EtcBase
8
10
  def self.key = :owner
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?(String) ||
17
- RSpec::PathMatchers.matcher?(expected)
18
-
19
- failure_messages <<
20
- "expected `#{key}:` to be a Matcher or a String, but was #{expected.inspect}"
21
- end
22
-
23
- # Returns nil if the path is owned by the expected owner
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
- return if unsupported_platform?
29
-
30
- actual = Etc.getpwuid(File.lstat(path).uid).name
31
-
32
- case expected
33
- when String then match_string(actual, expected, failure_messages)
34
- else match_matcher(actual, expected, failure_messages)
35
- end
36
- end
37
-
38
- # private methods
39
-
40
- private_class_method def self.unsupported_platform?
41
- return false if Etc.respond_to?(:getpwuid)
42
-
43
- # If the platform doesn't support ownership, warn the user and skip the check
44
- message = 'WARNING: Owner expectations are not supported on this platform and will be skipped.'
45
- RSpec.configuration.reporter.message(message)
46
- true
47
- end
48
-
49
- private_class_method def self.match_string(actual, expected, failure_messages)
50
- return if expected == actual
51
-
52
- failure_messages << "expected owner to be #{expected.inspect}, but was #{actual.inspect}"
53
- end
54
-
55
- private_class_method def self.match_matcher(actual, expected, failure_messages)
56
- return if expected.matches?(actual)
57
-
58
- failure_messages << "expected owner to #{expected.description}, but was #{actual.inspect}"
59
- end
11
+ def self.stat_source_method = :lstat
12
+ def self.stat_attribute = :uid
13
+ def self.etc_method = :getpwuid
60
14
  end
61
15
  end
62
16
  end
@@ -1,53 +1,16 @@
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
  # target: <expected>
7
- class SymlinkTarget
9
+ class SymlinkTarget < Base
8
10
  def self.key = :target
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?(String) ||
17
- RSpec::PathMatchers.matcher?(expected)
18
-
19
- failure_messages <<
20
- "expected `#{key}:` to be a Matcher or a String, but was #{expected.inspect}"
21
- end
22
-
23
- # Populates failure_messages if expected value does not match actual value
24
- # @param path [String] the path of the entry to check
25
- # @param expected [Object] the expected value
26
- # @param failure_messages [Array<String>] the array to populate with failure messages
27
- # @return [Void]
28
- #
29
- def self.match(path, expected, failure_messages)
30
- actual = File.readlink(path)
31
-
32
- case expected
33
- when String then match_string(actual, expected, failure_messages)
34
- else match_matcher(actual, expected, failure_messages)
35
- end
36
- end
37
-
38
- # private methods
39
-
40
- private_class_method def self.match_string(actual, expected, failure_messages)
41
- return if expected == actual
42
-
43
- failure_messages << "expected #{key} to be #{expected.inspect}, but was #{actual.inspect}"
44
- end
45
-
46
- private_class_method def self.match_matcher(actual, expected, failure_messages)
47
- return if expected.matches?(actual)
48
-
49
- failure_messages << "expected #{key} to #{expected.description}, but was #{actual.inspect}"
50
- end
11
+ def self.valid_expected_types = [String]
12
+ def self.normalize_expected_literal(expected) = expected.to_s
13
+ def self.fetch_actual(path, _failures) = File.readlink(path)
51
14
  end
52
15
  end
53
16
  end
@@ -1,52 +1,17 @@
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
  # target_exist: <expected>
7
- class SymlinkTargetExist
9
+ class SymlinkTargetExist < Base
8
10
  def self.key = :target_exist
11
+ def self.valid_expected_types = [TrueClass, FalseClass]
9
12
 
10
- def self.description(expected)
11
- RSpec::PathMatchers.matcher?(expected) ? expected.description : expected.to_s
12
- end
13
-
14
- def self.validate_expected(expected, failure_messages)
15
- return if expected == NOT_GIVEN ||
16
- expected.is_a?(TrueClass) || expected.is_a?(FalseClass) ||
17
- RSpec::PathMatchers.matcher?(expected)
18
-
19
- failure_messages <<
20
- "expected `#{key}:` to be a Matcher, true, or false but was #{expected.inspect}"
21
- end
22
-
23
- # Populates failure_messages if expected value does not match actual value
24
- # @param path [String] the path of the entry to check
25
- # @param expected [Object] the expected value
26
- # @param failure_messages [Array<String>] the array to populate with failure messages
27
- # @return [Void]
28
- #
29
- def self.match(path, expected, failure_messages)
30
- actual = File.exist?(File.expand_path(File.readlink(path), File.dirname(path)))
31
-
32
- case expected
33
- when true, false then match_boolean(actual, expected, failure_messages)
34
- else match_matcher(actual, expected, failure_messages)
35
- end
36
- end
37
-
38
- # private methods
39
-
40
- private_class_method def self.match_boolean(actual, expected, failure_messages)
41
- return if expected == actual
42
-
43
- failure_messages << "expected #{key} to be #{expected.inspect}, but was #{actual.inspect}"
44
- end
45
-
46
- private_class_method def self.match_matcher(actual, expected, failure_messages)
47
- return if expected.matches?(actual)
48
-
49
- failure_messages << "expected #{key} to #{expected.description}, but was #{actual.inspect}"
13
+ def self.fetch_actual(path, _failures) # rubocop:disable Naming/PredicateMethod
14
+ File.exist?(File.expand_path(File.readlink(path), File.dirname(path)))
50
15
  end
51
16
  end
52
17
  end
@@ -1,57 +1,34 @@
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
  # target_type: <expected>
7
- class SymlinkTargetType
9
+ #
10
+ # Checks the type of the entry a symlink points to (e.g., 'file', 'directory').
11
+ #
12
+ class SymlinkTargetType < Base
8
13
  def self.key = :target_type
14
+ def self.valid_expected_types = [String, Symbol]
15
+ def self.normalize_expected_literal(expected) = expected.to_s
9
16
 
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) || expected.is_a?(Symbol) ||
17
- RSpec::PathMatchers.matcher?(expected)
18
-
19
- failure_messages <<
20
- "expected `#{key}:` to be a Matcher, a String, or a Symbol but was #{expected.inspect}"
21
- end
22
-
23
- # Populates failure_messages if expected value does not match actual value
24
- # @param path [String] the path of the entry to check
25
- # @param expected [Object] the expected value
26
- # @param failure_messages [Array<String>] the array to populate with failure messages
27
- # @return [Void]
17
+ # Overrides the base match method to gracefully handle dangling symlinks
28
18
  #
29
- def self.match(path, expected, failure_messages)
30
- begin
31
- actual = File.ftype(File.expand_path(File.readlink(path), File.dirname(path)))
32
- rescue Errno::ENOENT => e
33
- failure_messages << "expected the symlink target to exist, but got error: #{e.message}"
34
- return
35
- end
36
-
37
- case expected
38
- when String, Symbol then match_string(actual, expected, failure_messages)
39
- else match_matcher(actual, expected, failure_messages)
40
- end
41
- end
42
-
43
- # private methods
44
-
45
- private_class_method def self.match_string(actual, expected, failure_messages)
46
- return if expected.to_s == actual
47
-
48
- failure_messages << "expected #{key} to be #{expected.to_s.inspect}, but was #{actual.inspect}"
19
+ # If `File.ftype` fails because the target doesn't exist, it adds a
20
+ # descriptive failure instead of crashing.
21
+ #
22
+ def self.match(path, expected, failures)
23
+ super
24
+ rescue Errno::ENOENT => e
25
+ message = "expected the symlink target to exist, but got error: #{e.message}"
26
+ add_failure(message, failures)
27
+ nil
49
28
  end
50
29
 
51
- private_class_method def self.match_matcher(actual, expected, failure_messages)
52
- return if expected.matches?(actual)
53
-
54
- failure_messages << "expected #{key} to #{expected.description}, but was #{actual.inspect}"
30
+ def self.fetch_actual(path, _failures)
31
+ File.ftype(File.expand_path(File.readlink(path), File.dirname(path)))
55
32
  end
56
33
  end
57
34
  end
@@ -2,42 +2,17 @@
2
2
 
3
3
  require 'yaml'
4
4
 
5
+ require_relative 'parsed_content_base'
6
+
5
7
  module RSpec
6
8
  module PathMatchers
7
9
  module Options
8
10
  # yaml_content: <expected>
9
- class YamlContent
11
+ class YamlContent < ParsedContentBase
10
12
  def self.key = :yaml_content
11
-
12
- def self.description(expected)
13
- return 'be yaml content' if expected == true
14
-
15
- expected.description
16
- end
17
-
18
- def self.validate_expected(expected, failure_messages)
19
- return if expected == NOT_GIVEN ||
20
- expected == true ||
21
- RSpec::PathMatchers.matcher?(expected)
22
-
23
- failure_messages <<
24
- "expected `#{key}:` to be a Matcher or true, but was #{expected.inspect}"
25
- end
26
-
27
- # Returns nil if the path matches the expected content
28
- # @param path [String] the path of the entry to check
29
- # @return [String, nil]
30
- #
31
- def self.match(path, expected, failure_messages)
32
- require 'yaml'
33
- actual = YAML.safe_load_file(path)
34
-
35
- return if expected == true
36
-
37
- failure_messages << "expected YAML content to #{expected.description}" unless expected.matches?(actual)
38
- rescue Psych::SyntaxError => e
39
- failure_messages << "expected valid YAML content, but got error: #{e.message}"
40
- end
13
+ private_class_method def self.content_type = 'YAML'
14
+ private_class_method def self.parse(string) = YAML.safe_load(string)
15
+ private_class_method def self.parsing_error = Psych::SyntaxError
41
16
  end
42
17
  end
43
18
  end
@@ -5,6 +5,7 @@ module RSpec
5
5
  module Options
6
6
  # The value used to indicate that an option was not given by the user
7
7
  NOT_GIVEN = Object.new.freeze
8
+ FETCH_ERROR = Object.new.freeze
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module PathMatchers
5
+ # Refinements for various classes used in RSpec::PathMatchers
6
+ module Refinements
7
+ # Refinements for Array to provide a `to_sentence` method
8
+ #
9
+ # @example
10
+ # using RSpec::PathMatchers::Refinements::ArrayRefinements
11
+ #
12
+ module ArrayRefinements
13
+ DEFAULT_SENTENCE_OPTIONS = Data.define(:conjunction, :delimiter, :oxford) do
14
+ def initialize(conjunction: 'and', delimiter: ',', oxford: true)
15
+ super
16
+ end
17
+
18
+ def two_word_connector
19
+ "#{delimiter} "
20
+ end
21
+
22
+ def last_word_connector
23
+ oxford ? "#{delimiter} #{conjunction} " : " #{conjunction} "
24
+ end
25
+ end.new
26
+
27
+ refine Array do
28
+ # Converts an array to a sentence with proper conjunctions and delimiters
29
+ #
30
+ # @example
31
+ # using RSpec::PathMatchers::Refinements::ArrayRefinements
32
+ # [].to_sentence # => ''
33
+ # ['apple'].to_sentence # => 'apple'
34
+ # ['apple', 'banana'].to_sentence # => 'apple and banana'
35
+ # ['apple', 'banana', 'cherry'].to_sentence # => 'apple, banana, and cherry'
36
+ #
37
+ # @example using a different conjunction
38
+ # using RSpec::PathMatchers::Refinements::ArrayRefinements
39
+ # ['apple', 'banana', 'cherry'].to_sentence(conjunction: 'or')
40
+ # #=> 'apple, banana, or cherry'
41
+ #
42
+ # @example using a different delimiter
43
+ # using RSpec::PathMatchers::Refinements::ArrayRefinements
44
+ # ['apple', 'banana', 'cherry'].to_sentence(delimiter: ';')
45
+ # #=> 'apple; banana; and cherry'
46
+ #
47
+ # @example without the Oxford comma
48
+ # using RSpec::PathMatchers::Refinements::ArrayRefinements
49
+ # ['apple', 'banana', 'cherry'].to_sentence(oxford: false)
50
+ # #=> 'apple, banana and cherry'
51
+ #
52
+ # @param options_hash [Hash] Options to customize the sentence format
53
+ #
54
+ # @option options_hash [String] :conjunction ('and') The word to use
55
+ # before the last item in the sentence
56
+ #
57
+ # @option options_hash [String] :delimiter (',') The delimiter to use
58
+ # between items in the sentence when there are three or more items
59
+ #
60
+ # @option options_hash [Boolean] :oxford (true) Whether to use the
61
+ # Oxford comma before the conjunction
62
+ #
63
+ # @return [String] The array converted to a sentence
64
+ #
65
+ def to_sentence(options_hash = {})
66
+ options = DEFAULT_SENTENCE_OPTIONS.with(**options_hash)
67
+
68
+ case length
69
+ when 0 then ''
70
+ when 1 then first.to_s
71
+ when 2 then join(" #{options.conjunction} ")
72
+ else "#{self[0..-2].join(options.two_word_connector)}#{options.last_word_connector}#{last}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module PathMatchers
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.1'
6
6
  end
7
7
  end