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.
@@ -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
@@ -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
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./application"
4
+
5
+ require_relative "../tests"
6
+
7
+ # Load channels from tests
8
+ Anyt::Tests.load_all_tests
9
+
10
+ run ActionCable.server
@@ -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
@@ -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
@@ -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