quick_check 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 896e5b8930fe9d6f9c090ef32c1bbc70a57a05d415c7ad6996e5a4e211868963
4
+ data.tar.gz: ae0e8f1831e674357081df078d68cfdbf3966c22d09f6c826becaa7d6a21bd90
5
+ SHA512:
6
+ metadata.gz: cddc454b92ad6e3c2c1f9afe08dab40305c9ee29bfe91d35a44fe578b7c52bb27cc09dbd1806c13d6cb9350bb4e945a0e2e1c24c757bbb005865739b2cd9281f
7
+ data.tar.gz: 7952bbeb5140d87b51b189b464ee7309367a49fc687e099bdcd124b9d5744b0914735d0336b36d500a6dbe781b2bae5101101519a9446af319f2bbb442256fc4
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Kasvit
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,126 @@
1
+ # quick_check
2
+
3
+ Run only the tests that changed (RSpec or Minitest). Fast.
4
+
5
+ `quick_check` installs a `qc` command that finds changed/added test files (staged, unstaged, and untracked by default) and runs them using the appropriate test runner automatically.
6
+
7
+ ## Basic Usage
8
+
9
+ Install the gem and run `qc` to run the tests that changed.
10
+
11
+ ```bash
12
+ gem install quick_check
13
+ qc
14
+ ```
15
+
16
+ ## Features
17
+
18
+ - Detects changed test files under:
19
+ - RSpec: `spec/**/*_spec.rb`
20
+ - Minitest: `test/**/*_test.rb`
21
+ - Also maps changed source files to their tests:
22
+ - `app/models/user.rb` -> `spec/models/user_spec.rb` / `test/models/user_test.rb`
23
+ - `app/controllers/home_controller.rb` -> `spec/requests/home_spec.rb` or `spec/requests/home_controller_spec.rb` (fallbacks), and controller tests as fallback; similarly for Minitest under `test/integration` and `test/controllers`
24
+ - `lib/foo/bar.rb` -> `spec/lib/foo/bar_spec.rb` / `test/lib/foo/bar_test.rb`
25
+ - Includes (by default):
26
+ - Unstaged working tree changes
27
+ - Staged (index) changes
28
+ - Untracked files (new specs not yet added)
29
+ - All committed changes on your branch vs base (`main`/`master`)
30
+ - You can disable branch commits with `--no-committed`
31
+ - Auto-detects base branch (`main` or `master`) or configure via `.quick_check.yml`
32
+ - Auto-detects framework and command:
33
+ - RSpec: `bundle exec rspec <files>`
34
+ - Rails + Minitest: `bin/rails test <files>` (or `bundle exec rails test <files>`)
35
+ - Plain Minitest: per-file `ruby -I test <file>`
36
+ - Always prints the full command(s) before executing
37
+ - `--cmd` to override the command for advanced use-cases
38
+
39
+ ## Install
40
+
41
+ From source:
42
+
43
+ ```bash
44
+ gem install quick_check
45
+ ```
46
+
47
+ Once installed, the `qc` executable will be available in your shell.
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ # Run all changed tests in your branch (default: committed + staged + unstaged + untracked)
53
+ qc
54
+
55
+ # Print matching spec file paths only (no execution)
56
+ qc --print
57
+
58
+ # Dry-run: print the exact command that would run
59
+ qc --dry-run
60
+
61
+ # Only current edits (ignore previously committed changes in this branch)
62
+ qc --no-committed
63
+
64
+ # Specify/override the base branch
65
+ qc --base main
66
+
67
+ # Only committed changes vs base (ignore working tree)
68
+ qc --no-staged --no-unstaged
69
+
70
+ # Use a custom test command
71
+ qc --cmd "bundle exec rspec --fail-fast"
72
+
73
+ # Verbose output
74
+ qc --verbose
75
+ ```
76
+
77
+ ## Options
78
+
79
+ - `--base BRANCH`: Base branch to diff against (overrides config)
80
+ - `--committed` / `--no-committed`: Include or exclude committed changes vs base (default: include)
81
+ - `--no-staged`: Ignore staged changes
82
+ - `--no-unstaged`: Ignore unstaged changes
83
+ - `--cmd CMD`: Override test command (default: auto-detected runner)
84
+ - `-p`, `--print`: Only print matched files, do not run
85
+ - `-n`, `--dry-run`: Print the command that would run and exit
86
+ - `-v`, `--verbose`: Verbose/debug output
87
+ - `-h`, `--help`: Show help
88
+
89
+ ## Configuration
90
+
91
+ Create `.quick_check.yml` in the repo root (or current directory) to set defaults:
92
+
93
+ ```yml
94
+ # .quick_check.yml
95
+ base_branch: main
96
+ ```
97
+
98
+ If no config is present, `qc` will use the first existing branch among `main` or `master`.
99
+
100
+ ## How it works
101
+
102
+ - Unstaged: `git diff --name-only --diff-filter=ACMR`
103
+ - Staged: `git diff --name-only --cached --diff-filter=ACMR`
104
+ - Untracked: `git ls-files --others --exclude-standard`
105
+ - Committed vs base: `git diff --name-only --diff-filter=ACMR <base>...HEAD`
106
+
107
+ Files are filtered to `spec/**/*_spec.rb` and/or `test/**/*_test.rb`, de-duplicated, sorted, and then:
108
+
109
+ - For RSpec: run once via `bundle exec rspec <files>`
110
+ - For Rails + Minitest: run once via `rails test <files>`
111
+ - For plain Minitest: run each file via `ruby -I test <file>`
112
+
113
+ The CLI prints each full command before executing. With `--dry-run`, it only prints and exits.
114
+
115
+ Notes:
116
+ - We intentionally exclude deletions (`D`) so removed tests are not attempted to run.
117
+ - Renames (`R`) and copies (`C`) are included, so moved tests are still executed.
118
+
119
+ ## Exit status
120
+
121
+ - Returns non-zero if any executed command fails
122
+ - Returns `0` if no changed/added spec files are detected
123
+
124
+ ## License
125
+
126
+ MIT
data/bin/qc ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "shellwords"
5
+ require "quick_check"
6
+
7
+ exit(QuickCheck::CLI.start(ARGV))
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "open3"
5
+ require "yaml"
6
+ require "shellwords"
7
+
8
+ module QuickCheck
9
+ class CLI
10
+ DEFAULT_BASE_BRANCHES = ["main", "master"].freeze
11
+
12
+ def self.start(argv)
13
+ new(argv).run
14
+ end
15
+
16
+ def initialize(argv)
17
+ @argv = argv
18
+ @options = {
19
+ base_branch: nil,
20
+ include_committed_diff: true,
21
+ include_staged: true,
22
+ include_unstaged: true,
23
+ custom_command: nil,
24
+ print_only: false,
25
+ dry_run: false,
26
+ debug: false
27
+ }
28
+ end
29
+
30
+ def run
31
+ parse_options!
32
+
33
+ changed = determine_changed_test_files
34
+ if changed[:rspec].empty? && changed[:minitest].empty?
35
+ $stdout.puts("No changed/added test files detected.")
36
+ return 0
37
+ end
38
+
39
+ if @options[:print_only]
40
+ (changed[:rspec] + changed[:minitest]).each { |f| $stdout.puts(f) }
41
+ return 0
42
+ end
43
+
44
+ exit_status = 0
45
+
46
+ # Run RSpec files if any
47
+ unless changed[:rspec].empty?
48
+ cmd = build_command_for(:rspec, changed[:rspec])
49
+ print_and_maybe_run(cmd)
50
+ exit_status = nonzero_status(exit_status)
51
+ end
52
+
53
+ # Run Minitest files if any
54
+ unless changed[:minitest].empty?
55
+ minitest_cmd = build_command_for(:minitest, changed[:minitest])
56
+ if minitest_cmd == :per_file_minitest
57
+ changed[:minitest].each do |file|
58
+ cmd = ["ruby", "-I", "test", file]
59
+ print_and_maybe_run(cmd)
60
+ exit_status = nonzero_status(exit_status)
61
+ end
62
+ else
63
+ print_and_maybe_run(minitest_cmd)
64
+ exit_status = nonzero_status(exit_status)
65
+ end
66
+ end
67
+
68
+ exit_status
69
+ end
70
+
71
+ private
72
+
73
+ def parse_options!
74
+ parser = OptionParser.new do |opts|
75
+ opts.banner = "Usage: qc [options]"
76
+
77
+ opts.on("--base BRANCH", "Base branch to diff against (overrides config)") do |v|
78
+ @options[:base_branch] = v
79
+ end
80
+
81
+ opts.on("--no-committed", "Do not include committed changes vs base branch") do
82
+ @options[:include_committed_diff] = false
83
+ end
84
+
85
+ opts.on("--committed", "Include committed changes vs base branch") do
86
+ @options[:include_committed_diff] = true
87
+ end
88
+
89
+ opts.on("--no-staged", "Ignore staged changes") do
90
+ @options[:include_staged] = false
91
+ end
92
+
93
+ opts.on("--no-unstaged", "Ignore unstaged changes") do
94
+ @options[:include_unstaged] = false
95
+ end
96
+
97
+ opts.on("--cmd CMD", "Override test command (auto-detected when omitted)") do |v|
98
+ @options[:custom_command] = Shellwords.split(v)
99
+ end
100
+
101
+ opts.on("-p", "--print", "Only print matched spec files, do not run") do
102
+ @options[:print_only] = true
103
+ end
104
+
105
+ opts.on("-n", "--dry-run", "Print command that would run") do
106
+ @options[:dry_run] = true
107
+ end
108
+
109
+ opts.on("-v", "--verbose", "Verbose/debug output") do
110
+ @options[:debug] = true
111
+ end
112
+
113
+ opts.on("-h", "--help", "Show help") do
114
+ $stdout.puts(opts)
115
+ exit 0
116
+ end
117
+ end
118
+
119
+ parser.parse!(@argv)
120
+ end
121
+
122
+ def determine_changed_test_files
123
+ ensure_git_repo!
124
+
125
+ base_branch = resolve_base_branch
126
+ files = []
127
+
128
+ if @options[:include_unstaged]
129
+ files.concat(git_diff_name_only(["--name-only", "--diff-filter=ACMR"]))
130
+ files.concat(git_untracked_files)
131
+ end
132
+
133
+ if @options[:include_staged]
134
+ files.concat(git_diff_name_only(["--name-only", "--cached", "--diff-filter=ACMR"]))
135
+ end
136
+
137
+ if @options[:include_committed_diff]
138
+ current_branch = git_current_branch
139
+ if current_branch && base_branch && current_branch != base_branch
140
+ # Include files changed on this branch vs base
141
+ range = diff_range_against_base(base_branch)
142
+ files.concat(git_diff_name_only(["--name-only", "--diff-filter=ACMR", range])) if range
143
+ end
144
+ end
145
+
146
+ files = files.compact.uniq
147
+ rspec_specs = files.select { |f| f.match?(%r{\Aspec/.+_spec\.rb\z}) }
148
+ minitest_tests = files.select { |f| f.match?(%r{\Atest/.+_test\.rb\z}) }
149
+
150
+ # Infer tests from source changes (e.g., app/models/user.rb -> spec/models/user_spec.rb)
151
+ source_files = files.reject { |f| f.match?(%r{\A(spec/|test/)}) }
152
+ unless source_files.empty?
153
+ inferred_rspec = source_files.flat_map { |src| infer_rspec_paths_for_source(src) }
154
+ inferred_minitest = source_files.flat_map { |src| infer_minitest_paths_for_source(src) }
155
+ rspec_specs.concat(inferred_rspec)
156
+ minitest_tests.concat(inferred_minitest)
157
+ end
158
+
159
+ { rspec: rspec_specs.uniq, minitest: minitest_tests.uniq }
160
+ end
161
+
162
+ def ensure_git_repo!
163
+ run_cmd(["git", "rev-parse", "--is-inside-work-tree"]).tap do |ok, out, _err|
164
+ unless ok && out.to_s.strip == "true"
165
+ $stderr.puts("qc must be run inside a git repository")
166
+ exit 2
167
+ end
168
+ end
169
+ end
170
+
171
+ def resolve_base_branch
172
+ return @options[:base_branch] if @options[:base_branch]&.strip&.length&.positive?
173
+
174
+ # Load from config file if present
175
+ cfg = read_config
176
+ if cfg && cfg["base_branch"]&.strip&.length&.positive?
177
+ return cfg["base_branch"].strip
178
+ end
179
+
180
+ # Default to first existing branch from defaults
181
+ DEFAULT_BASE_BRANCHES.find { |b| branch_exists?(b) } || DEFAULT_BASE_BRANCHES.first
182
+ end
183
+
184
+ def read_config
185
+ paths = possible_config_paths
186
+ paths.each do |path|
187
+ next unless File.file?(path)
188
+ begin
189
+ data = YAML.safe_load(File.read(path))
190
+ return data if data.is_a?(Hash)
191
+ rescue StandardError
192
+ # Ignore malformed config
193
+ end
194
+ end
195
+ nil
196
+ end
197
+
198
+ def possible_config_paths
199
+ cwd = Dir.pwd
200
+ repo_root = git_repo_root || cwd
201
+ [
202
+ File.join(cwd, ".quick_check.yml"),
203
+ File.join(repo_root, ".quick_check.yml")
204
+ ].uniq
205
+ end
206
+
207
+ def branch_exists?(name)
208
+ ok, _out, _err = run_cmd(["git", "show-ref", "--verify", "--quiet", "refs/heads/#{name}"])
209
+ return true if ok
210
+
211
+ # fall back to remote branch
212
+ ok, _out, _err = run_cmd(["git", "ls-remote", "--heads", "origin", name])
213
+ ok && !_out.to_s.strip.empty?
214
+ end
215
+
216
+ def git_current_branch
217
+ ok, out, _err = run_cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"])
218
+ ok ? out.to_s.strip : nil
219
+ end
220
+
221
+ def git_repo_root
222
+ ok, out, _err = run_cmd(["git", "rev-parse", "--show-toplevel"])
223
+ ok ? out.to_s.strip : nil
224
+ end
225
+
226
+ def diff_range_against_base(base)
227
+ return nil unless branch_exists?(base)
228
+ # If upstream exists, prefer merge-base to HEAD to include all branch commits
229
+ # The symmetric range base...HEAD ensures we include commits unique to the branch
230
+ "#{base}...HEAD"
231
+ end
232
+
233
+ def git_diff_name_only(args)
234
+ cmd = ["git", "diff"] + args
235
+ ok, out, _err = run_cmd(cmd)
236
+ return [] unless ok
237
+
238
+ out.split("\n").map(&:strip).reject(&:empty?)
239
+ end
240
+
241
+ def git_untracked_files
242
+ ok, out, _err = run_cmd(["git", "ls-files", "--others", "--exclude-standard"])
243
+ return [] unless ok
244
+
245
+ out.split("\n").map(&:strip).reject(&:empty?)
246
+ end
247
+
248
+ def infer_rspec_paths_for_source(source_path)
249
+ candidates = []
250
+ if source_path.start_with?("app/")
251
+ rel = source_path.sub(/^app\//, "")
252
+ # Prefer request specs for controllers
253
+ if rel.start_with?("controllers/")
254
+ sub_rel = rel.sub(/^controllers\//, "")
255
+ dir = File.dirname(sub_rel)
256
+ dir = "" if dir == "."
257
+ base_with_controller = File.basename(sub_rel, ".rb")
258
+ base_without_controller = base_with_controller.sub(/_controller\z/, "")
259
+ # Common conventions for request specs
260
+ candidates << File.join("spec", "requests", dir, "#{base_without_controller}_spec.rb")
261
+ candidates << File.join("spec", "requests", dir, "#{base_with_controller}_spec.rb")
262
+ end
263
+ candidates << File.join("spec", rel).sub(/\.rb\z/, "_spec.rb")
264
+ elsif source_path.start_with?("lib/")
265
+ rel = source_path.sub(/^lib\//, "")
266
+ candidates << File.join("spec", "lib", rel).sub(/\.rb\z/, "_spec.rb")
267
+ end
268
+
269
+ candidates.select { |p| File.file?(p) }
270
+ end
271
+
272
+ def infer_minitest_paths_for_source(source_path)
273
+ candidates = []
274
+ if source_path.start_with?("app/")
275
+ rel = source_path.sub(/^app\//, "")
276
+ # Prefer integration/request-style tests for controllers when present
277
+ if rel.start_with?("controllers/")
278
+ sub_rel = rel.sub(/^controllers\//, "")
279
+ dir = File.dirname(sub_rel)
280
+ dir = "" if dir == "."
281
+ base = File.basename(sub_rel, ".rb").sub(/_controller\z/, "")
282
+ integration_test = File.join("test", "integration", dir, "#{base}_test.rb")
283
+ candidates << integration_test
284
+ end
285
+ candidates << File.join("test", rel).sub(/\.rb\z/, "_test.rb")
286
+ elsif source_path.start_with?("lib/")
287
+ rel = source_path.sub(/^lib\//, "")
288
+ candidates << File.join("test", "lib", rel).sub(/\.rb\z/, "_test.rb")
289
+ end
290
+
291
+ candidates.select { |p| File.file?(p) }
292
+ end
293
+
294
+ def build_command_for(framework, files)
295
+ return (@options[:custom_command] + files) if @options[:custom_command]
296
+
297
+ case framework
298
+ when :rspec
299
+ ["bundle", "exec", "rspec"] + files
300
+ when :minitest
301
+ if rails_available?
302
+ rails_cmd + ["test"] + files
303
+ else
304
+ # Fallback: run per-file using ruby -Itest
305
+ :per_file_minitest
306
+ end
307
+ else
308
+ files
309
+ end
310
+ end
311
+
312
+ def rails_available?
313
+ bin_rails = File.join(Dir.pwd, "bin", "rails")
314
+ File.executable?(bin_rails) || File.file?(bin_rails) || gemfile_includes?("rails")
315
+ end
316
+
317
+ def rails_cmd
318
+ bin_rails = File.join(Dir.pwd, "bin", "rails")
319
+ if File.executable?(bin_rails) || File.file?(bin_rails)
320
+ [File.join("bin", "rails")]
321
+ else
322
+ ["bundle", "exec", "rails"]
323
+ end
324
+ end
325
+
326
+ def gemfile_includes?(gem_name)
327
+ gemfile_paths = [File.join(Dir.pwd, "Gemfile"), File.join(Dir.pwd, "gems.rb")]
328
+ gemfile_paths.any? do |path|
329
+ next false unless File.file?(path)
330
+ begin
331
+ content = File.read(path)
332
+ content.match?(/\bgem\s+["']#{Regexp.escape(gem_name)}["']/)
333
+ rescue StandardError
334
+ false
335
+ end
336
+ end
337
+ end
338
+
339
+ def print_and_maybe_run(cmd)
340
+ if cmd.is_a?(Array)
341
+ $stdout.puts(cmd.shelljoin)
342
+ return if @options[:dry_run]
343
+ system(*cmd)
344
+ else
345
+ # no-op for symbols like :per_file_minitest (handled by caller)
346
+ end
347
+ end
348
+
349
+ def nonzero_status(current_status)
350
+ return current_status if @options[:dry_run]
351
+ last = $?.exitstatus
352
+ if last && last != 0
353
+ last
354
+ else
355
+ current_status
356
+ end
357
+ end
358
+
359
+ def run_cmd(cmd)
360
+ stdout, stderr, status = Open3.capture3(*cmd)
361
+ [status.success?, stdout, stderr]
362
+ end
363
+ end
364
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuickCheck
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "quick_check/version"
4
+ require "quick_check/cli"
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "stringio"
5
+
6
+ RSpec.describe QuickCheck::CLI do
7
+ def run_cli(argv, git_outputs: {}, existing_files: [])
8
+ stdout_io = StringIO.new
9
+ stderr_io = StringIO.new
10
+
11
+ allow($stdout).to receive(:puts) { |msg| stdout_io.puts(msg) }
12
+ allow($stderr).to receive(:puts) { |msg| stderr_io.puts(msg) }
13
+
14
+ # Stub filesystem for File.file?
15
+ allow(File).to receive(:file?) do |path|
16
+ existing_files.include?(path)
17
+ end
18
+
19
+ # Stub Dir.pwd
20
+ allow(Dir).to receive(:pwd).and_return(Dir.pwd)
21
+
22
+ # Stub Open3.capture3 for git commands
23
+ allow(Open3).to receive(:capture3) do |*cmd|
24
+ key = cmd.join(" ")
25
+ if git_outputs.key?(key)
26
+ out = git_outputs[key]
27
+ [out[:stdout] || "", out[:stderr] || "", instance_double(Process::Status, success?: out.fetch(:success, true), exitstatus: out.fetch(:exitstatus, 0))]
28
+ else
29
+ # Default: success with empty output
30
+ ["", "", instance_double(Process::Status, success?: true, exitstatus: 0)]
31
+ end
32
+ end
33
+
34
+ status = described_class.start(argv)
35
+ [status, stdout_io.string, stderr_io.string]
36
+ end
37
+
38
+ let(:base_git_stubs) do
39
+ {
40
+ "git rev-parse --is-inside-work-tree" => { stdout: "true\n" },
41
+ "git rev-parse --abbrev-ref HEAD" => { stdout: "feature\n" },
42
+ "git rev-parse --show-toplevel" => { stdout: Dir.pwd + "\n" },
43
+ "git show-ref --verify --quiet refs/heads/main" => { success: false, exitstatus: 1 },
44
+ "git ls-remote --heads origin main" => { stdout: "refs/heads/main\n" }
45
+ }
46
+ end
47
+
48
+ it "prints and exits when no changed tests" do
49
+ status, out, _err = run_cli([], git_outputs: base_git_stubs)
50
+ expect(status).to eq(0)
51
+ expect(out).to include("No changed/added test files detected.")
52
+ end
53
+
54
+ it "runs rspec for changed rspec files" do
55
+ stubs = base_git_stubs.merge(
56
+ "git diff --name-only --diff-filter=ACMR" => { stdout: "spec/models/user_spec.rb\n" }
57
+ )
58
+
59
+ status, out, _err = run_cli(["--dry-run"], git_outputs: stubs)
60
+ expect(status).to eq(0)
61
+ expect(out.lines.map(&:strip)).to include("bundle exec rspec spec/models/user_spec.rb")
62
+ end
63
+
64
+ it "infers rspec from app source change" do
65
+ stubs = base_git_stubs.merge(
66
+ "git diff --name-only --diff-filter=ACMR" => { stdout: "app/models/user.rb\n" }
67
+ )
68
+ status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: [
69
+ File.join("spec", "models", "user_spec.rb")
70
+ ])
71
+ expect(status).to eq(0)
72
+ expect(out).to include("bundle exec rspec spec/models/user_spec.rb")
73
+ end
74
+
75
+ it "maps controller to request spec with both base names" do
76
+ stubs = base_git_stubs.merge(
77
+ "git diff --name-only --diff-filter=ACMR" => { stdout: "app/controllers/account/users_controller.rb\n" }
78
+ )
79
+ existing = [
80
+ File.join("spec", "requests", "account", "users_spec.rb"),
81
+ File.join("spec", "requests", "account", "users_controller_spec.rb")
82
+ ]
83
+ status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: existing)
84
+ expect(out).to include("bundle exec rspec spec/requests/account/users_spec.rb spec/requests/account/users_controller_spec.rb")
85
+ expect(status).to eq(0)
86
+ end
87
+
88
+ it "falls back to controller spec when no request spec exists" do
89
+ stubs = base_git_stubs.merge(
90
+ "git diff --name-only --diff-filter=ACMR" => { stdout: "app/controllers/home_controller.rb\n" }
91
+ )
92
+ existing = [File.join("spec", "controllers", "home_controller_spec.rb")]
93
+ status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: existing)
94
+ expect(out).to include("bundle exec rspec spec/controllers/home_controller_spec.rb")
95
+ expect(status).to eq(0)
96
+ end
97
+
98
+ it "runs minitest through rails test when test files change and rails present" do
99
+ stubs = base_git_stubs.merge(
100
+ "git diff --name-only --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
101
+ )
102
+ allow_any_instance_of(QuickCheck::CLI).to receive(:rails_available?).and_return(true)
103
+ status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: [File.join("bin", "rails")])
104
+ expect(out).to include("bundle exec rails test test/models/user_test.rb")
105
+ expect(status).to eq(0)
106
+ end
107
+
108
+ it "runs per-file minitest when no rails" do
109
+ stubs = base_git_stubs.merge(
110
+ "git diff --name-only --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
111
+ )
112
+
113
+ status, out, _err = run_cli(["--dry-run"], git_outputs: stubs)
114
+ expect(out.lines.map(&:strip)).to include("ruby -I test test/models/user_test.rb")
115
+ expect(status).to eq(0)
116
+ end
117
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "quick_check"
5
+ require "open3"
6
+
7
+ RSpec.configure do |config|
8
+ config.disable_monkey_patching!
9
+ config.order = :random
10
+ Kernel.srand config.seed
11
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quick_check
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kasvit
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-08-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Adds the `qc` command to run only changed or newly added RSpec files
14
+ from uncommitted changes and vs base branch (main/master).
15
+ email:
16
+ - kasvit93@gmail.com
17
+ executables:
18
+ - qc
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rspec"
23
+ - LICENSE.txt
24
+ - README.md
25
+ - bin/qc
26
+ - lib/quick_check.rb
27
+ - lib/quick_check/cli.rb
28
+ - lib/quick_check/version.rb
29
+ - spec/quick_check/cli_spec.rb
30
+ - spec/spec_helper.rb
31
+ homepage: https://github.com/kasvit/quick_check
32
+ licenses:
33
+ - MIT
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.6'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.1.6
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Run changed/added RSpec specs quickly
54
+ test_files: []