specwrk 0.16.3 → 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 +4 -4
- data/Specwrk.watchfile.rb +1 -1
- data/lib/specwrk/store/base.rb +107 -0
- data/lib/specwrk/store/bucket_store.rb +33 -0
- data/lib/specwrk/store/completed_store.rb +53 -0
- data/lib/specwrk/store/pending_store.rb +168 -0
- data/lib/specwrk/store/processing_store.rb +8 -0
- data/lib/specwrk/store/worker_store.rb +47 -0
- data/lib/specwrk/store.rb +6 -324
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/endpoints/base.rb +8 -28
- data/lib/specwrk/web/endpoints/complete_and_pop.rb +9 -9
- data/lib/specwrk/web/endpoints/health.rb +0 -6
- data/lib/specwrk/web/endpoints/heartbeat.rb +0 -6
- data/lib/specwrk/web/endpoints/popable.rb +10 -4
- data/lib/specwrk/web/endpoints/report.rb +0 -6
- data/lib/specwrk/web/endpoints/seed.rb +5 -6
- data/lib/specwrk/web/endpoints/shutdown.rb +0 -4
- data/lib/specwrk/worker.rb +1 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ef3257927cc46b8361f2fe0ee98db82890b5f8a25882045e122317c2b223ccd
|
|
4
|
+
data.tar.gz: 8dac946fb5a4e8e673e4fef15eab5b4f16e1ffb4959688c1c6af967b9c06b34d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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,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 "
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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"
|
data/lib/specwrk/version.rb
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
75
|
-
#
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
@@ -6,23 +6,22 @@ module Specwrk
|
|
|
6
6
|
class Web
|
|
7
7
|
module Endpoints
|
|
8
8
|
class Seed < Base
|
|
9
|
-
def
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
with_lock do
|
|
23
|
+
pending.merge!(examples_with_run_times)
|
|
24
|
+
end
|
|
26
25
|
|
|
27
26
|
ok
|
|
28
27
|
end
|
data/lib/specwrk/worker.rb
CHANGED
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.
|
|
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
|