wsdirector-core 1.0.1 → 1.0.2
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/CHANGELOG.md +4 -0
- data/lib/.rbnext/2.7/wsdirector/cli.rb +152 -0
- data/lib/.rbnext/2.7/wsdirector/client.rb +112 -0
- data/lib/.rbnext/2.7/wsdirector/protocols/base.rb +288 -0
- data/lib/.rbnext/2.7/wsdirector/protocols/phoenix.rb +102 -0
- data/lib/.rbnext/2.7/wsdirector/scenario_reader.rb +172 -0
- data/lib/.rbnext/3.0/wsdirector/ext/formatting.rb +38 -0
- data/lib/.rbnext/3.0/wsdirector/protocols/base.rb +288 -0
- data/lib/.rbnext/3.1/wsdirector/cli.rb +152 -0
- data/lib/.rbnext/3.1/wsdirector/protocols/action_cable.rb +95 -0
- data/lib/.rbnext/3.1/wsdirector/protocols/phoenix.rb +102 -0
- data/lib/.rbnext/3.1/wsdirector/runner.rb +60 -0
- data/lib/.rbnext/3.1/wsdirector/scenario_reader.rb +172 -0
- data/lib/.rbnext/3.1/wsdirector/task.rb +56 -0
- data/lib/wsdirector/version.rb +1 -1
- metadata +15 -2
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require "json"
|
5
|
+
require "wsdirector/ext/deep_dup"
|
6
|
+
|
7
|
+
module WSDirector
|
8
|
+
# Read and parse different scenarios
|
9
|
+
class ScenarioReader
|
10
|
+
using WSDirector::Ext::DeepDup
|
11
|
+
|
12
|
+
include WSDirector::Utils
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def parse(scenario, **options)
|
16
|
+
new(**options).parse(scenario)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :default_connections_options, :locals
|
21
|
+
|
22
|
+
def initialize(scale: 1, connection_options: {}, locals: {})
|
23
|
+
@scale = scale
|
24
|
+
@default_connections_options = connection_options
|
25
|
+
@locals = locals
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse(scenario)
|
29
|
+
contents =
|
30
|
+
if scenario.is_a?(String)
|
31
|
+
if File.file?(scenario)
|
32
|
+
parse_file(scenario)
|
33
|
+
else
|
34
|
+
parse_from_str(scenario).then do |_1|
|
35
|
+
next _1 if _1.is_a?(Array)
|
36
|
+
[_1]
|
37
|
+
end
|
38
|
+
end.flatten
|
39
|
+
else
|
40
|
+
scenario
|
41
|
+
end
|
42
|
+
|
43
|
+
contents.map! do |item|
|
44
|
+
item.is_a?(String) ? {item => {}} : item
|
45
|
+
end
|
46
|
+
|
47
|
+
if contents.first&.key?("client")
|
48
|
+
contents = transform_with_loop(contents, multiple: true)
|
49
|
+
parse_multiple_scenarios(contents)
|
50
|
+
else
|
51
|
+
contents = transform_with_loop(contents)
|
52
|
+
{"total" => 1, "clients" => [parse_simple_scenario(contents)]}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
JSON_FILE_FORMAT = /.+.(json)\z/
|
59
|
+
private_constant :JSON_FILE_FORMAT
|
60
|
+
|
61
|
+
def parse_from_str(contents)
|
62
|
+
JSON.parse(contents)
|
63
|
+
rescue JSON::ParserError
|
64
|
+
parse_yaml(contents)
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_file(file)
|
68
|
+
if file.match?(JSON_FILE_FORMAT)
|
69
|
+
JSON.parse(File.read(file))
|
70
|
+
else
|
71
|
+
parse_yaml(file)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_yaml(path)
|
76
|
+
contents = File.file?(path) ? File.read(path) : path
|
77
|
+
|
78
|
+
if defined?(ERB)
|
79
|
+
contents = ERB.new(contents).result(erb_context)
|
80
|
+
end
|
81
|
+
|
82
|
+
::YAML.load(contents, aliases: true, permitted_classes: [Date, Time, Regexp]) || {}
|
83
|
+
rescue ArgumentError
|
84
|
+
::YAML.load(contents) || {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_steps(steps)
|
88
|
+
steps.flat_map.with_index do |step, id|
|
89
|
+
if step.is_a?(Hash)
|
90
|
+
type, data = step.to_a.first
|
91
|
+
|
92
|
+
data["sample"] = [1, parse_multiplier(data["sample"])].max if data["sample"]
|
93
|
+
|
94
|
+
multiplier = parse_multiplier(data.delete("multiplier") || "1")
|
95
|
+
Array.new(multiplier) { {"type" => type, "id" => id}.merge(data) }
|
96
|
+
else
|
97
|
+
{"type" => step, "id" => id}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_simple_scenario(
|
103
|
+
steps,
|
104
|
+
multiplier: 1, name: "default", ignore: nil, protocol: "base",
|
105
|
+
connection_options: {}
|
106
|
+
)
|
107
|
+
{
|
108
|
+
"multiplier" => multiplier,
|
109
|
+
"steps" => handle_steps(steps),
|
110
|
+
"name" => name,
|
111
|
+
"ignore" => ignore,
|
112
|
+
"protocol" => protocol,
|
113
|
+
"connection_options" => default_connections_options.merge(connection_options)
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_multiple_scenarios(definitions)
|
118
|
+
total_count = 0
|
119
|
+
clients = definitions.map.with_index do |client, i|
|
120
|
+
_, client = client.to_a.first
|
121
|
+
multiplier = parse_multiplier(client.delete("multiplier") || "1")
|
122
|
+
name = client.delete("name") || (i + 1).to_s
|
123
|
+
connection_options = client.delete("connection_options") || {}
|
124
|
+
total_count += multiplier
|
125
|
+
ignore = parse_ignore(client.fetch("ignore", nil))
|
126
|
+
protocol = client.fetch("protocol", "base")
|
127
|
+
|
128
|
+
parse_simple_scenario(
|
129
|
+
client.fetch("actions", []),
|
130
|
+
multiplier: multiplier,
|
131
|
+
name: name,
|
132
|
+
ignore: ignore,
|
133
|
+
protocol: protocol,
|
134
|
+
connection_options: connection_options
|
135
|
+
)
|
136
|
+
end
|
137
|
+
{"total" => total_count, "clients" => clients}
|
138
|
+
end
|
139
|
+
|
140
|
+
def transform_with_loop(contents, multiple: false)
|
141
|
+
contents.flat_map do |content|
|
142
|
+
loop_data = content.dig("client", "loop") || content.dig("loop")
|
143
|
+
next content unless loop_data
|
144
|
+
|
145
|
+
loop_multiplier = parse_multiplier(loop_data["multiplier"] || "1")
|
146
|
+
|
147
|
+
if multiple
|
148
|
+
content["client"]["actions"] = (loop_data["actions"] * loop_multiplier).map(&:deep_dup)
|
149
|
+
content
|
150
|
+
else
|
151
|
+
loop_data["actions"] * loop_multiplier
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def parse_ignore(str)
|
157
|
+
return unless str
|
158
|
+
|
159
|
+
Array(str)
|
160
|
+
end
|
161
|
+
|
162
|
+
def erb_context
|
163
|
+
binding.then do |b|
|
164
|
+
locals.each do |_1, _2|
|
165
|
+
b.local_variable_set(_1, _2)
|
166
|
+
end
|
167
|
+
|
168
|
+
b
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WSDirector
|
4
|
+
module Ext
|
5
|
+
# Extend Object through refinements
|
6
|
+
module Formatting
|
7
|
+
refine ::Object do
|
8
|
+
def truncate(*) ; itself; end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine ::String do
|
12
|
+
def truncate(limit)
|
13
|
+
return self if size <= limit
|
14
|
+
|
15
|
+
"#{self[0..(limit - 3)]}..."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
refine ::Hash do
|
20
|
+
def truncate(limit)
|
21
|
+
str = to_json
|
22
|
+
|
23
|
+
str.truncate(limit)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
refine ::Float do
|
28
|
+
def duration
|
29
|
+
if self > 1
|
30
|
+
"#{truncate(2)}s"
|
31
|
+
else
|
32
|
+
"#{(self * 1000).to_i}ms"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
require "wsdirector/ext/formatting"
|
5
|
+
|
6
|
+
module WSDirector
|
7
|
+
module Protocols
|
8
|
+
using Ext::Formatting
|
9
|
+
|
10
|
+
using(Module.new do
|
11
|
+
refine ::Object do
|
12
|
+
def matches?(other)
|
13
|
+
self == other
|
14
|
+
end
|
15
|
+
|
16
|
+
def partially_matches?(other)
|
17
|
+
self == other
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
refine ::Array do
|
22
|
+
def matches?(actual)
|
23
|
+
if actual.is_a?(::String)
|
24
|
+
actual = JSON.parse(actual) rescue nil # rubocop:disable Style/RescueModifier
|
25
|
+
end
|
26
|
+
|
27
|
+
return false unless actual
|
28
|
+
|
29
|
+
each.with_index do
|
30
|
+
return false unless _1.matches?(actual[_2])
|
31
|
+
end
|
32
|
+
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def partially_matches?(actual)
|
37
|
+
if actual.is_a?(::String)
|
38
|
+
actual = JSON.parse(actual) rescue nil # rubocop:disable Style/RescueModifier
|
39
|
+
end
|
40
|
+
|
41
|
+
return false unless actual
|
42
|
+
|
43
|
+
each.with_index do
|
44
|
+
return false unless _1.partially_matches?(actual[_2])
|
45
|
+
end
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
refine ::Hash do
|
52
|
+
def matches?(actual)
|
53
|
+
unless actual.is_a?(::Hash)
|
54
|
+
actual = JSON.parse(actual) rescue nil # rubocop:disable Style/RescueModifier
|
55
|
+
end
|
56
|
+
|
57
|
+
return false unless actual
|
58
|
+
|
59
|
+
actual.each_key do
|
60
|
+
return false unless actual[_1].matches?(self[_1])
|
61
|
+
end
|
62
|
+
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def partially_matches?(actual)
|
67
|
+
unless actual.is_a?(::Hash)
|
68
|
+
actual = JSON.parse(actual) rescue nil # rubocop:disable Style/RescueModifier
|
69
|
+
end
|
70
|
+
|
71
|
+
return false unless actual
|
72
|
+
|
73
|
+
each_key do
|
74
|
+
return false unless self[_1].partially_matches?(actual[_1])
|
75
|
+
end
|
76
|
+
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end)
|
81
|
+
|
82
|
+
class PartialMatcher
|
83
|
+
attr_reader :obj
|
84
|
+
|
85
|
+
def initialize(obj)
|
86
|
+
@obj = obj
|
87
|
+
end
|
88
|
+
|
89
|
+
def matches?(actual)
|
90
|
+
obj.partially_matches?(actual)
|
91
|
+
end
|
92
|
+
|
93
|
+
def inspect
|
94
|
+
"an object including #{obj.inspect}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def truncate(...) ; obj.truncate(...); end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Base protocol describes basic actions
|
101
|
+
class Base
|
102
|
+
include WSDirector::Utils
|
103
|
+
|
104
|
+
def initialize(task, scale: 1, logger: nil, id: nil, color: nil)
|
105
|
+
@task = task
|
106
|
+
@scale = scale
|
107
|
+
@logger = logger
|
108
|
+
@id = id
|
109
|
+
@color = color
|
110
|
+
end
|
111
|
+
|
112
|
+
def init_client(...)
|
113
|
+
log { "Connecting" }
|
114
|
+
|
115
|
+
@client = build_client(...)
|
116
|
+
|
117
|
+
log(:done) { "Connected" }
|
118
|
+
end
|
119
|
+
|
120
|
+
def handle_step(step)
|
121
|
+
type = step.delete("type")
|
122
|
+
raise Error, "Unknown step: #{type}" unless respond_to?(type)
|
123
|
+
|
124
|
+
return unless task.sampled?(step)
|
125
|
+
|
126
|
+
public_send(type, step)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Sleeps for a specified number of seconds.
|
130
|
+
#
|
131
|
+
# If "shift" is provided than the initial value is
|
132
|
+
# shifted by random number from (-shift, shift).
|
133
|
+
#
|
134
|
+
# Set "debug" to true to print the delay time.
|
135
|
+
def sleep(step)
|
136
|
+
delay = step.fetch("time").to_f
|
137
|
+
shift = step.fetch("shift", 0).to_f
|
138
|
+
|
139
|
+
delay = delay - shift * rand + shift * rand
|
140
|
+
|
141
|
+
log { "Sleep for #{delay}s" }
|
142
|
+
|
143
|
+
Kernel.sleep delay if delay > 0
|
144
|
+
|
145
|
+
log(:done) { "Slept for #{delay}s" }
|
146
|
+
end
|
147
|
+
|
148
|
+
# Prints provided message
|
149
|
+
def debug(step)
|
150
|
+
with_logger do
|
151
|
+
log(nil) { step.fetch("message") }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def receive(step)
|
156
|
+
expected = step["data"] || PartialMatcher.new(step["data>"])
|
157
|
+
ordered = step["ordered"]
|
158
|
+
|
159
|
+
log { "Receive a message: #{expected.truncate(50)}" }
|
160
|
+
|
161
|
+
received = nil
|
162
|
+
|
163
|
+
client.each_message do |msg, id|
|
164
|
+
received = msg
|
165
|
+
if expected.matches?(msg)
|
166
|
+
client.consumed(id)
|
167
|
+
break
|
168
|
+
end
|
169
|
+
|
170
|
+
if ordered
|
171
|
+
raise UnmatchedExpectationError, prepare_receive_error(expected, received)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
log(:done) { "Received a message: #{received&.truncate(50)}" }
|
176
|
+
rescue ThreadError
|
177
|
+
if received
|
178
|
+
raise UnmatchedExpectationError, prepare_receive_error(expected, received)
|
179
|
+
else
|
180
|
+
raise NoMessageError, "Expected to receive #{expected} but nothing has been received"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def receive_all(step)
|
185
|
+
messages = step.delete("messages")
|
186
|
+
raise ArgumentError, "Messages array must be specified" if
|
187
|
+
messages.nil? || messages.empty?
|
188
|
+
|
189
|
+
expected =
|
190
|
+
messages.map do |msg|
|
191
|
+
multiplier = parse_multiplier(msg.delete("multiplier") || "1")
|
192
|
+
[msg["data"] || PartialMatcher.new(msg["data>"]), multiplier]
|
193
|
+
end.to_h
|
194
|
+
|
195
|
+
total_expected = expected.values.sum
|
196
|
+
total_received = 0
|
197
|
+
|
198
|
+
log { "Receive #{total_expected} messages" }
|
199
|
+
|
200
|
+
total_expected.times do
|
201
|
+
received = client.receive
|
202
|
+
|
203
|
+
total_received += 1
|
204
|
+
|
205
|
+
match = expected.find { |k, _| k.matches?(received) }
|
206
|
+
|
207
|
+
raise UnexpectedMessageError, "Unexpected message received: #{received}" if
|
208
|
+
match.nil?
|
209
|
+
|
210
|
+
expected[match.first] -= 1
|
211
|
+
expected.delete(match.first) if expected[match.first].zero?
|
212
|
+
end
|
213
|
+
|
214
|
+
log(:done) { "Received #{total_expected} messages" }
|
215
|
+
rescue ThreadError
|
216
|
+
raise NoMessageError,
|
217
|
+
"Expected to receive #{total_expected} messages " \
|
218
|
+
"but received only #{total_received}"
|
219
|
+
end
|
220
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
221
|
+
|
222
|
+
def send(step)
|
223
|
+
data = step.fetch("data")
|
224
|
+
data = JSON.generate(data) if data.is_a?(Hash)
|
225
|
+
|
226
|
+
client.send(data)
|
227
|
+
|
228
|
+
log(nil) { "Sent message: #{data.truncate(50)}" }
|
229
|
+
end
|
230
|
+
|
231
|
+
def wait_all(_step)
|
232
|
+
log { "Wait all clients" }
|
233
|
+
task.global_holder.wait_all
|
234
|
+
log { "All clients" }
|
235
|
+
end
|
236
|
+
|
237
|
+
def to_proc
|
238
|
+
proc { |step| handle_step(step) }
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
attr_reader :client, :task, :logger, :id, :color
|
244
|
+
|
245
|
+
def build_client(...)
|
246
|
+
Client.new(...)
|
247
|
+
end
|
248
|
+
|
249
|
+
def prepare_receive_error(expected, received)
|
250
|
+
<<~MSG
|
251
|
+
Action failed: #receive
|
252
|
+
-- expected: #{expected.inspect}
|
253
|
+
++ got: #{received} (#{received.class})
|
254
|
+
MSG
|
255
|
+
end
|
256
|
+
|
257
|
+
def log(state = :begin)
|
258
|
+
return unless logger
|
259
|
+
|
260
|
+
if state == :begin
|
261
|
+
@last_event_at = Time.now.to_f
|
262
|
+
end
|
263
|
+
|
264
|
+
done_info =
|
265
|
+
if state == :done
|
266
|
+
" (#{(Time.now.to_f - @last_event_at).duration})"
|
267
|
+
else
|
268
|
+
""
|
269
|
+
end
|
270
|
+
|
271
|
+
msg = "[#{Time.now.strftime("%H:%I:%S.%L")}] client=#{id} #{yield}#{done_info}"
|
272
|
+
|
273
|
+
msg = msg.colorize(color) if color
|
274
|
+
|
275
|
+
logger.puts msg
|
276
|
+
end
|
277
|
+
|
278
|
+
def with_logger
|
279
|
+
return yield if logger
|
280
|
+
|
281
|
+
@logger = $stdout
|
282
|
+
yield
|
283
|
+
ensure
|
284
|
+
@logger = nil
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
require "wsdirector"
|
7
|
+
|
8
|
+
module WSDirector
|
9
|
+
# Command line interface for WsDirector
|
10
|
+
class CLI
|
11
|
+
class Configuration
|
12
|
+
attr_accessor :ws_url, :scenario_path, :colorize, :scale,
|
13
|
+
:sync_timeout, :json_scenario, :subprotocol, :verbose
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
reset!
|
17
|
+
end
|
18
|
+
|
19
|
+
def colorize?
|
20
|
+
colorize == true
|
21
|
+
end
|
22
|
+
|
23
|
+
# Restore to defaults
|
24
|
+
def reset!
|
25
|
+
@scale = 1
|
26
|
+
@colorize = $stdout.tty?
|
27
|
+
@sync_timeout = 5
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :config
|
32
|
+
|
33
|
+
def run
|
34
|
+
@config = Configuration.new
|
35
|
+
|
36
|
+
parse_args!
|
37
|
+
|
38
|
+
begin
|
39
|
+
require "colorize" if config.colorize?
|
40
|
+
rescue LoadError
|
41
|
+
config.colorize = false
|
42
|
+
warn "Install colorize to use colored output"
|
43
|
+
end
|
44
|
+
|
45
|
+
connection_options = {
|
46
|
+
subprotocol: config.subprotocol
|
47
|
+
}.compact
|
48
|
+
|
49
|
+
scenario = config.scenario_path || config.json_scenario
|
50
|
+
|
51
|
+
config.ws_url = "ws://#{config.ws_url}" unless config.ws_url.start_with?(/wss?:\/\//)
|
52
|
+
|
53
|
+
url = config.ws_url
|
54
|
+
scale = config.scale
|
55
|
+
sync_timeout = config.sync_timeout
|
56
|
+
colorize = config.colorize
|
57
|
+
|
58
|
+
logger = $stdout if config.verbose
|
59
|
+
|
60
|
+
result = WSDirector.run(
|
61
|
+
scenario,
|
62
|
+
url: url,
|
63
|
+
connection_options: connection_options,
|
64
|
+
scale: scale,
|
65
|
+
sync_timeout: sync_timeout,
|
66
|
+
logger: logger,
|
67
|
+
colorize: colorize
|
68
|
+
)
|
69
|
+
|
70
|
+
puts "\n\n" if config.verbose
|
71
|
+
|
72
|
+
result.print_summary(colorize: config.colorize?)
|
73
|
+
result.success? || exit(1)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
FILE_FORMAT = /.+.(json|yml)\z/
|
79
|
+
private_constant :FILE_FORMAT
|
80
|
+
|
81
|
+
def parse_args!
|
82
|
+
parser = OptionParser.new do |opts|
|
83
|
+
opts.banner = "Usage: wsdirector scenario_path ws_url [options]"
|
84
|
+
|
85
|
+
opts.on("-s SCALE", "--scale=SCALE", Integer, "Scale factor") do
|
86
|
+
config.scale = _1
|
87
|
+
end
|
88
|
+
|
89
|
+
opts.on("-t TIMEOUT", "--timeout=TIMEOUT", Integer, "Synchronization (wait_all) timeout") do
|
90
|
+
config.sync_timeout = _1
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on("-i JSON", "--include=JSON", String, "Include JSON to parse") do
|
94
|
+
config.json_scenario = _1
|
95
|
+
end
|
96
|
+
|
97
|
+
opts.on("-u URL", "--url=URL", Object, "Websocket server URL") do
|
98
|
+
config.ws_url = _1
|
99
|
+
end
|
100
|
+
|
101
|
+
opts.on("-f PATH", "--file=PATH", String, "Scenario path") do
|
102
|
+
config.scenario_path = _1
|
103
|
+
end
|
104
|
+
|
105
|
+
opts.on("--subprotocol=VALUE", String, "WebSocket subprotocol") do
|
106
|
+
config.subprotocol = _1
|
107
|
+
end
|
108
|
+
|
109
|
+
opts.on("-c", "--[no-]color", "Colorize output") do
|
110
|
+
config.colorize = _1
|
111
|
+
end
|
112
|
+
|
113
|
+
opts.on("-v", "--version", "Print version") do
|
114
|
+
$stdout.puts WSDirector::VERSION
|
115
|
+
exit 0
|
116
|
+
end
|
117
|
+
|
118
|
+
opts.on("-vv", "Print verbose logs") do
|
119
|
+
config.verbose = true
|
120
|
+
end
|
121
|
+
|
122
|
+
opts.on("-r", "--require=PATH", "Load Ruby file (e.g., protocol)") do
|
123
|
+
Kernel.load(_1)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
parser.parse!
|
128
|
+
|
129
|
+
unless config.scenario_path
|
130
|
+
config.scenario_path = ARGV.grep(FILE_FORMAT).last
|
131
|
+
end
|
132
|
+
|
133
|
+
unless config.ws_url
|
134
|
+
config.ws_url = ARGV.grep(URI::DEFAULT_PARSER.make_regexp).last
|
135
|
+
end
|
136
|
+
|
137
|
+
check_for_errors
|
138
|
+
end
|
139
|
+
|
140
|
+
def check_for_errors
|
141
|
+
if config.json_scenario.nil?
|
142
|
+
raise(Error, "Scenario is missing") unless config.scenario_path
|
143
|
+
|
144
|
+
unless File.file?(config.scenario_path)
|
145
|
+
raise(Error, "File doesn't exist # config.scenario_path}")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
raise(Error, "Websocket server url is missing") unless config.ws_url
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|