wsdirector-cli 0.2.1

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.
@@ -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: []