specwrk 0.16.2 → 0.17.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: 847769b30d93e9ada115b91fafaf4a159b716d1be6d2e006ff0b06b392a8ce38
4
- data.tar.gz: b26b910849c516713dcc0a48a35148cecd377a804d847ea8216e622b34cb207f
3
+ metadata.gz: 2ef3257927cc46b8361f2fe0ee98db82890b5f8a25882045e122317c2b223ccd
4
+ data.tar.gz: 8dac946fb5a4e8e673e4fef15eab5b4f16e1ffb4959688c1c6af967b9c06b34d
5
5
  SHA512:
6
- metadata.gz: 46080d6e8ec222e8ab29cfe76ac9ba9b93d54ec38246fe19328109e28718ea4df4dbe06db780cbe2ed20ede49ad3fe4a796151e6883d9a985e9f5c8722b5a17e
7
- data.tar.gz: '099be5b21ee944e882dbd6eac126a21c7e7d9211df50dcef437b6e2689b12bf38ccb68c75635347a1aff7192a4f66dd7e6f40f1160d801ffd63ee20bba1a7ad2'
6
+ metadata.gz: 5c9ea3eb229ba95a345f981ad5bcb9faa9998094b4c9ca29ea310756a6d746670a7dfbe3bcca29a01eb8067073795bc3e8a7dead802f8bf1e8263a63f8217225
7
+ data.tar.gz: 938de3505dcb4ac54daecadb4b00eea0ffda3c3a3868e85341334dc8e4f11be570f64c241f433fba68415ce22afe791b54e8222d0a771c7c838b207afcbd6dae
data/Specwrk.watchfile.rb CHANGED
@@ -16,7 +16,7 @@ end
16
16
  # path.gsub(/app\/models\/(.+)\.rb/, "spec/models/\\1_spec.rb")
17
17
  # end
18
18
  #
19
- # If a controlelr file changes (assuming rails app structure), run the controller and system specs file
19
+ # If a controller file changes (assuming rails app structure), run the controller and system specs file
20
20
  # map(/app\/controllers\/.*.rb$/) do |path|
21
21
  # [
22
22
  # path.gsub(/app\/controllers\/(.+)\.rb/, "spec/controllers/\\1_spec.rb"),
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Specwrk
6
+ class Store
7
+ class << self
8
+ def with_lock(uri, key)
9
+ adapter_klass(uri).with_lock(uri, key) { yield }
10
+ end
11
+
12
+ def adapter_klass(uri)
13
+ case uri.scheme
14
+ when "memory"
15
+ require "specwrk/store/memory_adapter" unless defined?(MemoryAdapter)
16
+
17
+ MemoryAdapter
18
+ when "file"
19
+ require "specwrk/store/file_adapter" unless defined?(FileAdapter)
20
+
21
+ FileAdapter
22
+ when /redis/
23
+ begin
24
+ require "specwrk/store/redis_adapter" unless defined?(RedisAdapter)
25
+ rescue LoadError
26
+ warn "Unable use RedisAdapter with #{uri}, gem not found. Add `gem 'specwrk-store-redis_adapter'` to your Gemfile and bundle install."
27
+ exit(1)
28
+ end
29
+
30
+ RedisAdapter
31
+ end
32
+ end
33
+ end
34
+
35
+ def initialize(uri_string, scope)
36
+ @uri = URI(uri_string)
37
+ @scope = scope
38
+ end
39
+
40
+ def [](key)
41
+ adapter[key.to_s]
42
+ end
43
+
44
+ def multi_read(*keys)
45
+ adapter.multi_read(*keys)
46
+ end
47
+
48
+ def []=(key, value)
49
+ adapter[key.to_s] = value
50
+ end
51
+
52
+ def keys
53
+ all_keys = adapter.keys
54
+
55
+ all_keys.reject { |k| k.start_with? "____" }
56
+ end
57
+
58
+ def length
59
+ keys.length
60
+ end
61
+
62
+ def any?
63
+ !empty?
64
+ end
65
+
66
+ def empty?
67
+ adapter.empty?
68
+ end
69
+
70
+ def delete(*keys)
71
+ adapter.delete(*keys)
72
+ end
73
+
74
+ def merge!(h2)
75
+ h2.transform_keys!(&:to_s)
76
+ adapter.merge!(h2)
77
+ end
78
+
79
+ def clear
80
+ adapter.clear
81
+ end
82
+
83
+ def to_h
84
+ multi_read(*keys).transform_keys!(&:to_sym)
85
+ end
86
+
87
+ def inspect
88
+ reload.to_h.dup
89
+ end
90
+
91
+ # Bypass any cached values. Helpful when you have two instances
92
+ # of the same store where one mutates data and the other needs to check
93
+ # on the status of that data (i.e. endpoint tests)
94
+ def reload
95
+ @adapter = nil
96
+ self
97
+ end
98
+
99
+ private
100
+
101
+ attr_reader :uri, :scope
102
+
103
+ def adapter
104
+ @adapter ||= self.class.adapter_klass(uri).new uri, scope
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specwrk/store/base"
4
+
5
+ module Specwrk
6
+ class BucketStore < Store
7
+ EXAMPLES_KEY = :____examples
8
+
9
+ def examples=(val)
10
+ @examples = nil
11
+
12
+ self[EXAMPLES_KEY] = if val.nil? || val.length.zero?
13
+ nil
14
+ else
15
+ val
16
+ end
17
+ end
18
+
19
+ def examples
20
+ @examples ||= self[EXAMPLES_KEY] || []
21
+ end
22
+
23
+ def clear
24
+ @examples = nil
25
+ super
26
+ end
27
+
28
+ def reload
29
+ @examples = nil
30
+ super
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "specwrk/store/base"
5
+
6
+ module Specwrk
7
+ class CompletedStore < Store
8
+ def dump
9
+ @run_times = []
10
+ @first_started_at = Time.new(2999, 1, 1, 0, 0, 0) # TODO: Make future proof /s
11
+ @last_finished_at = Time.new(1900, 1, 1, 0, 0, 0)
12
+
13
+ @output = {
14
+ file_totals: Hash.new { |h, filename| h[filename] = 0.0 },
15
+ meta: {failures: 0, passes: 0, pending: 0},
16
+ examples: {}
17
+ }
18
+
19
+ to_h.values.each { |example| calculate(example) }
20
+
21
+ @output[:meta][:total_run_time] = @run_times.sum
22
+ @output[:meta][:average_run_time] = @output[:meta][:total_run_time] / [@run_times.length, 1].max.to_f
23
+ @output[:meta][:first_started_at] = @first_started_at.iso8601(6)
24
+ @output[:meta][:last_finished_at] = @last_finished_at.iso8601(6)
25
+
26
+ @output
27
+ end
28
+
29
+ private
30
+
31
+ def calculate(example)
32
+ @run_times << example[:run_time]
33
+ @output[:file_totals][example[:file_path]] += example[:run_time]
34
+
35
+ started_at = Time.parse(example[:started_at])
36
+ finished_at = Time.parse(example[:finished_at])
37
+
38
+ @first_started_at = started_at if started_at < @first_started_at
39
+ @last_finished_at = finished_at if finished_at > @last_finished_at
40
+
41
+ case example[:status]
42
+ when "passed"
43
+ @output[:meta][:passes] += 1
44
+ when "failed"
45
+ @output[:meta][:failures] += 1
46
+ when "pending"
47
+ @output[:meta][:pending] += 1
48
+ end
49
+
50
+ @output[:examples][example[:id]] = example
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ require "specwrk/store/base"
6
+ require "specwrk/store/bucket_store"
7
+
8
+ module Specwrk
9
+ class PendingStore < Store
10
+ RUN_TIME_BUCKET_MAXIMUM_KEY = :____run_time_bucket_maximum
11
+ MAX_RETRIES_KEY = :____max_retries
12
+ BUCKET_IDS_KEY = :____bucket_ids
13
+
14
+ def run_time_bucket_maximum=(val)
15
+ @run_time_bucket_maximum = self[RUN_TIME_BUCKET_MAXIMUM_KEY] = val
16
+ end
17
+
18
+ def run_time_bucket_maximum
19
+ @run_time_bucket_maximum ||= self[RUN_TIME_BUCKET_MAXIMUM_KEY]
20
+ end
21
+
22
+ def max_retries=(val)
23
+ @max_retries = self[MAX_RETRIES_KEY] = val
24
+ end
25
+
26
+ def max_retries
27
+ @max_retries ||= self[MAX_RETRIES_KEY] || 0
28
+ end
29
+
30
+ def bucket_ids=(val)
31
+ @bucket_ids = nil
32
+
33
+ self[BUCKET_IDS_KEY] = if val.nil? || val.length.zero?
34
+ nil
35
+ else
36
+ val
37
+ end
38
+ end
39
+
40
+ def bucket_ids
41
+ @bucket_ids ||= self[BUCKET_IDS_KEY] || []
42
+ end
43
+
44
+ def merge!(hash)
45
+ return self if hash.nil? || hash.empty?
46
+
47
+ buckets = grouped_examples(hash.values)
48
+ new_bucket_ids = buckets.map { |examples| write_bucket(examples) }
49
+
50
+ self.bucket_ids = bucket_ids + new_bucket_ids
51
+ self
52
+ end
53
+
54
+ def clear
55
+ bucket_ids.each { |bucket_id| delete_bucket(bucket_id) }
56
+ @bucket_ids = nil
57
+
58
+ super
59
+ end
60
+
61
+ def reload
62
+ @max_retries = nil
63
+ @bucket_ids = nil
64
+ super
65
+ end
66
+
67
+ def shift_bucket
68
+ return nil if bucket_ids.empty?
69
+
70
+ bucket_id = bucket_ids.first
71
+ self.bucket_ids = bucket_ids.drop(1)
72
+ bucket_id
73
+ end
74
+
75
+ def push_examples(examples)
76
+ return self if examples.nil? || examples.empty?
77
+
78
+ new_bucket_id = write_bucket(examples)
79
+ self.bucket_ids = bucket_ids + [new_bucket_id]
80
+ self
81
+ end
82
+
83
+ def bucket_store_for(bucket_id)
84
+ BucketStore.new(uri.to_s, File.join(scope, "buckets", bucket_id))
85
+ end
86
+
87
+ def delete_bucket(bucket_id)
88
+ bucket_store_for(bucket_id).clear
89
+ end
90
+
91
+ def keys
92
+ bucket_ids
93
+ end
94
+
95
+ def length
96
+ bucket_ids.length
97
+ end
98
+
99
+ private
100
+
101
+ def write_bucket(examples)
102
+ bucket_id = SecureRandom.uuid
103
+ bucket_store_for(bucket_id).examples = examples
104
+ bucket_id
105
+ end
106
+
107
+ def grouped_examples(examples)
108
+ return [] if examples.empty?
109
+
110
+ examples_to_group = examples.dup
111
+
112
+ case grouping_strategy
113
+ when :file
114
+ group_by_file(examples_to_group)
115
+ else
116
+ group_by_timings(examples_to_group)
117
+ end
118
+ end
119
+
120
+ # Take consecutive examples with the same file_path
121
+ def group_by_file(examples)
122
+ buckets = []
123
+
124
+ examples.each do |example|
125
+ current_bucket = buckets.last
126
+
127
+ if current_bucket.nil? || current_bucket.first[:file_path] != example[:file_path]
128
+ buckets << [example]
129
+ else
130
+ current_bucket << example
131
+ end
132
+ end
133
+
134
+ buckets
135
+ end
136
+
137
+ # Take elements until the average runtime bucket has filled
138
+ def group_by_timings(examples)
139
+ buckets = []
140
+ return group_by_file(examples) unless run_time_bucket_maximum&.positive?
141
+
142
+ estimated_run_time_total = 0
143
+ current_bucket = []
144
+
145
+ examples.each do |example|
146
+ estimated_run_time_total += example[:expected_run_time] || run_time_bucket_maximum
147
+
148
+ if estimated_run_time_total > run_time_bucket_maximum && current_bucket.length.positive?
149
+ buckets << current_bucket
150
+ current_bucket = [example]
151
+ estimated_run_time_total = example[:expected_run_time] || run_time_bucket_maximum
152
+ next
153
+ end
154
+
155
+ current_bucket << example
156
+ end
157
+
158
+ buckets << current_bucket if current_bucket.any?
159
+ buckets
160
+ end
161
+
162
+ def grouping_strategy
163
+ return :file unless run_time_bucket_maximum&.positive?
164
+
165
+ (ENV["SPECWRK_SRV_GROUP_BY"] == "file") ? :file : :timings
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specwrk/store/base"
4
+
5
+ module Specwrk
6
+ class ProcessingStore < Store
7
+ end
8
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "specwrk/store/base"
5
+
6
+ module Specwrk
7
+ class WorkerStore < Store
8
+ FIRST_SEEN_AT_KEY = :____first_seen_at_key
9
+ LAST_SEEN_AT_KEY = :____last_seen_at_key
10
+
11
+ def first_seen_at=(val)
12
+ @first_seen_at = nil
13
+
14
+ self[FIRST_SEEN_AT_KEY] = val.to_i
15
+ end
16
+
17
+ def first_seen_at
18
+ @first_seen_at ||= begin
19
+ value = self[FIRST_SEEN_AT_KEY]
20
+ return @first_seen_at = value unless value
21
+
22
+ @first_seen_at = Time.at(value.to_i)
23
+ end
24
+ end
25
+
26
+ def last_seen_at=(val)
27
+ @last_seen_at = nil
28
+
29
+ self[LAST_SEEN_AT_KEY] = val.to_i
30
+ end
31
+
32
+ def last_seen_at
33
+ @last_seen_at ||= begin
34
+ value = self[LAST_SEEN_AT_KEY]
35
+ return @last_seen_at = value unless value
36
+
37
+ @last_seen_at = Time.at(value.to_i)
38
+ end
39
+ end
40
+
41
+ def reload
42
+ @last_seen_at = nil
43
+ @first_seen_at = nil
44
+ super
45
+ end
46
+ end
47
+ end
data/lib/specwrk/store.rb CHANGED
@@ -1,326 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "time"
4
-
5
- module Specwrk
6
- class Store
7
- class << self
8
- def with_lock(uri, key)
9
- adapter_klass(uri).with_lock(uri, key) { yield }
10
- end
11
-
12
- def adapter_klass(uri)
13
- case uri.scheme
14
- when "memory"
15
- require "specwrk/store/memory_adapter" unless defined?(MemoryAdapter)
16
-
17
- MemoryAdapter
18
- when "file"
19
- require "specwrk/store/file_adapter" unless defined?(FileAdapter)
20
-
21
- FileAdapter
22
- when /redis/
23
- begin
24
- require "specwrk/store/redis_adapter" unless defined?(RedisAdapter)
25
- rescue LoadError
26
- warn "Unable use RedisAdapter with #{uri}, gem not found. Add `gem 'specwrk-store-redis_adapter'` to your Gemfile and bundle install."
27
- exit(1)
28
- end
29
-
30
- RedisAdapter
31
- end
32
- end
33
- end
34
-
35
- def initialize(uri_string, scope)
36
- @uri = URI(uri_string)
37
- @scope = scope
38
- end
39
-
40
- def [](key)
41
- adapter[key.to_s]
42
- end
43
-
44
- def multi_read(*keys)
45
- adapter.multi_read(*keys)
46
- end
47
-
48
- def []=(key, value)
49
- adapter[key.to_s] = value
50
- end
51
-
52
- def keys
53
- all_keys = adapter.keys
54
-
55
- all_keys.reject { |k| k.start_with? "____" }
56
- end
57
-
58
- def length
59
- keys.length
60
- end
61
-
62
- def any?
63
- !empty?
64
- end
65
-
66
- def empty?
67
- adapter.empty?
68
- end
69
-
70
- def delete(*keys)
71
- adapter.delete(*keys)
72
- end
73
-
74
- def merge!(h2)
75
- h2.transform_keys!(&:to_s)
76
- adapter.merge!(h2)
77
- end
78
-
79
- def clear
80
- adapter.clear
81
- end
82
-
83
- def to_h
84
- multi_read(*keys).transform_keys!(&:to_sym)
85
- end
86
-
87
- def inspect
88
- reload.to_h.dup
89
- end
90
-
91
- # Bypass any cached values. Helpful when you have two instances
92
- # of the same store where one mutates data and the other needs to check
93
- # on the status of that data (i.e. endpoint tests)
94
- def reload
95
- @adapter = nil
96
- self
97
- end
98
-
99
- private
100
-
101
- attr_reader :uri, :scope
102
-
103
- def adapter
104
- @adapter ||= self.class.adapter_klass(uri).new uri, scope
105
- end
106
- end
107
-
108
- class WorkerStore < Store
109
- FIRST_SEEN_AT_KEY = :____first_seen_at_key
110
- LAST_SEEN_AT_KEY = :____last_seen_at_key
111
-
112
- def first_seen_at=(val)
113
- @first_seen_at = nil
114
-
115
- self[FIRST_SEEN_AT_KEY] = val.to_i
116
- end
117
-
118
- def first_seen_at
119
- @first_seen_at ||= begin
120
- value = self[FIRST_SEEN_AT_KEY]
121
- return @first_seen_at = value unless value
122
-
123
- @first_seen_at = Time.at(value.to_i)
124
- end
125
- end
126
-
127
- def last_seen_at=(val)
128
- @last_seen_at = nil
129
-
130
- self[LAST_SEEN_AT_KEY] = val.to_i
131
- end
132
-
133
- def last_seen_at
134
- @last_seen_at ||= begin
135
- value = self[LAST_SEEN_AT_KEY]
136
- return @last_seen_at = value unless value
137
-
138
- @last_seen_at = Time.at(value.to_i)
139
- end
140
- end
141
-
142
- def reload
143
- @last_seen_at = nil
144
- @first_seen_at = nil
145
- super
146
- end
147
- end
148
-
149
- class PendingStore < Store
150
- RUN_TIME_BUCKET_MAXIMUM_KEY = :____run_time_bucket_maximum
151
- ORDER_KEY = :____order
152
- MAX_RETRIES_KEY = :____max_retries
153
-
154
- def run_time_bucket_maximum=(val)
155
- @run_time_bucket_maximum = self[RUN_TIME_BUCKET_MAXIMUM_KEY] = val
156
- end
157
-
158
- def run_time_bucket_maximum
159
- @run_time_bucket_maximum ||= self[RUN_TIME_BUCKET_MAXIMUM_KEY]
160
- end
161
-
162
- def order=(val)
163
- @order = nil
164
-
165
- self[ORDER_KEY] = if val.nil? || val.length.zero?
166
- nil
167
- else
168
- val
169
- end
170
- end
171
-
172
- def order
173
- @order ||= self[ORDER_KEY] || []
174
- end
175
-
176
- def max_retries=(val)
177
- @max_retries = self[MAX_RETRIES_KEY] = val
178
- end
179
-
180
- def max_retries
181
- @max_retries ||= self[MAX_RETRIES_KEY] || 0
182
- end
183
-
184
- def keys
185
- return super if order.length.zero?
186
-
187
- order
188
- end
189
-
190
- def merge!(hash)
191
- super
192
-
193
- self.order = order + (hash.keys - order)
194
- end
195
-
196
- def clear
197
- @order = nil
198
- super
199
- end
200
-
201
- def reload
202
- @order = nil
203
- @max_retries = nil
204
- super
205
- end
206
-
207
- def shift_bucket
208
- return bucket_by_file unless run_time_bucket_maximum&.positive?
209
-
210
- case ENV["SPECWRK_SRV_GROUP_BY"]
211
- when "file"
212
- bucket_by_file
213
- else
214
- bucket_by_timings
215
- end
216
- end
217
-
218
- private
219
-
220
- # Take elements from the hash where the file_path is the same
221
- # Expects that the examples were merged in order of filename
222
- def bucket_by_file
223
- bucket = []
224
- consumed_keys = []
225
-
226
- all_keys = keys
227
- key = all_keys.first
228
- return [] if key.nil?
229
-
230
- file_path = self[key][:file_path]
231
-
232
- catch(:full) do
233
- all_keys.each_slice(24).each do |key_group|
234
- examples = multi_read(*key_group)
235
-
236
- examples.each do |key, example|
237
- throw :full unless example[:file_path] == file_path
238
-
239
- bucket << example
240
- consumed_keys << key
241
- end
242
- end
243
- end
244
-
245
- delete(*consumed_keys)
246
- self.order = order - consumed_keys
247
- bucket
248
- end
249
-
250
- # Take elements from the hash until the average runtime bucket has filled
251
- def bucket_by_timings
252
- bucket = []
253
- consumed_keys = []
254
-
255
- estimated_run_time_total = 0
256
-
257
- catch(:full) do
258
- keys.each_slice(24).each do |key_group|
259
- examples = multi_read(*key_group)
260
-
261
- examples.each do |key, example|
262
- estimated_run_time_total += example[:expected_run_time] || run_time_bucket_maximum
263
- throw :full if estimated_run_time_total > run_time_bucket_maximum && bucket.length.positive?
264
-
265
- bucket << example
266
- consumed_keys << key
267
- end
268
- end
269
- end
270
-
271
- delete(*consumed_keys)
272
- self.order = order - consumed_keys
273
- bucket
274
- end
275
- end
276
-
277
- class ProcessingStore < Store
278
- end
279
-
280
- class CompletedStore < Store
281
- def dump
282
- @run_times = []
283
- @first_started_at = Time.new(2999, 1, 1, 0, 0, 0) # TODO: Make future proof /s
284
- @last_finished_at = Time.new(1900, 1, 1, 0, 0, 0)
285
-
286
- @output = {
287
- file_totals: Hash.new { |h, filename| h[filename] = 0.0 },
288
- meta: {failures: 0, passes: 0, pending: 0},
289
- examples: {}
290
- }
291
-
292
- to_h.values.each { |example| calculate(example) }
293
-
294
- @output[:meta][:total_run_time] = @run_times.sum
295
- @output[:meta][:average_run_time] = @output[:meta][:total_run_time] / [@run_times.length, 1].max.to_f
296
- @output[:meta][:first_started_at] = @first_started_at.iso8601(6)
297
- @output[:meta][:last_finished_at] = @last_finished_at.iso8601(6)
298
-
299
- @output
300
- end
301
-
302
- private
303
-
304
- def calculate(example)
305
- @run_times << example[:run_time]
306
- @output[:file_totals][example[:file_path]] += example[:run_time]
307
-
308
- started_at = Time.parse(example[:started_at])
309
- finished_at = Time.parse(example[:finished_at])
310
-
311
- @first_started_at = started_at if started_at < @first_started_at
312
- @last_finished_at = finished_at if finished_at > @last_finished_at
313
-
314
- case example[:status]
315
- when "passed"
316
- @output[:meta][:passes] += 1
317
- when "failed"
318
- @output[:meta][:failures] += 1
319
- when "pending"
320
- @output[:meta][:pending] += 1
321
- end
322
-
323
- @output[:examples][example[:id]] = example
324
- end
325
- end
326
- end
3
+ require "specwrk/store/base"
4
+ require "specwrk/store/worker_store"
5
+ require "specwrk/store/pending_store"
6
+ require "specwrk/store/processing_store"
7
+ require "specwrk/store/completed_store"
8
+ require "specwrk/store/bucket_store"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.16.2"
4
+ VERSION = "0.17.0"
5
5
  end
@@ -21,23 +21,15 @@ module Specwrk
21
21
 
22
22
  payload # parse the payload before any locking
23
23
 
24
- before_lock
25
-
26
24
  worker.first_seen_at ||= Time.now
27
25
  worker.last_seen_at = Time.now
28
26
 
29
- final_response = with_lock do
30
- started_at = metadata[:started_at] ||= Time.now.iso8601
31
- @started_at = Time.parse(started_at)
27
+ started_at = metadata[:started_at] ||= Time.now.iso8601
28
+ @started_at = Time.parse(started_at)
32
29
 
33
- with_response
30
+ with_response.tap do |response|
31
+ response[1]["x-specwrk-status"] = worker_status.to_s
34
32
  end
35
-
36
- after_lock
37
-
38
- final_response[1]["x-specwrk-status"] = worker_status.to_s
39
-
40
- final_response
41
33
  end
42
34
 
43
35
  def with_response
@@ -48,16 +40,6 @@ module Specwrk
48
40
 
49
41
  attr_reader :request
50
42
 
51
- def skip_lock
52
- false
53
- end
54
-
55
- def before_lock
56
- end
57
-
58
- def after_lock
59
- end
60
-
61
43
  def not_found
62
44
  if request.head?
63
45
  [404, {}, []]
@@ -133,12 +115,10 @@ module Specwrk
133
115
  end
134
116
 
135
117
  def with_lock
136
- if skip_lock
137
- yield
138
- else
139
- with_mutex do
140
- Store.with_lock(URI(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///")), run_id) { yield }
141
- end
118
+ return yield unless run_id
119
+
120
+ with_mutex do
121
+ Store.with_lock(URI(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///")), run_id) { yield }
142
122
  end
143
123
  end
144
124
 
@@ -11,19 +11,20 @@ module Specwrk
11
11
  def with_response
12
12
  completed.merge!(completed_examples)
13
13
  processing.delete(*(completed_examples.keys + retry_examples.keys))
14
- pending.merge!(retry_examples)
14
+
15
+ with_lock do
16
+ pending.merge!(retry_examples)
17
+ end
18
+
15
19
  failure_counts.merge!(retry_examples_new_failure_counts)
16
20
 
21
+ update_run_times
22
+
17
23
  with_pop_response
18
24
  end
19
25
 
20
26
  private
21
27
 
22
- def before_lock
23
- processing_examples
24
- completed_examples
25
- end
26
-
27
28
  def all_examples
28
29
  @all_examples ||= payload.map { |example| [example[:id], example] if processing_examples[example[:id]] }.compact.to_h
29
30
  end
@@ -71,9 +72,8 @@ module Specwrk
71
72
  @completed_examples_status_counts ||= completed_examples.values.map { |example| example[:status] }.tally
72
73
  end
73
74
 
74
- def after_lock
75
- # We don't care about exact values here, just approximate run times are fine
76
- # So if we overwrite run times from another process it is nbd
75
+ def update_run_times
76
+ # Rough run time tracking does not require holding the store lock
77
77
  run_times.merge! run_time_data
78
78
 
79
79
  # workers are single process, single-threaded, so safe to do this work without the lock
@@ -16,8 +16,9 @@ module Specwrk
16
16
  elsif completed.any? && processing.empty?
17
17
  [410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
18
18
  elsif expired_examples.length.positive?
19
- pending.merge!(expired_examples.each { |_id, example| example[:worker_id] = worker_id })
20
- processing.delete(*expired_examples.keys)
19
+ expired_examples.each { |_id, example| example[:worker_id] = worker_id }
20
+ with_lock { pending.push_examples(expired_examples.values) }
21
+ processing.delete(*expired_examples.keys.map(&:to_s))
21
22
  @examples = nil
22
23
 
23
24
  [200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
@@ -28,7 +29,11 @@ module Specwrk
28
29
 
29
30
  def examples
30
31
  @examples ||= begin
31
- examples = pending.shift_bucket
32
+ bucket_id = with_lock { pending.shift_bucket }
33
+ return [] if bucket_id.nil?
34
+
35
+ bucket = pending.bucket_store_for(bucket_id)
36
+ examples = bucket.examples
32
37
 
33
38
  processing_data = examples.map do |example|
34
39
  [
@@ -37,13 +42,14 @@ module Specwrk
37
42
  end
38
43
 
39
44
  processing.merge!(processing_data.to_h)
45
+ bucket.clear
40
46
 
41
47
  examples
42
48
  end
43
49
  end
44
50
 
45
51
  def expired_examples
46
- return unless processing.any?
52
+ return {} unless processing.any?
47
53
 
48
54
  @expired_examples ||= processing.to_h.select { |_id, example| expired?(example) }
49
55
  end
@@ -13,12 +13,6 @@ module Specwrk
13
13
 
14
14
  [200, {"content-type" => "application/json"}, [JSON.generate(completed_dump)]]
15
15
  end
16
-
17
- private
18
-
19
- def skip_lock
20
- true
21
- end
22
16
  end
23
17
  end
24
18
  end
@@ -6,23 +6,22 @@ module Specwrk
6
6
  class Web
7
7
  module Endpoints
8
8
  class Seed < Base
9
- def before_lock
9
+ def with_response
10
10
  examples_with_run_times
11
- end
12
11
 
13
- def with_response
14
12
  pending.clear
15
13
  processing.clear
16
14
  failure_counts.clear
15
+ completed.clear
17
16
 
18
17
  pending.max_retries = payload.fetch(:max_retries, "0").to_i
19
18
 
20
19
  new_run_time_bucket_maximums = [pending.run_time_bucket_maximum, @seeds_run_time_bucket_maximum.to_f].compact
21
20
  pending.run_time_bucket_maximum = new_run_time_bucket_maximums.sum.to_f / new_run_time_bucket_maximums.length.to_f
22
21
 
23
- pending.merge!(examples_with_run_times)
24
- processing.clear
25
- completed.clear
22
+ with_lock do
23
+ pending.merge!(examples_with_run_times)
24
+ end
26
25
 
27
26
  ok
28
27
  end
@@ -14,10 +14,6 @@ module Specwrk
14
14
 
15
15
  private
16
16
 
17
- def skip_lock
18
- true
19
- end
20
-
21
17
  def interupt!
22
18
  Thread.new do
23
19
  # give the socket a moment to flush the response
@@ -18,7 +18,10 @@ module Specwrk
18
18
  dur_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(4)
19
19
 
20
20
  remote = env["REMOTE_ADDR"] || env["REMOTE_HOST"] || "-"
21
- @out.puts "#{remote} [#{start_time.iso8601(6)}] #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]} → #{status} (#{dur_ms}ms)"
21
+ worker_parts = [env["HTTP_X_SPECWRK_RUN"], env["HTTP_X_SPECWRK_ID"]].compact
22
+ context = " #{worker_parts.join(" ")}" unless worker_parts.empty?
23
+
24
+ @out.puts "#{remote} [#{start_time.iso8601(6)}] #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}#{context} → #{status} (#{dur_ms}ms)"
22
25
  [status, headers, body]
23
26
  end
24
27
  end
@@ -54,6 +54,7 @@ module Specwrk
54
54
  end
55
55
  end
56
56
 
57
+ executor.flush_log
57
58
  executor.final_output.tap(&:rewind).each_line { |line| final_output.write line }
58
59
 
59
60
  @heartbeat_thread.kill
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.16.2
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Westendorf
@@ -209,9 +209,15 @@ files:
209
209
  - lib/specwrk/list_examples.rb
210
210
  - lib/specwrk/seed_loop.rb
211
211
  - lib/specwrk/store.rb
212
+ - lib/specwrk/store/base.rb
212
213
  - lib/specwrk/store/base_adapter.rb
214
+ - lib/specwrk/store/bucket_store.rb
215
+ - lib/specwrk/store/completed_store.rb
213
216
  - lib/specwrk/store/file_adapter.rb
214
217
  - lib/specwrk/store/memory_adapter.rb
218
+ - lib/specwrk/store/pending_store.rb
219
+ - lib/specwrk/store/processing_store.rb
220
+ - lib/specwrk/store/worker_store.rb
215
221
  - lib/specwrk/version.rb
216
222
  - lib/specwrk/watcher.rb
217
223
  - lib/specwrk/web.rb