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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ae68ee19cdebad14cf65d06299355715b64c3b47ed79c74253c31c1bdcda3238
4
+ data.tar.gz: 1fa836b7f123ab72b334c7b833ba4e27cbed1c14eaf2b5050ad37f3e72af557b
5
+ SHA512:
6
+ metadata.gz: 1ea96ebd772336a3db9e43d5bfa66d1827f703fb14d370bb1e53d94e07b13993bc59dd9f0c641887f0c721d9000fcd0b676e21de160ec4d5a6d953a2e39f6efd
7
+ data.tar.gz: 1d4eca98af763909756a56e8df767dd42d2bc9cd3de1ec31062da5ede31e7a8272afe02e7d347e42170ce88804b298bd76b25322dd33e0e6df9657302386d0a5
data/.commitlintrc.yml ADDED
@@ -0,0 +1,37 @@
1
+ ---
2
+ extends: '@commitlint/config-conventional'
3
+
4
+ rules:
5
+ # See: https://commitlint.js.org/reference/rules.html
6
+ #
7
+ # Rules are made up by a name and a configuration array. The configuration
8
+ # array contains:
9
+ #
10
+ # * Severity [0..2]: 0 disable rule, 1 warning if violated, or 2 error if
11
+ # violated
12
+ # * Applicability [always|never]: never inverts the rule
13
+ # * Value: value to use for this rule (if applicable)
14
+ #
15
+ # Run `npx commitlint --print-config` to see the current setting for all
16
+ # rules.
17
+ #
18
+ header-max-length: [2, always, 100] # Header can not exceed 100 chars
19
+
20
+ type-case: [2, always, lower-case] # Type must be lower case
21
+ type-empty: [2, never] # Type must not be empty
22
+
23
+ # Supported conventional commit types
24
+ type-enum: [2, always, [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test]]
25
+
26
+ scope-case: [2, always, lower-case] # Scope must be lower case
27
+
28
+ # Error if subject is one of these cases (encourages lower-case)
29
+ subject-case: [2, never, [sentence-case, start-case, pascal-case, upper-case]]
30
+ subject-empty: [2, never] # Subject must not be empty
31
+ subject-full-stop: [2, never, "."] # Subject must not end with a period
32
+
33
+ body-leading-blank: [2, always] # Body must have a blank line before it
34
+ body-max-line-length: [2, always, 100] # Body lines can not exceed 100 chars
35
+
36
+ footer-leading-blank: [2, always] # Footer must have a blank line before it
37
+ footer-max-line-length: [2, always, 100] # Footer lines can not exceed 100 chars
data/.husky/commit-msg ADDED
@@ -0,0 +1 @@
1
+ npx --no-install commitlint --edit "$1"
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.0"
3
+ }
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ inherit_gem:
2
+ main_branch_shared_rubocop_config: config/rubocop.yml
3
+
4
+ Metrics/MethodLength:
5
+ Exclude:
6
+ - "spec/spec_helper.rb"
7
+ - "spec/**/*_spec.rb"
8
+
9
+ Metrics/AbcSize:
10
+ Exclude:
11
+ - "spec/spec_helper.rb"
12
+ - "spec/**/*_spec.rb"
13
+
14
+ AllCops:
15
+ TargetRubyVersion: 3.2
@@ -0,0 +1,2 @@
1
+ {
2
+ }
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ ## [Unreleased]
2
+
3
+ ## 0.1.0 (2025-06-25)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * update lowest version of Ruby supported from 3.1.x to 3.2.x
9
+ * rename the gem from rspec-file_systems to rspec-path_matchers
10
+
11
+ ### Features
12
+
13
+ * Add custom description for each matcher ([a305d05](https://github.com/main-branch/rspec-path_matchers/commit/a305d05a4008b3ec0e2f5e052ecb4960d99b0bdf))
14
+ * Add nested matchers for the have_directory matcher ([d0fc5bf](https://github.com/main-branch/rspec-path_matchers/commit/d0fc5bff30bac4c5ce22bfa65c0348918c8f7a74))
15
+ * Add options to the have_symlink matcher to test the symlink target and target type ([6228db4](https://github.com/main-branch/rspec-path_matchers/commit/6228db4b768a9792fc93f2fc0e8263d7261c8f12))
16
+ * Add support for negative (aka not_to) matches in have_file, have_dir, and have_symlink ([69bdc59](https://github.com/main-branch/rspec-path_matchers/commit/69bdc595f764778d4bca33ff4d6d2b20fbeb7e7b))
17
+ * Add the 'target_exist?' option to the have_symlink matcher ([a47c900](https://github.com/main-branch/rspec-path_matchers/commit/a47c90049d39e87c5a4f9bdf8652c83ed7ad8a19))
18
+ * Allow no_file, no_dir, and no_symlink matchers in the have_dir block ([43b0c67](https://github.com/main-branch/rspec-path_matchers/commit/43b0c67e3f56b73a708e59f16a667c5e37adecd5))
19
+ * Implement the have_dir exact option ([9a405ce](https://github.com/main-branch/rspec-path_matchers/commit/9a405ce92561c276679f8408184d0c256a066bf4))
20
+ * Initial versions of the have_file, have_dir and have_symlink matchers ([51a9e2e](https://github.com/main-branch/rspec-path_matchers/commit/51a9e2e836a5238f2bf311116970dd49400a89a1))
21
+ * Initial versions of the have_file, have_dir and have_symlink matchers ([31e6228](https://github.com/main-branch/rspec-path_matchers/commit/31e62285d536aa5b5cba26708eeaf93cf4b7e4c1))
22
+ * Rename the gem from rspec-file_systems to rspec-path_matchers ([6c36917](https://github.com/main-branch/rspec-path_matchers/commit/6c36917fa1d07176960fce5aec1cac98d8e0b584))
23
+ * Update lowest version of Ruby supported from 3.1.x to 3.2.x ([7206fa8](https://github.com/main-branch/rspec-path_matchers/commit/7206fa8418a6e1beb7171ecce31b539384ff90d0))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * Correct rubocop offenses in the design doc ([9906b5d](https://github.com/main-branch/rspec-path_matchers/commit/9906b5d980eb8c23591db5a1a0bb8c06bdbfa43f))
29
+ * Fix the description of the json and yaml matchers when given 'true' ([fd84705](https://github.com/main-branch/rspec-path_matchers/commit/fd84705962ec8b52ae0405140ae0cb96c03d589c))
30
+
31
+
32
+ ### Other Changes
33
+
34
+ * Add tests for the have_dir exact: option ([01e8708](https://github.com/main-branch/rspec-path_matchers/commit/01e8708a6c4104cb1a337b948a0d89a51fbffebf))
35
+ * Call the block passed to the HaveDirectory matcher `specification_block` ([2cd2587](https://github.com/main-branch/rspec-path_matchers/commit/2cd2587088d6ad28296454c884d8b3a6a5c84724))
36
+ * Do not run yard:audit or yard:coverage as part of the CI build ([c8f3471](https://github.com/main-branch/rspec-path_matchers/commit/c8f3471f0a342c74a019ffe3892c1d48fcdca0d0))
37
+ * Implement continuous delivery ([818567d](https://github.com/main-branch/rspec-path_matchers/commit/818567df9f53515607d105430557468d775d815c))
38
+ * Reset gem version for CD pipeline ([640bf30](https://github.com/main-branch/rspec-path_matchers/commit/640bf30926766a7299282fdfe3a2cde0738cfe9c))
39
+ * Reset gem version for CD pipeline ([ade971a](https://github.com/main-branch/rspec-path_matchers/commit/ade971a35cd365e18ffbced2a4b28e4c36a0c2f2))
40
+
41
+ ## [0.1.0] - 2025-06-07
42
+
43
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 James Couball
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,409 @@
1
+ # The rspec-path_matchers gem
2
+
3
+ [![Gem
4
+ Version](https://img.shields.io/gem/v/rspec-path_matchers.svg)](https://rubygems.org/gems/rspec-path_matchers)
5
+ [![Build
6
+ Status](https://img.shields.io/github/actions/workflow/status/main-branch/rspec-path_matchers/main.yml?branch=main)](https://github.com/main-branch/rspec-path_matchers/actions)
7
+ [![MIT
8
+ License](https://img.shields.io/badge/license-MIT-green)](https://opensource.org/licenses/MIT)
9
+
10
+ - [Summary](#summary)
11
+ - [Installation](#installation)
12
+ - [Setup](#setup)
13
+ - [Usage \& Examples](#usage--examples)
14
+ - [Basic Assertions](#basic-assertions)
15
+ - [Negative Assertions (Checking for Absence)](#negative-assertions-checking-for-absence)
16
+ - [File Content Assertions](#file-content-assertions)
17
+ - [Attribute Assertions](#attribute-assertions)
18
+ - [Directory Structure Assertions](#directory-structure-assertions)
19
+ - [Exact Directory Contents](#exact-directory-contents)
20
+ - [Development](#development)
21
+ - [Contributing](#contributing)
22
+ - [License](#license)
23
+
24
+ ## Summary
25
+
26
+ **RSpec::PathMatchers** provides a comprehensive suite of RSpec matchers for
27
+ testing file system entries and structures.
28
+
29
+ Verifying that a generator, build script, or any file-manipulating process has
30
+ produced the correct output can be tedious and verbose. This gem makes those
31
+ assertions simple, declarative, and easy to read, allowing you to describe an entire
32
+ file tree and its properties within your specs.
33
+
34
+ Here’s a breakdown of the value this API provides over what is available in standard
35
+ RSpec.
36
+
37
+ <h3>1. Abstraction and Readability: From Imperative to Declarative</h3>
38
+
39
+ Standard RSpec forces you to describe *how* to test something. This API allows you
40
+ to declaratively state *what* the directory structure should look like.
41
+
42
+ Without this API (Imperative Style):
43
+
44
+ ```ruby
45
+ # This code describes the HOW: stat the file, get the mode, convert to octal...
46
+ # It's a script, not a specification.
47
+ path = '/var/www/html/index.html'
48
+ expect(File.exist?(path)).to be true
49
+ expect(File.stat(path).mode.to_s(8)[-4..]).to eq('0644')
50
+ expect(File.stat(path).owned?).to be true # This just checks if UID matches script runner
51
+ ```
52
+
53
+ With this API (Declarative Style):
54
+
55
+ ```ruby
56
+ # This code describes the WHAT. The implementation details are hidden.
57
+ # It reads like a specification document.
58
+ expect('/var/www/html').to have_file('index.html', mode: '0644', owner: 'httpd')
59
+ ```
60
+
61
+ This hides the complex, imperative logic inside the matcher and exposes a clean,
62
+ readable, domain-specific language (DSL).
63
+
64
+ <h3>2. Conciseness and Cohesion: Grouping Related Assertions</h3>
65
+
66
+ Without this API, testing multiple attributes of a single file requires
67
+ fragmented, repetitive `expect` calls. Your API groups these assertions into one
68
+ cohesive, logical block.
69
+
70
+ Without this API:
71
+
72
+ ```ruby
73
+ path = '/var/data/status.json'
74
+ expect(File.file?(path)).to be true
75
+ expect(File.size(path)).to be > 0
76
+ expect(File.read(path)).not_to include('error')
77
+ expect(JSON.parse(File.read(path))['status']).to eq('complete')
78
+ ```
79
+
80
+ With this API:
81
+
82
+ ```ruby
83
+ expect('/var/data').to have_file('status.json',
84
+ size: be > 0,
85
+ content: not(/error/),
86
+ json_content: include('status' => 'complete')
87
+ )
88
+ ```
89
+
90
+ This is far easier to read and maintain because all the assertions about
91
+ `status.json` are in one place.
92
+
93
+ <h3>3. The Nested Directory DSL Adds to the Expressive Power</h3>
94
+
95
+ This is where this API provides something that base RSpec simply cannot do
96
+ elegantly. Describing the state of a directory tree with standard RSpec is
97
+ incredibly verbose and difficult to read.
98
+
99
+ Without this API:
100
+
101
+ ```ruby
102
+ # This is hard to read and mentally parse.
103
+ dir_path = '/etc/service/nginx'
104
+ expect(Dir.exist?(dir_path)).to be true
105
+ expect(File.exist?(File.join(dir_path, 'run'))).to be true
106
+ expect(Dir.exist?(File.join(dir_path, 'log'))).to be true
107
+ expect(File.exist?(File.join(dir_path, 'down'))).to be false
108
+ ```
109
+
110
+ With this API:
111
+
112
+ ```ruby
113
+ # This is a clear, hierarchical specification of the directory's contents.
114
+ expect('/etc/service').to(have_dir('nginx') do
115
+ file('run')
116
+ dir('log')
117
+ no_file('down')
118
+ end)
119
+ ```
120
+
121
+ The nested block is a leap in expressiveness and power, allowing you to write
122
+ complex integration and infrastructure tests with ease.
123
+
124
+ <h3>4. Descriptive and Intelligible Failure Messages</h3>
125
+
126
+ When a complex, nested expectation fails, this gem pinpoints the exact failure,
127
+ saving you valuable debugging time.
128
+
129
+ **Standard RSpec Failure:**
130
+
131
+ ```
132
+ expected: true
133
+ got: false
134
+ ```
135
+
136
+ This kind of message forces you to manually inspect the directory structure to understand
137
+ what went wrong.
138
+
139
+ **With this API:**
140
+
141
+ You get a detailed, hierarchical report that shows the full expectation and
142
+ clearly marks what failed.
143
+
144
+ ```
145
+ the entry 'my-app' at '/tmp/d20250622-12345-abcdef' was expected to satisfy the following but did not:
146
+ - have directory "config" containing:
147
+ - have file "database.yml" with owner "db_user" and mode "0600"
148
+ - expected owner to be "db_user", but was "root"
149
+ - expected mode to be "0600", but was "0644"
150
+ ```
151
+
152
+ ## Installation
153
+
154
+ Add this line to your application's `Gemfile` in the `:test` or `:development` group:
155
+
156
+ ```ruby
157
+ group :test, :development do
158
+ gem 'rspec-path_matchers'
159
+ end
160
+ ```
161
+
162
+ And then execute:
163
+
164
+ ```bash
165
+ $ bundle install
166
+ ```
167
+
168
+ Or install it yourself as:
169
+
170
+ ```bash
171
+ $ gem install rspec-path_matchers
172
+ ```
173
+
174
+ ## Setup
175
+
176
+ Require the gem in your `spec/spec_helper.rb` file:
177
+
178
+ ```ruby
179
+ # spec/spec_helper.rb
180
+ require 'rspec/path_matchers'
181
+ ```
182
+
183
+ ## Usage & Examples
184
+
185
+ All matchers operate on a base path, making your tests clean and portable.
186
+
187
+ ### Basic Assertions
188
+
189
+ At its simplest, you can check for the existence of files and directories.
190
+
191
+ ```ruby
192
+ it "creates the basic structure" do
193
+ # Setup: Create some files and directories in our temp dir
194
+ FileUtils.mkdir_p(File.join(@tmpdir, "app/models"))
195
+ FileUtils.touch(File.join(@tmpdir, "config.yml"))
196
+
197
+ # Assertions
198
+ expect(@tmpdir).to have_file("config.yml")
199
+ expect(@tmpdir).to have_dir("app")
200
+ end
201
+ ```
202
+
203
+ ### Negative Assertions (Checking for Absence)
204
+
205
+ You can use `not_to` to ensure that a file, directory, or symlink of a specific
206
+ type does not exist.
207
+
208
+ - `not_to have_file` passes if the entry is a directory, a symlink, or non-existent.
209
+ - `not_to have_dir` passes if the entry is a file, a symlink, or non-existent.
210
+ - `not_to have_symlink` passes if the entry is a file, a directory, or non-existent.
211
+
212
+ **Important:** Negative matchers cannot be given options (`:mode`, `:content`, etc.)
213
+ or blocks.
214
+
215
+ ```ruby
216
+ it "can check for the absence of entries" do
217
+ # Setup
218
+ Dir.mkdir(File.join(@tmpdir, "existing_dir"))
219
+ File.write(File.join(@tmpdir, "existing_file.txt"), "content")
220
+
221
+ # Assert that a path that doesn't exist fails all checks
222
+ expect(@tmpdir).not_to have_file("non_existent.txt")
223
+ expect(@tmpdir).not_to have_dir("non_existent_dir")
224
+ expect(@tmpdir).not_to have_symlink("non_existent_link")
225
+
226
+ # Assert that an existing directory is NOT a file or symlink
227
+ expect(@tmpdir).not_to have_file("existing_dir")
228
+ expect(@tmpdir).not_to have_symlink("existing_dir")
229
+ # expect(@tmpdir).not_to have_dir("existing_dir") # This would fail
230
+
231
+ # Assert that an existing file is NOT a directory or symlink
232
+ expect(@tmpdir).not_to have_dir("existing_file.txt")
233
+ expect(@tmpdir).not_to have_symlink("existing_file.txt")
234
+ # expect(@tmpdir).not_to have_file("existing_file.txt") # This would fail
235
+ end
236
+ ```
237
+
238
+ ### File Content Assertions
239
+
240
+ Go beyond existence and inspect what's inside a file.
241
+
242
+ ```ruby
243
+ before do
244
+ File.write(File.join(@tmpdir, "app.log"), "INFO: User logged in\nWARN: Low disk space")
245
+ File.write(File.join(@tmpdir, "config.json"), '{"theme":"dark","version":2}')
246
+ FileUtils.touch(File.join(@tmpdir, "empty.file"))
247
+ end
248
+
249
+ it "validates file content" do
250
+ # Check for content with a string or regex
251
+ expect(@tmpdir).to have_file("app.log", content: "INFO: User logged in")
252
+ expect(@tmpdir).to have_file("app.log", content: /WARN:.*space/)
253
+
254
+ # Check for the absence of content
255
+ expect(@tmpdir).to have_file("app.log", content: not(/ERROR/))
256
+
257
+ # Check if a file is empty
258
+ expect(@tmpdir).to have_file("empty.file", size: 0)
259
+
260
+ # Check for valid JSON and match its structure
261
+ expect(@tmpdir).to have_file("config.json", json_content: {
262
+ "theme" => "dark",
263
+ "version" => an_instance_of(Integer)
264
+ })
265
+ end
266
+ ```
267
+
268
+ ### Attribute Assertions
269
+
270
+ Matcher options allow detailed expectations on files, directories, and symlinks. Here
271
+ are the options available on the three top level matchers:
272
+
273
+ ```text
274
+ expect(path).to have_file(
275
+ name, mode:, owner:, group:, ctime:, mtime:, size:, content:, json_content:, yaml_content:,
276
+ )
277
+
278
+ expect(path).to have_dir(
279
+ name, mode:, owner:, group:, ctime:, mtime:, exact:
280
+ )
281
+
282
+ expect(path).to have_symlink(
283
+ name, mode:, owner:, group:, ctime:, mtime:, target:, target_type:, dangling:
284
+ )
285
+ ```
286
+
287
+ Here is a detailed example of using options:
288
+
289
+ ```ruby
290
+ before do
291
+ # Create a script, a secret key, and a symlink
292
+ script_path = File.join(@tmpdir, "deploy.sh")
293
+ File.write(script_path, "#!/bin/bash\n...")
294
+ FileUtils.chmod(0755, script_path)
295
+
296
+ key_path = File.join(@tmpdir, "secret.key")
297
+ File.write(key_path, "KEY_DATA")
298
+ FileUtils.chmod(0600, key_path)
299
+
300
+ FileUtils.ln_s("deploy.sh", File.join(@tmpdir, "latest_script"))
301
+ end
302
+
303
+ it "validates file attributes" do
304
+ # A single file can have many attributes checked at once
305
+ expect(@tmpdir).to have_file("deploy.sh", mode: "0755", size: be > 10)
306
+
307
+ # On Unix systems, you can check ownership
308
+ current_user = Etc.getlogin
309
+ expect(@tmpdir).to have_file("secret.key", owner: current_user, mode: "0600")
310
+
311
+ # Check symlinks and their targets
312
+ expect(@tmpdir).to have_symlink("latest_script", target: "deploy.sh")
313
+ end
314
+ ```
315
+
316
+ ### Directory Structure Assertions
317
+
318
+ The block syntax is the most powerful feature. It allows you to describe and verify
319
+ an entire file tree, including both the presence and *absence* of entries using
320
+ methods like `no_file`, `no_dir`, and `no_symlink`.
321
+
322
+ ```ruby
323
+ before do
324
+ # Generate a complex directory structure
325
+ app_dir = File.join(@tmpdir, "my-app")
326
+ FileUtils.mkdir_p(File.join(app_dir, "bin"))
327
+ FileUtils.mkdir_p(File.join(app_dir, "config"))
328
+ FileUtils.mkdir_p(File.join(app_dir, "log"))
329
+
330
+ File.write(File.join(app_dir, "bin/run"), "#!/bin/bash")
331
+ FileUtils.chmod(0755, File.join(app_dir, "bin/run"))
332
+
333
+ File.write(File.join(app_dir, "config/database.yml"), "adapter: postgresql")
334
+ FileUtils.ln_s("database.yml", File.join(app_dir, "config/db.yml"))
335
+ end
336
+
337
+ it "validates a nested directory structure" do
338
+ # Note the parentheses around the matcher and its block
339
+ expect(@tmpdir).to(have_dir("my-app") do
340
+ # Assert on the 'bin' directory and its contents
341
+ dir "bin" do
342
+ file "run", mode: "0755", content: /bash/
343
+ end
344
+
345
+ # Assert on the 'config' directory and its contents
346
+ dir "config" do
347
+ file "database.yml"
348
+ symlink "db.yml", target: "database.yml"
349
+ no_file "secrets.yml" # Assert that a file is NOT present
350
+ end
351
+
352
+ # Assert that the 'log' directory is present and empty
353
+ dir "log"
354
+
355
+ # Assert the absence of other entries at the root of 'my-app'
356
+ no_dir "tmp"
357
+ no_file "README.md"
358
+ end)
359
+ end
360
+ ```
361
+
362
+ ### Exact Directory Contents
363
+
364
+ You can enforce that a directory contains *only* the entries defined in your
365
+ specification block by using the `exact: true` option. This is perfect for testing
366
+ generators or build scripts that should produce a clean, specific output without any
367
+ extra files.
368
+
369
+ If any undeclared entries are found on the PathMatchers, the matcher will fail.
370
+
371
+ ```ruby
372
+ it "creates a directory with only the expected files" do
373
+ # Setup: Create a directory with an extra, unexpected file.
374
+ FileUtils.mkdir(File.join(@tmpdir, 'dist'))
375
+ File.write(File.join(@tmpdir, 'dist/app.js'), '// ...')
376
+ File.write(File.join(@tmpdir, 'dist/unexpected.log'), 'debug info')
377
+
378
+ # This test will fail because 'unexpected.log' was not declared.
379
+ expect(@tmpdir).to(
380
+ have_dir('dist', exact: true) do
381
+ file 'app.js'
382
+ end
383
+ )
384
+ end
385
+
386
+ # Failure Message:
387
+ #
388
+ # the entry 'dist' at '...' was expected to satisfy the following but did not:
389
+ # - did not expect entries ["unexpected.log"] to be present
390
+ ```
391
+
392
+ ## Development
393
+
394
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
395
+ spec` to run the tests. You can also run `bin/console` for an interactive prompt that
396
+ will allow you to experiment.
397
+
398
+ To install this gem onto your local machine, run `bundle exec rake install`.
399
+
400
+ ## Contributing
401
+
402
+ Bug reports and pull requests are welcome on GitHub at
403
+ https://github.com/main-branch/rspec-path_matchers. This project is intended to be a
404
+ safe, welcoming space for collaboration.
405
+
406
+ ## License
407
+
408
+ The gem is available as open source under the terms of the [MIT
409
+ License](https://opensource.org/licenses/MIT).