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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6f2753749c4fdc2460290c8ead228965ba766624fe19b52119e6769f682a7681
|
4
|
+
data.tar.gz: 0b773de7c6366e102fc1299630e9aeb983f8284c75f392fd733dc6a8fc714af8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0014b21f279efc09cfd4edf3e5cc5b472ac46096966bc9f487ceeed3f1d1d20dfcf567122a13a594792e98fb33536bd8e51310814a38af96c9f6c3bf628372f0
|
7
|
+
data.tar.gz: 066dfcb0decca0dd8a3cb10b73acbcd6f791ff16ec1a9e2ca8ea4a56d95a5b2d23a56e88ff95dc5780c5fa3b529ea122edfb2b82fcb56c9e6b02a59545fd2a28
|
data/.rspec
ADDED
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Specwrk
|
2
|
+
TODO!
|
3
|
+
|
4
|
+
## Contributing
|
5
|
+
|
6
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dwestendorf/specwrk.
|
7
|
+
|
8
|
+
## License
|
9
|
+
|
10
|
+
The gem is available as open source under the terms of the [LGLPv3 License](http://www.gnu.org/licenses/lgpl-3.0.html).
|
data/Rakefile
ADDED
data/exe/specwrk
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "specwrk"
|
4
|
+
require "specwrk/cli"
|
5
|
+
|
6
|
+
trap("INT") do
|
7
|
+
if Specwrk.starting_pid == Process.pid && !Specwrk.force_quit
|
8
|
+
warn "Waiting for in-progress work to finish. Interrupt again to force quit (warning: at_exit hooks will be skipped if you force quit)."
|
9
|
+
|
10
|
+
Specwrk.force_quit = true
|
11
|
+
elsif Specwrk.starting_pid != Process.pid
|
12
|
+
exit(1) if Specwrk.force_quit
|
13
|
+
Specwrk.force_quit = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Dry::CLI.new(Specwrk::CLI).call
|
data/lib/specwrk/cli.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/cli"
|
4
|
+
|
5
|
+
require "specwrk"
|
6
|
+
require "specwrk/hookable"
|
7
|
+
|
8
|
+
module Specwrk
|
9
|
+
module CLI
|
10
|
+
extend Dry::CLI::Registry
|
11
|
+
|
12
|
+
module Clientable
|
13
|
+
extend Hookable
|
14
|
+
|
15
|
+
on_included do |base|
|
16
|
+
base.option :uri, type: :string, default: ENV.fetch("SPECWRK_SRV_URI", "https://localhost:#{ENV.fetch("SPECWRK_SRV_PORT", "5138")}"), desc: "HTTP URI of the server to pull jobs from"
|
17
|
+
base.option :key, type: :string, default: ENV.fetch("SPECWRK_SRV_KEY", ""), aliases: ["-k"], desc: "Authentication key for accessing the server"
|
18
|
+
base.option :run, type: :string, default: ENV.fetch("SPECWRK_SRV_KEY", "main"), aliases: ["-r"], desc: "The run identifier for this job execution"
|
19
|
+
base.option :timeout, type: :integer, default: ENV.fetch("SPECWRK_TIMEOUT", "5"), aliases: ["-t"], desc: "The amount of time to wait for the server to respond"
|
20
|
+
end
|
21
|
+
|
22
|
+
on_setup do |uri:, key:, run:, timeout:, **|
|
23
|
+
ENV["SPECWRK_SRV_URI"] = uri
|
24
|
+
ENV["SPECWRK_SRV_KEY"] = key
|
25
|
+
ENV["SPECWRK_RUN"] = run
|
26
|
+
ENV["SPECWRK_TIMEOUT"] = timeout
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Workable
|
31
|
+
extend Hookable
|
32
|
+
|
33
|
+
on_included do |base|
|
34
|
+
base.option :id, type: :string, default: "specwrk-worker", desc: "The identifier for this worker"
|
35
|
+
base.option :count, type: :integer, default: 1, aliases: ["-c"], desc: "The number of worker processes you want to start"
|
36
|
+
base.option :output, type: :string, default: ENV.fetch("SPECWRK_OUT", ".specwrk/"), aliases: ["-o"], desc: "Directory where worker output is stored"
|
37
|
+
end
|
38
|
+
|
39
|
+
on_setup do |id:, count:, output:, **|
|
40
|
+
ENV["SPECWRK_ID"] = id
|
41
|
+
ENV["SPECWRK_COUNT"] = count.to_s
|
42
|
+
ENV["SPECWRK_OUT"] = Pathname.new(output).expand_path(Dir.pwd).to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
def start_workers
|
46
|
+
@worker_pids = worker_count.times.map do |i|
|
47
|
+
Process.fork do
|
48
|
+
ENV["TEST_ENV_NUMBER"] = ENV["SPECWRK_FORKED"] = (i + 1).to_s
|
49
|
+
ENV["SPECWRK_ID"] = ENV["SPECWRK_ID"] + "-#{i + 1}"
|
50
|
+
|
51
|
+
require "specwrk/worker"
|
52
|
+
|
53
|
+
Specwrk::Worker.run!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def worker_count
|
59
|
+
@worker_count ||= [1, ENV["SPECWRK_COUNT"].to_i].max
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module Servable
|
64
|
+
extend Hookable
|
65
|
+
|
66
|
+
on_included do |base|
|
67
|
+
base.option :port, type: :integer, default: ENV.fetch("SPECWRK_SRV_PORT", "5138"), aliases: ["-p"], desc: "Server port"
|
68
|
+
base.option :key, type: :string, aliases: ["-k"], default: ENV.fetch("SPECWRK_SRV_KEY", ""), desc: "Authentication key clients must use for access"
|
69
|
+
base.option :output, type: :string, default: ENV.fetch("SPECWRK_OUT", ".specwrk/"), aliases: ["-o"], desc: "Directory where worker output is stored"
|
70
|
+
base.option :single_run, type: :boolean, default: !ENV["SPECWRK_SRV_SINGLE_RUN"].nil?, desc: "Act on shutdown requests from clients"
|
71
|
+
base.option :group_by, values: %w[file timings], default: ENV.fetch("SPECWERK_SRV_GROUP_BY", "timings"), desc: "How examples will be grouped for workers; fallback to file if no timings are found"
|
72
|
+
end
|
73
|
+
|
74
|
+
on_setup do |output:, port:, key:, single_run:, group_by:, **|
|
75
|
+
ENV["SPECWRK_SRV_OUTPUT"] = Pathname.new(File.join(output, "report.json")).expand_path(Dir.pwd).to_s if output
|
76
|
+
ENV["SPECWRK_SRV_PORT"] = port
|
77
|
+
ENV["SPECWRK_SRV_KEY"] = key
|
78
|
+
ENV["SPECWRK_SRV_SINGLE_RUN"] = "1" if single_run
|
79
|
+
ENV["SPECWRK_SRV_GROUP_BY"] = group_by
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Version < Dry::CLI::Command
|
84
|
+
desc "Print version"
|
85
|
+
|
86
|
+
def call(*)
|
87
|
+
puts VERSION
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Seed < Dry::CLI::Command
|
92
|
+
include Clientable
|
93
|
+
|
94
|
+
desc "Seed the server with a list of specs for the run"
|
95
|
+
|
96
|
+
argument :dir, required: false, default: "spec", desc: "Relative spec directory to run against"
|
97
|
+
|
98
|
+
def call(dir:, **args)
|
99
|
+
self.class.setup(**args)
|
100
|
+
|
101
|
+
require "specwrk/list_examples"
|
102
|
+
require "specwrk/client"
|
103
|
+
|
104
|
+
examples = ListExamples.new(dir).examples
|
105
|
+
|
106
|
+
Client.wait_for_server!
|
107
|
+
Client.new.seed(examples)
|
108
|
+
rescue Errno::ECONNREFUSED
|
109
|
+
puts "Server at #{ENV.fetch("SPECWRK_SRV_URI", "http://localhost:5138")} is refusing connections, exiting...#{ENV["SPECWRK_FLUSH_DELIMINATOR"]}"
|
110
|
+
rescue Errno::ECONNRESET
|
111
|
+
puts "Server at #{ENV.fetch("SPECWRK_SRV_URI", "http://localhost:5138")} stopped responding to connections, exiting...#{ENV["SPECWRK_FLUSH_DELIMINATOR"]}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Work < Dry::CLI::Command
|
116
|
+
include Workable
|
117
|
+
include Clientable
|
118
|
+
|
119
|
+
desc "Start one or more worker processes"
|
120
|
+
|
121
|
+
def call(**args)
|
122
|
+
self.class.setup(**args)
|
123
|
+
|
124
|
+
start_workers
|
125
|
+
Process.waitall
|
126
|
+
|
127
|
+
require "specwrk/cli_reporter"
|
128
|
+
status = Specwrk::CLIReporter.new.report
|
129
|
+
|
130
|
+
exit(status)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Serve < Dry::CLI::Command
|
135
|
+
include Servable
|
136
|
+
|
137
|
+
desc "Start a server"
|
138
|
+
|
139
|
+
def call(**args)
|
140
|
+
self.class.setup(**args)
|
141
|
+
|
142
|
+
require "specwrk/web"
|
143
|
+
require "specwrk/web/app"
|
144
|
+
|
145
|
+
Specwrk::Web::App.run!
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class Start < Dry::CLI::Command
|
150
|
+
include Clientable
|
151
|
+
include Workable
|
152
|
+
include Servable
|
153
|
+
|
154
|
+
desc "Start a server and workers, monitor until complete"
|
155
|
+
argument :dir, required: false, default: "spec", desc: "Relative spec directory to run against"
|
156
|
+
|
157
|
+
def call(dir:, **args)
|
158
|
+
self.class.setup(**args)
|
159
|
+
$stdout.sync = true
|
160
|
+
|
161
|
+
web_pid = Process.fork do
|
162
|
+
require "specwrk/web"
|
163
|
+
require "specwrk/web/app"
|
164
|
+
|
165
|
+
ENV["SPECWRK_FORKED"] = "1"
|
166
|
+
ENV["SPECWRK_SRV_SINGLE_RUN"] = "1"
|
167
|
+
status "Starting queue server..."
|
168
|
+
Specwrk::Web::App.run!
|
169
|
+
end
|
170
|
+
|
171
|
+
seed_pid = Process.fork do
|
172
|
+
require "specwrk/list_examples"
|
173
|
+
require "specwrk/client"
|
174
|
+
|
175
|
+
examples = ListExamples.new(dir).examples
|
176
|
+
|
177
|
+
status "Waiting for server to respond..."
|
178
|
+
Client.wait_for_server!
|
179
|
+
status "Server responding ✓"
|
180
|
+
status "Seeding #{examples.length} examples..."
|
181
|
+
Client.new.seed(examples)
|
182
|
+
status "Samples seeded ✓"
|
183
|
+
end
|
184
|
+
|
185
|
+
wait_for_pids_exit([seed_pid])
|
186
|
+
|
187
|
+
status "Starting #{worker_count} workers..."
|
188
|
+
start_workers
|
189
|
+
|
190
|
+
status "#{worker_count} workers started ✓\n"
|
191
|
+
wait_for_pids_exit(@worker_pids)
|
192
|
+
|
193
|
+
require "specwrk/cli_reporter"
|
194
|
+
status = Specwrk::CLIReporter.new.report
|
195
|
+
|
196
|
+
wait_for_pids_exit([web_pid, seed_pid] + @worker_pids)
|
197
|
+
exit(status)
|
198
|
+
end
|
199
|
+
|
200
|
+
def wait_for_pids_exit(pids)
|
201
|
+
exited_pids = {}
|
202
|
+
|
203
|
+
loop do
|
204
|
+
pids.each do |pid|
|
205
|
+
next if exited_pids.key? pid
|
206
|
+
|
207
|
+
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
208
|
+
exited_pids[pid] = status.exitstatus if status&.exitstatus
|
209
|
+
rescue Errno::ECHILD
|
210
|
+
exited_pids[pid] = 0
|
211
|
+
end
|
212
|
+
|
213
|
+
break if exited_pids.keys.length == pids.length
|
214
|
+
sleep 0.1
|
215
|
+
end
|
216
|
+
|
217
|
+
exited_pids
|
218
|
+
end
|
219
|
+
|
220
|
+
def status(msg)
|
221
|
+
print "\e[2K\r#{msg}"
|
222
|
+
$stdout.flush
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
register "version", Version, aliases: ["v", "-v", "--version"]
|
227
|
+
register "work", Work, aliases: ["wrk", "twerk", "w"]
|
228
|
+
register "serve", Serve, aliases: ["srv", "s"]
|
229
|
+
register "seed", Seed
|
230
|
+
register "start", Start
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
require "specwrk/client"
|
6
|
+
|
7
|
+
require "rspec"
|
8
|
+
require "rspec/core/formatters/helpers"
|
9
|
+
require "rspec/core/formatters/console_codes"
|
10
|
+
|
11
|
+
module Specwrk
|
12
|
+
class CLIReporter
|
13
|
+
def report
|
14
|
+
return 1 unless Client.connect?
|
15
|
+
|
16
|
+
puts "\nFinished in #{total_duration} " \
|
17
|
+
"(total execution time of #{total_run_time})\n"
|
18
|
+
|
19
|
+
client.shutdown
|
20
|
+
|
21
|
+
if failure_count.positive?
|
22
|
+
puts colorizer.wrap(totals_line, :red)
|
23
|
+
1
|
24
|
+
elsif pending_count.positive?
|
25
|
+
puts colorizer.wrap(totals_line, :yellow)
|
26
|
+
0
|
27
|
+
else
|
28
|
+
puts colorizer.wrap(totals_line, :green)
|
29
|
+
0
|
30
|
+
end
|
31
|
+
rescue Specwrk::UnhandledResponseError
|
32
|
+
puts colorizer.wrap("No examples run.", :red)
|
33
|
+
1
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def totals_line
|
39
|
+
summary = RSpec::Core::Formatters::Helpers.pluralize(example_count, "example") +
|
40
|
+
", " + RSpec::Core::Formatters::Helpers.pluralize(failure_count, "failure")
|
41
|
+
summary += ", #{pending_count} pending" if pending_count > 0
|
42
|
+
|
43
|
+
summary
|
44
|
+
end
|
45
|
+
|
46
|
+
def stats
|
47
|
+
@stats ||= client.stats
|
48
|
+
end
|
49
|
+
|
50
|
+
def total_duration
|
51
|
+
Time.parse(stats.dig(:completed, :meta, :last_finished_at)) - Time.parse(stats.dig(:completed, :meta, :first_started_at))
|
52
|
+
end
|
53
|
+
|
54
|
+
def total_run_time
|
55
|
+
stats.dig(:completed, :meta, :total_run_time)
|
56
|
+
end
|
57
|
+
|
58
|
+
def failure_count
|
59
|
+
stats.dig(:completed, :meta, :failures)
|
60
|
+
end
|
61
|
+
|
62
|
+
def pending_count
|
63
|
+
stats.dig(:completed, :meta, :pending)
|
64
|
+
end
|
65
|
+
|
66
|
+
def example_count
|
67
|
+
stats.dig(:completed, :examples).length
|
68
|
+
end
|
69
|
+
|
70
|
+
def client
|
71
|
+
@client ||= Client.new
|
72
|
+
end
|
73
|
+
|
74
|
+
def colorizer
|
75
|
+
::RSpec::Core::Formatters::ConsoleCodes
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module Specwrk
|
8
|
+
class Client
|
9
|
+
def self.connect?
|
10
|
+
http = build_http
|
11
|
+
http.start
|
12
|
+
http.finish
|
13
|
+
|
14
|
+
true
|
15
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build_http
|
20
|
+
uri = URI(ENV.fetch("SPECWRK_SRV_URI", "http://localhost:5138"))
|
21
|
+
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
22
|
+
http.open_timeout = ENV.fetch("SPECWRK_TIMEOUT", "5").to_i
|
23
|
+
http.read_timeout = ENV.fetch("SPECWRK_TIMEOUT", "5").to_i
|
24
|
+
http.keep_alive_timeout = 300
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.wait_for_server!
|
29
|
+
timeout = Time.now + ENV.fetch("SPECWRK_TIMEOUT", "5").to_i
|
30
|
+
connected = false
|
31
|
+
|
32
|
+
until connected || Time.now > timeout
|
33
|
+
connected = connect?
|
34
|
+
sleep 0.1 unless connected
|
35
|
+
end
|
36
|
+
|
37
|
+
raise Errno::ECONNREFUSED unless connected
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :last_request_at
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@mutex = Mutex.new
|
44
|
+
@http = self.class.build_http
|
45
|
+
@http.start
|
46
|
+
end
|
47
|
+
|
48
|
+
def close
|
49
|
+
@mutex.synchronize { @http.finish }
|
50
|
+
end
|
51
|
+
|
52
|
+
def heartbeat
|
53
|
+
response = get "/heartbeat"
|
54
|
+
|
55
|
+
response.code == "200"
|
56
|
+
end
|
57
|
+
|
58
|
+
def stats
|
59
|
+
response = get "/stats"
|
60
|
+
|
61
|
+
if response.code == "200"
|
62
|
+
JSON.parse(response.body, symbolize_names: true)
|
63
|
+
else
|
64
|
+
raise UnhandledResponseError.new("#{response.code}: #{response.body}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def shutdown
|
69
|
+
response = delete "/shutdown"
|
70
|
+
|
71
|
+
if response.code == "200"
|
72
|
+
response.body
|
73
|
+
else
|
74
|
+
raise UnhandledResponseError.new("#{response.code}: #{response.body}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def fetch_examples
|
79
|
+
response = post "/pop"
|
80
|
+
|
81
|
+
case response.code
|
82
|
+
when "200"
|
83
|
+
JSON.parse(response.body, symbolize_names: true)
|
84
|
+
when "404"
|
85
|
+
raise NoMoreExamplesError
|
86
|
+
when "410"
|
87
|
+
raise CompletedAllExamplesError
|
88
|
+
else
|
89
|
+
raise UnhandledResponseError.new("#{response.code}: #{response.body}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def complete_examples(examples)
|
94
|
+
response = post "/complete", body: examples.to_json
|
95
|
+
|
96
|
+
(response.code == "200") ? true : UnhandledResponseError.new("#{response.code}: #{response.body}")
|
97
|
+
end
|
98
|
+
|
99
|
+
def seed(examples)
|
100
|
+
response = post "/seed", body: examples.to_json
|
101
|
+
|
102
|
+
(response.code == "200") ? true : UnhandledResponseError.new("#{response.code}: #{response.body}")
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def get(path, headers: default_headers, body: nil)
|
108
|
+
request = Net::HTTP::Get.new(path, headers)
|
109
|
+
request.body = body if body
|
110
|
+
|
111
|
+
make_request(request)
|
112
|
+
end
|
113
|
+
|
114
|
+
def post(path, headers: default_headers, body: nil)
|
115
|
+
request = Net::HTTP::Post.new(path, headers)
|
116
|
+
request.body = body if body
|
117
|
+
|
118
|
+
make_request(request)
|
119
|
+
end
|
120
|
+
|
121
|
+
def put(path, headers: default_headers, body: nil)
|
122
|
+
request = Net::HTTP::Put.new(path, headers)
|
123
|
+
request.body = body if body
|
124
|
+
|
125
|
+
make_request(request)
|
126
|
+
end
|
127
|
+
|
128
|
+
def delete(path, headers: default_headers, body: nil)
|
129
|
+
request = Net::HTTP::Delete.new(path, headers)
|
130
|
+
request.body = body if body
|
131
|
+
|
132
|
+
make_request(request)
|
133
|
+
end
|
134
|
+
|
135
|
+
def make_request(request)
|
136
|
+
@mutex.synchronize do
|
137
|
+
@last_request_at = Time.now
|
138
|
+
@http.request(request)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def default_headers
|
143
|
+
@default_headers ||= {}.tap do |h|
|
144
|
+
h["Authorization"] = "Bearer #{ENV["SPECWRK_SRV_KEY"]}" if ENV["SPECWRK_SRV_KEY"]
|
145
|
+
h["X-Specwrk-Run"] = ENV["SPECWRK_RUN"] if ENV["SPECWRK_RUN"]
|
146
|
+
h["Content-Type"] = "application/json"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Hookable
|
2
|
+
def self.extended(base)
|
3
|
+
base.instance_variable_set(:@included_hooks, [])
|
4
|
+
base.instance_variable_set(:@setup_hooks, []) # unless base.instance_variable_defined?(:@setup_hooks)
|
5
|
+
end
|
6
|
+
|
7
|
+
def on_included(&block)
|
8
|
+
included_hooks << block
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_setup(&block)
|
12
|
+
setup_hooks << block
|
13
|
+
end
|
14
|
+
|
15
|
+
def included(base)
|
16
|
+
super if defined?(super)
|
17
|
+
|
18
|
+
base.extend ClassMethods
|
19
|
+
|
20
|
+
host_hooks = base.instance_variable_defined?(:@setup_hooks) ?
|
21
|
+
base.instance_variable_get(:@setup_hooks) :
|
22
|
+
[]
|
23
|
+
merged = host_hooks + setup_hooks
|
24
|
+
base.instance_variable_set(:@setup_hooks, merged)
|
25
|
+
|
26
|
+
included_hooks.each { |blk| blk.call(base) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def included_hooks
|
30
|
+
@included_hooks
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup_hooks
|
34
|
+
@setup_hooks
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
def setup(**args)
|
39
|
+
setup_hooks.each { |blk| blk.call(**args) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def on_setup(&block)
|
43
|
+
setup_hooks << block
|
44
|
+
end
|
45
|
+
|
46
|
+
def setup_hooks
|
47
|
+
@setup_hooks
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
require "rspec/core"
|
6
|
+
|
7
|
+
module Specwrk
|
8
|
+
class ListExamples
|
9
|
+
def initialize(dir)
|
10
|
+
@dir = dir
|
11
|
+
end
|
12
|
+
|
13
|
+
def examples
|
14
|
+
return @examples if defined?(@examples)
|
15
|
+
|
16
|
+
@examples = []
|
17
|
+
|
18
|
+
RSpec.configuration.files_or_directories_to_run = @dir
|
19
|
+
RSpec::Core::Formatters.register self.class, :stop
|
20
|
+
RSpec.configuration.add_formatter(self)
|
21
|
+
|
22
|
+
unless RSpec::Core::Runner.new(options).run($stderr, out).zero?
|
23
|
+
out.tap(&:rewind).each_line { |line| $stdout.print line }
|
24
|
+
end
|
25
|
+
|
26
|
+
@examples
|
27
|
+
end
|
28
|
+
|
29
|
+
# Called as the formatter
|
30
|
+
def stop(group_notification)
|
31
|
+
group_notification.notifications.map do |notification|
|
32
|
+
@examples << {
|
33
|
+
id: notification.example.id,
|
34
|
+
file_path: notification.example.metadata[:file_path]
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def out
|
42
|
+
@out ||= Tempfile.new.tap do |f|
|
43
|
+
f.define_singleton_method(:tty?) { true }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def options
|
48
|
+
RSpec::Core::ConfigurationOptions.new(
|
49
|
+
["--dry-run", *RSpec.configuration.files_to_run]
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|