specwrk 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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +5 -0
- data/README.md +10 -0
- data/Rakefile +10 -0
- data/exe/specwrk +17 -0
- data/lib/specwrk/cli.rb +232 -0
- data/lib/specwrk/cli_reporter.rb +78 -0
- data/lib/specwrk/client.rb +150 -0
- data/lib/specwrk/hookable.rb +50 -0
- data/lib/specwrk/list_examples.rb +53 -0
- data/lib/specwrk/queue.rb +204 -0
- data/lib/specwrk/version.rb +5 -0
- data/lib/specwrk/web/app.rb +77 -0
- data/lib/specwrk/web/auth.rb +31 -0
- data/lib/specwrk/web/endpoints.rb +156 -0
- data/lib/specwrk/web.rb +15 -0
- data/lib/specwrk/worker/completion_formatter.rb +30 -0
- data/lib/specwrk/worker/executor.rb +82 -0
- data/lib/specwrk/worker/null_formatter.rb +18 -0
- data/lib/specwrk/worker/progress_formatter.rb +45 -0
- data/lib/specwrk/worker.rb +86 -0
- data/lib/specwrk.rb +21 -0
- metadata +179 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Specwrk
|
7
|
+
# Thread-safe Hash access
|
8
|
+
class Queue
|
9
|
+
def initialize(hash = {})
|
10
|
+
if block_given?
|
11
|
+
@mutex = Monitor.new # Reentrant locking is required here
|
12
|
+
# It's possible to enter the proc from two threads, so we need to ||= in case
|
13
|
+
# one thread has set a value prior to the yield.
|
14
|
+
hash.default_proc = proc { |h, key| @mutex.synchronize { yield(h, key) } }
|
15
|
+
end
|
16
|
+
|
17
|
+
@mutex ||= Mutex.new # Monitor is up-to 20% slower than Mutex, so if no block is given, use a mutex
|
18
|
+
@hash = hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def synchronize(&blk)
|
22
|
+
@mutex.synchronize { yield(@hash) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(name, *args, &block)
|
26
|
+
if @hash.respond_to?(name)
|
27
|
+
@mutex.synchronize { @hash.public_send(name, *args, &block) }
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to_missing?(name, include_private = false)
|
34
|
+
@hash.respond_to?(name, include_private) || super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class PendingQueue < Queue
|
39
|
+
attr_reader :previous_run_times
|
40
|
+
|
41
|
+
def shift_bucket
|
42
|
+
return bucket_by_file unless previous_run_times
|
43
|
+
|
44
|
+
case ENV["SPECWRK_SRV_GROUP_BY"]
|
45
|
+
when "file"
|
46
|
+
bucket_by_file
|
47
|
+
else
|
48
|
+
bucket_by_timings
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def run_time_bucket_threshold
|
53
|
+
return 1 unless previous_run_times
|
54
|
+
|
55
|
+
previous_run_times.dig(:meta, :average_run_time)
|
56
|
+
end
|
57
|
+
|
58
|
+
# TODO: move reading the file to the getter method
|
59
|
+
def previous_run_times_file=(path)
|
60
|
+
return unless path
|
61
|
+
return unless File.exist? path
|
62
|
+
|
63
|
+
File.open(path, "r") do |file|
|
64
|
+
file.flock(File::LOCK_EX)
|
65
|
+
|
66
|
+
@previous_run_times = JSON.parse(file.read, symbolize_names: true)
|
67
|
+
|
68
|
+
file.flock(File::LOCK_UN)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def merge_with_previous_run_times!(h2)
|
73
|
+
@mutex.synchronize do
|
74
|
+
h2.each { |_id, example| merge_example(example) }
|
75
|
+
|
76
|
+
# Sort by exepcted run time, slowest to fastest
|
77
|
+
@hash = @hash.sort_by { |_, example| example[:expected_run_time] }.reverse.to_h
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Take elements from the hash where the file_path is the same
|
84
|
+
def bucket_by_file
|
85
|
+
bucket = []
|
86
|
+
|
87
|
+
@mutex.synchronize do
|
88
|
+
key = @hash.keys.first
|
89
|
+
break if key.nil?
|
90
|
+
|
91
|
+
file_path = @hash[key][:file_path]
|
92
|
+
@hash.each do |id, example|
|
93
|
+
next unless example[:file_path] == file_path
|
94
|
+
|
95
|
+
bucket << example
|
96
|
+
@hash.delete id
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
bucket
|
101
|
+
end
|
102
|
+
|
103
|
+
# Take elements from the hash until the average runtime bucket has filled
|
104
|
+
def bucket_by_timings
|
105
|
+
bucket = []
|
106
|
+
|
107
|
+
@mutex.synchronize do
|
108
|
+
estimated_run_time_total = 0
|
109
|
+
|
110
|
+
while estimated_run_time_total < run_time_bucket_threshold
|
111
|
+
key = @hash.keys.first
|
112
|
+
break if key.nil?
|
113
|
+
|
114
|
+
estimated_run_time_total += @hash.dig(key, :expected_run_time)
|
115
|
+
break if estimated_run_time_total > run_time_bucket_threshold && bucket.length.positive?
|
116
|
+
|
117
|
+
bucket << @hash[key]
|
118
|
+
@hash.delete key
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
bucket
|
123
|
+
end
|
124
|
+
|
125
|
+
# Ensure @mutex is held when calling this method
|
126
|
+
def merge_example(example)
|
127
|
+
return if @hash.key? example[:id]
|
128
|
+
return if @hash.key? example[:file_path]
|
129
|
+
|
130
|
+
@hash[example[:id]] = if previous_run_times
|
131
|
+
example.merge!(
|
132
|
+
expected_run_time: previous_run_times.dig(:examples, example[:id].to_sym, :run_time) || 99999.9 # run "unknown" files first
|
133
|
+
)
|
134
|
+
else
|
135
|
+
example.merge!(
|
136
|
+
expected_run_time: 99999.9 # run "unknown" files first
|
137
|
+
)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class CompletedQueue < Queue
|
143
|
+
def dump_and_write(path)
|
144
|
+
write_output_to(path, dump)
|
145
|
+
end
|
146
|
+
|
147
|
+
def dump
|
148
|
+
@mutex.synchronize do
|
149
|
+
@run_times = []
|
150
|
+
@first_started_at = Time.new(2999, 1, 1, 0, 0, 0) # TODO: Make future proof /s
|
151
|
+
@last_finished_at = Time.new(1900, 1, 1, 0, 0, 0)
|
152
|
+
|
153
|
+
@output = {
|
154
|
+
file_totals: Hash.new { |h, filename| h[filename] = 0.0 },
|
155
|
+
meta: {failures: 0, passes: 0, pending: 0},
|
156
|
+
examples: {}
|
157
|
+
}
|
158
|
+
|
159
|
+
@hash.values.each { |example| calculate(example) }
|
160
|
+
|
161
|
+
@output[:meta][:total_run_time] = @run_times.sum
|
162
|
+
@output[:meta][:average_run_time] = @output[:meta][:total_run_time] / @run_times.length.to_f
|
163
|
+
@output[:meta][:first_started_at] = @first_started_at.iso8601(6)
|
164
|
+
@output[:meta][:last_finished_at] = @last_finished_at.iso8601(6)
|
165
|
+
|
166
|
+
@output
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def calculate(example)
|
173
|
+
@run_times << example[:run_time]
|
174
|
+
@output[:file_totals][example[:file_path]] += example[:run_time]
|
175
|
+
|
176
|
+
started_at = Time.parse(example[:started_at])
|
177
|
+
finished_at = Time.parse(example[:finished_at])
|
178
|
+
|
179
|
+
@first_started_at = started_at if started_at < @first_started_at
|
180
|
+
@last_finished_at = finished_at if finished_at > @last_finished_at
|
181
|
+
|
182
|
+
case example[:status]
|
183
|
+
when "passed"
|
184
|
+
@output[:meta][:passes] += 1
|
185
|
+
when "failed"
|
186
|
+
@output[:meta][:failures] += 1
|
187
|
+
when "pending"
|
188
|
+
@output[:meta][:pending] += 1
|
189
|
+
end
|
190
|
+
|
191
|
+
@output[:examples][example[:id]] = example
|
192
|
+
end
|
193
|
+
|
194
|
+
def write_output_to(path, output)
|
195
|
+
File.open(path, "w") do |file|
|
196
|
+
file.flock(File::LOCK_EX)
|
197
|
+
|
198
|
+
file.write JSON.pretty_generate(output)
|
199
|
+
|
200
|
+
file.flock(File::LOCK_UN)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webrick"
|
4
|
+
require "rack"
|
5
|
+
|
6
|
+
# rack v3 or v2
|
7
|
+
begin
|
8
|
+
require "rackup/handler/webrick"
|
9
|
+
rescue LoadError
|
10
|
+
require "rack/handler/webrick"
|
11
|
+
end
|
12
|
+
|
13
|
+
require "specwrk/web/auth"
|
14
|
+
require "specwrk/web/endpoints"
|
15
|
+
|
16
|
+
module Specwrk
|
17
|
+
class Web
|
18
|
+
class App
|
19
|
+
def self.run!
|
20
|
+
Process.setproctitle "specwrk-server"
|
21
|
+
|
22
|
+
server_opts = {
|
23
|
+
Port: ENV.fetch("SPECWRK_SRV_PORT", "5138").to_i,
|
24
|
+
Logger: WEBrick::Log.new($stdout, WEBrick::Log::FATAL),
|
25
|
+
AccessLog: [],
|
26
|
+
KeepAliveTimeout: 300
|
27
|
+
}
|
28
|
+
|
29
|
+
# rack v3 or v2
|
30
|
+
handler_klass = defined?(Rackup::Handler) ? Rackup::Handler::WEBrick : Rack::Handler.get("webrick")
|
31
|
+
|
32
|
+
handler_klass.run(rackup, **server_opts) do |server|
|
33
|
+
["INT", "TERM"].each do |sig|
|
34
|
+
trap(sig) do
|
35
|
+
puts "\n→ Shutting down gracefully..." unless ENV["SPECWRK_FORKED"]
|
36
|
+
server.shutdown
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(env)
|
43
|
+
request = Rack::Request.new(env)
|
44
|
+
|
45
|
+
route(method: request.request_method, path: request.path_info)
|
46
|
+
.new(request)
|
47
|
+
.response
|
48
|
+
end
|
49
|
+
|
50
|
+
private_class_method def self.rackup
|
51
|
+
Rack::Builder.new do
|
52
|
+
use Specwrk::Web::Auth # global auth check
|
53
|
+
run Specwrk::Web::App.new # your router
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def route(method:, path:)
|
58
|
+
case [method, path]
|
59
|
+
when ["GET", "/heartbeat"]
|
60
|
+
Endpoints::Heartbeat
|
61
|
+
when ["POST", "/pop"]
|
62
|
+
Endpoints::Pop
|
63
|
+
when ["POST", "/complete"]
|
64
|
+
Endpoints::Complete
|
65
|
+
when ["POST", "/seed"]
|
66
|
+
Endpoints::Seed
|
67
|
+
when ["GET", "/stats"]
|
68
|
+
Endpoints::Stats
|
69
|
+
when ["DELETE", "/shutdown"]
|
70
|
+
Endpoints::Shutdown
|
71
|
+
else
|
72
|
+
Endpoints::NotFound
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/auth/abstract/request"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
class Auth
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
return @app.call(env) if [nil, ""].include? ENV["SPECWRK_SRV_KEY"]
|
14
|
+
|
15
|
+
auth = Rack::Auth::AbstractRequest.new(env)
|
16
|
+
|
17
|
+
return unauthorized unless auth.provided?
|
18
|
+
return unauthorized unless auth.scheme == "bearer"
|
19
|
+
return unauthorized unless Rack::Utils.secure_compare(auth.params, ENV["SPECWRK_SRV_KEY"])
|
20
|
+
|
21
|
+
@app.call(env)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def unauthorized
|
27
|
+
[401, {"Content-Type" => "application/json"}, ["Unauthorized"]]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
module Endpoints
|
8
|
+
class Base
|
9
|
+
def initialize(request)
|
10
|
+
@request = request
|
11
|
+
end
|
12
|
+
|
13
|
+
def response
|
14
|
+
not_found
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :request
|
20
|
+
|
21
|
+
def not_found
|
22
|
+
[404, {"Content-Type" => "text/plain"}, ["This is not the path you're looking for, 'ol chap..."]]
|
23
|
+
end
|
24
|
+
|
25
|
+
def ok
|
26
|
+
[200, {"Content-Type" => "text/plain"}, ["OK, 'ol chap"]]
|
27
|
+
end
|
28
|
+
|
29
|
+
def payload
|
30
|
+
@payload ||= JSON.parse(body, symbolize_names: true)
|
31
|
+
end
|
32
|
+
|
33
|
+
def body
|
34
|
+
@body ||= request.body.read
|
35
|
+
end
|
36
|
+
|
37
|
+
def pending_queue
|
38
|
+
Web::PENDING_QUEUES[request.get_header("HTTP_X_SPECWRK_RUN")]
|
39
|
+
end
|
40
|
+
|
41
|
+
def processing_queue
|
42
|
+
Web::PROCESSING_QUEUES[request.get_header("HTTP_X_SPECWRK_RUN")]
|
43
|
+
end
|
44
|
+
|
45
|
+
def completed_queue
|
46
|
+
Web::COMPLETED_QUEUES[request.get_header("HTTP_X_SPECWRK_RUN")]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Base default response is 404
|
51
|
+
NotFound = Class.new(Base)
|
52
|
+
|
53
|
+
class Heartbeat < Base
|
54
|
+
def response
|
55
|
+
ok
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Seed < Base
|
60
|
+
def response
|
61
|
+
examples = payload.map { |hash| [hash[:id], hash] }.to_h
|
62
|
+
pending_queue.merge_with_previous_run_times!(examples)
|
63
|
+
|
64
|
+
ok
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Complete < Base
|
69
|
+
def response
|
70
|
+
processing_queue.synchronize do |processing_queue_hash|
|
71
|
+
payload.each do |example|
|
72
|
+
processing_queue_hash.delete(example[:id])
|
73
|
+
completed_queue[example[:id]] = example
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
if pending_queue.length.zero? && processing_queue.length.zero? && ENV["SPECWRK_SRV_OUTPUT"]
|
78
|
+
completed_queue.dump_and_write(ENV["SPECWRK_SRV_OUTPUT"])
|
79
|
+
end
|
80
|
+
|
81
|
+
ok
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Pop < Base
|
86
|
+
def response
|
87
|
+
processing_queue.synchronize do |processing_queue_hash|
|
88
|
+
@examples = pending_queue.shift_bucket
|
89
|
+
|
90
|
+
@examples.each do |example|
|
91
|
+
processing_queue_hash[example[:id]] = example
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if @examples.length.positive?
|
96
|
+
[200, {"Content-Type" => "application/json"}, [JSON.generate(@examples)]]
|
97
|
+
elsif processing_queue.length.zero?
|
98
|
+
[410, {"Content-Type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
99
|
+
else
|
100
|
+
not_found
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Stats < Base
|
106
|
+
def response
|
107
|
+
data = {
|
108
|
+
pending: {count: pending_queue.length},
|
109
|
+
processing: {count: processing_queue.length},
|
110
|
+
completed: completed_queue.dump
|
111
|
+
}
|
112
|
+
|
113
|
+
if data.dig(:completed, :examples).length.positive?
|
114
|
+
[200, {"Content-Type" => "application/json"}, [JSON.generate(data)]]
|
115
|
+
else
|
116
|
+
not_found
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Shutdown < Base
|
122
|
+
def response
|
123
|
+
if ENV["SPECWRK_SRV_SINGLE_RUN"]
|
124
|
+
interupt!
|
125
|
+
elsif processing_queue.length.positive?
|
126
|
+
# Push any processing jobs back into the pending queue
|
127
|
+
processing_queue.synchronize do |processing_queue_hash|
|
128
|
+
pending_queue.synchronize do |pending_queue_hash|
|
129
|
+
processing_queue_hash.each do |id, example|
|
130
|
+
pending_queue_hash[id] = example
|
131
|
+
processing_queue_hash.delete(id)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
elsif processing_queue.length.zero? && pending_queue.length.zero?
|
137
|
+
# All done, we can clear the completed queue
|
138
|
+
completed_queue.clear
|
139
|
+
end
|
140
|
+
|
141
|
+
# TODO: clear any zombie queues
|
142
|
+
|
143
|
+
[200, {"Content-Type" => "text/plain"}, ["✌️"]]
|
144
|
+
end
|
145
|
+
|
146
|
+
def interupt!
|
147
|
+
Thread.new do
|
148
|
+
# give the socket a moment to flush the response
|
149
|
+
sleep 0.2
|
150
|
+
Process.kill("INT", Process.pid)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/specwrk/web.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "specwrk/queue"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
PENDING_QUEUES = Queue.new { |h, key| h[key] = PendingQueue.new.tap { |q| q.previous_run_times_file = ENV["SPECWRK_SRV_OUTPUT"] } }
|
8
|
+
PROCESSING_QUEUES = Queue.new { |h, key| h[key] = Queue.new }
|
9
|
+
COMPLETED_QUEUES = Queue.new { |h, key| h[key] = CompletedQueue.new }
|
10
|
+
|
11
|
+
def self.clear_queues
|
12
|
+
[PENDING_QUEUES, PROCESSING_QUEUES, COMPLETED_QUEUES].each(&:clear)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Specwrk
|
4
|
+
class Worker
|
5
|
+
class CompletionFormatter
|
6
|
+
RSpec::Core::Formatters.register self, :stop
|
7
|
+
|
8
|
+
attr_reader :examples
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@examples = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def stop(group_notification)
|
15
|
+
group_notification.notifications.map do |notification|
|
16
|
+
examples << {
|
17
|
+
id: notification.example.id,
|
18
|
+
full_description: notification.example.full_description,
|
19
|
+
status: notification.example.metadata[:execution_result].status,
|
20
|
+
file_path: notification.example.metadata[:file_path],
|
21
|
+
line_number: notification.example.metadata[:line_number],
|
22
|
+
started_at: notification.example.metadata[:execution_result].started_at.iso8601(6),
|
23
|
+
finished_at: notification.example.metadata[:execution_result].finished_at.iso8601(6),
|
24
|
+
run_time: notification.example.metadata[:execution_result].run_time
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
require "rspec"
|
6
|
+
|
7
|
+
require "specwrk/worker/progress_formatter"
|
8
|
+
require "specwrk/worker/completion_formatter"
|
9
|
+
require "specwrk/worker/null_formatter"
|
10
|
+
|
11
|
+
module Specwrk
|
12
|
+
class Worker
|
13
|
+
class Executor
|
14
|
+
def examples
|
15
|
+
completion_formatter.examples
|
16
|
+
end
|
17
|
+
|
18
|
+
def final_output
|
19
|
+
progress_formatter.final_output
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(examples)
|
23
|
+
reset!
|
24
|
+
|
25
|
+
example_ids = examples.map { |example| example[:id] }
|
26
|
+
|
27
|
+
options = RSpec::Core::ConfigurationOptions.new rspec_options + example_ids
|
28
|
+
RSpec::Core::Runner.new(options).run($stderr, $stdout)
|
29
|
+
end
|
30
|
+
|
31
|
+
# https://github.com/skroutz/rspecq/blob/341383ce3ca25f42fad5483cbb6a00ba1c405570/lib/rspecq/worker.rb#L208-L224
|
32
|
+
def reset!
|
33
|
+
completion_formatter.examples.clear
|
34
|
+
|
35
|
+
RSpec.clear_examples
|
36
|
+
|
37
|
+
# see https://github.com/rspec/rspec-core/pull/2723
|
38
|
+
if Gem::Version.new(RSpec::Core::Version::STRING) <= Gem::Version.new("3.9.1")
|
39
|
+
RSpec.world.instance_variable_set(
|
40
|
+
:@example_group_counts_by_spec_file, Hash.new(0)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
# RSpec.clear_examples does not reset those, which causes issues when
|
45
|
+
# a non-example error occurs (subsequent jobs are not executed)
|
46
|
+
RSpec.world.non_example_failure = false
|
47
|
+
|
48
|
+
# we don't want an error that occured outside of the examples (which
|
49
|
+
# would set this to `true`) to stop the worker
|
50
|
+
RSpec.world.wants_to_quit = Specwrk.force_quit
|
51
|
+
|
52
|
+
RSpec.configuration.silence_filter_announcements = true
|
53
|
+
|
54
|
+
RSpec.configuration.add_formatter progress_formatter
|
55
|
+
RSpec.configuration.add_formatter completion_formatter
|
56
|
+
|
57
|
+
# This formatter may be specified by the runner options so
|
58
|
+
# it will be initialized by RSpec
|
59
|
+
RSpec.configuration.add_formatter NullFormatter
|
60
|
+
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
# We want to persist this object between example runs
|
65
|
+
def progress_formatter
|
66
|
+
@progress_formatter ||= ProgressFormatter.new($stdout)
|
67
|
+
end
|
68
|
+
|
69
|
+
def completion_formatter
|
70
|
+
@completion_formatter ||= CompletionFormatter.new
|
71
|
+
end
|
72
|
+
|
73
|
+
def rspec_options
|
74
|
+
@rspec_options ||= if ENV["SPECWRK_OUT"]
|
75
|
+
["--format", "json", "--out", File.join(ENV["SPECWRK_OUT"], "#{ENV.fetch("SPECWRK_ID", "specwrk-worker")}.json")]
|
76
|
+
else
|
77
|
+
["--format", "Specwrk::Worker::NullFormatter"]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Specwrk
|
4
|
+
class Worker
|
5
|
+
class NullFormatter
|
6
|
+
RSpec::Core::Formatters.register self, :example_passed
|
7
|
+
|
8
|
+
attr_reader :output
|
9
|
+
|
10
|
+
def initialize(output)
|
11
|
+
@output = output
|
12
|
+
end
|
13
|
+
|
14
|
+
def example_passed(_notification)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
RSpec::Support.require_rspec_core "formatters/base_text_formatter"
|
6
|
+
RSpec::Support.require_rspec_core "formatters/console_codes"
|
7
|
+
|
8
|
+
module Specwrk
|
9
|
+
class Worker
|
10
|
+
class ProgressFormatter
|
11
|
+
RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :dump_failures, :dump_pending
|
12
|
+
attr_reader :output, :final_output
|
13
|
+
|
14
|
+
def initialize(output)
|
15
|
+
@output = output
|
16
|
+
|
17
|
+
@final_output = Tempfile.new
|
18
|
+
@final_output.define_singleton_method(:tty?) { true }
|
19
|
+
@final_output.sync = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def example_passed(_notification)
|
23
|
+
output.print RSpec::Core::Formatters::ConsoleCodes.wrap(".", :success)
|
24
|
+
end
|
25
|
+
|
26
|
+
def example_pending(_notification)
|
27
|
+
output.print RSpec::Core::Formatters::ConsoleCodes.wrap("*", :pending)
|
28
|
+
end
|
29
|
+
|
30
|
+
def example_failed(_notification)
|
31
|
+
output.print RSpec::Core::Formatters::ConsoleCodes.wrap("F", :failure)
|
32
|
+
end
|
33
|
+
|
34
|
+
def dump_failures(notification)
|
35
|
+
return if notification.failure_notifications.empty?
|
36
|
+
final_output.puts notification.fully_formatted_failed_examples
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump_pending(notification)
|
40
|
+
return if notification.pending_examples.empty?
|
41
|
+
final_output.puts notification.fully_formatted_pending_examples
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|