specwrk 0.7.1 → 0.8.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/docker/Dockerfile.server +7 -11
- data/docker/entrypoint.server.sh +2 -2
- data/docker/pitchfork.conf +5 -0
- data/lib/specwrk/queue.rb +82 -38
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/app.rb +26 -0
- data/lib/specwrk/web/auth.rb +1 -1
- data/lib/specwrk/web/endpoints.rb +55 -61
- data/lib/specwrk/web.rb +12 -4
- metadata +2 -2
- data/lib/specwrk/filestore.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2122e3e3925fdf1c12b6d33899c52ba1b8287e46e7518754ae0ca8188ac149ce
|
4
|
+
data.tar.gz: 89579242f18a5f17aac60ac52664e1197cf61ef35228d142298b4ad250057a25
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c357e649df3ad4533290c7e53be49d1f4a3bf86870b5cc01ed4bc2219393bc026d4056e65c82b21c9f251724fe3adcebf6efda7a2643b85d873e4ede7efe17d3
|
7
|
+
data.tar.gz: 1aea05a1f33cd4bfeddbbe76d7d97c7dd3535ef4d1c24b5116042bcfe37201acc7a70c0839a6ef28ba9a5ecd9380535a7007c8a118e5277fc8d798899d7f28e1
|
data/docker/Dockerfile.server
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
FROM ruby:3.4-alpine
|
2
2
|
|
3
|
-
RUN apk add --no-cache
|
4
|
-
build-base \
|
5
|
-
ruby-dev \
|
6
|
-
linux-headers \
|
7
|
-
zlib-dev \
|
8
|
-
libffi-dev
|
3
|
+
RUN apk add --no-cache build-base
|
9
4
|
|
10
5
|
WORKDIR /app
|
11
6
|
|
@@ -13,14 +8,15 @@ RUN mkdir .specwrk/
|
|
13
8
|
|
14
9
|
ARG SPECWRK_SRV_PORT=5138
|
15
10
|
ARG SPECWRK_VERSION=latest
|
16
|
-
ARG
|
11
|
+
ARG GEM_FILE=specwrk-$SPECWRK_VERSION.gem
|
17
12
|
|
18
|
-
COPY $
|
19
|
-
RUN gem install ./$
|
20
|
-
RUN rm ./$
|
13
|
+
COPY $GEM_FILE ./
|
14
|
+
RUN gem install ./$GEM_FILE --no-document
|
15
|
+
RUN rm ./$GEM_FILE
|
21
16
|
|
22
|
-
RUN gem install
|
17
|
+
RUN gem install pitchfork thruster
|
23
18
|
COPY config.ru ./
|
19
|
+
COPY docker/pitchfork.conf ./
|
24
20
|
|
25
21
|
COPY docker/entrypoint.server.sh /usr/local/bin/entrypoint
|
26
22
|
RUN chmod +x /usr/local/bin/entrypoint
|
data/docker/entrypoint.server.sh
CHANGED
@@ -2,6 +2,6 @@
|
|
2
2
|
|
3
3
|
export THRUSTER_HTTP_PORT=${PORT:-5138}
|
4
4
|
export THRUSTER_TARGET_PORT=3000
|
5
|
-
export THRUSTER_HTTP_IDLE_TIMEOUT=${IDLE_TIMEOUT:-
|
5
|
+
export THRUSTER_HTTP_IDLE_TIMEOUT=${IDLE_TIMEOUT:-300}
|
6
6
|
|
7
|
-
exec thrust
|
7
|
+
exec thrust pitchfork -c pitchfork.conf
|
data/lib/specwrk/queue.rb
CHANGED
@@ -4,7 +4,44 @@ require "time"
|
|
4
4
|
require "json"
|
5
5
|
|
6
6
|
module Specwrk
|
7
|
-
|
7
|
+
# Thread-safe Hash access
|
8
|
+
class Queue
|
9
|
+
attr_reader :created_at
|
10
|
+
|
11
|
+
def initialize(hash = {})
|
12
|
+
@created_at = Time.now
|
13
|
+
|
14
|
+
if block_given?
|
15
|
+
@mutex = Monitor.new # Reentrant locking is required here
|
16
|
+
# It's possible to enter the proc from two threads, so we need to ||= in case
|
17
|
+
# one thread has set a value prior to the yield.
|
18
|
+
hash.default_proc = proc { |h, key| @mutex.synchronize { yield(h, key) } }
|
19
|
+
end
|
20
|
+
|
21
|
+
@mutex ||= Mutex.new # Monitor is up-to 20% slower than Mutex, so if no block is given, use a mutex
|
22
|
+
@hash = hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def synchronize(&blk)
|
26
|
+
if @mutex.owned?
|
27
|
+
yield(@hash)
|
28
|
+
else
|
29
|
+
@mutex.synchronize { yield(@hash) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(name, *args, &block)
|
34
|
+
if @hash.respond_to?(name)
|
35
|
+
@mutex.synchronize { @hash.public_send(name, *args, &block) }
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond_to_missing?(name, include_private = false)
|
42
|
+
@hash.respond_to?(name, include_private) || super
|
43
|
+
end
|
44
|
+
end
|
8
45
|
|
9
46
|
class PendingQueue < Queue
|
10
47
|
def shift_bucket
|
@@ -44,12 +81,12 @@ module Specwrk
|
|
44
81
|
end
|
45
82
|
|
46
83
|
def merge_with_previous_run_times!(h2)
|
47
|
-
|
84
|
+
synchronize do
|
85
|
+
h2.each { |_id, example| merge_example(example) }
|
48
86
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
merge!(new_h)
|
87
|
+
# Sort by exepcted run time, slowest to fastest
|
88
|
+
@hash = @hash.sort_by { |_, example| example[:expected_run_time] }.reverse.to_h
|
89
|
+
end
|
53
90
|
end
|
54
91
|
|
55
92
|
private
|
@@ -67,15 +104,17 @@ module Specwrk
|
|
67
104
|
def bucket_by_file
|
68
105
|
bucket = []
|
69
106
|
|
70
|
-
|
71
|
-
|
107
|
+
@mutex.synchronize do
|
108
|
+
key = @hash.keys.first
|
109
|
+
break if key.nil?
|
72
110
|
|
73
|
-
|
74
|
-
|
75
|
-
|
111
|
+
file_path = @hash[key][:file_path]
|
112
|
+
@hash.each do |id, example|
|
113
|
+
next unless example[:file_path] == file_path
|
76
114
|
|
77
|
-
|
78
|
-
|
115
|
+
bucket << example
|
116
|
+
@hash.delete id
|
117
|
+
end
|
79
118
|
end
|
80
119
|
|
81
120
|
bucket
|
@@ -85,27 +124,30 @@ module Specwrk
|
|
85
124
|
def bucket_by_timings
|
86
125
|
bucket = []
|
87
126
|
|
88
|
-
|
127
|
+
@mutex.synchronize do
|
128
|
+
estimated_run_time_total = 0
|
89
129
|
|
90
|
-
|
91
|
-
|
92
|
-
|
130
|
+
while estimated_run_time_total < run_time_bucket_threshold
|
131
|
+
key = @hash.keys.first
|
132
|
+
break if key.nil?
|
93
133
|
|
94
|
-
|
95
|
-
|
134
|
+
estimated_run_time_total += @hash.dig(key, :expected_run_time)
|
135
|
+
break if estimated_run_time_total > run_time_bucket_threshold && bucket.length.positive?
|
96
136
|
|
97
|
-
|
98
|
-
|
137
|
+
bucket << @hash[key]
|
138
|
+
@hash.delete key
|
139
|
+
end
|
99
140
|
end
|
100
141
|
|
101
142
|
bucket
|
102
143
|
end
|
103
144
|
|
145
|
+
# Ensure @mutex is held when calling this method
|
104
146
|
def merge_example(example)
|
105
|
-
return if key? example[:id]
|
106
|
-
return if key? example[:file_path]
|
147
|
+
return if @hash.key? example[:id]
|
148
|
+
return if @hash.key? example[:file_path]
|
107
149
|
|
108
|
-
|
150
|
+
@hash[example[:id]] = if previous_run_times
|
109
151
|
example.merge!(
|
110
152
|
expected_run_time: previous_run_times.dig(:examples, example[:id].to_sym, :run_time) || 99999.9 # run "unknown" files first
|
111
153
|
)
|
@@ -123,24 +165,26 @@ module Specwrk
|
|
123
165
|
end
|
124
166
|
|
125
167
|
def dump
|
126
|
-
@
|
127
|
-
|
128
|
-
|
168
|
+
@mutex.synchronize do
|
169
|
+
@run_times = []
|
170
|
+
@first_started_at = Time.new(2999, 1, 1, 0, 0, 0) # TODO: Make future proof /s
|
171
|
+
@last_finished_at = Time.new(1900, 1, 1, 0, 0, 0)
|
129
172
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
173
|
+
@output = {
|
174
|
+
file_totals: Hash.new { |h, filename| h[filename] = 0.0 },
|
175
|
+
meta: {failures: 0, passes: 0, pending: 0},
|
176
|
+
examples: {}
|
177
|
+
}
|
135
178
|
|
136
|
-
|
179
|
+
@hash.values.each { |example| calculate(example) }
|
137
180
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
181
|
+
@output[:meta][:total_run_time] = @run_times.sum
|
182
|
+
@output[:meta][:average_run_time] = @output[:meta][:total_run_time] / [@run_times.length, 1].max.to_f
|
183
|
+
@output[:meta][:first_started_at] = @first_started_at.iso8601(6)
|
184
|
+
@output[:meta][:last_finished_at] = @last_finished_at.iso8601(6)
|
142
185
|
|
143
|
-
|
186
|
+
@output
|
187
|
+
end
|
144
188
|
end
|
145
189
|
|
146
190
|
private
|
data/lib/specwrk/version.rb
CHANGED
data/lib/specwrk/web/app.rb
CHANGED
@@ -21,6 +21,8 @@ require "specwrk/web/endpoints"
|
|
21
21
|
module Specwrk
|
22
22
|
class Web
|
23
23
|
class App
|
24
|
+
REAP_INTERVAL = 330 # HTTP connection timeout + some buffer
|
25
|
+
|
24
26
|
class << self
|
25
27
|
def run!
|
26
28
|
Process.setproctitle "specwrk-server"
|
@@ -72,6 +74,10 @@ module Specwrk
|
|
72
74
|
end
|
73
75
|
end
|
74
76
|
|
77
|
+
def initialize
|
78
|
+
@reaper_thread = Thread.new { reaper } unless ENV["SPECWRK_SRV_SINGLE_RUN"]
|
79
|
+
end
|
80
|
+
|
75
81
|
def call(env)
|
76
82
|
env[:request] ||= Rack::Request.new(env)
|
77
83
|
|
@@ -100,6 +106,26 @@ module Specwrk
|
|
100
106
|
Endpoints::NotFound
|
101
107
|
end
|
102
108
|
end
|
109
|
+
|
110
|
+
def reaper
|
111
|
+
until Specwrk.force_quit
|
112
|
+
sleep REAP_INTERVAL
|
113
|
+
|
114
|
+
reap
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def reap
|
119
|
+
Web::WORKERS.each do |run, workers|
|
120
|
+
most_recent_last_seen_at = workers.map { |id, worker| worker[:last_seen_at] }.max
|
121
|
+
next unless most_recent_last_seen_at
|
122
|
+
|
123
|
+
# Don't consider runs which aren't at least REAP_INTERVAL sec stale
|
124
|
+
if most_recent_last_seen_at < Time.now - REAP_INTERVAL
|
125
|
+
Web.clear_run_queues(run)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
103
129
|
end
|
104
130
|
end
|
105
131
|
end
|
data/lib/specwrk/web/auth.rb
CHANGED
@@ -6,40 +6,14 @@ module Specwrk
|
|
6
6
|
class Web
|
7
7
|
module Endpoints
|
8
8
|
class Base
|
9
|
-
attr_reader :pending_queue, :processing_queue, :completed_queue, :workers, :started_at
|
10
|
-
|
11
9
|
def initialize(request)
|
12
10
|
@request = request
|
13
|
-
end
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
datastore.with_lock do |db|
|
19
|
-
@started_at = if db[:started_at]
|
20
|
-
Time.parse(db[:started_at])
|
21
|
-
else
|
22
|
-
db[:started_at] = Time.now
|
23
|
-
end
|
24
|
-
|
25
|
-
@pending_queue = PendingQueue.new.merge!(db[:pending] || {})
|
26
|
-
@processing_queue = Queue.new.merge!(db[:processing] || {})
|
27
|
-
@completed_queue = CompletedQueue.new.merge!(db[:completed] || {})
|
28
|
-
@workers = db[:workers] ||= {}
|
29
|
-
|
30
|
-
worker[:first_seen_at] ||= Time.now
|
31
|
-
worker[:last_seen_at] = Time.now
|
32
|
-
|
33
|
-
with_response.tap do
|
34
|
-
db[:pending] = pending_queue.to_h
|
35
|
-
db[:processing] = processing_queue.to_h
|
36
|
-
db[:completed] = completed_queue.to_h
|
37
|
-
db[:workers] = workers.to_h
|
38
|
-
end
|
39
|
-
end
|
12
|
+
worker[:first_seen_at] ||= Time.now
|
13
|
+
worker[:last_seen_at] = Time.now
|
40
14
|
end
|
41
15
|
|
42
|
-
def
|
16
|
+
def response
|
43
17
|
not_found
|
44
18
|
end
|
45
19
|
|
@@ -48,11 +22,11 @@ module Specwrk
|
|
48
22
|
attr_reader :request
|
49
23
|
|
50
24
|
def not_found
|
51
|
-
[404, {"
|
25
|
+
[404, {"content-type" => "text/plain"}, ["This is not the path you're looking for, 'ol chap..."]]
|
52
26
|
end
|
53
27
|
|
54
28
|
def ok
|
55
|
-
[200, {"
|
29
|
+
[200, {"content-type" => "text/plain"}, ["OK, 'ol chap"]]
|
56
30
|
end
|
57
31
|
|
58
32
|
def payload
|
@@ -63,8 +37,24 @@ module Specwrk
|
|
63
37
|
@body ||= request.body.read
|
64
38
|
end
|
65
39
|
|
40
|
+
def pending_queue
|
41
|
+
Web::PENDING_QUEUES[run_id]
|
42
|
+
end
|
43
|
+
|
44
|
+
def processing_queue
|
45
|
+
Web::PROCESSING_QUEUES[request.get_header("HTTP_X_SPECWRK_RUN")]
|
46
|
+
end
|
47
|
+
|
48
|
+
def completed_queue
|
49
|
+
Web::COMPLETED_QUEUES[request.get_header("HTTP_X_SPECWRK_RUN")]
|
50
|
+
end
|
51
|
+
|
52
|
+
def workers
|
53
|
+
Web::WORKERS[request.get_header("HTTP_X_SPECWRK_RUN")]
|
54
|
+
end
|
55
|
+
|
66
56
|
def worker
|
67
|
-
workers[request.get_header("HTTP_X_SPECWRK_ID")]
|
57
|
+
workers[request.get_header("HTTP_X_SPECWRK_ID")]
|
68
58
|
end
|
69
59
|
|
70
60
|
def run_id
|
@@ -72,11 +62,7 @@ module Specwrk
|
|
72
62
|
end
|
73
63
|
|
74
64
|
def run_report_file_path
|
75
|
-
@run_report_file_path ||= File.join(ENV["SPECWRK_OUT"],
|
76
|
-
end
|
77
|
-
|
78
|
-
def datastore
|
79
|
-
Web.datastore[File.join(ENV["SPECWRK_OUT"], run_id, "queues.json").to_s]
|
65
|
+
@run_report_file_path ||= File.join(ENV["SPECWRK_OUT"], "#{completed_queue.created_at.strftime("%Y%m%dT%H%M%S")}-report-#{run_id}.json").to_s
|
80
66
|
end
|
81
67
|
end
|
82
68
|
|
@@ -84,22 +70,27 @@ module Specwrk
|
|
84
70
|
NotFound = Class.new(Base)
|
85
71
|
|
86
72
|
class Health < Base
|
87
|
-
def
|
73
|
+
def response
|
88
74
|
[200, {}, []]
|
89
75
|
end
|
90
76
|
end
|
91
77
|
|
92
78
|
class Heartbeat < Base
|
93
|
-
def
|
79
|
+
def response
|
94
80
|
ok
|
95
81
|
end
|
96
82
|
end
|
97
83
|
|
98
84
|
class Seed < Base
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
|
85
|
+
def response
|
86
|
+
pending_queue.synchronize do |pending_queue_hash|
|
87
|
+
unless ENV["SPECWRK_SRV_SINGLE_SEED_PER_RUN"] && pending_queue_hash.length.positive?
|
88
|
+
examples = payload.map { |hash| [hash[:id], hash] }.to_h
|
89
|
+
|
90
|
+
pending_queue.merge_with_previous_run_times!(examples)
|
91
|
+
|
92
|
+
ok
|
93
|
+
end
|
103
94
|
end
|
104
95
|
|
105
96
|
ok
|
@@ -107,15 +98,16 @@ module Specwrk
|
|
107
98
|
end
|
108
99
|
|
109
100
|
class Complete < Base
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
|
101
|
+
def response
|
102
|
+
processing_queue.synchronize do |processing_queue_hash|
|
103
|
+
payload.each do |example|
|
104
|
+
next unless processing_queue_hash.delete(example[:id])
|
105
|
+
completed_queue[example[:id]] = example
|
106
|
+
end
|
114
107
|
end
|
115
108
|
|
116
109
|
if pending_queue.length.zero? && processing_queue.length.zero? && completed_queue.length.positive? && ENV["SPECWRK_OUT"]
|
117
110
|
completed_queue.dump_and_write(run_report_file_path)
|
118
|
-
FileUtils.ln_sf(run_report_file_path, File.join(ENV["SPECWRK_OUT"], "report.json"))
|
119
111
|
end
|
120
112
|
|
121
113
|
ok
|
@@ -123,19 +115,21 @@ module Specwrk
|
|
123
115
|
end
|
124
116
|
|
125
117
|
class Pop < Base
|
126
|
-
def
|
127
|
-
|
118
|
+
def response
|
119
|
+
processing_queue.synchronize do |processing_queue_hash|
|
120
|
+
@examples = pending_queue.shift_bucket
|
128
121
|
|
129
|
-
|
130
|
-
|
122
|
+
@examples.each do |example|
|
123
|
+
processing_queue_hash[example[:id]] = example
|
124
|
+
end
|
131
125
|
end
|
132
126
|
|
133
127
|
if @examples.length.positive?
|
134
|
-
[200, {"
|
128
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(@examples)]]
|
135
129
|
elsif pending_queue.length.zero? && processing_queue.length.zero? && completed_queue.length.zero?
|
136
|
-
[204, {"
|
130
|
+
[204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
|
137
131
|
elsif completed_queue.length.positive? && processing_queue.length.zero?
|
138
|
-
[410, {"
|
132
|
+
[410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
139
133
|
else
|
140
134
|
not_found
|
141
135
|
end
|
@@ -143,11 +137,11 @@ module Specwrk
|
|
143
137
|
end
|
144
138
|
|
145
139
|
class Report < Base
|
146
|
-
def
|
140
|
+
def response
|
147
141
|
if data
|
148
|
-
[200, {"
|
142
|
+
[200, {"content-type" => "application/json"}, [data]]
|
149
143
|
else
|
150
|
-
[404, {"
|
144
|
+
[404, {"content-type" => "text/plain"}, ["Unable to report on run #{run_id}; no file matching #{"*-report-#{run_id}.json"}"]]
|
151
145
|
end
|
152
146
|
end
|
153
147
|
|
@@ -166,15 +160,15 @@ module Specwrk
|
|
166
160
|
end
|
167
161
|
|
168
162
|
def most_recent_run_report_file
|
169
|
-
@most_recent_run_report_file ||= Dir.glob(File.join(ENV["SPECWRK_OUT"],
|
163
|
+
@most_recent_run_report_file ||= Dir.glob(File.join(ENV["SPECWRK_OUT"], "*-report-#{run_id}.json")).last
|
170
164
|
end
|
171
165
|
end
|
172
166
|
|
173
167
|
class Shutdown < Base
|
174
|
-
def
|
168
|
+
def response
|
175
169
|
interupt! if ENV["SPECWRK_SRV_SINGLE_RUN"]
|
176
170
|
|
177
|
-
[200, {"
|
171
|
+
[200, {"content-type" => "text/plain"}, ["✌️"]]
|
178
172
|
end
|
179
173
|
|
180
174
|
def interupt!
|
data/lib/specwrk/web.rb
CHANGED
@@ -1,13 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "specwrk/queue"
|
4
|
-
require "specwrk/filestore"
|
5
4
|
|
6
5
|
module Specwrk
|
7
6
|
class Web
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
PENDING_QUEUES = Queue.new { |h, key| h[key] = PendingQueue.new }
|
8
|
+
PROCESSING_QUEUES = Queue.new { |h, key| h[key] = Queue.new }
|
9
|
+
COMPLETED_QUEUES = Queue.new { |h, key| h[key] = CompletedQueue.new }
|
10
|
+
WORKERS = Hash.new { |h, key| h[key] = Hash.new { |h, key| h[key] = {} } }
|
11
|
+
|
12
|
+
def self.clear_queues
|
13
|
+
[PENDING_QUEUES, PROCESSING_QUEUES, COMPLETED_QUEUES, WORKERS].each(&:clear)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.clear_run_queues(run)
|
17
|
+
[PENDING_QUEUES, PROCESSING_QUEUES, COMPLETED_QUEUES, WORKERS].each do |queue|
|
18
|
+
queue.delete(run)
|
11
19
|
end
|
12
20
|
end
|
13
21
|
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.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Westendorf
|
@@ -152,6 +152,7 @@ files:
|
|
152
152
|
- config.ru
|
153
153
|
- docker/Dockerfile.server
|
154
154
|
- docker/entrypoint.server.sh
|
155
|
+
- docker/pitchfork.conf
|
155
156
|
- examples/circleci/config.yml
|
156
157
|
- examples/github/specwrk-multi-node.yml
|
157
158
|
- examples/github/specwrk-single-node.yml
|
@@ -160,7 +161,6 @@ files:
|
|
160
161
|
- lib/specwrk/cli.rb
|
161
162
|
- lib/specwrk/cli_reporter.rb
|
162
163
|
- lib/specwrk/client.rb
|
163
|
-
- lib/specwrk/filestore.rb
|
164
164
|
- lib/specwrk/hookable.rb
|
165
165
|
- lib/specwrk/list_examples.rb
|
166
166
|
- lib/specwrk/queue.rb
|
data/lib/specwrk/filestore.rb
DELETED
@@ -1,64 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "fileutils"
|
4
|
-
|
5
|
-
module Specwrk
|
6
|
-
class Filestore
|
7
|
-
@mutexes = {}
|
8
|
-
@mutexes_mutex = Mutex.new # 🐢🐢🐢🐢
|
9
|
-
|
10
|
-
class << self
|
11
|
-
def [](path)
|
12
|
-
new(path)
|
13
|
-
end
|
14
|
-
|
15
|
-
def mutex_for(path)
|
16
|
-
@mutexes_mutex.synchronize do
|
17
|
-
@mutexes[path] ||= Mutex.new
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def initialize(path)
|
23
|
-
@path = path
|
24
|
-
@tmpfile_path = @path + ".tmp"
|
25
|
-
@lock_path = @path + ".lock"
|
26
|
-
|
27
|
-
FileUtils.mkdir_p File.dirname(@path)
|
28
|
-
File.open(@path, "a") {} # multi-process and multi-thread safe touch
|
29
|
-
end
|
30
|
-
|
31
|
-
def with_lock
|
32
|
-
self.class.mutex_for(@path).synchronize do
|
33
|
-
lock_file.flock(File::LOCK_EX)
|
34
|
-
|
35
|
-
hash = read
|
36
|
-
result = yield(hash)
|
37
|
-
|
38
|
-
# Will truncate if already exists
|
39
|
-
File.open(@tmpfile_path, "w") do |tmpfile|
|
40
|
-
tmpfile.write(hash.to_json)
|
41
|
-
tmpfile.fsync
|
42
|
-
tmpfile.close
|
43
|
-
end
|
44
|
-
|
45
|
-
File.rename(@tmpfile_path, @path)
|
46
|
-
result
|
47
|
-
ensure
|
48
|
-
lock_file.flock(File::LOCK_UN)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def lock_file
|
55
|
-
@lock_file ||= File.open(@lock_path, "w")
|
56
|
-
end
|
57
|
-
|
58
|
-
def read
|
59
|
-
JSON.parse(File.read(@path), symbolize_names: true)
|
60
|
-
rescue JSON::ParserError
|
61
|
-
{}
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|