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
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"
|
data/.rspec
ADDED
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
|
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
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
|
+
[](https://rubygems.org/gems/rspec-path_matchers)
|
5
|
+
[](https://github.com/main-branch/rspec-path_matchers/actions)
|
7
|
+
[](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).
|