anyt 0.2.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 +7 -0
- data/.gitignore +43 -0
- data/.rubocop.yml +68 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/MIT-LICENSE +20 -0
- data/Makefile +5 -0
- data/README.md +35 -0
- data/anyt.gemspec +41 -0
- data/bin/anyt +16 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/circle.yml +14 -0
- data/lib/anyt.rb +14 -0
- data/lib/anyt/cli.rb +119 -0
- data/lib/anyt/client.rb +102 -0
- data/lib/anyt/command.rb +50 -0
- data/lib/anyt/config.rb +23 -0
- data/lib/anyt/dummy/application.rb +31 -0
- data/lib/anyt/dummy/config.ru +10 -0
- data/lib/anyt/ext/minitest.rb +119 -0
- data/lib/anyt/rpc.rb +40 -0
- data/lib/anyt/tests.rb +50 -0
- data/lib/anyt/tests/core/ping_test.rb +23 -0
- data/lib/anyt/tests/core/welcome_test.rb +10 -0
- data/lib/anyt/tests/request/connection_test.rb +52 -0
- data/lib/anyt/tests/streams/multiple_clients_test.rb +61 -0
- data/lib/anyt/tests/streams/multiple_test.rb +60 -0
- data/lib/anyt/tests/streams/single_test.rb +58 -0
- data/lib/anyt/tests/subscriptions/ack_test.rb +41 -0
- data/lib/anyt/tests/subscriptions/perform_test.rb +62 -0
- data/lib/anyt/tests/subscriptions/transmissions_test.rb +33 -0
- data/lib/anyt/utils.rb +3 -0
- data/lib/anyt/utils/async_helpers.rb +16 -0
- data/lib/anyt/version.rb +5 -0
- metadata +289 -0
data/lib/anyt/client.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anyt
|
4
|
+
# Synchronous websocket client
|
5
|
+
# Based on https://github.com/rails/rails/blob/v5.0.1/actioncable/test/client_test.rb
|
6
|
+
class Client
|
7
|
+
require "websocket-client-simple"
|
8
|
+
require "concurrent"
|
9
|
+
|
10
|
+
class TimeoutError < StandardError; end
|
11
|
+
|
12
|
+
WAIT_WHEN_EXPECTING_EVENT = 5
|
13
|
+
WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5
|
14
|
+
|
15
|
+
# rubocop: disable Metrics/AbcSize
|
16
|
+
# rubocop: disable Metrics/MethodLength
|
17
|
+
def initialize(
|
18
|
+
ignore: [], url: Anyt.config.target_url, qs: "",
|
19
|
+
cookies: "", headers: {}
|
20
|
+
)
|
21
|
+
ignore_message_types = @ignore_message_types = ignore
|
22
|
+
messages = @messages = Queue.new
|
23
|
+
closed = @closed = Concurrent::Event.new
|
24
|
+
has_messages = @has_messages = Concurrent::Semaphore.new(0)
|
25
|
+
|
26
|
+
headers = headers.merge('cookie' => cookies)
|
27
|
+
|
28
|
+
open = Concurrent::Promise.new
|
29
|
+
|
30
|
+
@ws = WebSocket::Client::Simple.connect(
|
31
|
+
url + "?#{qs}",
|
32
|
+
headers: headers
|
33
|
+
) do |ws|
|
34
|
+
ws.on(:error) do |event|
|
35
|
+
event = RuntimeError.new(event.message) unless event.is_a?(Exception)
|
36
|
+
|
37
|
+
if open.pending?
|
38
|
+
open.fail(event)
|
39
|
+
else
|
40
|
+
messages << event
|
41
|
+
has_messages.release
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
ws.on(:open) do |_event|
|
46
|
+
open.set(true)
|
47
|
+
end
|
48
|
+
|
49
|
+
ws.on(:message) do |event|
|
50
|
+
if event.type == :close
|
51
|
+
closed.set
|
52
|
+
else
|
53
|
+
message = JSON.parse(event.data)
|
54
|
+
|
55
|
+
next if ignore_message_types.include?(message["type"])
|
56
|
+
messages << message
|
57
|
+
has_messages.release
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ws.on(:close) do |_event|
|
62
|
+
closed.set
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
open.wait!(WAIT_WHEN_EXPECTING_EVENT)
|
67
|
+
end
|
68
|
+
# rubocop: enable Metrics/AbcSize
|
69
|
+
# rubocop: enable Metrics/MethodLength
|
70
|
+
|
71
|
+
def receive(timeout: WAIT_WHEN_EXPECTING_EVENT)
|
72
|
+
raise TimeoutError, "Timed out to receive message" unless
|
73
|
+
@has_messages.try_acquire(1, timeout)
|
74
|
+
|
75
|
+
msg = @messages.pop(true)
|
76
|
+
raise msg if msg.is_a?(Exception)
|
77
|
+
|
78
|
+
msg
|
79
|
+
end
|
80
|
+
|
81
|
+
def send(message)
|
82
|
+
@ws.send(JSON.generate(message))
|
83
|
+
end
|
84
|
+
|
85
|
+
def close
|
86
|
+
sleep WAIT_WHEN_NOT_EXPECTING_EVENT
|
87
|
+
|
88
|
+
raise "#{@messages.size} messages unprocessed" unless @messages.empty?
|
89
|
+
|
90
|
+
@ws.close
|
91
|
+
wait_for_close
|
92
|
+
end
|
93
|
+
|
94
|
+
def wait_for_close
|
95
|
+
@closed.wait(WAIT_WHEN_EXPECTING_EVENT)
|
96
|
+
end
|
97
|
+
|
98
|
+
def closed?
|
99
|
+
@closed.set?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/anyt/command.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anyt
|
4
|
+
# Runs system command (websocket server)
|
5
|
+
module Command
|
6
|
+
class << self
|
7
|
+
attr_accessor :running
|
8
|
+
|
9
|
+
# rubocop: disable Metrics/MethodLength
|
10
|
+
# rubocop: disable Metrics/AbcSize
|
11
|
+
def run
|
12
|
+
return if @running
|
13
|
+
|
14
|
+
Anycable.logger.debug "Running command: #{Anyt.config.command}"
|
15
|
+
|
16
|
+
out = Anycable.config.debug ? STDOUT : IO::NULL
|
17
|
+
|
18
|
+
@pid = Process.spawn(
|
19
|
+
Anyt.config.command,
|
20
|
+
out: out,
|
21
|
+
err: out
|
22
|
+
)
|
23
|
+
|
24
|
+
Process.detach(@pid)
|
25
|
+
|
26
|
+
Anycable.logger.debug "Command PID: #{@pid}"
|
27
|
+
|
28
|
+
@running = true
|
29
|
+
|
30
|
+
sleep Anyt.config.wait_command
|
31
|
+
end
|
32
|
+
# rubocop: enable Metrics/MethodLength
|
33
|
+
# rubocop: enable Metrics/AbcSize
|
34
|
+
|
35
|
+
def stop
|
36
|
+
return unless @running
|
37
|
+
|
38
|
+
Anycable.logger.debug "Terminate PID: #{@pid}"
|
39
|
+
|
40
|
+
Process.kill("SIGKILL", @pid)
|
41
|
+
|
42
|
+
@running = false
|
43
|
+
end
|
44
|
+
|
45
|
+
def running?
|
46
|
+
@running == true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/anyt/config.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anyway"
|
4
|
+
|
5
|
+
module Anyt
|
6
|
+
# Anyt configuration
|
7
|
+
class Config < Anyway::Config
|
8
|
+
attr_config :command,
|
9
|
+
:only_tests,
|
10
|
+
target_url: "ws://localhost:9292/",
|
11
|
+
wait_command: 2
|
12
|
+
|
13
|
+
def filter_tests?
|
14
|
+
!only_tests.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def tests_filter
|
18
|
+
@tests_filter ||= begin
|
19
|
+
/(#{only_tests.join('|')})/
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "action_cable"
|
5
|
+
require "action_dispatch/middleware/cookies"
|
6
|
+
|
7
|
+
module ApplicationCable
|
8
|
+
class Connection < ActionCable::Connection::Base
|
9
|
+
delegate :params, to: :request
|
10
|
+
|
11
|
+
def connect
|
12
|
+
logger.info "Connected"
|
13
|
+
Anyt::ConnectHandlers.call(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def disconnect
|
17
|
+
logger.info "Disconnected"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ApplicationCable
|
23
|
+
class Channel < ActionCable::Channel::Base
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ActionCable.server.config.cable = { "adapter" => "redis" }
|
28
|
+
ActionCable.server.config.connection_class = -> { ApplicationCable::Connection }
|
29
|
+
ActionCable.server.config.disable_request_forgery_protection = true
|
30
|
+
ActionCable.server.config.logger =
|
31
|
+
Rails.logger = Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest/spec"
|
4
|
+
require "minitest/reporters"
|
5
|
+
|
6
|
+
module Anyt
|
7
|
+
# Common tests helpers
|
8
|
+
module TestHelpers
|
9
|
+
def self.included(base)
|
10
|
+
base.let(:client) { build_client(ignore: %w[ping welcome]) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_client(*args)
|
14
|
+
Anyt::Client.new(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Anyt
|
20
|
+
# Namespace for test channels
|
21
|
+
module TestChannels; end
|
22
|
+
|
23
|
+
# Custom #connect handlers management
|
24
|
+
module ConnectHandlers
|
25
|
+
class << self
|
26
|
+
def call(connection)
|
27
|
+
handlers_for(connection).each do |(_, handler)|
|
28
|
+
connection.reject_unauthorized_connection unless
|
29
|
+
connection.instance_eval(&handler)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add(tag, &block)
|
34
|
+
handlers << [tag, block]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def handlers_for(connection)
|
40
|
+
handlers.select do |(tag, _)|
|
41
|
+
connection.params['test'] == tag
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def handlers
|
46
|
+
@handlers ||= []
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Kernel extensions
|
53
|
+
module Kernel
|
54
|
+
## Wraps `describe` and include shared helpers
|
55
|
+
private def feature(*args, &block)
|
56
|
+
cls = describe(*args, &block)
|
57
|
+
cls.include Anyt::TestHelpers
|
58
|
+
cls
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Extend Minitest Spec DSL with custom methodss
|
63
|
+
module Minitest::Spec::DSL
|
64
|
+
# Simplified version of `it` which doesn't care
|
65
|
+
# about unique method names
|
66
|
+
def scenario(desc, &block)
|
67
|
+
block ||= proc { skip "(no tests defined)" }
|
68
|
+
|
69
|
+
define_method "test_ #{desc}", &block
|
70
|
+
|
71
|
+
desc
|
72
|
+
end
|
73
|
+
|
74
|
+
# Generates Channel class dynamically and
|
75
|
+
# add memoized helper to access its name
|
76
|
+
def channel(id = nil, &block)
|
77
|
+
class_name = @name.gsub(/\s+/, '_')
|
78
|
+
class_name += "_#{id}" if id
|
79
|
+
class_name += "_channel"
|
80
|
+
|
81
|
+
cls = Class.new(ApplicationCable::Channel, &block)
|
82
|
+
|
83
|
+
Anyt::TestChannels.const_set(class_name.classify, cls)
|
84
|
+
|
85
|
+
helper_name = id ? "#{id}_channel" : "channel"
|
86
|
+
|
87
|
+
let(helper_name) { cls.name }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add new #connect handler
|
91
|
+
def connect_handler(tag, &block)
|
92
|
+
Anyt::ConnectHandlers.add(tag, &block)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module Anyt
|
97
|
+
# Patch Minitest load_plugins to disable Rails plugin
|
98
|
+
# See: https://github.com/kern/minitest-reporters/issues/230
|
99
|
+
module MinitestPatch
|
100
|
+
def load_plugins
|
101
|
+
super
|
102
|
+
extensions.delete('rails')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Patch Spec reporter
|
107
|
+
module ReporterPatch # :nodoc:
|
108
|
+
def record_print_status(test)
|
109
|
+
test_name = test.name.gsub(/^test_/, '').strip
|
110
|
+
print pad_test(test_name)
|
111
|
+
print_colored_status(test)
|
112
|
+
print(" (%.2fs)" % test.time) unless test.time.nil?
|
113
|
+
puts
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Minitest::Reporters::SpecReporter.prepend Anyt::ReporterPatch
|
119
|
+
Minitest.singleton_class.prepend Anyt::MinitestPatch
|
data/lib/anyt/rpc.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anyt # :nodoc:
|
4
|
+
require "anyt/dummy/application"
|
5
|
+
require "anycable"
|
6
|
+
|
7
|
+
# Runs AnyCable RPC server in the background
|
8
|
+
module RPC
|
9
|
+
using AsyncHelpers
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :running
|
13
|
+
|
14
|
+
def start
|
15
|
+
Anycable.logger.debug "Starting RPC server ..."
|
16
|
+
|
17
|
+
@thread = Thread.new { Anycable::Server.start }
|
18
|
+
@thread.abort_on_exception = true
|
19
|
+
|
20
|
+
wait(2) { running? }
|
21
|
+
|
22
|
+
Anycable.logger.debug "RPC server started"
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
return unless running?
|
27
|
+
|
28
|
+
Anycable::Server.grpc_server.stop
|
29
|
+
end
|
30
|
+
|
31
|
+
def running?
|
32
|
+
Anycable::Server.grpc_server&.running_state == :running
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Anycable.configure do |config|
|
37
|
+
config.connection_factory = ActionCable.server.config.connection_class.call
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/anyt/tests.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anyt
|
4
|
+
# Loads and runs test cases
|
5
|
+
module Tests
|
6
|
+
require "anyt/client"
|
7
|
+
require_relative "ext/minitest"
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Run all loaded tests
|
11
|
+
def run
|
12
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
13
|
+
|
14
|
+
Anycable.logger.debug "Run tests against: #{Anyt.config.target_url}"
|
15
|
+
Minitest.run
|
16
|
+
end
|
17
|
+
|
18
|
+
# Load tests code (filtered if present)
|
19
|
+
#
|
20
|
+
# NOTE: We should run this before launching RPC server
|
21
|
+
|
22
|
+
# rubocop:disable Metrics/AbcSize
|
23
|
+
# rubocop:disable Metrics/MethodLength
|
24
|
+
def load_tests
|
25
|
+
return load_all_tests unless Anyt.config.filter_tests?
|
26
|
+
|
27
|
+
pattern = File.expand_path("tests/**/*.rb", __dir__)
|
28
|
+
skipped = []
|
29
|
+
filter = Anyt.config.tests_filter
|
30
|
+
|
31
|
+
Dir.glob(pattern).each do |file|
|
32
|
+
if file.match?(filter)
|
33
|
+
require file
|
34
|
+
else
|
35
|
+
skipped << file.gsub(File.join(__dir__, 'tests/'), '').gsub('_test.rb', '')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
$stdout.print "Skipping tests: #{skipped.join(', ')}\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Load all test files
|
43
|
+
def load_all_tests
|
44
|
+
pattern = File.expand_path("tests/**/*.rb", __dir__)
|
45
|
+
|
46
|
+
Dir.glob(pattern).each { |file| require file }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Ping" do
|
4
|
+
scenario %{
|
5
|
+
Client receives pings with timestamps
|
6
|
+
} do
|
7
|
+
client = build_client(ignore: ["welcome"])
|
8
|
+
|
9
|
+
previous_stamp = 0
|
10
|
+
|
11
|
+
2.times do
|
12
|
+
ping = client.receive
|
13
|
+
|
14
|
+
current_stamp = ping["message"]
|
15
|
+
|
16
|
+
assert_equal ping["type"], "ping"
|
17
|
+
assert_kind_of Integer, current_stamp
|
18
|
+
refute_operator previous_stamp, :>=, current_stamp
|
19
|
+
|
20
|
+
previous_stamp = current_stamp
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|