wsdirector-cli 0.4.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/LICENSE.txt +1 -1
- data/README.md +238 -42
- metadata +18 -164
- data/bin/wsdirector +0 -15
- data/lib/wsdirector/cli.rb +0 -78
- data/lib/wsdirector/client.rb +0 -66
- data/lib/wsdirector/clients_holder.rb +0 -23
- data/lib/wsdirector/configuration.rb +0 -24
- data/lib/wsdirector/ext/deep_dup.rb +0 -34
- data/lib/wsdirector/printer.rb +0 -11
- data/lib/wsdirector/protocols/action_cable.rb +0 -75
- data/lib/wsdirector/protocols/base.rb +0 -135
- data/lib/wsdirector/protocols.rb +0 -28
- data/lib/wsdirector/result.rb +0 -56
- data/lib/wsdirector/results_holder.rb +0 -48
- data/lib/wsdirector/runner.rb +0 -45
- data/lib/wsdirector/scenario_reader.rb +0 -77
- data/lib/wsdirector/task.rb +0 -47
- data/lib/wsdirector/utils.rb +0 -15
- data/lib/wsdirector/version.rb +0 -5
- data/lib/wsdirector-cli.rb +0 -3
- data/lib/wsdirector.rb +0 -23
data/bin/wsdirector
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "rubygems"
|
4
|
-
|
5
|
-
$:.unshift(File.expand_path(__dir__ + "/../lib"))
|
6
|
-
|
7
|
-
require "wsdirector/cli"
|
8
|
-
|
9
|
-
begin
|
10
|
-
WSDirector::CLI.new.tap { |cli| cli.run }
|
11
|
-
rescue => e
|
12
|
-
STDERR.puts e.message
|
13
|
-
STDERR.puts e.backtrace.join("\n")
|
14
|
-
exit 1
|
15
|
-
end
|
data/lib/wsdirector/cli.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "optparse"
|
4
|
-
|
5
|
-
require "wsdirector"
|
6
|
-
require "wsdirector/scenario_reader"
|
7
|
-
require "wsdirector/runner"
|
8
|
-
|
9
|
-
module WSDirector
|
10
|
-
# Command line interface for WsDirector
|
11
|
-
class CLI
|
12
|
-
def initialize
|
13
|
-
end
|
14
|
-
|
15
|
-
def run
|
16
|
-
parse_args!
|
17
|
-
|
18
|
-
begin
|
19
|
-
require "colorize" if WSDirector.config.colorize?
|
20
|
-
rescue LoadError
|
21
|
-
WSDirector.config.colorize = false
|
22
|
-
warn "Install colorize to use colored output"
|
23
|
-
end
|
24
|
-
|
25
|
-
scenario = WSDirector::ScenarioReader.parse(
|
26
|
-
WSDirector.config.scenario_path
|
27
|
-
)
|
28
|
-
|
29
|
-
if WSDirector::Runner.new(scenario).start
|
30
|
-
exit 0
|
31
|
-
else
|
32
|
-
exit 1
|
33
|
-
end
|
34
|
-
rescue Error => e
|
35
|
-
warn e.message
|
36
|
-
exit 1
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def parse_args!
|
42
|
-
# rubocop: disable Metrics/LineLength
|
43
|
-
parser = OptionParser.new do |opts|
|
44
|
-
opts.banner = "Usage: wsdirector scenario_path ws_url [options]"
|
45
|
-
|
46
|
-
opts.on("-s SCALE", "--scale=SCALE", Integer, "Scale factor") do |v|
|
47
|
-
WSDirector.config.scale = v
|
48
|
-
end
|
49
|
-
|
50
|
-
opts.on("-t TIMEOUT", "--timeout=TIMEOUT", Integer, "Synchronization (wait_all) timeout") do |v|
|
51
|
-
WSDirector.config.sync_timeout = v
|
52
|
-
end
|
53
|
-
|
54
|
-
opts.on("-c", "--[no-]color", "Colorize output") do |v|
|
55
|
-
WSDirector.config.colorize = v
|
56
|
-
end
|
57
|
-
|
58
|
-
opts.on("-v", "--version", "Print versin") do
|
59
|
-
$stdout.puts WSDirector::VERSION
|
60
|
-
exit 0
|
61
|
-
end
|
62
|
-
end
|
63
|
-
# rubocop: enable Metrics/LineLength
|
64
|
-
|
65
|
-
parser.parse!
|
66
|
-
|
67
|
-
WSDirector.config.scenario_path = ARGV[0]
|
68
|
-
WSDirector.config.ws_url = ARGV[1]
|
69
|
-
|
70
|
-
raise(Error, "Scenario path is missing") if WSDirector.config.scenario_path.nil?
|
71
|
-
|
72
|
-
raise(Error, "File doesn't exist #{WSDirector.config.scenario_path}") unless
|
73
|
-
File.file?(WSDirector.config.scenario_path)
|
74
|
-
|
75
|
-
raise(Error, "Websocket server url is missing") if WSDirector.config.ws_url.nil?
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
data/lib/wsdirector/client.rb
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "websocket-client-simple"
|
4
|
-
require "securerandom"
|
5
|
-
|
6
|
-
module WSDirector
|
7
|
-
# WebSocket client
|
8
|
-
class Client
|
9
|
-
WAIT_WHEN_EXPECTING_EVENT = 5
|
10
|
-
|
11
|
-
attr_reader :ws, :id
|
12
|
-
|
13
|
-
# Create new WebSocket client and connect to WSDirector
|
14
|
-
# ws URL.
|
15
|
-
#
|
16
|
-
# Optionally provide an ignore pattern (to ignore incoming message,
|
17
|
-
# for example, pings)
|
18
|
-
def initialize(ignore: nil)
|
19
|
-
@ignore = ignore
|
20
|
-
has_messages = @has_messages = Concurrent::Semaphore.new(0)
|
21
|
-
messages = @messages = Queue.new
|
22
|
-
path = WSDirector.config.ws_url
|
23
|
-
open = Concurrent::Promise.new
|
24
|
-
client = self
|
25
|
-
|
26
|
-
@id = SecureRandom.hex(6)
|
27
|
-
@ws = WebSocket::Client::Simple.connect(path) do |ws|
|
28
|
-
ws.on(:open) do |_event|
|
29
|
-
open.set(true)
|
30
|
-
end
|
31
|
-
|
32
|
-
ws.on :message do |msg|
|
33
|
-
data = msg.data
|
34
|
-
next if data.empty?
|
35
|
-
next if client.ignored?(data)
|
36
|
-
messages << data
|
37
|
-
has_messages.release
|
38
|
-
end
|
39
|
-
|
40
|
-
ws.on :error do |e|
|
41
|
-
messages << Error.new("WebSocket Error #{e.inspect} #{e.backtrace}")
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
open.wait!(WAIT_WHEN_EXPECTING_EVENT)
|
46
|
-
rescue Errno::ECONNREFUSED
|
47
|
-
raise Error, "Failed to connect to #{path}"
|
48
|
-
end
|
49
|
-
|
50
|
-
def receive(timeout = WAIT_WHEN_EXPECTING_EVENT)
|
51
|
-
@has_messages.try_acquire(1, timeout)
|
52
|
-
msg = @messages.pop(true)
|
53
|
-
raise msg if msg.is_a?(Exception)
|
54
|
-
msg
|
55
|
-
end
|
56
|
-
|
57
|
-
def send(msg)
|
58
|
-
@ws.send(msg)
|
59
|
-
end
|
60
|
-
|
61
|
-
def ignored?(msg)
|
62
|
-
return false unless @ignore
|
63
|
-
@ignore.any? { |pattern| msg =~ pattern }
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WSDirector
|
4
|
-
# Acts as a re-usable global barrier for a fixed number of clients.
|
5
|
-
# Barrier is reset if sucessfully passed in time.
|
6
|
-
class ClientsHolder
|
7
|
-
def initialize(count)
|
8
|
-
@barrier = Concurrent::CyclicBarrier.new(count)
|
9
|
-
end
|
10
|
-
|
11
|
-
def wait_all
|
12
|
-
result = barrier.wait(WSDirector.config.sync_timeout)
|
13
|
-
raise Error, "Timeout (#{WSDirector.config.sync_timeout}s) exceeded for #wait_all" unless
|
14
|
-
result
|
15
|
-
barrier.reset
|
16
|
-
result
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
attr_reader :barrier
|
22
|
-
end
|
23
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WSDirector
|
4
|
-
# WSDirector configuration
|
5
|
-
class Configuration
|
6
|
-
attr_accessor :ws_url, :scenario_path, :colorize, :scale,
|
7
|
-
:sync_timeout
|
8
|
-
|
9
|
-
def initialize
|
10
|
-
reset!
|
11
|
-
end
|
12
|
-
|
13
|
-
def colorize?
|
14
|
-
colorize == true
|
15
|
-
end
|
16
|
-
|
17
|
-
# Restore to defaults
|
18
|
-
def reset!
|
19
|
-
@scale = 1
|
20
|
-
@colorize = false
|
21
|
-
@sync_timeout = 5
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WSDirector
|
4
|
-
module Ext
|
5
|
-
# Extend Object through refinements
|
6
|
-
module DeepDup
|
7
|
-
refine ::Hash do
|
8
|
-
# Based on ActiveSupport http://api.rubyonrails.org/classes/Hash.html#method-i-deep_dup
|
9
|
-
def deep_dup
|
10
|
-
each_with_object(dup) do |(key, value), hash|
|
11
|
-
hash[key] = if value.is_a?(::Hash) || value.is_a?(::Array)
|
12
|
-
value.deep_dup
|
13
|
-
else
|
14
|
-
value
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
refine ::Array do
|
21
|
-
# From ActiveSupport http://api.rubyonrails.org/classes/Array.html#method-i-deep_dup
|
22
|
-
def deep_dup
|
23
|
-
map do |value|
|
24
|
-
if value.is_a?(::Hash) || value.is_a?(::Array)
|
25
|
-
value.deep_dup
|
26
|
-
else
|
27
|
-
value
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
data/lib/wsdirector/printer.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WSDirector
|
4
|
-
# Print messages (optionally colorized) to STDOUT
|
5
|
-
class Printer
|
6
|
-
def self.out(message, color = nil)
|
7
|
-
message = message.colorize(color) if WSDirector.config.colorize? && color
|
8
|
-
$stdout.puts(message)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
@@ -1,75 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WSDirector
|
4
|
-
module Protocols
|
5
|
-
# ActionCable protocol
|
6
|
-
class ActionCable < Base
|
7
|
-
WELCOME_MSG = {type: "welcome"}.to_json
|
8
|
-
PING_IGNORE = /['"]type['"]:\s*['"]ping['"]/
|
9
|
-
|
10
|
-
# Add ping ignore and make sure that we receive Welcome message
|
11
|
-
def init_client(**options)
|
12
|
-
options[:ignore] ||= [PING_IGNORE]
|
13
|
-
|
14
|
-
super(**options)
|
15
|
-
|
16
|
-
receive("data" => WELCOME_MSG)
|
17
|
-
end
|
18
|
-
|
19
|
-
def subscribe(step)
|
20
|
-
identifier = extract_identifier(step)
|
21
|
-
|
22
|
-
client.send({command: "subscribe", identifier: identifier}.to_json)
|
23
|
-
|
24
|
-
begin
|
25
|
-
receive(
|
26
|
-
"data" => {"type" => "confirm_subscription", "identifier" => identifier}
|
27
|
-
)
|
28
|
-
rescue UnmatchedExpectationError => e
|
29
|
-
raise unless /reject_subscription/.match?(e.message)
|
30
|
-
raise UnmatchedExpectationError, "Subscription rejected to #{identifier}"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def perform(step)
|
35
|
-
identifier = extract_identifier(step)
|
36
|
-
action = step.delete("action")
|
37
|
-
|
38
|
-
raise Error, "Action is missing" unless action
|
39
|
-
|
40
|
-
data = step.fetch("data", {}).merge(action: action).to_json
|
41
|
-
|
42
|
-
client.send({command: "message", data: data, identifier: identifier}.to_json)
|
43
|
-
end
|
44
|
-
|
45
|
-
def receive(step)
|
46
|
-
return super unless step.key?("channel")
|
47
|
-
|
48
|
-
identifier = extract_identifier(step)
|
49
|
-
message = step.fetch("data", {})
|
50
|
-
super("data" => {"identifier" => identifier, "message" => message})
|
51
|
-
end
|
52
|
-
|
53
|
-
def receive_all(step)
|
54
|
-
messages = step["messages"]
|
55
|
-
|
56
|
-
return super if messages.nil? || messages.empty?
|
57
|
-
|
58
|
-
messages.each do |msg|
|
59
|
-
next unless msg.key?("channel")
|
60
|
-
identifier = extract_identifier(msg)
|
61
|
-
msg["data"] = {"identifier" => identifier, "message" => msg["data"]}
|
62
|
-
end
|
63
|
-
|
64
|
-
super
|
65
|
-
end
|
66
|
-
|
67
|
-
private
|
68
|
-
|
69
|
-
def extract_identifier(step)
|
70
|
-
channel = step.delete("channel")
|
71
|
-
step.fetch("params", {}).merge(channel: channel).to_json
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
@@ -1,135 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "time"
|
4
|
-
|
5
|
-
module WSDirector
|
6
|
-
module Protocols
|
7
|
-
# Base protocol describes basic actions
|
8
|
-
class Base
|
9
|
-
include WSDirector::Utils
|
10
|
-
|
11
|
-
def initialize(task)
|
12
|
-
@task = task
|
13
|
-
end
|
14
|
-
|
15
|
-
def init_client(**options)
|
16
|
-
@client = build_client(**options)
|
17
|
-
end
|
18
|
-
|
19
|
-
def handle_step(step)
|
20
|
-
type = step.delete("type")
|
21
|
-
raise Error, "Unknown step: #{type}" unless respond_to?(type)
|
22
|
-
|
23
|
-
return unless task.sampled?(step)
|
24
|
-
|
25
|
-
public_send(type, step)
|
26
|
-
end
|
27
|
-
|
28
|
-
# Sleeps for a specified number of seconds.
|
29
|
-
#
|
30
|
-
# If "shift" is provided than the initial value is
|
31
|
-
# shifted by random number from (-shift, shift).
|
32
|
-
#
|
33
|
-
# Set "debug" to true to print the delay time.
|
34
|
-
def sleep(step)
|
35
|
-
delay = step.fetch("time").to_f
|
36
|
-
shift = step.fetch("shift", 0).to_f
|
37
|
-
|
38
|
-
delay = delay - shift * rand + shift * rand
|
39
|
-
|
40
|
-
print("Sleep for #{delay}s") if step.fetch("debug", false)
|
41
|
-
|
42
|
-
Kernel.sleep delay if delay > 0
|
43
|
-
end
|
44
|
-
|
45
|
-
# Prints provided message
|
46
|
-
def debug(step)
|
47
|
-
print(step.fetch("message"))
|
48
|
-
end
|
49
|
-
|
50
|
-
def receive(step)
|
51
|
-
expected = step.fetch("data")
|
52
|
-
received = client.receive
|
53
|
-
raise UnmatchedExpectationError, prepare_receive_error(expected, received) unless
|
54
|
-
receive_matches?(expected, received)
|
55
|
-
rescue ThreadError
|
56
|
-
raise NoMessageError, "Expected to receive #{expected} but nothing has been received"
|
57
|
-
end
|
58
|
-
|
59
|
-
# rubocop: disable Metrics/CyclomaticComplexity
|
60
|
-
def receive_all(step)
|
61
|
-
messages = step.delete("messages")
|
62
|
-
raise ArgumentError, "Messages array must be specified" if
|
63
|
-
messages.nil? || messages.empty?
|
64
|
-
|
65
|
-
expected =
|
66
|
-
Hash[messages.map do |msg|
|
67
|
-
multiplier = parse_multiplier(msg.delete("multiplier") || "1")
|
68
|
-
[msg["data"], multiplier]
|
69
|
-
end]
|
70
|
-
|
71
|
-
total_expected = expected.values.sum
|
72
|
-
total_received = 0
|
73
|
-
|
74
|
-
total_expected.times do
|
75
|
-
received = client.receive
|
76
|
-
|
77
|
-
total_received += 1
|
78
|
-
|
79
|
-
match = expected.find { |k, _| receive_matches?(k, received) }
|
80
|
-
|
81
|
-
raise UnexpectedMessageError, "Unexpected message received: #{received}" if
|
82
|
-
match.nil?
|
83
|
-
|
84
|
-
expected[match.first] -= 1
|
85
|
-
expected.delete(match.first) if expected[match.first].zero?
|
86
|
-
end
|
87
|
-
rescue ThreadError
|
88
|
-
raise NoMessageError,
|
89
|
-
"Expected to receive #{total_expected} messages " \
|
90
|
-
"but received only #{total_received}"
|
91
|
-
end
|
92
|
-
# rubocop: enable Metrics/CyclomaticComplexity
|
93
|
-
|
94
|
-
def send(step)
|
95
|
-
data = step.fetch("data")
|
96
|
-
data = JSON.generate(data) if data.is_a?(Hash)
|
97
|
-
client.send(data)
|
98
|
-
end
|
99
|
-
|
100
|
-
def wait_all(_step)
|
101
|
-
task.global_holder.wait_all
|
102
|
-
end
|
103
|
-
|
104
|
-
def to_proc
|
105
|
-
proc { |step| handle_step(step) }
|
106
|
-
end
|
107
|
-
|
108
|
-
private
|
109
|
-
|
110
|
-
attr_reader :client, :task
|
111
|
-
|
112
|
-
def build_client(**options)
|
113
|
-
Client.new(**options)
|
114
|
-
end
|
115
|
-
|
116
|
-
def receive_matches?(expected, received)
|
117
|
-
received = JSON.parse(received) if expected.is_a?(Hash)
|
118
|
-
|
119
|
-
received == expected
|
120
|
-
end
|
121
|
-
|
122
|
-
def prepare_receive_error(expected, received)
|
123
|
-
<<~MSG
|
124
|
-
Action failed: #receive
|
125
|
-
-- expected: #{expected}
|
126
|
-
++ got: #{received}
|
127
|
-
MSG
|
128
|
-
end
|
129
|
-
|
130
|
-
def print(msg)
|
131
|
-
$stdout.puts "DEBUG #{Time.now.iso8601} client=#{client.id} #{msg}\n"
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
data/lib/wsdirector/protocols.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "wsdirector/protocols/base"
|
4
|
-
require "wsdirector/protocols/action_cable"
|
5
|
-
|
6
|
-
module WSDirector
|
7
|
-
ID2CLASS = {
|
8
|
-
"base" => "Base",
|
9
|
-
"action_cable" => "ActionCable"
|
10
|
-
}.freeze
|
11
|
-
|
12
|
-
module Protocols # :nodoc:
|
13
|
-
# Raised when received not expected message
|
14
|
-
class UnmatchedExpectationError < WSDirector::Error; end
|
15
|
-
# Raised when received message is unexpected
|
16
|
-
class UnexpectedMessageError < WSDirector::Error; end
|
17
|
-
# Raised when nothing has been received
|
18
|
-
class NoMessageError < WSDirector::Error; end
|
19
|
-
|
20
|
-
class << self
|
21
|
-
def get(id)
|
22
|
-
raise Error, "Unknown protocol: #{id}" unless ID2CLASS.key?(id)
|
23
|
-
class_name = ID2CLASS.fetch(id)
|
24
|
-
const_get(class_name)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/lib/wsdirector/result.rb
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module WSDirector
|
4
|
-
# Handle results from all clients from the group
|
5
|
-
class Result
|
6
|
-
attr_reader :group, :errors
|
7
|
-
|
8
|
-
def initialize(group)
|
9
|
-
@group = group
|
10
|
-
@errors = Concurrent::Array.new
|
11
|
-
|
12
|
-
@all = Concurrent::AtomicFixnum.new(0)
|
13
|
-
@failures = Concurrent::AtomicFixnum.new(0)
|
14
|
-
|
15
|
-
@sampling_mutex = Mutex.new
|
16
|
-
@sampling_counter = Hash.new { |h, k| h[k] = 0 }
|
17
|
-
end
|
18
|
-
|
19
|
-
# Called when client successfully finished it's work
|
20
|
-
def succeed
|
21
|
-
all.increment
|
22
|
-
end
|
23
|
-
|
24
|
-
# Called when client failed
|
25
|
-
def failed(error_message)
|
26
|
-
errors << error_message
|
27
|
-
all.increment
|
28
|
-
failures.increment
|
29
|
-
end
|
30
|
-
|
31
|
-
def success?
|
32
|
-
failures.value.zero?
|
33
|
-
end
|
34
|
-
|
35
|
-
def total_count
|
36
|
-
all.value
|
37
|
-
end
|
38
|
-
|
39
|
-
def failures_count
|
40
|
-
failures.value
|
41
|
-
end
|
42
|
-
|
43
|
-
def track_sample(id, max)
|
44
|
-
sampling_mutex.synchronize do
|
45
|
-
return false if sampling_counter[id] >= max
|
46
|
-
|
47
|
-
sampling_counter[id] += 1
|
48
|
-
true
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
attr_reader :all, :success, :failures, :sampling_counter, :sampling_mutex
|
55
|
-
end
|
56
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "wsdirector/printer"
|
4
|
-
|
5
|
-
module WSDirector
|
6
|
-
# Holds all results for all groups of clients
|
7
|
-
class ResultsHolder
|
8
|
-
def initialize
|
9
|
-
@groups = Concurrent::Map.new
|
10
|
-
end
|
11
|
-
|
12
|
-
def success?
|
13
|
-
@groups.values.all?(&:success?)
|
14
|
-
end
|
15
|
-
|
16
|
-
def print_summary
|
17
|
-
single_group = groups.size == 1
|
18
|
-
groups.each do |group, result|
|
19
|
-
color = result.success? ? :green : :red
|
20
|
-
prefix = single_group ? "" : "Group #{group}: "
|
21
|
-
Printer.out(
|
22
|
-
"#{prefix}#{result.total_count} clients, #{result.failures_count} failures\n",
|
23
|
-
color
|
24
|
-
)
|
25
|
-
|
26
|
-
unless result.success?
|
27
|
-
print_errors(result.errors)
|
28
|
-
Printer.out "\n"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def <<(result)
|
34
|
-
groups[result.group] = result
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
attr_reader :groups
|
40
|
-
|
41
|
-
def print_errors(errors)
|
42
|
-
Printer.out "\n"
|
43
|
-
errors.each.with_index do |error, i|
|
44
|
-
Printer.out "#{i + 1}) #{error}\n", :red
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
data/lib/wsdirector/runner.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "wsdirector/clients_holder"
|
4
|
-
require "wsdirector/results_holder"
|
5
|
-
require "wsdirector/result"
|
6
|
-
require "wsdirector/task"
|
7
|
-
require "wsdirector/ext/deep_dup"
|
8
|
-
|
9
|
-
module WSDirector
|
10
|
-
# Initiates all clients as a separate tasks (=threads)
|
11
|
-
class Runner
|
12
|
-
using WSDirector::Ext::DeepDup
|
13
|
-
|
14
|
-
def initialize(scenario)
|
15
|
-
@scenario = scenario
|
16
|
-
@total_count = scenario["total"]
|
17
|
-
@global_holder = ClientsHolder.new(total_count)
|
18
|
-
@results_holder = ResultsHolder.new
|
19
|
-
end
|
20
|
-
|
21
|
-
def start
|
22
|
-
Thread.abort_on_exception = true
|
23
|
-
|
24
|
-
tasks = scenario["clients"].flat_map do |client|
|
25
|
-
result = Result.new(client.fetch("name"))
|
26
|
-
results_holder << result
|
27
|
-
|
28
|
-
Array.new(client.fetch("multiplier")) do
|
29
|
-
Thread.new do
|
30
|
-
Task.new(client.deep_dup, global_holder: global_holder, result: result)
|
31
|
-
.run
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
tasks.each(&:join)
|
37
|
-
results_holder.print_summary
|
38
|
-
results_holder.success?
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
attr_reader :scenario, :total_count, :global_holder, :results_holder
|
44
|
-
end
|
45
|
-
end
|