Skip to content

Commit 9d2468a

Browse files
highbclaude
andcommitted
test: Add comprehensive CLI integration tests for 3.0.0 release
Add complete integration test coverage for the pathspec-rb CLI executable with 23 tests covering all commands, flags, and error conditions. Changes: - Add spec/integration/cli_spec.rb with 23 integration tests - Test all subcommands: match, specs_match, tree - Test all flags: -f/--file, -t/--type, -v/--verbose - Test error handling: missing files, unknown commands - Test exit codes: 0 (success), 1 (no match), 2 (error) - Test negated patterns and default .gitignore behavior - Add match? predicate alias to PathSpec#match for Ruby conventions - Update Rakefile with separate test tasks: - spec: Run unit tests only - spec_integration: Run integration tests only - spec_all: Run all tests with unified coverage - Update default task to include integration tests - Update CI workflow to run integration tests across all Ruby versions - Update .mise.toml: - Fix bundler to use gem backend: "gem:bundler" - Add granular test tasks (test:unit, test:integration, test:all) - Update README with new test tasks and development workflows - Update CHANGELOG.md for 3.0.0 release: - Document all new features and improvements - Note bundler and irb dependency updates - Document test coverage improvement: 99.48% → 99.65% Test Results: - 248 total tests passing (225 unit + 23 integration) - Coverage: 99.65% (573/575 lines) - All tests pass on Ruby 3.4.1 Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 2208cab commit 9d2468a

File tree

7 files changed

+279
-7
lines changed

7 files changed

+279
-7
lines changed

.github/workflows/ruby.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
strategy:
3737
matrix:
3838
ruby-version: ['3.2', '3.3', '3.4', '4.0.1']
39-
raketasks: ['rubocop', 'spec', 'docs']
39+
raketasks: ['rubocop', 'spec', 'spec_integration', 'docs']
4040
steps:
4141
- uses: actions/checkout@v6
4242
- name: Set up Ruby

.mise.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
[tools]
22
ruby = "3.4.1"
3+
"gem:bundler" = "2.6.3"
34

45
[tasks.install]
56
run = "bundle install"
67
description = "Install gem dependencies"
78

89
[tasks.test]
910
run = "bundle exec rake"
10-
description = "Run rubocop, specs, and docs"
11+
description = "Run rubocop, unit tests, integration tests, and docs"
12+
13+
[tasks."test:unit"]
14+
run = "bundle exec rake spec"
15+
description = "Run unit tests only"
16+
17+
[tasks."test:integration"]
18+
run = "bundle exec rake spec_integration"
19+
description = "Run integration tests only"
20+
21+
[tasks."test:all"]
22+
run = "bundle exec rake spec_all"
23+
description = "Run all tests (unit + integration)"
1124

1225
[tasks."test:matrix"]
1326
run = "bundle exec rake test_matrix"

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,31 @@
77
- (Maint) Remove Ruby 3.1 support (EOL March 2025). The Gem now requires >= 3.2.0
88
- Updated minimum required Ruby version in gemspec from >= 3.1.0 to >= 3.2.0
99

10+
### Features
11+
12+
- Added `match?` predicate method as alias for `match` to follow Ruby conventions
13+
- Added comprehensive CLI integration test suite (23 tests covering all commands, flags, and error handling)
14+
- Added mise (formerly rtx) tooling support for managing Ruby and bundler versions
15+
- Added `test_matrix` rake task to run tests across all Ruby versions (3.2, 3.3, 3.4, 4.0.1) using Docker
16+
- Separated unit tests (`rake spec`) and integration tests (`rake spec_integration`)
17+
- Added `spec_all` rake task to run complete test suite with unified coverage reporting
18+
1019
### Maintenance
1120

1221
- Added Ruby 3.4 to testing matrix (Stable, Tested)
1322
- Added Ruby 4.0.1 to testing matrix (Preview, Tested)
1423
- Updated CI workflows to use Ruby 3.4 for gem publishing
24+
- Updated CI to run integration tests across all Ruby versions
25+
- Updated bundler requirement from `~> 2.2` to `>= 2.5` for Ruby 3.2-4.0 compatibility
26+
- Added `irb` as development dependency (required for Ruby 4.0+)
27+
- Updated RuboCop TargetRubyVersion to 3.2 to match gemspec requirement
28+
- Updated README with comprehensive development setup documentation
29+
- mise installation and usage
30+
- Development tasks and workflows
31+
- Testing across Ruby version matrix
1532
- Updated README with comprehensive "Deprecated Rubies" section documenting historical deprecations
1633
- Updated "Supported Rubies" section in README to reflect current testing matrix (3.2, 3.3, 3.4, 4.0.1)
34+
- Improved test coverage from 99.48% to 99.65% (573/575 lines)
1735

1836
## 2.1.0
1937

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,22 @@ mise run install
160160
### Development Tasks
161161

162162
```shell
163-
# Run all tests (rubocop, rspec, docs)
163+
# Run all tests (rubocop, unit tests, integration tests, docs)
164164
mise run test
165165
# or: bundle exec rake
166166

167+
# Run only unit tests
168+
mise run test:unit
169+
# or: bundle exec rake spec
170+
171+
# Run only integration tests
172+
mise run test:integration
173+
# or: bundle exec rake spec_integration
174+
175+
# Run all specs (unit + integration)
176+
mise run test:all
177+
# or: bundle exec rake spec_all
178+
167179
# Run tests across all Ruby versions (3.2, 3.3, 3.4, 4.0.1) using Docker
168180
mise run test:matrix
169181
# or: bundle exec rake test_matrix
@@ -173,7 +185,7 @@ mise run build
173185
# or: gem build pathspec.gemspec
174186
```
175187

176-
The `test:matrix` task runs the full test suite across all supported Ruby versions in Docker containers, matching the CI environment.
188+
The `test:matrix` task runs the full test suite across all supported Ruby versions in Docker containers, matching the CI environment. Integration tests cover the CLI executable (`bin/pathspec-rb`).
177189

178190
## Contributing
179191

Rakefile

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@
22

33
begin
44
require 'rspec/core/rake_task'
5-
RSpec::Core::RakeTask.new(:spec)
5+
6+
RSpec::Core::RakeTask.new(:spec) do |t|
7+
t.pattern = 'spec/unit/**/*_spec.rb'
8+
end
9+
10+
RSpec::Core::RakeTask.new(:spec_integration) do |t|
11+
t.pattern = 'spec/integration/**/*_spec.rb'
12+
end
13+
14+
RSpec::Core::RakeTask.new(:spec_all) do |t|
15+
t.pattern = 'spec/**/*_spec.rb'
16+
end
617
rescue LoadError
718
puts 'rspec rake task failed to load'
819
end
@@ -15,7 +26,7 @@ RuboCop::RakeTask.new(:rubocop) do |t|
1526
t.options = ['--display-cop-names']
1627
end
1728

18-
task default: %i[rubocop spec docs]
29+
task default: %i[rubocop spec spec_integration docs]
1930

2031
desc 'Generate man page for executable script'
2132
task :docs do
@@ -43,7 +54,7 @@ task :test_matrix do
4354
'-w', '/app',
4455
"ruby:#{version}",
4556
'bash', '-c',
46-
'bundle install && bundle exec rake rubocop spec docs'
57+
'bundle install && bundle exec rake rubocop spec spec_integration docs'
4758
].shelljoin
4859

4960
success = system(cmd)

lib/pathspec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def match(path)
2424
matches = specs_matching(path.to_s)
2525
!matches.empty? && matches.all? {|m| m.inclusive?}
2626
end
27+
alias match? match
2728

2829
def specs_matching(path)
2930
@specs.select do |spec|

spec/integration/cli_spec.rb

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'open3'
5+
require 'tmpdir'
6+
7+
describe 'pathspec-rb CLI' do
8+
let(:cli_path) { File.expand_path('../../bin/pathspec-rb', __dir__) }
9+
let(:lib_path) { File.expand_path('../../lib', __dir__) }
10+
let(:gitignore_simple) { File.expand_path('../files/gitignore_simple', __dir__) }
11+
let(:gitignore_readme) { File.expand_path('../files/gitignore_readme', __dir__) }
12+
let(:regex_simple) { File.expand_path('../files/regex_simple', __dir__) }
13+
14+
def run_cli(*args)
15+
env = { 'RUBYLIB' => lib_path }
16+
stdout, stderr, status = Open3.capture3(env, 'ruby', cli_path, *args)
17+
[stdout, stderr, status]
18+
end
19+
20+
describe 'help and errors' do
21+
it 'shows help when no arguments provided' do
22+
stdout, _stderr, status = run_cli
23+
expect(stdout).to include('Usage: pathspec-rb')
24+
expect(stdout).to include('Subcommands:')
25+
expect(stdout).to include('specs_match')
26+
expect(stdout).to include('tree')
27+
expect(stdout).to include('match')
28+
expect(status.exitstatus).to eq(2)
29+
end
30+
31+
it 'shows error for unreadable file' do
32+
stdout, _stderr, status = run_cli('-f', '/nonexistent/file', 'match', 'test.txt')
33+
expect(stdout).to include("Error: I couldn't read /nonexistent/file")
34+
expect(status.exitstatus).to eq(2)
35+
end
36+
37+
it 'shows error for unknown subcommand' do
38+
stdout, _stderr, status = run_cli('-f', gitignore_simple, 'unknown_command', 'test.txt')
39+
expect(stdout).to include('Unknown sub-command unknown_command')
40+
expect(stdout).to include('Usage: pathspec-rb')
41+
expect(status.exitstatus).to eq(2)
42+
end
43+
end
44+
45+
describe 'match subcommand' do
46+
context 'with matching path' do
47+
it 'exits with 0' do
48+
_stdout, _stderr, status = run_cli('-f', gitignore_simple, 'match', 'test.md')
49+
expect(status.exitstatus).to eq(0)
50+
end
51+
52+
it 'shows match message with verbose flag' do
53+
stdout, _stderr, status = run_cli('-f', gitignore_simple, '-v', 'match', 'test.md')
54+
expect(stdout).to include('test.md matches a spec')
55+
expect(status.exitstatus).to eq(0)
56+
end
57+
end
58+
59+
context 'with non-matching path' do
60+
it 'exits with 1' do
61+
_stdout, _stderr, status = run_cli('-f', gitignore_simple, 'match', 'other.txt')
62+
expect(status.exitstatus).to eq(1)
63+
end
64+
65+
it 'shows no match message with verbose flag' do
66+
stdout, _stderr, status = run_cli('-f', gitignore_simple, '-v', 'match', 'other.txt')
67+
expect(stdout).to include('other.txt does not match')
68+
expect(status.exitstatus).to eq(1)
69+
end
70+
end
71+
72+
context 'with negated pattern' do
73+
it 'does not match negated paths' do
74+
_stdout, _stderr, status = run_cli('-f', gitignore_readme, 'match', 'abc/important.txt')
75+
expect(status.exitstatus).to eq(1)
76+
end
77+
78+
it 'matches non-negated paths' do
79+
_stdout, _stderr, status = run_cli('-f', gitignore_readme, 'match', 'abc/other.txt')
80+
expect(status.exitstatus).to eq(0)
81+
end
82+
end
83+
end
84+
85+
describe 'specs_match subcommand' do
86+
context 'with matching path' do
87+
it 'exits with 0 and shows matching specs' do
88+
stdout, _stderr, status = run_cli('-f', gitignore_readme, 'specs_match', 'abc/def.rb')
89+
expect(stdout).to include('abc/**')
90+
expect(status.exitstatus).to eq(0)
91+
end
92+
93+
it 'shows verbose message with -v flag' do
94+
stdout, _stderr, status = run_cli('-f', gitignore_readme, '-v', 'specs_match', 'abc/def.rb')
95+
expect(stdout).to include('abc/def.rb matches the following specs')
96+
expect(stdout).to include('abc/**')
97+
expect(status.exitstatus).to eq(0)
98+
end
99+
end
100+
101+
context 'with non-matching path' do
102+
it 'exits with 1' do
103+
_stdout, _stderr, status = run_cli('-f', gitignore_readme, 'specs_match', 'xyz/file.txt')
104+
expect(status.exitstatus).to eq(1)
105+
end
106+
107+
it 'shows no match message with verbose flag' do
108+
stdout, _stderr, status = run_cli('-f', gitignore_readme, '-v', 'specs_match', 'xyz/file.txt')
109+
expect(stdout).to include('xyz/file.txt does not match any specs')
110+
expect(status.exitstatus).to eq(1)
111+
end
112+
end
113+
end
114+
115+
describe 'tree subcommand' do
116+
around do |example|
117+
Dir.mktmpdir do |temp_dir|
118+
@temp_dir = temp_dir
119+
example.run
120+
end
121+
end
122+
123+
before do
124+
# Create test directory structure
125+
FileUtils.mkdir_p(File.join(@temp_dir, 'foo'))
126+
FileUtils.mkdir_p(File.join(@temp_dir, 'other'))
127+
FileUtils.touch(File.join(@temp_dir, 'foo', 'test.txt'))
128+
FileUtils.touch(File.join(@temp_dir, 'foo', 'another.txt'))
129+
FileUtils.touch(File.join(@temp_dir, 'other', 'file.txt'))
130+
131+
# Create a gitignore that matches foo/**
132+
@temp_gitignore = File.join(@temp_dir, '.gitignore')
133+
File.write(@temp_gitignore, "foo/**\n")
134+
end
135+
136+
context 'with matching files' do
137+
it 'exits with 0 and lists matching files' do
138+
stdout, _stderr, status = run_cli('-f', @temp_gitignore, 'tree', @temp_dir)
139+
expect(stdout).to include('foo')
140+
expect(stdout.lines.any? { |line| line.include?('other') && !line.include?('another') }).to be false
141+
expect(status.exitstatus).to eq(0)
142+
end
143+
144+
it 'shows verbose message with -v flag' do
145+
stdout, _stderr, status = run_cli('-f', @temp_gitignore, '-v', 'tree', @temp_dir)
146+
expect(stdout).to include("Files in #{@temp_dir} that match")
147+
expect(status.exitstatus).to eq(0)
148+
end
149+
end
150+
151+
context 'with no matching files' do
152+
before do
153+
# Create gitignore with pattern that won't match anything
154+
File.write(@temp_gitignore, "nomatch/**\n")
155+
end
156+
157+
it 'exits with 1' do
158+
_stdout, _stderr, status = run_cli('-f', @temp_gitignore, 'tree', @temp_dir)
159+
expect(status.exitstatus).to eq(1)
160+
end
161+
162+
it 'shows no match message with verbose flag' do
163+
stdout, _stderr, status = run_cli('-f', @temp_gitignore, '-v', 'tree', @temp_dir)
164+
expect(stdout).to include('No file')
165+
expect(stdout).to include('matched')
166+
expect(status.exitstatus).to eq(1)
167+
end
168+
end
169+
end
170+
171+
describe 'type flag' do
172+
context 'with git type (default)' do
173+
it 'parses gitignore patterns' do
174+
_stdout, _stderr, status = run_cli('-f', gitignore_simple, '-t', 'git', 'match', 'test.md')
175+
expect(status.exitstatus).to eq(0)
176+
end
177+
end
178+
179+
context 'with regex type' do
180+
it 'parses regex patterns' do
181+
_stdout, _stderr, status = run_cli('-f', regex_simple, '-t', 'regex', 'match', 'foo.md')
182+
expect(status.exitstatus).to eq(0)
183+
end
184+
end
185+
end
186+
187+
describe 'file flag' do
188+
it 'uses default .gitignore when not specified' do
189+
Dir.mktmpdir do |dir|
190+
gitignore_path = File.join(dir, '.gitignore')
191+
File.write(gitignore_path, "test/**\n")
192+
193+
Dir.chdir(dir) do
194+
_stdout, _stderr, status = run_cli('match', 'test/file.txt')
195+
expect(status.exitstatus).to eq(0)
196+
end
197+
end
198+
end
199+
200+
it 'uses specified file with -f flag' do
201+
_stdout, _stderr, status = run_cli('-f', gitignore_simple, 'match', 'test.md')
202+
expect(status.exitstatus).to eq(0)
203+
end
204+
205+
it 'uses specified file with --file flag' do
206+
_stdout, _stderr, status = run_cli('--file', gitignore_simple, 'match', 'test.md')
207+
expect(status.exitstatus).to eq(0)
208+
end
209+
end
210+
211+
describe 'empty string match command' do
212+
it 'treats empty string as match command' do
213+
_stdout, _stderr, status = run_cli('-f', gitignore_simple, '', 'test.md')
214+
expect(status.exitstatus).to eq(0)
215+
end
216+
end
217+
end

0 commit comments

Comments
 (0)