quick_check 0.1.1 → 0.1.3
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 +4 -4
- data/README.md +4 -3
- data/lib/quick_check/cli.rb +103 -17
- data/lib/quick_check/version.rb +1 -1
- data/spec/quick_check/cli_spec.rb +54 -6
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e665ad565d365abca56c8521560fb97dd507683c8eec6d7c81084b2b393cf2d0
|
|
4
|
+
data.tar.gz: e82c198675cbc2aa956395d6b327088db5f5d4ad85f96a39c5abafb6830f57e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60f09a0569f3d80c90d95b41bc1d289cf136331bdf639e50d3c85931262488e9f6a5edc9678241b6ed1b9c570bfd8791031b8056acd1b51f7769cd85a3da3505
|
|
7
|
+
data.tar.gz: 2e38a08ee04981404e46b4099882a07927397aa66a9961704f480bb30b48cdb691af989e5c48cfb84d17475767e81309b8e3aed9801b616840b45ec2fbe9556b
|
data/README.md
CHANGED
|
@@ -27,6 +27,7 @@ qc
|
|
|
27
27
|
- Staged (index) changes
|
|
28
28
|
- Untracked files (new specs not yet added)
|
|
29
29
|
- All committed changes on your branch vs base (`main`/`master`)
|
|
30
|
+
- Renames and copies are tracked so moved tests still run
|
|
30
31
|
- You can disable branch commits with `--no-committed`
|
|
31
32
|
- Auto-detects base branch (`main` or `master`) or configure via `.quick_check.yml`
|
|
32
33
|
- Auto-detects framework and command:
|
|
@@ -99,10 +100,10 @@ If no config is present, `qc` will use the first existing branch among `main` or
|
|
|
99
100
|
|
|
100
101
|
## How it works
|
|
101
102
|
|
|
102
|
-
- Unstaged: `git diff --name-only --diff-filter=
|
|
103
|
-
- Staged: `git diff --name-only --cached --diff-filter=
|
|
103
|
+
- Unstaged: `git diff --name-only -M -C --diff-filter=ACMR`
|
|
104
|
+
- Staged: `git diff --name-only --cached -M -C --diff-filter=ACMR`
|
|
104
105
|
- Untracked: `git ls-files --others --exclude-standard`
|
|
105
|
-
- Committed vs base: `git diff --name-only --diff-filter=
|
|
106
|
+
- Committed vs base: `git diff --name-only -M -C --diff-filter=ACMR <base>...HEAD`
|
|
106
107
|
|
|
107
108
|
Files are filtered to `spec/**/*_spec.rb` and/or `test/**/*_test.rb`, de-duplicated, sorted, and then:
|
|
108
109
|
|
data/lib/quick_check/cli.rb
CHANGED
|
@@ -17,7 +17,7 @@ module QuickCheck
|
|
|
17
17
|
@argv = argv
|
|
18
18
|
@options = {
|
|
19
19
|
base_branch: nil,
|
|
20
|
-
include_committed_diff:
|
|
20
|
+
include_committed_diff: true,
|
|
21
21
|
include_staged: true,
|
|
22
22
|
include_unstaged: true,
|
|
23
23
|
custom_command: nil,
|
|
@@ -126,28 +126,72 @@ module QuickCheck
|
|
|
126
126
|
files = []
|
|
127
127
|
|
|
128
128
|
if @options[:include_unstaged]
|
|
129
|
-
files.concat(git_diff_name_only(["--name-only", "--diff-filter=
|
|
129
|
+
files.concat(git_diff_name_only(["--name-only", "-M", "-C", "--diff-filter=ACMR"]))
|
|
130
130
|
files.concat(git_untracked_files)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
if @options[:include_staged]
|
|
134
|
-
files.concat(git_diff_name_only(["--name-only", "--cached", "--diff-filter=
|
|
134
|
+
files.concat(git_diff_name_only(["--name-only", "--cached", "-M", "-C", "--diff-filter=ACMR"]))
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
if @options[:include_committed_diff]
|
|
138
138
|
current_branch = git_current_branch
|
|
139
139
|
if current_branch && base_branch && current_branch != base_branch
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
upstream_branch = git_upstream_branch(current_branch)
|
|
141
|
+
# Prefer upstream tracking branch to correctly handle rebases (only shows local changes)
|
|
142
|
+
if upstream_branch && upstream_has_differences?(upstream_branch)
|
|
143
|
+
changed_files = diff_range_against_upstream(upstream_branch)
|
|
144
|
+
files.concat(changed_files) if changed_files
|
|
145
|
+
elsif !upstream_branch
|
|
146
|
+
# Fall back to base branch if no upstream exists
|
|
147
|
+
changed_files = diff_range_against_base(base_branch)
|
|
148
|
+
files.concat(changed_files) if changed_files
|
|
149
|
+
end
|
|
143
150
|
end
|
|
144
151
|
end
|
|
145
152
|
|
|
146
153
|
files = files.compact.uniq
|
|
147
154
|
rspec_specs = files.select { |f| f.match?(%r{\Aspec/.+_spec\.rb\z}) }
|
|
155
|
+
rspec_specs += infer_rspec_from_source(files)
|
|
148
156
|
minitest_tests = files.select { |f| f.match?(%r{\Atest/.+_test\.rb\z}) }
|
|
149
157
|
|
|
150
|
-
{ rspec: rspec_specs.
|
|
158
|
+
{ rspec: rspec_specs.uniq, minitest: minitest_tests.uniq }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def infer_rspec_from_source(files)
|
|
162
|
+
candidates = []
|
|
163
|
+
files.each do |path|
|
|
164
|
+
next unless path.end_with?(".rb")
|
|
165
|
+
|
|
166
|
+
if path =~ %r{\Aapp/models/(.+)\.rb\z}
|
|
167
|
+
spec_path = File.join("spec", "models", "#{$1}_spec.rb")
|
|
168
|
+
candidates << spec_path if File.file?(spec_path)
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if path =~ %r{\Aapp/controllers/(.+?)(?:_controller)?\.rb\z}
|
|
173
|
+
controller_path = Regexp.last_match(1)
|
|
174
|
+
req_base = File.join("spec", "requests", controller_path)
|
|
175
|
+
req_variants = [
|
|
176
|
+
"#{req_base}_spec.rb",
|
|
177
|
+
"#{req_base}_controller_spec.rb"
|
|
178
|
+
].select { |p| File.file?(p) }
|
|
179
|
+
if req_variants.any?
|
|
180
|
+
candidates.concat(req_variants)
|
|
181
|
+
else
|
|
182
|
+
ctrl_spec = File.join("spec", "controllers", "#{controller_path}_controller_spec.rb")
|
|
183
|
+
candidates << ctrl_spec if File.file?(ctrl_spec)
|
|
184
|
+
end
|
|
185
|
+
next
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if path =~ %r{\Alib/(.+)\.rb\z}
|
|
189
|
+
spec_path = File.join("spec", "lib", "#{$1}_spec.rb")
|
|
190
|
+
candidates << spec_path if File.file?(spec_path)
|
|
191
|
+
next
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
candidates
|
|
151
195
|
end
|
|
152
196
|
|
|
153
197
|
def ensure_git_repo!
|
|
@@ -196,12 +240,17 @@ module QuickCheck
|
|
|
196
240
|
end
|
|
197
241
|
|
|
198
242
|
def branch_exists?(name)
|
|
243
|
+
local_branch_exists?(name) || remote_branch_exists?(name)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def local_branch_exists?(name)
|
|
199
247
|
ok, _out, _err = run_cmd(["git", "show-ref", "--verify", "--quiet", "refs/heads/#{name}"])
|
|
200
|
-
|
|
248
|
+
ok
|
|
249
|
+
end
|
|
201
250
|
|
|
202
|
-
|
|
203
|
-
ok,
|
|
204
|
-
ok && !
|
|
251
|
+
def remote_branch_exists?(name)
|
|
252
|
+
ok, out, _err = run_cmd(["git", "ls-remote", "--heads", "origin", name])
|
|
253
|
+
ok && !out.to_s.strip.empty?
|
|
205
254
|
end
|
|
206
255
|
|
|
207
256
|
def git_current_branch
|
|
@@ -214,13 +263,50 @@ module QuickCheck
|
|
|
214
263
|
ok ? out.to_s.strip : nil
|
|
215
264
|
end
|
|
216
265
|
|
|
217
|
-
def
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
266
|
+
def git_upstream_branch(branch)
|
|
267
|
+
# Get the upstream tracking branch for the current branch
|
|
268
|
+
ok, out, _err = run_cmd(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "#{branch}@{u}"])
|
|
269
|
+
if ok && !out.to_s.strip.empty?
|
|
270
|
+
upstream = out.to_s.strip
|
|
271
|
+
# Verify the upstream branch actually exists
|
|
272
|
+
ok_check, _out_check, _err_check = run_cmd(["git", "rev-parse", "--verify", "--quiet", upstream])
|
|
273
|
+
return upstream if ok_check
|
|
223
274
|
end
|
|
275
|
+
nil
|
|
276
|
+
rescue StandardError
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def upstream_has_differences?(upstream)
|
|
281
|
+
# Only use upstream if it has local commits (avoids testing already-pushed changes)
|
|
282
|
+
ok, out, _err = run_cmd(["git", "rev-list", "--count", "#{upstream}..HEAD"])
|
|
283
|
+
return false unless ok
|
|
284
|
+
out.to_s.strip.to_i > 0
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def diff_range_against_upstream(upstream)
|
|
288
|
+
files = git_log_first_parent_files(upstream, "HEAD")
|
|
289
|
+
files.empty? ? nil : files
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def diff_range_against_base(base)
|
|
293
|
+
base_ref = if local_branch_exists?(base)
|
|
294
|
+
base
|
|
295
|
+
elsif remote_branch_exists?(base)
|
|
296
|
+
"origin/#{base}"
|
|
297
|
+
else
|
|
298
|
+
return nil
|
|
299
|
+
end
|
|
300
|
+
files = git_log_first_parent_files(base_ref, "HEAD")
|
|
301
|
+
files.empty? ? nil : files
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def git_log_first_parent_files(base_ref, head_ref)
|
|
305
|
+
# Use --first-parent to exclude merge commits, only showing changes from direct branch commits
|
|
306
|
+
cmd = ["git", "log", "--first-parent", "--name-only", "--diff-filter=ACMR", "--format=", "#{base_ref}..#{head_ref}"]
|
|
307
|
+
ok, out, _err = run_cmd(cmd)
|
|
308
|
+
return [] unless ok
|
|
309
|
+
out.split("\n").map(&:strip).reject(&:empty?).uniq
|
|
224
310
|
end
|
|
225
311
|
|
|
226
312
|
def git_diff_name_only(args)
|
data/lib/quick_check/version.rb
CHANGED
|
@@ -53,7 +53,7 @@ RSpec.describe QuickCheck::CLI do
|
|
|
53
53
|
|
|
54
54
|
it "runs rspec for changed rspec files" do
|
|
55
55
|
stubs = base_git_stubs.merge(
|
|
56
|
-
"git diff --name-only --diff-filter=ACMR" => { stdout: "spec/models/user_spec.rb\n" }
|
|
56
|
+
"git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "spec/models/user_spec.rb\n" }
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
status, out, _err = run_cli(["--dry-run"], git_outputs: stubs)
|
|
@@ -63,7 +63,7 @@ RSpec.describe QuickCheck::CLI do
|
|
|
63
63
|
|
|
64
64
|
it "infers rspec from app source change" do
|
|
65
65
|
stubs = base_git_stubs.merge(
|
|
66
|
-
"git diff --name-only --diff-filter=ACMR" => { stdout: "app/models/user.rb\n" }
|
|
66
|
+
"git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "app/models/user.rb\n" }
|
|
67
67
|
)
|
|
68
68
|
status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: [
|
|
69
69
|
File.join("spec", "models", "user_spec.rb")
|
|
@@ -74,7 +74,7 @@ RSpec.describe QuickCheck::CLI do
|
|
|
74
74
|
|
|
75
75
|
it "maps controller to request spec with both base names" do
|
|
76
76
|
stubs = base_git_stubs.merge(
|
|
77
|
-
"git diff --name-only --diff-filter=ACMR" => { stdout: "app/controllers/account/users_controller.rb\n" }
|
|
77
|
+
"git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "app/controllers/account/users_controller.rb\n" }
|
|
78
78
|
)
|
|
79
79
|
existing = [
|
|
80
80
|
File.join("spec", "requests", "account", "users_spec.rb"),
|
|
@@ -87,7 +87,7 @@ RSpec.describe QuickCheck::CLI do
|
|
|
87
87
|
|
|
88
88
|
it "falls back to controller spec when no request spec exists" do
|
|
89
89
|
stubs = base_git_stubs.merge(
|
|
90
|
-
"git diff --name-only --diff-filter=ACMR" => { stdout: "app/controllers/home_controller.rb\n" }
|
|
90
|
+
"git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "app/controllers/home_controller.rb\n" }
|
|
91
91
|
)
|
|
92
92
|
existing = [File.join("spec", "controllers", "home_controller_spec.rb")]
|
|
93
93
|
status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: existing)
|
|
@@ -97,7 +97,7 @@ RSpec.describe QuickCheck::CLI do
|
|
|
97
97
|
|
|
98
98
|
it "runs minitest through rails test when test files change and rails present" do
|
|
99
99
|
stubs = base_git_stubs.merge(
|
|
100
|
-
"git diff --name-only --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
|
|
100
|
+
"git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
|
|
101
101
|
)
|
|
102
102
|
allow_any_instance_of(QuickCheck::CLI).to receive(:rails_available?).and_return(true)
|
|
103
103
|
status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: [File.join("bin", "rails")])
|
|
@@ -107,11 +107,59 @@ RSpec.describe QuickCheck::CLI do
|
|
|
107
107
|
|
|
108
108
|
it "runs per-file minitest when no rails" do
|
|
109
109
|
stubs = base_git_stubs.merge(
|
|
110
|
-
"git diff --name-only --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
|
|
110
|
+
"git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
status, out, _err = run_cli(["--dry-run"], git_outputs: stubs)
|
|
114
114
|
expect(out.lines.map(&:strip)).to include("ruby -I test test/models/user_test.rb")
|
|
115
115
|
expect(status).to eq(0)
|
|
116
116
|
end
|
|
117
|
+
|
|
118
|
+
it "uses first-parent to handle rebases correctly" do
|
|
119
|
+
stubs = base_git_stubs.merge(
|
|
120
|
+
"git show-ref --verify --quiet refs/heads/main" => { success: true },
|
|
121
|
+
"git log --first-parent --name-only --diff-filter=ACMR --format= main..HEAD" => { stdout: "spec/my_feature_spec.rb\n" }
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
|
|
125
|
+
expect(status).to eq(0)
|
|
126
|
+
expect(out).to include("bundle exec rspec spec/my_feature_spec.rb")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "prefers upstream branch over base branch when upstream has differences" do
|
|
130
|
+
stubs = base_git_stubs.merge(
|
|
131
|
+
"git rev-parse --abbrev-ref --symbolic-full-name feature@{u}" => { stdout: "origin/feature\n" },
|
|
132
|
+
"git rev-parse --verify --quiet origin/feature" => { success: true },
|
|
133
|
+
"git rev-list --count origin/feature..HEAD" => { stdout: "5\n" },
|
|
134
|
+
"git log --first-parent --name-only --diff-filter=ACMR --format= origin/feature..HEAD" => { stdout: "spec/my_local_change_spec.rb\n" }
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
|
|
138
|
+
expect(status).to eq(0)
|
|
139
|
+
expect(out).to include("bundle exec rspec spec/my_local_change_spec.rb")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "does not include committed changes when upstream is in sync" do
|
|
143
|
+
stubs = base_git_stubs.merge(
|
|
144
|
+
"git rev-parse --abbrev-ref --symbolic-full-name feature@{u}" => { stdout: "origin/feature\n" },
|
|
145
|
+
"git rev-parse --verify --quiet origin/feature" => { success: true },
|
|
146
|
+
"git rev-list --count origin/feature..HEAD" => { stdout: "0\n" }
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
|
|
150
|
+
expect(status).to eq(0)
|
|
151
|
+
expect(out).to include("No changed/added test files detected.")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "falls back to base branch when no upstream exists" do
|
|
155
|
+
stubs = base_git_stubs.merge(
|
|
156
|
+
"git rev-parse --abbrev-ref --symbolic-full-name feature@{u}" => { success: false, exitstatus: 128 },
|
|
157
|
+
"git show-ref --verify --quiet refs/heads/main" => { success: true },
|
|
158
|
+
"git log --first-parent --name-only --diff-filter=ACMR --format= main..HEAD" => { stdout: "spec/my_feature_spec.rb\n" }
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
|
|
162
|
+
expect(status).to eq(0)
|
|
163
|
+
expect(out).to include("bundle exec rspec spec/my_feature_spec.rb")
|
|
164
|
+
end
|
|
117
165
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quick_check
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kasvit
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: Adds the `qc` command to run only changed or newly added RSpec files
|
|
14
13
|
from uncommitted changes and vs base branch (main/master).
|
|
@@ -32,7 +31,6 @@ homepage: https://github.com/kasvit/quick_check
|
|
|
32
31
|
licenses:
|
|
33
32
|
- MIT
|
|
34
33
|
metadata: {}
|
|
35
|
-
post_install_message:
|
|
36
34
|
rdoc_options: []
|
|
37
35
|
require_paths:
|
|
38
36
|
- lib
|
|
@@ -47,8 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
47
45
|
- !ruby/object:Gem::Version
|
|
48
46
|
version: '0'
|
|
49
47
|
requirements: []
|
|
50
|
-
rubygems_version: 3.
|
|
51
|
-
signing_key:
|
|
48
|
+
rubygems_version: 3.6.9
|
|
52
49
|
specification_version: 4
|
|
53
50
|
summary: Run changed/added RSpec specs quickly
|
|
54
51
|
test_files: []
|