wsdirector-cli 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 12469915784ca778073f50ab6b6157c73c014bee
4
+ data.tar.gz: '09f32425b8005d8cade46678259222286f4eff8b'
5
+ SHA512:
6
+ metadata.gz: 1985ee2404b98a54e9a63f40b35db59e139738f9d4d3f3eeb79bc451533f97f3a85c0f2abebdb5aa473e8a7f836a4597bdf058d6bd8cf8c016d85226545606b4
7
+ data.tar.gz: 55994d15c789a8f8c3bad3b2847a9d274a7ed581081e5928c071c6daeb6f4a1e2d225e844e7499364bf051982dca87cd28e4e9f4f563deb52c22bbe9087e1be6
@@ -0,0 +1,9 @@
1
+ # Change log
2
+
3
+ ## 0.2.0 (2017-11-05)
4
+
5
+ - Initial version. ([@palkan][], [@Kirillvs][], [@Grandman][])
6
+
7
+ [@palkan]: https://github.com/palkan
8
+ [@Kirillvs]: https://github.com/Kirillvs
9
+ [@Grandman]: https://github.com/Grandman
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 palkan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,143 @@
1
+ [![Gem Version](https://badge.fury.io/rb/wsdirector-cli.svg)](https://rubygems.org/gems/wsdirector-cli) [![CircleCI](https://circleci.com/gh/palkan/wsdirector.svg?style=svg)](https://circleci.com/gh/palkan/wsdirector)
2
+
3
+ # WebSocket Director
4
+
5
+ Command line tool for testing websocket servers using scenarios.
6
+
7
+ Suitable for testing any websocket server implementation, like [Action Cable](https://github.com/rails/rails/tree/master/actioncable), [Websocket Eventmachine Server](https://github.com/imanel/websocket-eventmachine-server), [Litecable](https://github.com/palkan/litecable) and so on.
8
+
9
+ <a href="https://evilmartians.com/">
10
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ $ gem install wsdirector-cli
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Create YAML file with simle testing script:
21
+
22
+ ```yml
23
+ # script.yml
24
+ - receive: "Welcome" # expect to receive message
25
+ - send:
26
+ data: "send message" # send message, all messages in data will be parse to json
27
+ - receive:
28
+ data: "receive message" # expect to receive json message
29
+ ```
30
+
31
+ and run it with this command:
32
+
33
+ ```bash
34
+ wsdirector script.yml ws://websocket.server:9876/ws
35
+
36
+ #=> 1 clients, 0 failures
37
+ ```
38
+
39
+ You can create more complex scenarios with multiple client groups:
40
+
41
+ ```yml
42
+ # script.yml
43
+ - client: # first clients group
44
+ name: "publisher" # optional group name
45
+ multiplier: ":scale" # :scale take number from -s param, and run :scale number of clients in this group
46
+ actions: #
47
+ - receive:
48
+ data: "Welcome"
49
+ - wait_all # makes all clients in all groups wait untill every client get this point (global barrier)
50
+ - send:
51
+ data: "test message"
52
+ - client:
53
+ name: "listeners"
54
+ multiplier: ":scale * 2"
55
+ actions:
56
+ - receive:
57
+ data: "Welcome"
58
+ - wait_all
59
+ - receive:
60
+ multiplier: ":scale" # you can use multiplier with any action
61
+ data: "test message"
62
+ ```
63
+
64
+ Run with scale factor:
65
+
66
+
67
+ ```bash
68
+ wsdirector script.yml ws://websocket.server:9876 -s 10
69
+
70
+ #=> Group publisher: 10 clients, 0 failures
71
+ #=> Group listeners: 20 clients, 0 failures
72
+ ```
73
+
74
+ The simpliest scenario is just checking that socket is succesfully connected:
75
+
76
+ ```yml
77
+ - client:
78
+ name: connection check
79
+ # no actions
80
+ ```
81
+
82
+ ### Protocols
83
+
84
+ WSDirector uses protocols to handle different actions.
85
+ Currently, we support "base" protocol (with `send`, `receive`, `wait_all` actions) and "action_cable" protocol, which extends "base" with `subscribe` and `perform` actions.
86
+
87
+ #### ActionCable Example
88
+
89
+ Channel code:
90
+
91
+ ```ruby
92
+ class ChatChannel < ApplicationCable::Channel
93
+ def subscribed
94
+ stream_from "chat_test"
95
+ end
96
+
97
+ def echo(data)
98
+ transmit data
99
+ end
100
+
101
+ def broadcast(data)
102
+ ActionCable.server.broadcast "chat_test", data
103
+ end
104
+ end
105
+ ```
106
+
107
+ Scenario:
108
+
109
+ ```yml
110
+ - client:
111
+ multiplier: ":scale"
112
+ name: "publisher"
113
+ protocol: "action_cable"
114
+ actions:
115
+ - subscribe:
116
+ channel: "ChatChannel"
117
+ - wait_all
118
+ - perform:
119
+ channel: "ChatChannel"
120
+ action: "broadcast"
121
+ data:
122
+ text: "hello"
123
+ - client:
124
+ name: "listener"
125
+ protocol: "action_cable"
126
+ actions:
127
+ - subscribe:
128
+ channel: "ChatChannel"
129
+ - wait_all
130
+ - receive:
131
+ channel: "ChatChannel"
132
+ data:
133
+ text: "hello"
134
+ ```
135
+
136
+ ## Contributing
137
+
138
+ Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/wsdirector.
139
+
140
+
141
+ ## License
142
+
143
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require "yaml"
5
+ require "json"
6
+
7
+ require "wsdirector/version"
8
+ require "wsdirector/configuration"
9
+
10
+ # Command line tool for testing websocket servers using scenarios.
11
+ module WSDirector
12
+ class Error < StandardError
13
+ end
14
+
15
+ class << self
16
+ def config
17
+ @config ||= Configuration.new
18
+ end
19
+ end
20
+ end
21
+
22
+ require "wsdirector/cli"
@@ -0,0 +1,77 @@
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; end
13
+
14
+ def run
15
+ parse_args!
16
+
17
+ begin
18
+ require "colorize" if WSDirector.config.colorize?
19
+ rescue LoadError
20
+ WSDirector.config.colorize = false
21
+ warn "Install colorize to use colored output"
22
+ end
23
+
24
+ scenario = WSDirector::ScenarioReader.parse(
25
+ WSDirector.config.scenario_path
26
+ )
27
+
28
+ if WSDirector::Runner.new(scenario).start
29
+ exit 0
30
+ else
31
+ exit 1
32
+ end
33
+ rescue Error => e
34
+ STDERR.puts e.message
35
+ exit 1
36
+ end
37
+
38
+ private
39
+
40
+ def parse_args!
41
+ # rubocop: disable Metrics/LineLength
42
+ parser = OptionParser.new do |opts|
43
+ opts.banner = "Usage: wsdirector scenario_path ws_url [options]"
44
+
45
+ opts.on("-s SCALE", "--scale=SCALE", Integer, "Scale factor") do |v|
46
+ WSDirector.config.scale = v
47
+ end
48
+
49
+ opts.on("-t TIMEOUT", "--timeout=TIMEOUT", Integer, "Synchronization (wait_all) timeout") do |v|
50
+ WSDirector.config.sync_timeout = v
51
+ end
52
+
53
+ opts.on("-c", "--[no-]color", "Colorize output") do |v|
54
+ WSDirector.config.colorize = v
55
+ end
56
+
57
+ opts.on("-v", "--version", "Print versin") do
58
+ STDOUT.puts WSDirector::VERSION
59
+ exit 0
60
+ end
61
+ end
62
+ # rubocop: enable Metrics/LineLength
63
+
64
+ parser.parse!
65
+
66
+ WSDirector.config.scenario_path = ARGV[0]
67
+ WSDirector.config.ws_url = ARGV[1]
68
+
69
+ raise(Error, "Scenario path is missing") if WSDirector.config.scenario_path.nil?
70
+
71
+ raise(Error, "File doesn't exist #{WSDirector.config.scenario_path}") unless
72
+ File.file?(WSDirector.config.scenario_path)
73
+
74
+ raise(Error, "Websocket server url is missing") if WSDirector.config.ws_url.nil?
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "websocket-client-simple"
4
+
5
+ module WSDirector
6
+ # WebSocket client
7
+ class Client
8
+ WAIT_WHEN_EXPECTING_EVENT = 5
9
+
10
+ attr_reader :ws
11
+
12
+ # Create new WebSocket client and connect to WSDirector
13
+ # ws URL.
14
+ #
15
+ # Optionally provide an ignore pattern (to ignore incoming message,
16
+ # for example, pings)
17
+ def initialize(ignore: nil)
18
+ @ignore = ignore
19
+ has_messages = @has_messages = Concurrent::Semaphore.new(0)
20
+ messages = @messages = Queue.new
21
+ path = WSDirector.config.ws_url
22
+ open = Concurrent::Promise.new
23
+ client = self
24
+
25
+ @ws = WebSocket::Client::Simple.connect(path) do |ws|
26
+ ws.on(:open) do |_event|
27
+ open.set(true)
28
+ end
29
+
30
+ ws.on :message do |msg|
31
+ data = msg.data
32
+ next if data.empty?
33
+ next if client.ignored?(data)
34
+ messages << data
35
+ has_messages.release
36
+ end
37
+
38
+ ws.on :error do |e|
39
+ messages << Error.new("WebSocket Error #{e.inspect} #{e.backtrace}")
40
+ end
41
+ end
42
+
43
+ open.wait!(WAIT_WHEN_EXPECTING_EVENT)
44
+ rescue Errno::ECONNREFUSED
45
+ raise Error, "Failed to connect to #{path}"
46
+ end
47
+
48
+ def receive(timeout = WAIT_WHEN_EXPECTING_EVENT)
49
+ @has_messages.try_acquire(1, timeout)
50
+ msg = @messages.pop(true)
51
+ raise msg if msg.is_a?(Exception)
52
+ msg
53
+ end
54
+
55
+ def send(msg)
56
+ @ws.send(msg)
57
+ end
58
+
59
+ def ignored?(msg)
60
+ return false unless @ignore
61
+ @ignore.any? { |pattern| msg =~ pattern }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,26 @@
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 nothing has been received
16
+ class NoMessageError < WSDirector::Error; end
17
+
18
+ class << self
19
+ def get(id)
20
+ raise Error, "Unknown protocol: #{id}" unless ID2CLASS.key?(id)
21
+ class_name = ID2CLASS.fetch(id)
22
+ const_get(class_name)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,61 @@
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 e.message =~ /reject_subscription/
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
+ private
54
+
55
+ def extract_identifier(step)
56
+ channel = step.delete("channel")
57
+ step.fetch("params", {}).merge(channel: channel).to_json
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WSDirector
4
+ module Protocols
5
+ # Base protocol describes basic actions
6
+ class Base
7
+ def initialize(task)
8
+ @task = task
9
+ end
10
+
11
+ def init_client(**options)
12
+ @client = build_client(**options)
13
+ end
14
+
15
+ def handle_step(step)
16
+ type = step.delete("type")
17
+ raise Error, "Unknown step: #{type}" unless respond_to?(type)
18
+ public_send(type, step)
19
+ end
20
+
21
+ def receive(step)
22
+ expected = step.fetch("data")
23
+ received = client.receive
24
+ receive_matches?(expected, received)
25
+ rescue ThreadError
26
+ raise NoMessageError, "Expected to receive #{expected} but nothing has been received"
27
+ end
28
+
29
+ def send(step)
30
+ data = step.fetch("data")
31
+ data = JSON.generate(data) if data.is_a?(Hash)
32
+ client.send(data)
33
+ end
34
+
35
+ def wait_all(_step)
36
+ task.global_holder.wait_all
37
+ end
38
+
39
+ def to_proc
40
+ proc { |step| handle_step(step) }
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :client, :task
46
+
47
+ def build_client(**options)
48
+ Client.new(**options)
49
+ end
50
+
51
+ def receive_matches?(expected, received)
52
+ received = JSON.parse(received) if expected.is_a?(Hash)
53
+
54
+ raise UnmatchedExpectationError, prepare_receive_error(expected, received) if
55
+ received != expected
56
+ end
57
+
58
+ def prepare_receive_error(expected, received)
59
+ <<~MSG
60
+ Action failed: #receive
61
+ -- expected: #{expected}
62
+ ++ got: #{received}
63
+ MSG
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
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
+ end
15
+
16
+ # Called when client successfully finished it's work
17
+ def succeed
18
+ all.increment
19
+ end
20
+
21
+ # Called when client failed
22
+ def failed(error_message)
23
+ errors << error_message
24
+ all.increment
25
+ failures.increment
26
+ end
27
+
28
+ def success?
29
+ failures.value.zero?
30
+ end
31
+
32
+ def total_count
33
+ all.value
34
+ end
35
+
36
+ def failures_count
37
+ failures.value
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :all, :success, :failures
43
+ end
44
+ end
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,45 @@
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
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WSDirector
4
+ # Read and parse YAML scenario
5
+ class ScenarioReader
6
+ MULTIPLIER_FORMAT = /^[-+*\\\d ]+$/
7
+
8
+ class << self
9
+ def parse(file_path)
10
+ contents = YAML.load_file(file_path)
11
+
12
+ if contents.first.key?("client")
13
+ parse_multiple_scenarios(contents)
14
+ else
15
+ { "total" => 1, "clients" => [parse_simple_scenario(contents)] }
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def handle_steps(steps)
22
+ steps.flat_map do |step|
23
+ if step.is_a?(Hash)
24
+ type, data = step.to_a.first
25
+ multiplier = parse_multiplier(data.delete("multiplier") || "1")
26
+ Array.new(multiplier) { { "type" => type }.merge(data) }
27
+ else
28
+ { "type" => step }
29
+ end
30
+ end
31
+ end
32
+
33
+ def parse_simple_scenario(
34
+ steps,
35
+ multiplier: 1, name: "default", ignore: nil, protocol: "base"
36
+ )
37
+ {
38
+ "multiplier" => multiplier,
39
+ "steps" => handle_steps(steps),
40
+ "name" => name,
41
+ "ignore" => ignore,
42
+ "protocol" => protocol
43
+ }
44
+ end
45
+
46
+ def parse_multiple_scenarios(definitions)
47
+ total_count = 0
48
+ clients = definitions.map.with_index do |client, i|
49
+ _, client = client.to_a.first
50
+ multiplier = parse_multiplier(client.delete("multiplier") || "1")
51
+ name = client.delete("name") || (i + 1).to_s
52
+ total_count += multiplier
53
+ ignore = parse_ingore(client.fetch("ignore", nil))
54
+ parse_simple_scenario(
55
+ client.fetch("actions", []),
56
+ multiplier: multiplier,
57
+ name: name,
58
+ ignore: ignore,
59
+ protocol: client.fetch("protocol", "base")
60
+ )
61
+ end
62
+ { "total" => total_count, "clients" => clients }
63
+ end
64
+
65
+ def parse_multiplier(str)
66
+ prepared = str.to_s.gsub(":scale", WSDirector.config.scale.to_s)
67
+ raise WSDirector::Error, "Unknown multiplier format: #{str}" unless
68
+ prepared =~ MULTIPLIER_FORMAT
69
+
70
+ eval(prepared) # rubocop:disable Security/Eval
71
+ end
72
+
73
+ def parse_ingore(str)
74
+ return unless str
75
+
76
+ Array(str)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wsdirector/client"
4
+ require "wsdirector/protocols"
5
+
6
+ module WSDirector
7
+ # Single client operator
8
+ class Task
9
+ attr_reader :global_holder, :client
10
+
11
+ def initialize(config, global_holder:, result:)
12
+ @ignore = config.fetch("ignore")
13
+ @steps = config.fetch("steps")
14
+ @global_holder = global_holder
15
+ @result = result
16
+
17
+ protocol_class = Protocols.get(config.fetch("protocol", "base"))
18
+ @protocol = protocol_class.new(self)
19
+ end
20
+
21
+ def run
22
+ connect!
23
+
24
+ steps.each(&protocol)
25
+
26
+ result.succeed
27
+ rescue Error => e
28
+ result.failed(e.message)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :steps, :result, :protocol
34
+
35
+ def connect!
36
+ protocol.init_client(ignore: @ignore)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WSDirector
4
+ VERSION = "0.2.1"
5
+ end
metadata ADDED
@@ -0,0 +1,237 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wsdirector-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Kirill Arkhipov
8
+ - Grandman
9
+ - palkan
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2017-11-05 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: websocket-client-simple
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '0.3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '0.3'
29
+ - !ruby/object:Gem::Dependency
30
+ name: concurrent-ruby
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: 1.0.5
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: 1.0.5
43
+ - !ruby/object:Gem::Dependency
44
+ name: colorize
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: websocket-eventmachine-server
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: 1.0.1
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: 1.0.1
71
+ - !ruby/object:Gem::Dependency
72
+ name: rack
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '2.0'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '2.0'
85
+ - !ruby/object:Gem::Dependency
86
+ name: litecable
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '0.5'
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '0.5'
99
+ - !ruby/object:Gem::Dependency
100
+ name: puma
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: '3.6'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: '3.6'
113
+ - !ruby/object:Gem::Dependency
114
+ name: bundler
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.13'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '1.13'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rake
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '10.0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '10.0'
141
+ - !ruby/object:Gem::Dependency
142
+ name: rspec
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '3.5'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: '3.5'
155
+ - !ruby/object:Gem::Dependency
156
+ name: minitest
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: '5.9'
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: '5.9'
169
+ - !ruby/object:Gem::Dependency
170
+ name: rubocop
171
+ requirement: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - "~>"
174
+ - !ruby/object:Gem::Version
175
+ version: '0.50'
176
+ type: :development
177
+ prerelease: false
178
+ version_requirements: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - "~>"
181
+ - !ruby/object:Gem::Version
182
+ version: '0.50'
183
+ description: Command line tool for testing websocket servers using scenarios.
184
+ email:
185
+ - kirillvs@mail.ru
186
+ - root@grandman73.ru
187
+ - dementiev.vm@gmail.com
188
+ executables:
189
+ - wsdirector
190
+ extensions: []
191
+ extra_rdoc_files: []
192
+ files:
193
+ - CHANGELOG.md
194
+ - LICENSE.txt
195
+ - README.md
196
+ - bin/wsdirector
197
+ - lib/wsdirector.rb
198
+ - lib/wsdirector/cli.rb
199
+ - lib/wsdirector/client.rb
200
+ - lib/wsdirector/clients_holder.rb
201
+ - lib/wsdirector/configuration.rb
202
+ - lib/wsdirector/ext/deep_dup.rb
203
+ - lib/wsdirector/printer.rb
204
+ - lib/wsdirector/protocols.rb
205
+ - lib/wsdirector/protocols/action_cable.rb
206
+ - lib/wsdirector/protocols/base.rb
207
+ - lib/wsdirector/result.rb
208
+ - lib/wsdirector/results_holder.rb
209
+ - lib/wsdirector/runner.rb
210
+ - lib/wsdirector/scenario_reader.rb
211
+ - lib/wsdirector/task.rb
212
+ - lib/wsdirector/version.rb
213
+ homepage: https://github.com/palkan/wsdirector
214
+ licenses:
215
+ - MIT
216
+ metadata: {}
217
+ post_install_message:
218
+ rdoc_options: []
219
+ require_paths:
220
+ - lib
221
+ required_ruby_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ required_rubygems_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ requirements: []
232
+ rubyforge_project:
233
+ rubygems_version: 2.6.13
234
+ signing_key:
235
+ specification_version: 4
236
+ summary: Command line tool for testing websocket servers using scenarios.
237
+ test_files: []