rails-quality-assurance 1.4.2 → 1.6.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +53 -1
- data/lib/rails_quality_assurance/error.rb +6 -0
- data/lib/rails_quality_assurance/parallel_spec_config.rb +58 -0
- data/lib/rails_quality_assurance/run_lock.rb +113 -0
- data/lib/rails_quality_assurance/version.rb +1 -1
- data/lib/rails_quality_assurance.rb +8 -2
- data/lib/tasks/quality_assurance.rake +10 -10
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82e64c7980fb29c2994a22bf443e2215380a1dd16a9810d1790770624f47854a
|
|
4
|
+
data.tar.gz: 02dc1dd0d3537f7ededa2b4e2e7dca210b6142f2392f3383b150da0c6118697f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8be0b839b55b6c9af8aa85110e47fdd1d394c2d5e279a07e51817d32cac9622734c74d1e79e4e9e33243d3deb3ff6c0e4bf35440f771f1a50a641c74d8fabdfe
|
|
7
|
+
data.tar.gz: 0f609c8d4f8cba9ea6b93a60ba8121502efbcda7bdfa815319fa31196b527d84126d52c90ffc37ea49b2aaf49e5cd17735c3e08aa085f00693cf66734b16c7a5
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.6.0] - 2026-09-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `spec:parallel` holds a cross-process `flock` lock
|
|
13
|
+
(`tmp/qa/spec_parallel.lock`). A second run aborts immediately with the
|
|
14
|
+
holder's command, PID, and elapsed time instead of piling workers onto the
|
|
15
|
+
machine. The kernel releases the lock when the holder exits, including on
|
|
16
|
+
SIGKILL, and the descriptor is inherited by spawned workers so the lock
|
|
17
|
+
outlives a killed coordinator for as long as any worker keeps running.
|
|
18
|
+
|
|
19
|
+
## [1.5.0] - 2026-09-25
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `spec:parallel` defaults `PARALLEL_TEST_PROCESSORS` to the number of available
|
|
24
|
+
CPUs instead of a hardcoded 3. On a 6-CPU machine the default run uses 6
|
|
25
|
+
workers: 5 for the regular suite plus 1 isolated worker for `spec/system/`.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `ISOLATE_SPEC_TASKS=0` now disables system spec isolation. `--isolate-n 0`
|
|
30
|
+
was silently ignored by parallel_tests, which still reserved a dedicated
|
|
31
|
+
worker and kept system specs out of the regular grouping.
|
|
32
|
+
|
|
8
33
|
## [1.4.2] - 2026-09-16
|
|
9
34
|
|
|
10
35
|
### Fixed
|
data/README.md
CHANGED
|
@@ -27,7 +27,9 @@ Opinionated quality assurance kit for Ruby on Rails applications.
|
|
|
27
27
|
|
|
28
28
|
- **RSpec**: Shared helpers and configuration.
|
|
29
29
|
- **Playwright + Capybara**: Remote Chromium headless driver support with auto port assignment for parallel workers and HTML error state dumps on failure (`require 'rails_quality_assurance/playwright'`).
|
|
30
|
-
- **Parallel Tests**: `rake spec:parallel` task
|
|
30
|
+
- **Parallel Tests**: `rake spec:parallel` task that matches the CPU count by
|
|
31
|
+
default, isolates system specs into a dedicated worker, and serializes runs
|
|
32
|
+
behind a cross-process lock.
|
|
31
33
|
- **SimpleCov**: 100% line and branch coverage threshold enforcement, LCOV reporting, and parallel process reporting (`require 'rails_quality_assurance/simplecov'`).
|
|
32
34
|
|
|
33
35
|
### 4. Continuous Integration Harness
|
|
@@ -84,6 +86,35 @@ That single require sets up:
|
|
|
84
86
|
|
|
85
87
|
*(If you prefer to load them individually, `require 'rails_quality_assurance/simplecov'` and `require 'rails_quality_assurance/playwright'` remain available.)*
|
|
86
88
|
|
|
89
|
+
#### Configuring SimpleCov (`.simplecov`)
|
|
90
|
+
|
|
91
|
+
`rails_quality_assurance` requires SimpleCov and then calls
|
|
92
|
+
`SimpleCov.start 'rails'`. SimpleCov automatically loads a `.simplecov` file
|
|
93
|
+
from the project root while `require 'simplecov'` runs, so anything configured
|
|
94
|
+
there is applied **before** coverage starts:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# .simplecov
|
|
98
|
+
SimpleCov.configure do
|
|
99
|
+
cover_views # measure ActionView templates (SimpleCov 1.2+)
|
|
100
|
+
track_tests # record which test covered each line
|
|
101
|
+
group 'Components', 'app/components'
|
|
102
|
+
skip 'app/views/pwa'
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use SimpleCov's native DSL for any option the gem does not wrap — `cover`,
|
|
107
|
+
`track_tests`, `group`, `minimum_coverage`, `maximum_coverage_drop`,
|
|
108
|
+
`baseline_file`, and so on. Because SimpleCov reads the file itself, a new
|
|
109
|
+
SimpleCov option is available to a project without a new
|
|
110
|
+
`rails-quality-assurance` release.
|
|
111
|
+
|
|
112
|
+
`.simplecov` is loaded with plain `load`, so bare method calls resolve against
|
|
113
|
+
`main`. Wrap them in `SimpleCov.configure` (which evaluates the block in
|
|
114
|
+
SimpleCov's context, as above) or prefix each call with `SimpleCov.`. Keep it
|
|
115
|
+
to configuration only: `SimpleCov.start` belongs in `spec/spec_helper.rb`, and
|
|
116
|
+
SimpleCov 1.3 deprecates calling it from `.simplecov`.
|
|
117
|
+
|
|
87
118
|
### 3. Rails Test Setup (spec/rails_helper.rb)
|
|
88
119
|
|
|
89
120
|
After the Rails environment boots, one require applies the shared Rails test configuration:
|
|
@@ -138,6 +169,27 @@ bin/rails qa:frontend # Runs Biome and Stylelint
|
|
|
138
169
|
bin/rails spec:parallel # Runs RSpec in parallel with system specs isolated
|
|
139
170
|
```
|
|
140
171
|
|
|
172
|
+
`spec:parallel` reads these environment variables:
|
|
173
|
+
|
|
174
|
+
| Variable | Default | Purpose |
|
|
175
|
+
| --- | --- | --- |
|
|
176
|
+
| `PARALLEL_TEST_PROCESSORS` | Number of CPUs | Total worker count |
|
|
177
|
+
| `ISOLATE_SPEC_TASKS` | `1` (or `0` on a single CPU) | Workers reserved for `spec/system/` |
|
|
178
|
+
| `PARALLEL_SPEC_OPTIONS` | - | Extra options passed to RSpec |
|
|
179
|
+
|
|
180
|
+
Isolation workers are carved out of the total. On a 6-CPU machine the default
|
|
181
|
+
run uses 6 workers: 5 for the regular suite and 1 isolated worker for
|
|
182
|
+
`spec/system/`. Set `ISOLATE_SPEC_TASKS=0` to skip isolation entirely; system
|
|
183
|
+
specs then run as ordinary specs.
|
|
184
|
+
|
|
185
|
+
`spec:parallel` takes an exclusive `flock` on `tmp/qa/spec_parallel.lock` for
|
|
186
|
+
the whole run. Starting a second run while one is in progress aborts
|
|
187
|
+
immediately with the holder's command, PID, and elapsed time instead of
|
|
188
|
+
doubling the worker count. The kernel releases the lock when the holder exits,
|
|
189
|
+
including on SIGKILL, so a killed run never leaves a stale lock behind; spawned
|
|
190
|
+
workers inherit the descriptor, so the lock stays held while any of them is
|
|
191
|
+
still alive.
|
|
192
|
+
|
|
141
193
|
---
|
|
142
194
|
|
|
143
195
|
## Development
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parallel'
|
|
4
|
+
|
|
5
|
+
module RailsQualityAssurance
|
|
6
|
+
# Resolves the parallel spec worker layout from the environment.
|
|
7
|
+
#
|
|
8
|
+
# `PARALLEL_TEST_PROCESSORS` is the total worker count and defaults to the
|
|
9
|
+
# number of available CPUs. `ISOLATE_SPEC_TASKS` carves that many workers out
|
|
10
|
+
# of the total for `spec/system/`; `0` disables isolation and system specs run
|
|
11
|
+
# as ordinary specs.
|
|
12
|
+
class ParallelSpecConfig
|
|
13
|
+
ISOLATE_PATTERN = 'spec/system/'
|
|
14
|
+
DEFAULT_ISOLATE_PROCESSORS = 1
|
|
15
|
+
|
|
16
|
+
attr_reader :processors, :isolate_processors
|
|
17
|
+
|
|
18
|
+
def self.from_env(env = ENV)
|
|
19
|
+
processors = env.fetch('PARALLEL_TEST_PROCESSORS', Parallel.processor_count).to_i
|
|
20
|
+
isolate = env.fetch('ISOLATE_SPEC_TASKS', default_isolate_processors(processors))
|
|
21
|
+
|
|
22
|
+
new(processors:, isolate_processors: isolate)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A single CPU cannot spare a worker for isolation.
|
|
26
|
+
def self.default_isolate_processors(processors)
|
|
27
|
+
processors > 1 ? DEFAULT_ISOLATE_PROCESSORS : 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(processors:, isolate_processors:)
|
|
31
|
+
@processors = Integer(processors)
|
|
32
|
+
@isolate_processors = Integer(isolate_processors)
|
|
33
|
+
|
|
34
|
+
validate
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def isolate?
|
|
38
|
+
isolate_processors.positive?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parallel_options
|
|
42
|
+
return '' unless isolate?
|
|
43
|
+
|
|
44
|
+
"--isolate --single #{ISOLATE_PATTERN} --isolate-n #{isolate_processors}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate
|
|
50
|
+
raise ArgumentError, 'PARALLEL_TEST_PROCESSORS must be a positive integer' if processors <= 0
|
|
51
|
+
raise ArgumentError, 'ISOLATE_SPEC_TASKS must be zero or greater' if isolate_processors.negative?
|
|
52
|
+
|
|
53
|
+
return if isolate_processors < processors
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, 'ISOLATE_SPEC_TASKS must be less than PARALLEL_TEST_PROCESSORS'
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'rails_quality_assurance/error'
|
|
7
|
+
|
|
8
|
+
module RailsQualityAssurance
|
|
9
|
+
# Cross-process exclusive lock that serializes heavy runs such as the
|
|
10
|
+
# parallel spec suite.
|
|
11
|
+
#
|
|
12
|
+
# The lock is held through `flock`, so the kernel releases it when the last
|
|
13
|
+
# holder exits, even on SIGKILL. The holder descriptor is made inheritable so
|
|
14
|
+
# workers spawned by the guarded run keep the lock alive if the coordinating
|
|
15
|
+
# process dies mid-run.
|
|
16
|
+
class RunLock
|
|
17
|
+
# Raised when another run already holds the lock.
|
|
18
|
+
class Busy < Error; end
|
|
19
|
+
|
|
20
|
+
def self.guard(path, command: nil)
|
|
21
|
+
lock = new(path)
|
|
22
|
+
lock.acquire(command)
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
yield
|
|
26
|
+
ensure
|
|
27
|
+
lock.release
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(path)
|
|
32
|
+
@path = path
|
|
33
|
+
@file = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Takes the lock and records the holder, or raises Busy with the current
|
|
37
|
+
# holder's details so the caller can tell what is already running.
|
|
38
|
+
def acquire(command = nil)
|
|
39
|
+
@file = open_file
|
|
40
|
+
|
|
41
|
+
return record_holder(command) if try_lock
|
|
42
|
+
|
|
43
|
+
message = busy_message
|
|
44
|
+
release
|
|
45
|
+
raise Busy, message
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def release
|
|
49
|
+
return unless @file
|
|
50
|
+
|
|
51
|
+
@file.flock(File::LOCK_UN)
|
|
52
|
+
@file.close
|
|
53
|
+
@file = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def try_lock
|
|
59
|
+
@file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
60
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def record_holder(command)
|
|
65
|
+
# Spawned workers must inherit the descriptor, otherwise the lock is
|
|
66
|
+
# released the moment the coordinating process dies while they still run.
|
|
67
|
+
@file.close_on_exec = false
|
|
68
|
+
write_metadata(command)
|
|
69
|
+
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def open_file
|
|
74
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
75
|
+
File.open(@path, File::RDWR | File::CREAT, 0o644)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write_metadata(command)
|
|
79
|
+
@file.rewind
|
|
80
|
+
@file.truncate(0)
|
|
81
|
+
@file.write(JSON.generate('pid' => Process.pid, 'started_at' => Time.now.utc.iso8601, 'command' => command))
|
|
82
|
+
@file.flush
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def busy_message
|
|
86
|
+
holder = metadata
|
|
87
|
+
return 'Another quality assurance run is already in progress; wait for it to finish.' if holder.empty?
|
|
88
|
+
|
|
89
|
+
"Another quality assurance run is already in progress: #{holder['command']} " \
|
|
90
|
+
"(pid #{holder['pid']}, started #{elapsed(holder['started_at'])} ago). " \
|
|
91
|
+
'Wait for it to finish before starting another run.'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def metadata
|
|
95
|
+
@file.rewind
|
|
96
|
+
JSON.parse(@file.read)
|
|
97
|
+
rescue JSON::ParserError, TypeError
|
|
98
|
+
{}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def elapsed(started_at)
|
|
102
|
+
seconds = (Time.now.utc - Time.iso8601(started_at.to_s)).round
|
|
103
|
+
return "#{seconds}s" if seconds < 60
|
|
104
|
+
|
|
105
|
+
minutes = seconds / 60
|
|
106
|
+
return "#{minutes}m#{seconds % 60}s" if minutes < 60
|
|
107
|
+
|
|
108
|
+
"#{minutes / 60}h#{minutes % 60}m"
|
|
109
|
+
rescue ArgumentError, TypeError
|
|
110
|
+
'an unknown time'
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'rails_quality_assurance/error'
|
|
3
4
|
require 'rails_quality_assurance/version'
|
|
4
5
|
require 'rails_quality_assurance/ci'
|
|
6
|
+
require 'rails_quality_assurance/parallel_spec_config'
|
|
7
|
+
require 'rails_quality_assurance/run_lock'
|
|
5
8
|
|
|
6
9
|
module RailsQualityAssurance
|
|
7
|
-
class Error < StandardError; end
|
|
8
|
-
|
|
9
10
|
def self.root
|
|
10
11
|
File.expand_path('..', __dir__)
|
|
11
12
|
end
|
|
@@ -33,6 +34,11 @@ module RailsQualityAssurance
|
|
|
33
34
|
'bundle exec ruby -e \'require "importmap-rails"; require "importmap/map"; ' \
|
|
34
35
|
'ARGV.replace(["audit"]); require "importmap/commands"\''
|
|
35
36
|
end
|
|
37
|
+
|
|
38
|
+
# Shared lock file that serializes parallel spec runs for the current app.
|
|
39
|
+
def self.spec_lock_path
|
|
40
|
+
File.join('tmp', 'qa', 'spec_parallel.lock')
|
|
41
|
+
end
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
require 'rails_quality_assurance/railtie'
|
|
@@ -89,18 +89,18 @@ namespace :spec do
|
|
|
89
89
|
desc 'Run specs in parallel with system specs isolated'
|
|
90
90
|
task parallel: :environment do
|
|
91
91
|
ENV['PARALLEL_TEST_FIRST_IS_1'] ||= 'true'
|
|
92
|
-
|
|
93
|
-
isolate_tasks = ENV.fetch('ISOLATE_SPEC_TASKS', 1).to_s
|
|
92
|
+
config = RailsQualityAssurance::ParallelSpecConfig.from_env
|
|
94
93
|
test_options = ENV.fetch('PARALLEL_SPEC_OPTIONS', nil)
|
|
95
|
-
raise ArgumentError, 'PARALLEL_TEST_PROCESSORS must be a positive integer' if parallel_tasks.to_i <= 0
|
|
96
94
|
|
|
97
|
-
options =
|
|
98
|
-
options
|
|
95
|
+
options = config.parallel_options
|
|
96
|
+
options = "#{options} --test-options=\"#{test_options}\"" if test_options
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
RailsQualityAssurance::RunLock.guard(RailsQualityAssurance.spec_lock_path, command: 'spec:parallel') do
|
|
99
|
+
# Each phase runs in its own rake process: parallel_tests shells out to `$0`
|
|
100
|
+
# to load a schema per worker, which only works when `$0` is a rake runner.
|
|
101
|
+
sh "bundle exec rake parallel:create[#{config.processors}]"
|
|
102
|
+
sh "bundle exec rake parallel:load_schema[#{config.processors}]"
|
|
103
|
+
sh "bundle exec parallel_rspec spec/ -n #{config.processors} #{options}"
|
|
104
|
+
end
|
|
105
105
|
end
|
|
106
106
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-quality-assurance
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mauricio Zaffari
|
|
@@ -395,6 +395,8 @@ files:
|
|
|
395
395
|
- lib/rails_quality_assurance/all.rb
|
|
396
396
|
- lib/rails_quality_assurance/ci.rb
|
|
397
397
|
- lib/rails_quality_assurance/ci_runner.rb
|
|
398
|
+
- lib/rails_quality_assurance/error.rb
|
|
399
|
+
- lib/rails_quality_assurance/parallel_spec_config.rb
|
|
398
400
|
- lib/rails_quality_assurance/playwright.rb
|
|
399
401
|
- lib/rails_quality_assurance/playwright/helper.rb
|
|
400
402
|
- lib/rails_quality_assurance/rails/helper.rb
|
|
@@ -403,6 +405,7 @@ files:
|
|
|
403
405
|
- lib/rails_quality_assurance/railtie.rb
|
|
404
406
|
- lib/rails_quality_assurance/rspec.rb
|
|
405
407
|
- lib/rails_quality_assurance/rspec/helper.rb
|
|
408
|
+
- lib/rails_quality_assurance/run_lock.rb
|
|
406
409
|
- lib/rails_quality_assurance/simplecov.rb
|
|
407
410
|
- lib/rails_quality_assurance/simplecov/helper.rb
|
|
408
411
|
- lib/rails_quality_assurance/test_helper.rb
|