Skip to content

Commit 6cbc1a0

Browse files
committed
Pass through RSpec arguments except files
1 parent 5038ca3 commit 6cbc1a0

File tree

5 files changed

+180
-2
lines changed

5 files changed

+180
-2
lines changed

bin/rspecq

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ else
124124
redis_opts[:host] = opts[:redis_host]
125125
end
126126

127+
# Use the RSpec parser to parse any command line args intended for rspec such
128+
# as `-- --format JUnit -o foo.xml` so that we can pass these args to rspec
129+
# while removing the files_or_dirs_to_run since we want to pull those from the
130+
# queue. OptionParser above mutates ARGV, so only options after `--` or
131+
# non-flag arguments (such as files) will make it to this point.
132+
files_or_dirs_to_run = RSpec::Core::Parser.new(ARGV).parse[:files_or_directories_to_run]
133+
l = files_or_dirs_to_run.length
134+
if l.zero?
135+
opts[:rspec_args] = ARGV
136+
else
137+
opts[:rspec_args] = ARGV[0...-l]
138+
opts[:files_or_dirs_to_run] = files_or_dirs_to_run
139+
end
140+
127141
if opts[:report]
128142
reporter = RSpecQ::Reporter.new(
129143
build_id: opts[:build],
@@ -139,7 +153,8 @@ else
139153
redis_opts: redis_opts
140154
)
141155

142-
worker.files_or_dirs_to_run = ARGV[0] if ARGV[0]
156+
worker.rspec_args = opts[:rspec_args]
157+
worker.files_or_dirs_to_run = opts[:files_or_dirs_to_run] if opts[:files_or_dirs_to_run]
143158
worker.populate_timings = opts[:timings]
144159
worker.file_split_threshold = opts[:file_split_threshold]
145160
worker.max_requeues = opts[:max_requeues]

lib/rspecq/parser.rb

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
require "optparse"
2+
3+
module RSpecQ
4+
class Parser
5+
DEFAULT_REDIS_HOST = "127.0.0.1".freeze
6+
DEFAULT_REPORT_TIMEOUT = 3600 # 1 hour
7+
DEFAULT_MAX_REQUEUES = 3
8+
DEFAULT_FAIL_FAST = 0
9+
10+
def self.parse!(args)
11+
new(args).parse!
12+
end
13+
14+
attr_reader :args, :opts
15+
16+
def initialize(args)
17+
@args = args
18+
@opts = {}
19+
end
20+
21+
# This method mutates `args` in order to allow both rspecq and
22+
# rspec options to be passed to rspecq. ["--build", "foo",
23+
# "--", "--pattern", "bar"] will set `build: "foo"` for rspecq
24+
# options and leave ["--pattern", "bar"] to be passed to rspec
25+
def parse!
26+
parse_args!
27+
parse_env
28+
29+
# rubocop:disable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause
30+
raise OptionParser::MissingArgument.new(:build) if opts[:build].nil?
31+
raise OptionParser::MissingArgument.new(:worker) if !opts[:report] && opts[:worker].nil?
32+
# rubocop:enable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause
33+
34+
opts
35+
end
36+
37+
private
38+
39+
def parse_args!
40+
OptionParser.new do |o|
41+
name = File.basename($PROGRAM_NAME)
42+
43+
o.banner = <<~BANNER
44+
NAME:
45+
#{name} - Optimally distribute and run RSpec suites among parallel workers
46+
47+
USAGE:
48+
#{name} [<options>] [spec files or directories]
49+
#{name} [<options>] -- [<rspec options>] [spec files or directories]
50+
BANNER
51+
52+
o.separator ""
53+
o.separator "OPTIONS:"
54+
55+
o.on("-b", "--build ID", "A unique identifier for the build. Should be " \
56+
"common among workers participating in the same build.") do |v|
57+
opts[:build] = v
58+
end
59+
60+
o.on("-w", "--worker ID", "An identifier for the worker. Workers " \
61+
"participating in the same build should have distinct IDs.") do |v|
62+
opts[:worker] = v
63+
end
64+
65+
o.on("-r", "--redis HOST", "Redis host to connect to " \
66+
"(default: #{DEFAULT_REDIS_HOST}).") do |v|
67+
puts "--redis is deprecated. Use --redis-host or --redis-url instead"
68+
opts[:redis_host] = v
69+
end
70+
71+
o.on("--redis-host HOST", "Redis host to connect to " \
72+
"(default: #{DEFAULT_REDIS_HOST}).") do |v|
73+
opts[:redis_host] = v
74+
end
75+
76+
o.on("--redis-url URL", "The URL of the Redis host to connect to " \
77+
"(e.g.: redis://127.0.0.1:6379/0).") do |v|
78+
opts[:redis_url] = v
79+
end
80+
81+
o.on("--update-timings", "Update the global job timings key with the " \
82+
"timings of this build. Note: This key is used as the basis for job " \
83+
"scheduling.") do |v|
84+
opts[:timings] = v
85+
end
86+
87+
o.on("--file-split-threshold N", Integer, "Split spec files slower than N " \
88+
"seconds and schedule them as individual examples.") do |v|
89+
opts[:file_split_threshold] = v
90+
end
91+
92+
o.on("--report", "Enable reporter mode: do not pull tests off the queue; " \
93+
"instead print build progress and exit when it's " \
94+
"finished.\n#{o.summary_indent * 9} " \
95+
"Exits with a non-zero status code if there were any " \
96+
"failures.") do |v|
97+
opts[:report] = v
98+
end
99+
100+
o.on("--report-timeout N", Integer, "Fail if build is not finished after " \
101+
"N seconds. Only applicable if --report is enabled " \
102+
"(default: #{DEFAULT_REPORT_TIMEOUT}).") do |v|
103+
opts[:report_timeout] = v
104+
end
105+
106+
o.on("--max-requeues N", Integer, "Retry failed examples up to N times " \
107+
"before considering them legit failures " \
108+
"(default: #{DEFAULT_MAX_REQUEUES}).") do |v|
109+
opts[:max_requeues] = v
110+
end
111+
112+
o.on("--fail-fast N", Integer, "Abort build with a non-zero status code " \
113+
"after N failed examples.") do |v|
114+
opts[:fail_fast] = v
115+
end
116+
117+
o.on_tail("-h", "--help", "Show this message.") do
118+
puts o
119+
exit
120+
end
121+
122+
o.on_tail("-v", "--version", "Print the version and exit.") do
123+
puts "#{name} #{RSpecQ::VERSION}"
124+
exit
125+
end
126+
end.parse!(args)
127+
end
128+
129+
def parse_env
130+
opts[:report] = opts.fetch(:report, env_set?("RSPECQ_REPORT"))
131+
opts[:timings] = opts.fetch(:timings, env_set?("RSPECQ_UPDATE_TIMINGS"))
132+
133+
opts[:build] ||= ENV["RSPECQ_BUILD"]
134+
opts[:worker] ||= ENV["RSPECQ_WORKER"]
135+
opts[:redis_host] ||= ENV["RSPECQ_REDIS"] || DEFAULT_REDIS_HOST
136+
opts[:file_split_threshold] ||= Integer(ENV["RSPECQ_FILE_SPLIT_THRESHOLD"] || 9_999_999)
137+
opts[:report_timeout] ||= Integer(ENV["RSPECQ_REPORT_TIMEOUT"] || DEFAULT_REPORT_TIMEOUT)
138+
opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEUES)
139+
opts[:redis_url] ||= ENV["RSPECQ_REDIS_URL"]
140+
opts[:fail_fast] ||= Integer(ENV["RSPECQ_FAIL_FAST"] || DEFAULT_FAIL_FAST)
141+
end
142+
143+
def env_set?(var)
144+
["1", "true"].include?(ENV[var])
145+
end
146+
end
147+
end

lib/rspecq/worker.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class Worker
4646
# Defaults to 0
4747
attr_accessor :fail_fast
4848

49+
# Optional arguments to pass along to rspec.
50+
#
51+
# Defaults to nil
52+
attr_accessor :rspec_args
53+
4954
attr_reader :queue
5055

5156
def initialize(build_id:, worker_id:, redis_opts:)
@@ -107,7 +112,8 @@ def work
107112
RSpec.configuration.add_formatter(Formatters::JobTimingRecorder.new(queue, job))
108113
end
109114

110-
opts = RSpec::Core::ConfigurationOptions.new(["--format", "progress", job])
115+
args = [*rspec_args, "--format", "progress", job]
116+
opts = RSpec::Core::ConfigurationOptions.new(args)
111117
_result = RSpec::Core::Runner.new(opts).run($stderr, $stdout)
112118

113119
queue.acknowledge_job(job)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RSpec.describe do
2+
it("foo", :foo) { expect(true).to be true }
3+
it("bar", :bar) { expect(true).to be true }
4+
end

test/test_e2e.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,10 @@ def test_suite_with_failures_and_fail_fast
129129

130130
assert_includes [2, 3], queue.processed_jobs.length
131131
end
132+
133+
def test_suite_with_rspec_arguments
134+
queue = exec_build("tagged_suite", "-- --tag foo")
135+
136+
assert_equal 1, queue.example_count
137+
end
132138
end

0 commit comments

Comments
 (0)