specwrk 0.11.0 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f5fa0464c4d73ad78593cd2ce5626dac594bb74721d18303f1fee4d33e28615
4
- data.tar.gz: 262e478389d7dd07375fc5a864012c992709199838a2f32f7e72e09bbb412759
3
+ metadata.gz: c7b5abc9ec00673b175314cce03016affa2e47b7ca9f445235ea93fdadb7c79a
4
+ data.tar.gz: 7f30c131e7bbce5c787c527af0e1b58acb16345cc85c5cf22671be1281e8106e
5
5
  SHA512:
6
- metadata.gz: 21fda87f804947653887bcd897c45c97ebdc9227a63bc19019b137e01955dc96701f202f1ebe8a472ed3b871e799a52f54ab9b2872f4f76a2ebefc4e411801df
7
- data.tar.gz: 7a6a62993e9a1262a50c291021fdb1ec6662ed36453bc0ec3855aa7a01735647943500e354a6705c1b6e1f190e3bd0f297bd2317cc639d9fa576a115c4061e30
6
+ metadata.gz: 75d4427606c9491a4344d95713f2062df258e7aea660e520ee126dc2233b8b2bd2b24ca9f56c15d696b0b35b9c0a6211ac05c2b4e63af950df5723e0c625816e
7
+ data.tar.gz: '0885f9962b4cc30f84d72d8e0a99dbb3541588f6aefc292657d95f52576c80251c2cfc4d04af2f35f74df74ca09ebb5385493d55f67ff905cd7cb1522c99f1b4'
@@ -16,8 +16,8 @@ module Specwrk
16
16
  return 1
17
17
  end
18
18
 
19
- puts "\nFinished in #{total_duration} " \
20
- "(total execution time of #{total_run_time})\n"
19
+ puts "\nFinished in #{Specwrk.human_readable_duration total_duration} " \
20
+ "(total execution time of #{Specwrk.human_readable_duration total_run_time})\n"
21
21
 
22
22
  client.shutdown
23
23
 
@@ -44,12 +44,13 @@ module Specwrk
44
44
  raise Errno::ECONNREFUSED unless connected
45
45
  end
46
46
 
47
- attr_reader :last_request_at, :retry_count
47
+ attr_reader :last_request_at, :retry_count, :worker_status
48
48
 
49
49
  def initialize
50
50
  @mutex = Mutex.new
51
51
  @http = self.class.build_http
52
52
  @http.start
53
+ @worker_status = 1
53
54
  end
54
55
 
55
56
  def close
@@ -99,12 +100,6 @@ module Specwrk
99
100
  end
100
101
  end
101
102
 
102
- def complete_examples(examples)
103
- response = post "/complete", body: examples.to_json
104
-
105
- (response.code == "200") ? true : raise(UnhandledResponseError.new("#{response.code}: #{response.body}"))
106
- end
107
-
108
103
  def complete_and_fetch_examples(examples)
109
104
  response = post "/complete_and_pop", body: examples.to_json
110
105
 
@@ -171,7 +166,11 @@ module Specwrk
171
166
  def make_request(request)
172
167
  @mutex.synchronize do
173
168
  @last_request_at = Time.now
174
- @http.request(request).tap { @retry_count = 0 }
169
+ @http.request(request).tap do |response|
170
+ @retry_count = 0
171
+
172
+ @worker_status = response["x-specwrk-status"].to_i if response["x-specwrk-status"]
173
+ end
175
174
  end
176
175
  end
177
176
 
@@ -60,7 +60,6 @@ module Specwrk
60
60
  else
61
61
  filename = filename_for_key(key_string)
62
62
  write(filename, JSON.generate(value))
63
- known_key_pairs[key_string] = filename
64
63
  end
65
64
  end
66
65
 
@@ -71,16 +70,12 @@ module Specwrk
71
70
  def clear
72
71
  FileUtils.rm_rf(path)
73
72
  FileUtils.mkdir_p(path)
74
-
75
- @known_key_pairs = nil
76
73
  end
77
74
 
78
75
  def delete(*keys)
79
- filenames = keys.map { |key| known_key_pairs[key] }.compact
76
+ filenames = keys.map { |key| filename_for_key key }.compact
80
77
 
81
78
  FileUtils.rm_f(filenames)
82
-
83
- keys.each { |key| known_key_pairs.delete(key) }
84
79
  end
85
80
 
86
81
  def merge!(h2)
@@ -88,8 +83,6 @@ module Specwrk
88
83
  end
89
84
 
90
85
  def multi_read(*read_keys)
91
- known_key_pairs # precache before each thread tries to look them up
92
-
93
86
  result_queue = Queue.new
94
87
 
95
88
  read_keys.each do |key|
@@ -112,8 +105,6 @@ module Specwrk
112
105
  end
113
106
 
114
107
  def multi_write(hash)
115
- known_key_pairs # precache before each thread tries to look them up
116
-
117
108
  result_queue = Queue.new
118
109
 
119
110
  hash_with_filenames = hash.map { |key, value| [key.to_s, [filename_for_key(key.to_s), value]] }.to_h
@@ -126,7 +117,6 @@ module Specwrk
126
117
  end
127
118
 
128
119
  Thread.pass until result_queue.length == hash.length
129
- hash_with_filenames.each { |key, (filename, _value)| known_key_pairs[key] = filename }
130
120
  end
131
121
 
132
122
  def empty?
@@ -136,7 +126,7 @@ module Specwrk
136
126
  private
137
127
 
138
128
  def write(filename, content)
139
- tmp_filename = [filename, "tmp"].join(".")
129
+ tmp_filename = [filename, SecureRandom.uuid, "tmp"].join(".")
140
130
 
141
131
  File.binwrite(tmp_filename, content)
142
132
 
@@ -145,29 +135,19 @@ module Specwrk
145
135
  end
146
136
 
147
137
  def read(key)
148
- File.read(known_key_pairs[key]) if known_key_pairs.key? key
138
+ filename = filename_for_key key
139
+ File.read(filename)
140
+ rescue Errno::ENOENT
141
+ nil
149
142
  end
150
143
 
151
144
  def filename_for_key(key)
152
145
  File.join(
153
146
  path,
154
- [
155
- counter_prefix(key),
156
- encode_key(key)
157
- ].join("_")
147
+ encode_key(key)
158
148
  ) + EXT
159
149
  end
160
150
 
161
- def counter_prefix(key)
162
- count = keys.index(key) || counter.tap { @counter += 1 }
163
-
164
- "%012d" % count
165
- end
166
-
167
- def counter
168
- @counter ||= keys.length
169
- end
170
-
171
151
  def path
172
152
  @path ||= File.join(uri.path, scope).tap do |full_path|
173
153
  FileUtils.mkdir_p(full_path)
@@ -179,14 +159,14 @@ module Specwrk
179
159
  end
180
160
 
181
161
  def decode_key(key)
182
- encoded_key_part = File.basename(key).delete_suffix(EXT).split(/\A\d+_/).last
162
+ encoded_key_part = File.basename(key).delete_suffix(EXT)
183
163
  padding_count = (4 - encoded_key_part.length % 4) % 4
184
164
 
185
165
  Base64.urlsafe_decode64(encoded_key_part + ("=" * padding_count))
186
166
  end
187
167
 
188
168
  def known_key_pairs
189
- @known_key_pairs ||= Dir.entries(path).sort.map do |filename|
169
+ Dir.entries(path).sort.map do |filename|
190
170
  next if filename.start_with? "."
191
171
  next unless filename.end_with? EXT
192
172
 
data/lib/specwrk/store.rb CHANGED
@@ -72,7 +72,7 @@ module Specwrk
72
72
  end
73
73
 
74
74
  def to_h
75
- adapter.multi_read(*keys).transform_keys!(&:to_sym)
75
+ multi_read(*keys).transform_keys!(&:to_sym)
76
76
  end
77
77
 
78
78
  def inspect
@@ -98,6 +98,7 @@ module Specwrk
98
98
 
99
99
  class PendingStore < Store
100
100
  RUN_TIME_BUCKET_MAXIMUM_KEY = :____run_time_bucket_maximum
101
+ ORDER_KEY = :____order
101
102
 
102
103
  def run_time_bucket_maximum=(val)
103
104
  @run_time_bucket_maximum = self[RUN_TIME_BUCKET_MAXIMUM_KEY] = val
@@ -107,6 +108,41 @@ module Specwrk
107
108
  @run_time_bucket_maximum ||= self[RUN_TIME_BUCKET_MAXIMUM_KEY]
108
109
  end
109
110
 
111
+ def order=(val)
112
+ @order = nil
113
+
114
+ self[ORDER_KEY] = if val.nil? || val.length.zero?
115
+ nil
116
+ else
117
+ val
118
+ end
119
+ end
120
+
121
+ def order
122
+ @order ||= self[ORDER_KEY] || []
123
+ end
124
+
125
+ def keys
126
+ return super if order.length.zero?
127
+
128
+ order
129
+ end
130
+
131
+ def merge!(hash)
132
+ super
133
+ self.order = hash.keys
134
+ end
135
+
136
+ def clear
137
+ @order = nil
138
+ super
139
+ end
140
+
141
+ def reload
142
+ @order = nil
143
+ super
144
+ end
145
+
110
146
  def shift_bucket
111
147
  return bucket_by_file unless run_time_bucket_maximum&.positive?
112
148
 
@@ -146,6 +182,7 @@ module Specwrk
146
182
  end
147
183
 
148
184
  delete(*consumed_keys)
185
+ self.order = order - consumed_keys
149
186
  bucket
150
187
  end
151
188
 
@@ -171,6 +208,7 @@ module Specwrk
171
208
  end
172
209
 
173
210
  delete(*consumed_keys)
211
+ self.order = order - consumed_keys
174
212
  bucket
175
213
  end
176
214
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.11.0"
4
+ VERSION = "0.13.0"
5
5
  end
@@ -88,8 +88,6 @@ module Specwrk
88
88
  Endpoints::Heartbeat
89
89
  when ["POST", "/pop"]
90
90
  Endpoints::Pop
91
- when ["POST", "/complete"]
92
- Endpoints::Complete
93
91
  when ["POST", "/complete_and_pop"]
94
92
  Endpoints::CompleteAndPop
95
93
  when ["POST", "/seed"]
@@ -11,7 +11,7 @@ module Specwrk
11
11
  end
12
12
 
13
13
  def call(env)
14
- env[:request] ||= Rack::Request.new(env)
14
+ @request = env[:request] ||= Rack::Request.new(env)
15
15
 
16
16
  return @app.call(env) if [nil, ""].include? ENV["SPECWRK_SRV_KEY"]
17
17
  return @app.call(env) if @excluded_paths.include? env[:request].path_info
@@ -28,7 +28,11 @@ module Specwrk
28
28
  private
29
29
 
30
30
  def unauthorized
31
- [401, {"content-type" => "application/json"}, ["Unauthorized"]]
31
+ if @request.head?
32
+ [401, {}, []]
33
+ else
34
+ [401, {"content-type" => "application/json"}, ["Unauthorized"]]
35
+ end
32
36
  end
33
37
  end
34
38
  end
@@ -33,6 +33,8 @@ module Specwrk
33
33
 
34
34
  after_lock
35
35
 
36
+ final_response[1]["x-specwrk-status"] = worker_status.to_s
37
+
36
38
  final_response
37
39
  end
38
40
 
@@ -102,6 +104,12 @@ module Specwrk
102
104
  @worker ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "workers", request.get_header("HTTP_X_SPECWRK_ID").to_s))
103
105
  end
104
106
 
107
+ def worker_status
108
+ return 0 if worker[:failed].nil? && completed.any? # worker starts after run has completed
109
+
110
+ worker[:failed] || 1
111
+ end
112
+
105
113
  def run_id
106
114
  request.get_header("HTTP_X_SPECWRK_RUN")
107
115
  end
@@ -190,29 +198,6 @@ module Specwrk
190
198
  end
191
199
  end
192
200
 
193
- class Complete < Base
194
- def with_response
195
- warn "[DEPRECATED] This endpoint will be retired in favor of CompleteAndPop. Upgrade your clients."
196
- completed.merge!(completed_examples)
197
- processing.delete(*completed_examples.keys)
198
-
199
- ok
200
- end
201
-
202
- private
203
-
204
- def completed_examples
205
- @completed_data ||= payload.map { |example| [example[:id], example] if processing[example[:id]] }.compact.to_h
206
- end
207
-
208
- # We don't care about exact values here, just approximate run times are fine
209
- # So if we overwrite run times from another process it is nbd
210
- def after_lock
211
- run_time_data = payload.map { |example| [example[:id], example[:run_time]] }.to_h
212
- run_times.merge! run_time_data
213
- end
214
- end
215
-
216
201
  class Popable < Base
217
202
  private
218
203
 
@@ -261,6 +246,8 @@ module Specwrk
261
246
  end
262
247
 
263
248
  class CompleteAndPop < Popable
249
+ EXAMPLE_STATUSES = %w[passed failed pending]
250
+
264
251
  def with_response
265
252
  completed.merge!(completed_examples)
266
253
  processing.delete(*completed_examples.keys)
@@ -270,19 +257,26 @@ module Specwrk
270
257
 
271
258
  private
272
259
 
273
- def before_lock
274
- completed_examples
275
- run_time_data
276
- end
277
-
278
260
  def completed_examples
279
261
  @completed_data ||= payload.map { |example| [example[:id], example] if processing[example[:id]] }.compact.to_h
280
262
  end
281
263
 
282
- # We don't care about exact values here, just approximate run times are fine
283
- # So if we overwrite run times from another process it is nbd
264
+ def completed_examples_status_counts
265
+ @completed_examples_status_counts ||= completed_examples.values.map { |example| example[:status] }.tally
266
+ end
267
+
284
268
  def after_lock
269
+ # We don't care about exact values here, just approximate run times are fine
270
+ # So if we overwrite run times from another process it is nbd
285
271
  run_times.merge! run_time_data
272
+
273
+ # workers are single proces, single-threaded, so safe to do this work without the lock
274
+ existing_status_counts = worker.multi_read(*EXAMPLE_STATUSES)
275
+ new_status_counts = EXAMPLE_STATUSES.map do |status|
276
+ [status, existing_status_counts.fetch(status, 0) + completed_examples_status_counts.fetch(status, 0)]
277
+ end.to_h
278
+
279
+ worker.merge!(new_status_counts)
286
280
  end
287
281
 
288
282
  def run_time_data
@@ -5,19 +5,14 @@ module Specwrk
5
5
  class CompletionFormatter
6
6
  RSpec::Core::Formatters.register self, :stop
7
7
 
8
- attr_reader :examples, :failure
8
+ attr_reader :examples
9
9
 
10
10
  def initialize
11
11
  @examples = []
12
- @failure = false
13
12
  end
14
13
 
15
14
  def stop(group_notification)
16
15
  group_notification.notifications.map do |notification|
17
- unless failure
18
- @failure = notification.example.execution_result.status == :failed
19
- end
20
-
21
16
  examples << {
22
17
  id: notification.example.id,
23
18
  full_description: notification.example.full_description,
@@ -11,12 +11,6 @@ require "specwrk/worker/null_formatter"
11
11
  module Specwrk
12
12
  class Worker
13
13
  class Executor
14
- attr_reader :example_processed
15
-
16
- def failure
17
- completion_formatter.failure
18
- end
19
-
20
14
  def examples
21
15
  completion_formatter.examples
22
16
  end
@@ -29,7 +23,6 @@ module Specwrk
29
23
  reset!
30
24
 
31
25
  example_ids = examples.map { |example| example[:id] }
32
- @example_processed ||= true unless example_ids.size.zero? # only ever toggle from nil => true
33
26
 
34
27
  options = RSpec::Core::ConfigurationOptions.new rspec_options + example_ids
35
28
  RSpec::Core::Runner.new(options).run($stderr, $stdout)
@@ -40,6 +33,7 @@ module Specwrk
40
33
  completion_formatter.examples.clear
41
34
 
42
35
  RSpec.clear_examples
36
+ RSpec.configuration.backtrace_formatter.filter_gem "specwrk"
43
37
 
44
38
  # see https://github.com/rspec/rspec-core/pull/2723
45
39
  if Gem::Version.new(RSpec::Core::Version::STRING) <= Gem::Version.new("3.9.1")
@@ -110,11 +110,10 @@ module Specwrk
110
110
  attr_reader :running, :client, :executor
111
111
 
112
112
  def status
113
- return 1 if !executor.example_processed && !@all_examples_completed
114
- return 1 if executor.failure
113
+ return 0 if @all_examples_completed && client.worker_status.zero?
115
114
  return 1 if Specwrk.force_quit
116
115
 
117
- 0
116
+ client.worker_status
118
117
  end
119
118
 
120
119
  def warn(msg)
data/lib/specwrk.rb CHANGED
@@ -38,5 +38,21 @@ module Specwrk
38
38
 
39
39
  exited_pids
40
40
  end
41
+
42
+ def human_readable_duration(total_seconds, precision: 2)
43
+ secs = total_seconds.to_f
44
+ hours = (secs / 3600).to_i
45
+ mins = ((secs % 3600) / 60).to_i
46
+ seconds = secs % 60
47
+
48
+ parts = []
49
+ parts << "#{hours}h" if hours.positive?
50
+ parts << "#{mins}m" if mins.positive?
51
+ if seconds.positive?
52
+ sec_str = format("%0.#{precision}f", seconds).sub(/\.?0+$/, "")
53
+ parts << "#{sec_str}s"
54
+ end
55
+ parts.join(" ")
56
+ end
41
57
  end
42
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: specwrk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Westendorf