rspecq-instructure 0.8.1

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,53 @@
1
+ require "rspec_junit_formatter"
2
+
3
+ module RSpecQ
4
+ module Formatters
5
+ # Junit output formatter that handles outputting of requeued examples,
6
+ # parallel gem, and multiple suites per rspecq run.
7
+ class JUnitFormatter < RSpecJUnitFormatter
8
+ def initialize(queue, job, max_requeues, job_index, path)
9
+ @queue = queue
10
+ @job = job
11
+ @max_requeues = max_requeues
12
+ @requeued_passed_examples = []
13
+ @requeued_failed_examples = []
14
+ path = path.gsub(/{{TEST_ENV_NUMBER}}/, ENV["TEST_ENV_NUMBER"].to_s)
15
+ path = path.gsub(/{{JOB_INDEX}}/, job_index.to_s)
16
+ RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(path))
17
+ output_file = File.new(path, "w")
18
+ super(output_file)
19
+ end
20
+
21
+ def example_passed(notification)
22
+ # if it is a requeued run, store the notification
23
+ if !ENV["ERROR_CONTEXT_BASE_PATH"].nil?
24
+ @requeued_passed_examples << notification.example
25
+ end
26
+ end
27
+
28
+ def example_failed(notification)
29
+ # if it is a requeued run, store the notification
30
+ if !ENV["ERROR_CONTEXT_BASE_PATH"].nil?
31
+ @requeued_failed_examples << notification.example
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def example_count
38
+ @summary_notification.example_count - (@requeued_passed_examples.size + @requeued_failed_examples.size)
39
+ end
40
+
41
+ def failure_count
42
+ @summary_notification.failure_count - @requeued_failed_examples.size
43
+ end
44
+
45
+ def examples
46
+ ignore_examples = @requeued_failed_examples.union(@requeued_passed_examples)
47
+ @examples_notification.notifications.reject do |example_notification|
48
+ ignore_examples.map(&:id).include?(example_notification.example.id)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ module RSpecQ
2
+ module Formatters
3
+ # Updates the respective heartbeat key of the worker after each example.
4
+ #
5
+ # Refer to the documentation of the worker_liveness_sec configuration option for more info.
6
+ class WorkerHeartbeatRecorder
7
+ def initialize(worker)
8
+ @worker = worker
9
+ end
10
+
11
+ def example_finished(*)
12
+ @worker.update_heartbeat
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,252 @@
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_QUEUE_WAIT_TIMEOUT = 30
9
+ DEFAULT_FAIL_FAST = 0
10
+ DEFAULT_WORKER_LIVENESS_SEC = 60
11
+ DEFAULT_REDIS_CONNECT_TIMEOUT = 1.0
12
+ DEFAULT_REDIS_READ_TIMEOUT = 5.0
13
+ DEFAULT_REDIS_WRITE_TIMEOUT = 5.0
14
+ # Comma-separated backoff (seconds) before each reconnect attempt. An empty
15
+ # value disables reconnects. See Configuration for how this is parsed.
16
+ DEFAULT_REDIS_RECONNECT_ATTEMPTS = "0.05,0.1,0.25,0.5,1.0".freeze
17
+
18
+ def self.parse!(args)
19
+ new(args).parse!
20
+ end
21
+
22
+ attr_reader :args, :opts
23
+
24
+ def initialize(args)
25
+ @args = args
26
+ @opts = {}
27
+ end
28
+
29
+ # This method mutates `args` in order to allow both rspecq and
30
+ # rspec options to be passed to rspecq. ["--build", "foo",
31
+ # "--", "--pattern", "bar"] will set `build: "foo"` for rspecq
32
+ # options and leave ["--pattern", "bar"] to be passed to rspec
33
+ def parse!
34
+ parse_args!
35
+ parse_env
36
+
37
+ # rubocop:disable Style/RaiseArgs
38
+ raise OptionParser::MissingArgument.new(:build) if opts[:build].nil?
39
+ raise OptionParser::MissingArgument.new(:worker) if !opts[:report] && opts[:worker].nil?
40
+ # rubocop:enable Style/RaiseArgs
41
+
42
+ opts
43
+ end
44
+
45
+ private
46
+
47
+ def parse_args!
48
+ OptionParser.new do |o|
49
+ name = File.basename($PROGRAM_NAME)
50
+
51
+ o.banner = <<~BANNER
52
+ NAME:
53
+ #{name} - Optimally distribute and run RSpec suites among parallel workers
54
+
55
+ USAGE:
56
+ #{name} [<options>] [spec files or directories]
57
+ BANNER
58
+
59
+ o.separator ""
60
+ o.separator "OPTIONS:"
61
+
62
+ o.on("-b", "--build ID", "A unique identifier for the build. Should be " \
63
+ "common among workers participating in the same build.") do |v|
64
+ opts[:build] = v
65
+ end
66
+
67
+ o.on("-w", "--worker ID", "An identifier for the worker. Workers " \
68
+ "participating in the same build should have distinct IDs.") do |v|
69
+ opts[:worker] = v
70
+ end
71
+
72
+ o.on("--seed SEED", "The RSpec seed. Passing the seed can be helpful in " \
73
+ "many ways i.e reproduction and testing.") do |v|
74
+ opts[:seed] = v
75
+ end
76
+
77
+ o.on("-r", "--redis HOST", "Redis host to connect to " \
78
+ "(default: #{DEFAULT_REDIS_HOST}).") do |v|
79
+ puts "--redis is deprecated. Use --redis-host or --redis-url instead"
80
+ opts[:redis_host] = v
81
+ end
82
+
83
+ o.on("--redis-host HOST", "Redis host to connect to " \
84
+ "(default: #{DEFAULT_REDIS_HOST}).") do |v|
85
+ opts[:redis_host] = v
86
+ end
87
+
88
+ o.on("--redis-url URL", "The URL of the Redis host to connect to " \
89
+ "(e.g.: redis://127.0.0.1:6379/0).") do |v|
90
+ opts[:redis_url] = v
91
+ end
92
+
93
+ o.on("--redis-connect-timeout N", Float, "Seconds to wait when establishing a Redis " \
94
+ "connection (default: #{DEFAULT_REDIS_CONNECT_TIMEOUT}).") do |v|
95
+ opts[:redis_connect_timeout] = v
96
+ end
97
+
98
+ o.on("--redis-read-timeout N", Float, "Seconds to wait for a Redis read " \
99
+ "(default: #{DEFAULT_REDIS_READ_TIMEOUT}).") do |v|
100
+ opts[:redis_read_timeout] = v
101
+ end
102
+
103
+ o.on("--redis-write-timeout N", Float, "Seconds to wait for a Redis write " \
104
+ "(default: #{DEFAULT_REDIS_WRITE_TIMEOUT}).") do |v|
105
+ opts[:redis_write_timeout] = v
106
+ end
107
+
108
+ o.on("--redis-reconnect-attempts LIST", "Comma-separated backoff (seconds) " \
109
+ "before each Redis reconnect attempt, e.g. " \
110
+ "\"#{DEFAULT_REDIS_RECONNECT_ATTEMPTS}\". " \
111
+ "Empty disables reconnects.") do |v|
112
+ opts[:redis_reconnect_attempts] = v
113
+ end
114
+
115
+ o.on("--update-timings", "Update the global job timings key with the " \
116
+ "timings of this build. Note: This key is used as the basis for job " \
117
+ "scheduling.") do |v|
118
+ opts[:timings] = v
119
+ end
120
+
121
+ o.on("--timings-key KEY", "Promote timings to KEY instead of the default " \
122
+ "global timings key (requires --update-timings).") do |v|
123
+ opts[:timings_key] = v
124
+ end
125
+
126
+ o.on("--file-split-threshold N", Integer, "Split spec files slower than N " \
127
+ "seconds and schedule them as individual examples.") do |v|
128
+ opts[:file_split_threshold] = v
129
+ end
130
+
131
+ o.on("--chunk-target-duration N", Integer, "Target duration in seconds for " \
132
+ "time-balanced example chunks when splitting slow files " \
133
+ "(default: 30). Groups examples into chunks of approximately " \
134
+ "this duration to reduce Kernel.load overhead.") do |v|
135
+ opts[:chunk_target_duration] = v
136
+ end
137
+
138
+ o.on("--report", "Enable reporter mode: do not pull tests off the queue; " \
139
+ "instead print build progress and exit when it's " \
140
+ "finished.\n#{o.summary_indent * 9} " \
141
+ "Exits with a non-zero status code if there were any " \
142
+ "failures.") do |v|
143
+ opts[:report] = v
144
+ end
145
+
146
+ o.on("--report-timeout N", Integer, "Fail if build is not finished after " \
147
+ "N seconds. Only applicable if --report is enabled " \
148
+ "(default: #{DEFAULT_REPORT_TIMEOUT}).") do |v|
149
+ opts[:report_timeout] = v
150
+ end
151
+
152
+ o.on("--max-requeues N", Integer, "Retry failed examples up to N times " \
153
+ "before considering them legit failures " \
154
+ "(default: #{DEFAULT_MAX_REQUEUES}).") do |v|
155
+ opts[:max_requeues] = v
156
+ end
157
+
158
+ o.on("--queue-wait-timeout N", Integer, "Time to wait for a queue to be " \
159
+ "ready before considering it failed " \
160
+ "(default: #{DEFAULT_QUEUE_WAIT_TIMEOUT}).") do |v|
161
+ opts[:queue_wait_timeout] = v
162
+ end
163
+
164
+ o.on("--fail-fast N", Integer, "Abort build with a non-zero status code " \
165
+ "after N failed examples.") do |v|
166
+ opts[:fail_fast] = v
167
+ end
168
+
169
+ o.on("--reproduction", "Enable reproduction mode: run rspec on the given files " \
170
+ "and examples in the exact order they are given. Incompatible with " \
171
+ "--timings.") do |v|
172
+ opts[:reproduction] = v
173
+ end
174
+
175
+ o.on("--tag TAG", "Run examples with the specified tag, or exclude examples " \
176
+ "by prefixing the tag with ~ (e.g. ~slow). Repeatable. " \
177
+ "TAG is passed through to rspec.") do |tag|
178
+ (opts[:tags] ||= []) << tag
179
+ end
180
+
181
+ o.on("--junit-output filepath", String, "Output junit formatted xml " \
182
+ "for CI suites to the defined file path. Substitution parameters " \
183
+ "{{TEST_ENV_NUMBER}} - parallel gem proc number. " \
184
+ "{{JOB_INDEX}} - increments with each suite that is run.") do |v|
185
+ opts[:junit_output] = v
186
+ end
187
+
188
+ o.on("--include-pattern PATTERN", "Run tests matching the regex") do |v|
189
+ opts[:include_pattern] = /#{v}/
190
+ end
191
+
192
+ o.on("--exclude-pattern PATTERN", "Regex of tests to exclude from run.") do |v|
193
+ opts[:exclude_pattern] = /#{v}/
194
+ end
195
+
196
+ o.on("--worker-liveness-sec N", Integer, "Seconds before a worker is considered dead " \
197
+ "(default: #{DEFAULT_WORKER_LIVENESS_SEC})") do |v|
198
+ opts[:worker_liveness_sec] = v
199
+ end
200
+
201
+ o.on_tail("-h", "--help", "Show this message.") do
202
+ puts o
203
+ exit
204
+ end
205
+
206
+ o.on_tail("-v", "--version", "Print the version and exit.") do
207
+ puts "#{name} #{RSpecQ::VERSION}"
208
+ exit
209
+ end
210
+ end.parse!(args)
211
+ end
212
+
213
+ def parse_env
214
+ opts[:build] ||= ENV.fetch("RSPECQ_BUILD", nil)
215
+ opts[:worker] ||= ENV.fetch("RSPECQ_WORKER", nil)
216
+ opts[:seed] ||= ENV.fetch("RSPECQ_SEED", nil)
217
+ opts[:redis_host] ||= ENV["RSPECQ_REDIS"] || DEFAULT_REDIS_HOST
218
+ opts[:timings] = opts.fetch(:timings, env_set?("RSPECQ_UPDATE_TIMINGS"))
219
+ opts[:timings_key] ||= ENV.fetch("RSPECQ_TIMINGS_KEY", nil)
220
+ opts[:file_split_threshold] ||= Integer(ENV["RSPECQ_FILE_SPLIT_THRESHOLD"] || 9_999_999)
221
+ opts[:report] = opts.fetch(:report, env_set?("RSPECQ_REPORT"))
222
+ opts[:report_timeout] ||= Integer(ENV["RSPECQ_REPORT_TIMEOUT"] || DEFAULT_REPORT_TIMEOUT)
223
+ opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEUES)
224
+ opts[:queue_wait_timeout] ||= Integer(ENV["RSPECQ_QUEUE_WAIT_TIMEOUT"] || DEFAULT_QUEUE_WAIT_TIMEOUT)
225
+ opts[:redis_url] ||= ENV.fetch("RSPECQ_REDIS_URL", nil)
226
+ opts[:redis_connect_timeout] ||= env_float("RSPECQ_REDIS_CONNECT_TIMEOUT", DEFAULT_REDIS_CONNECT_TIMEOUT)
227
+ opts[:redis_read_timeout] ||= env_float("RSPECQ_REDIS_READ_TIMEOUT", DEFAULT_REDIS_READ_TIMEOUT)
228
+ opts[:redis_write_timeout] ||= env_float("RSPECQ_REDIS_WRITE_TIMEOUT", DEFAULT_REDIS_WRITE_TIMEOUT)
229
+ opts[:redis_reconnect_attempts] ||= ENV["RSPECQ_REDIS_RECONNECT_ATTEMPTS"] || DEFAULT_REDIS_RECONNECT_ATTEMPTS
230
+ opts[:fail_fast] ||= Integer(ENV["RSPECQ_FAIL_FAST"] || DEFAULT_FAIL_FAST)
231
+ opts[:reproduction] ||= env_set?("RSPECQ_REPRODUCTION")
232
+ opts[:tags] ||= []
233
+ opts[:junit_output] ||= ENV.fetch("RSPECQ_JUNIT_OUTPUT", nil)
234
+ opts[:include_pattern] ||= ENV.fetch("INCLUDE_PATTERN", nil)
235
+ opts[:exclude_pattern] ||= ENV.fetch("EXCLUDE_PATTERN", nil)
236
+ opts[:worker_liveness_sec] ||= Integer(ENV["RSPECQ_WORKER_LIVENESS_SEC"] || DEFAULT_WORKER_LIVENESS_SEC)
237
+ opts[:chunk_target_duration] ||= Integer(ENV["RSPECQ_CHUNK_TARGET_DURATION"] || 30)
238
+ end
239
+
240
+ def env_set?(var)
241
+ ["1", "true"].include?(ENV.fetch(var, nil))
242
+ end
243
+
244
+ # A Float from `var`, treating a blank value (unset or empty/whitespace) as
245
+ # absent and falling back to `default`. Jenkins string parameters commonly
246
+ # default to "", which would otherwise crash Float("").
247
+ def env_float(var, default)
248
+ value = ENV.fetch(var, nil)
249
+ value.nil? || value.strip.empty? ? default : Float(value)
250
+ end
251
+ end
252
+ end