wsdirector-cli 0.4.0 → 1.0.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/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
|