rspec-turbo 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.
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module RSpecTurbo
6
+ # Top-level orchestration: parse argv, set up the test databases, discover
7
+ # and plan the spec files, hand execution to the Executor, then print the
8
+ # report and (optionally) merge coverage. Exits non-zero on any failure.
9
+ class Runner
10
+ def initialize(argv)
11
+ @options = Options.new(argv)
12
+ @workers = Config.workers
13
+ end
14
+
15
+ def run
16
+ FileUtils.mkdir_p(Config.log_dir)
17
+ print_header
18
+
19
+ setup_databases
20
+ planner = plan(discover_files)
21
+
22
+ FileUtils.rm_rf("coverage") if Config.coverage?
23
+
24
+ display = Display.new(planner)
25
+ executor = Executor.new(planner, display, @options.rspec_options)
26
+ results = executor.run
27
+ wall_total = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - executor.wall_started).round
28
+
29
+ display.print_report(results, wall_total, @workers)
30
+ merge_coverage if Config.coverage?
31
+
32
+ exit((results.any? { |r| r[:status] == "FAIL" }) ? 1 : 0)
33
+ end
34
+
35
+ private
36
+
37
+ def print_header
38
+ puts
39
+ puts Terminal::SEP_THICK
40
+ puts " RSpec Turbo - Parallel"
41
+ puts Terminal::SEP_THICK
42
+ puts
43
+ end
44
+
45
+ def setup_databases
46
+ setup = DbSetup.new(@workers)
47
+ cached = !Config.force_setup? && setup.cached?
48
+ label = cached ? "DB cache hit" : "Setting up #{@workers} test DB(s)"
49
+
50
+ ok, elapsed = with_spinner(label) { setup.run! }
51
+
52
+ unless ok
53
+ print "\e[?25h" if Config::TTY
54
+ warn "✗ DB setup failed."
55
+ exit 2
56
+ end
57
+
58
+ puts " #{Terminal.c("32", "✓")} #{@workers} DB(s) ready (#{Terminal.fmt_duration(elapsed)})"
59
+ end
60
+
61
+ def discover_files
62
+ files = FileDiscovery.new(@options.folders, exclude_patterns: @options.exclude_patterns).files
63
+
64
+ if files.empty?
65
+ puts "Nothing to run."
66
+ exit 0
67
+ end
68
+
69
+ files
70
+ end
71
+
72
+ def plan(files)
73
+ planner = nil
74
+
75
+ _, elapsed = with_spinner("Counting examples (#{files.size} files)") do
76
+ planner = BatchPlanner.new(files, num_workers: @workers, rspec_options: @options.rspec_options).plan!
77
+ end
78
+
79
+ total = planner.counts.values.sum
80
+ avg = planner.batches.empty? ? 0 : (total.to_f / planner.batches.size).round
81
+ pending_str = planner.pending_count.positive? ? " · #{planner.pending_count} pending" : ""
82
+
83
+ puts " #{Terminal.c("32", "✓")} #{total} examples#{pending_str} · #{files.size} files · " \
84
+ "#{planner.batches.size} batches (~#{avg} each) (#{Terminal.fmt_duration(elapsed)})"
85
+ puts
86
+
87
+ planner
88
+ end
89
+
90
+ def merge_coverage
91
+ puts
92
+ print " Merging coverage reports..."
93
+ $stdout.flush
94
+
95
+ merge_log = Config.coverage_merge_log
96
+ ok = system("RAILS_ENV=test bundle exec rake coverage:merge", out: merge_log, err: [:child, :out])
97
+
98
+ if ok
99
+ puts " #{Terminal.c("32", "✓")} done"
100
+ else
101
+ puts " #{Terminal.c("31", "✗")} failed (run `rake coverage:merge` manually)"
102
+ last_lines = File.exist?(merge_log) ? File.readlines(merge_log).last(10).join.strip : ""
103
+ warn last_lines unless last_lines.empty?
104
+ end
105
+ end
106
+
107
+ # Animated single-line spinner wrapping a block. On a TTY it shows a
108
+ # spinning animation and clears it on completion; on CI it just runs the
109
+ # block. Returns [block_result, elapsed_seconds].
110
+ def with_spinner(label)
111
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
112
+
113
+ unless Config::TTY
114
+ result = yield
115
+
116
+ return [result, (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round]
117
+ end
118
+
119
+ frame = 0
120
+ running = true
121
+ print "\e[?25l"
122
+
123
+ thread = Thread.new do
124
+ while running
125
+ spin = Terminal::SPINNER_FRAMES[frame % Terminal::SPINNER_FRAMES.size]
126
+ print "\r \e[36m#{spin}\e[0m #{label}..."
127
+ $stdout.flush
128
+ sleep 0.1
129
+ frame += 1
130
+ end
131
+ end
132
+
133
+ result = yield
134
+ running = false
135
+ thread.join
136
+ elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round
137
+ print "\r\e[2K"
138
+ print "\e[?25h"
139
+
140
+ [result, elapsed]
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opt-in slow-test profiler, loaded into each worker by Worker.spawn.
4
+ #
5
+ # Enable with RSPEC_PROFILE_SLOW=1 to print, at the end of the run:
6
+ # - Top 20 slowest / query-heaviest examples
7
+ # - TOP 15 FILES BY TIME (parsed back by RSpecTurbo::Display for the report)
8
+ #
9
+ # Optional thresholds:
10
+ # RSPEC_PROFILE_THRESHOLD_TIME=0.2 # seconds; example must exceed to list
11
+ # RSPEC_PROFILE_THRESHOLD_QUERIES=30 # SQL queries; same idea
12
+ #
13
+ # Optional folder grouping:
14
+ # RSPEC_PROFILE_GROUP_BY=1
15
+ # Auto-detects the base from the rspec CLI args. Running `rspec
16
+ # spec/requests/v1` buckets examples by direct subfolder of that base.
17
+ # With multiple paths it uses their longest common directory.
18
+ # RSPEC_PROFILE_GROUP_BY=spec/requests/v1
19
+ # Explicit base path. Buckets by direct subfolder of that base.
20
+ # RSPEC_PROFILE_GROUP_BY=spec/requests/v1/items,spec/requests/v1/bins
21
+ # Explicit list of folders; each is its own bucket.
22
+ #
23
+ # Query counting relies on ActiveSupport::Notifications, so this block only
24
+ # does work in a Rails app and only when explicitly enabled.
25
+
26
+ return unless ENV["RSPEC_PROFILE_SLOW"] || ENV["RSPEC_PROFILE_GROUP_BY"]
27
+
28
+ RSpec.configure do |config|
29
+ slow_tests = []
30
+ file_times = Hash.new(0.0)
31
+ file_query_counts = Hash.new(0)
32
+ folder_times = Hash.new(0.0)
33
+ folder_query_counts = Hash.new(0)
34
+ folder_example_counts = Hash.new(0)
35
+
36
+ threshold_time = Float(ENV["RSPEC_PROFILE_THRESHOLD_TIME"] || "0.2")
37
+ threshold_queries = Integer(ENV["RSPEC_PROFILE_THRESHOLD_QUERIES"] || "30")
38
+
39
+ blank = ->(str) { str.nil? || str.strip.empty? }
40
+
41
+ # Resolve grouping config from env into one of:
42
+ # { mode: :subfolder, base: "spec/requests/v1" } - bucket by direct subfolder
43
+ # { mode: :list, bases: ["spec/foo", "spec/bar"] } - each folder is its own bucket
44
+ resolve_group_by = lambda do |raw|
45
+ next nil if blank.call(raw)
46
+
47
+ raw = raw.delete_suffix("/")
48
+
49
+ if raw.include?(",")
50
+ bases = raw.split(",").map { |p| p.strip.delete_prefix("./").delete_suffix("/") }.reject(&:empty?)
51
+ next nil if bases.empty?
52
+
53
+ next {mode: :list, bases: bases}
54
+ end
55
+
56
+ if ["1", "true", "auto"].include?(raw.downcase)
57
+ cli_paths = RSpec.configuration.files_to_run
58
+ next nil if cli_paths.empty?
59
+
60
+ dirs = cli_paths.map do |path|
61
+ cleaned = path.delete_prefix("./").sub(/:\d+\z/, "")
62
+ File.directory?(cleaned) ? cleaned : File.dirname(cleaned)
63
+ end.uniq
64
+
65
+ base =
66
+ if dirs.size == 1
67
+ dirs.first
68
+ else
69
+ parts = dirs.map { |d| d.split("/") }
70
+ common = []
71
+ parts.first.each_with_index do |segment, i|
72
+ break unless parts.all? { |p| p[i] == segment }
73
+
74
+ common << segment
75
+ end
76
+ common.empty? ? nil : common.join("/")
77
+ end
78
+
79
+ next nil if base.nil?
80
+
81
+ next {mode: :subfolder, base: base}
82
+ end
83
+
84
+ {mode: :subfolder, base: raw}
85
+ end
86
+
87
+ bucket_for = lambda do |file_path, group_config|
88
+ next nil unless group_config
89
+
90
+ normalized = file_path.to_s.delete_prefix("./")
91
+
92
+ case group_config[:mode]
93
+ when :subfolder
94
+ base = group_config[:base]
95
+ next nil unless normalized.start_with?("#{base}/")
96
+
97
+ rest = normalized[(base.length + 1)..]
98
+ first = rest.split("/").first
99
+ first ? "#{base}/#{first}" : nil
100
+ when :list
101
+ group_config[:bases].find { |b| normalized.start_with?("#{b}/") || normalized == b }
102
+ end
103
+ end
104
+
105
+ group_config = nil
106
+
107
+ config.before(:suite) do
108
+ group_config = resolve_group_by.call(ENV["RSPEC_PROFILE_GROUP_BY"])
109
+ end
110
+
111
+ # Query counting needs ActiveSupport; without it (non-Rails suites) we still
112
+ # profile by time and just report zero queries instead of crashing.
113
+ count_sql = defined?(ActiveSupport::Notifications)
114
+
115
+ config.around(:each) do |example|
116
+ query_count = 0
117
+ subscriber =
118
+ if count_sql
119
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload|
120
+ next if payload[:name] == "SCHEMA"
121
+ next if /\A\s*(SAVEPOINT|RELEASE|ROLLBACK|BEGIN|COMMIT)/i.match?(payload[:sql])
122
+
123
+ query_count += 1
124
+ end
125
+ end
126
+
127
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
128
+ example.run
129
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
130
+
131
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
132
+
133
+ file = example.metadata[:file_path]
134
+ file_times[file] += duration
135
+ file_query_counts[file] += query_count
136
+
137
+ if group_config
138
+ folder_key = bucket_for.call(file, group_config)
139
+
140
+ if folder_key
141
+ folder_times[folder_key] += duration
142
+ folder_query_counts[folder_key] += query_count
143
+ folder_example_counts[folder_key] += 1
144
+ end
145
+ end
146
+
147
+ next unless duration >= threshold_time || query_count >= threshold_queries
148
+
149
+ slow_tests << {
150
+ description: example.full_description,
151
+ duration: duration,
152
+ queries: query_count,
153
+ file: file
154
+ }
155
+ end
156
+
157
+ config.after(:suite) do
158
+ puts
159
+ puts
160
+ puts "#{"\e[34m=" * 96}\e[0m"
161
+ puts " \e[34mSLOW / EXPENSIVE EXAMPLES (time >= #{threshold_time}s OR queries >= #{threshold_queries})\e[0m"
162
+ puts "#{"\e[34m=" * 96}\e[0m"
163
+
164
+ puts "TIME QUERIES FILE EXAMPLE"
165
+
166
+ slow_tests.sort_by { |t| -t[:duration] }.first(20).each do |t|
167
+ puts format(
168
+ "%6.3fs %4d %-60s %s",
169
+ t[:duration],
170
+ t[:queries],
171
+ t[:file].to_s.sub(%r{^\./}, "").slice(0, 60),
172
+ t[:description].slice(0, 80)
173
+ )
174
+ end
175
+
176
+ puts
177
+ puts "#{"\e[31m=" * 96}\e[0m"
178
+ puts " \e[31mTOP 15 FILES BY TIME\e[0m"
179
+ puts "#{"\e[31m=" * 96}\e[0m"
180
+
181
+ puts "TIME QUERIES FILE"
182
+
183
+ file_times.sort_by { |_, t| -t }.first(15).each do |file, time|
184
+ puts format("%6.2fs %6d %s", time, file_query_counts[file], file.to_s.sub(%r{^\./}, ""))
185
+ end
186
+
187
+ if group_config && folder_times.size > 1
188
+ label =
189
+ case group_config[:mode]
190
+ when :subfolder then "FOLDERS UNDER #{group_config[:base]} BY TIME"
191
+ when :list then "FOLDERS BY TIME"
192
+ end
193
+
194
+ puts
195
+ puts "#{"\e[33m=" * 96}\e[0m"
196
+ puts " \e[33m#{label}\e[0m"
197
+ puts "#{"\e[33m=" * 96}\e[0m"
198
+
199
+ puts "TIME QUERIES EXAMPLES FOLDER"
200
+
201
+ folder_times.sort_by { |_, t| -t }.each do |folder, time|
202
+ puts format("%6.2fs %6d %4d %s", time, folder_query_counts[folder], folder_example_counts[folder], folder)
203
+ end
204
+ end
205
+
206
+ puts
207
+ end
208
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec_turbo"
4
+
5
+ namespace :spec do
6
+ desc "Run the full RSpec suite in parallel with rspec-turbo"
7
+ task :turbo do
8
+ # Runs the whole suite; the runner spawns its own per-worker rspec
9
+ # processes (so this task needs no :environment) and exits with the
10
+ # suite's status. For specific folders or flags, use `rspec-turbo` directly.
11
+ RSpecTurbo::Runner.new([]).run
12
+ end
13
+ end
14
+
15
+ namespace :coverage do
16
+ # Merge the per-worker SimpleCov result files into one report. Workers run
17
+ # with their own TEST_ENV_NUMBER, so each writes a separate .resultset.json
18
+ # (point them at coverage/$TEST_ENV_NUMBER/ in spec_helper — see the README).
19
+ # Invoked automatically by the runner when COVERAGE=1.
20
+ desc "Merge per-worker SimpleCov results (JSON on CI, HTML locally)"
21
+ task :merge do
22
+ begin
23
+ require "simplecov"
24
+ require "simplecov_json_formatter"
25
+ rescue LoadError => e
26
+ abort "coverage:merge needs `simplecov` and `simplecov_json_formatter` in your Gemfile — #{e.message}"
27
+ end
28
+
29
+ pattern = ENV.fetch("RSPEC_TURBO_COVERAGE_GLOB", "coverage/**/.resultset.json")
30
+ result_files = Dir[pattern]
31
+
32
+ if result_files.empty?
33
+ warn "coverage:merge: no result files matched #{pattern.inspect} — nothing to merge"
34
+ next
35
+ end
36
+
37
+ on_ci = %w[1 true].include?(ENV["CI"].to_s.downcase)
38
+ chosen_formatter = on_ci ? SimpleCov::Formatter::JSONFormatter : SimpleCov::Formatter::HTMLFormatter
39
+
40
+ puts "Merging #{result_files.size} coverage result file(s) → #{chosen_formatter}"
41
+
42
+ SimpleCov.collate(result_files) do
43
+ formatter chosen_formatter
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTurbo
4
+ # Pure presentation helpers shared across the reporting code: duration
5
+ # formatting, optional ANSI colour, spinner frames and rule separators.
6
+ #
7
+ # On CI (no TTY) colour is dropped and box-drawing characters fall back to
8
+ # plain ASCII, which CI log viewers render without mangling.
9
+ module Terminal
10
+ module_function
11
+
12
+ SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
13
+
14
+ SEP_THIN = (Config::TTY ? "─" : "=") * 68
15
+ SEP_THICK = (Config::TTY ? "═" : "=") * 68
16
+
17
+ def fmt_duration(seconds)
18
+ minutes, secs = seconds.divmod(60)
19
+
20
+ minutes.positive? ? format("%dm%02ds", minutes, secs) : format("%ds", secs)
21
+ end
22
+
23
+ # Wraps text in an ANSI escape sequence only when running in a TTY.
24
+ def c(code, text) = Config::TTY ? "\e[#{code}m#{text}\e[0m" : text
25
+
26
+ def strip_ansi(text) = text.gsub(/\e\[[0-9;]*m/, "")
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTurbo
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module RSpecTurbo
6
+ # A single RSpec process running one batch of work. Worker.spawn forks the
7
+ # process and returns a Worker the executor tracks until the process exits.
8
+ #
9
+ # Every worker is wired with two helper files shipped by the gem:
10
+ # * progress_reporter.rb — a formatter that streams the example count to a
11
+ # progress file so the parent can draw a global progress bar.
12
+ # * slow_profile.rb — an opt-in profiler (RSPEC_PROFILE_SLOW=1) that
13
+ # emits the "TOP N FILES BY TIME" block the report aggregates. It is a
14
+ # no-op when profiling is disabled, so requiring it is always safe.
15
+ class Worker
16
+ PROGRESS_REPORTER_PATH = File.expand_path("progress_reporter.rb", __dir__)
17
+ SLOW_PROFILE_PATH = File.expand_path("slow_profile.rb", __dir__)
18
+
19
+ attr_reader :pid, :label, :units, :slot, :started, :progress_file
20
+
21
+ def self.spawn(label:, units:, slot:, rspec_options:)
22
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+ progress_file = Config.progress_path(slot)
24
+ File.write(progress_file, "0")
25
+
26
+ pid = Process.spawn(
27
+ env(slot, progress_file),
28
+ "bundle", "exec", "rspec", "--color", "--order", "random",
29
+ "--require", PROGRESS_REPORTER_PATH,
30
+ "--require", SLOW_PROFILE_PATH,
31
+ "--format", "progress",
32
+ "--format", "RSpecTurbo::ProgressReporter",
33
+ *junit_args(slot),
34
+ *rspec_options, *rspec_args(units),
35
+ out: Config.log_path(label), err: [:child, :out]
36
+ )
37
+
38
+ new(pid: pid, label: label, units: units, slot: slot, started: started, progress_file: progress_file)
39
+ end
40
+
41
+ def self.env(slot, progress_file)
42
+ {
43
+ "TEST_ENV_NUMBER" => slot.to_s,
44
+ "RSPEC_TURBO_PROGRESS_FILE" => progress_file,
45
+ "COVERAGE" => ENV.fetch("COVERAGE", "0")
46
+ }.merge(profile_env)
47
+ end
48
+
49
+ # Profiling is on by default: turbo flips RSPEC_PROFILE_SLOW=1 in the child
50
+ # unless the user set it themselves. RSPEC_TURBO_NO_PROFILE=1 hard-unsets the
51
+ # profiler envs in the child (nil = unset), winning over any inherited value.
52
+ def self.profile_env
53
+ return {"RSPEC_PROFILE_SLOW" => nil, "RSPEC_PROFILE_GROUP_BY" => nil} unless Config.profile?
54
+
55
+ {"RSPEC_PROFILE_SLOW" => ENV.fetch("RSPEC_PROFILE_SLOW", "1")}
56
+ end
57
+
58
+ # A unit is either a file path (String) or a pre-resolved list of example
59
+ # IDs (Array) — the latter is already in `spec/...[1:2]` form.
60
+ def self.rspec_args(units)
61
+ units.flat_map { |unit| unit.is_a?(Array) ? unit : ["spec/#{unit}"] }
62
+ end
63
+
64
+ def self.junit_args(slot)
65
+ dir = Config.junit_dir
66
+ return [] unless dir
67
+
68
+ FileUtils.mkdir_p(dir)
69
+
70
+ ["--require", "rspec_junit_formatter",
71
+ "--format", "RspecJunitFormatter",
72
+ "--out", File.join(dir, "rspec-turbo-#{slot}.xml")]
73
+ end
74
+
75
+ private_class_method :env, :profile_env, :rspec_args, :junit_args
76
+
77
+ def initialize(pid:, label:, units:, slot:, started:, progress_file:)
78
+ @pid = pid
79
+ @label = label
80
+ @units = units
81
+ @slot = slot
82
+ @started = started
83
+ @progress_file = progress_file
84
+ end
85
+
86
+ def duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started).round
87
+ end
88
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rspec_turbo/version"
4
+ require_relative "rspec_turbo/config"
5
+ require_relative "rspec_turbo/terminal"
6
+ require_relative "rspec_turbo/options"
7
+ require_relative "rspec_turbo/db_setup"
8
+ require_relative "rspec_turbo/file_discovery"
9
+ require_relative "rspec_turbo/batch_planner"
10
+ require_relative "rspec_turbo/display"
11
+ require_relative "rspec_turbo/worker"
12
+ require_relative "rspec_turbo/executor"
13
+ require_relative "rspec_turbo/runner"
14
+
15
+ # Parallel RSpec runner with smart, dry-run-based example balancing.
16
+ #
17
+ # progress_reporter.rb and slow_profile.rb are intentionally NOT required here:
18
+ # they run inside the spawned worker processes (loaded by absolute path), not in
19
+ # the orchestrator, so the parent never has to load rspec-core or ActiveSupport.
20
+ module RSpecTurbo
21
+ end
22
+
23
+ # In a Rails app, register the `spec:turbo` Rake task (also reachable as
24
+ # `rails spec:turbo`). Skipped entirely when Rails isn't loaded.
25
+ require_relative "rspec_turbo/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-turbo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - thadeu
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2026-06-12 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: simplecov
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.22'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.22'
68
+ - !ruby/object:Gem::Dependency
69
+ name: simplecov_json_formatter
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.1'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: standard
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.50'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.50'
96
+ description: |
97
+ rspec-turbo runs your RSpec suite across N processes like parallel_tests,
98
+ but balances work by actual example count (from a single --dry-run) using an
99
+ LPT bin-packing heuristic, splitting oversized files across workers. It ships
100
+ a live TTY dashboard, a CI-friendly progress mode, schema-fingerprinted test
101
+ DB setup caching, JUnit output, coverage merging, and a slowest folders/files
102
+ report.
103
+ email:
104
+ - tadeuu@gmail.com
105
+ executables:
106
+ - rspec-turbo
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - CHANGELOG.md
111
+ - LICENSE.txt
112
+ - README.md
113
+ - exe/rspec-turbo
114
+ - lib/rspec-turbo.rb
115
+ - lib/rspec_turbo.rb
116
+ - lib/rspec_turbo/batch_planner.rb
117
+ - lib/rspec_turbo/config.rb
118
+ - lib/rspec_turbo/db_setup.rb
119
+ - lib/rspec_turbo/display.rb
120
+ - lib/rspec_turbo/executor.rb
121
+ - lib/rspec_turbo/file_discovery.rb
122
+ - lib/rspec_turbo/options.rb
123
+ - lib/rspec_turbo/progress_reporter.rb
124
+ - lib/rspec_turbo/railtie.rb
125
+ - lib/rspec_turbo/runner.rb
126
+ - lib/rspec_turbo/slow_profile.rb
127
+ - lib/rspec_turbo/tasks.rake
128
+ - lib/rspec_turbo/terminal.rb
129
+ - lib/rspec_turbo/version.rb
130
+ - lib/rspec_turbo/worker.rb
131
+ homepage: https://github.com/thadeu/rspec-turbo
132
+ licenses:
133
+ - MIT
134
+ metadata:
135
+ source_code_uri: https://github.com/thadeu/rspec-turbo
136
+ changelog_uri: https://github.com/thadeu/rspec-turbo/blob/main/CHANGELOG.md
137
+ rubygems_mfa_required: 'true'
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '3.0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubygems_version: 3.6.2
153
+ specification_version: 4
154
+ summary: Parallel RSpec runner with smart, dry-run-based example balancing.
155
+ test_files: []