anyt 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|