diff options
36 files changed, 472 insertions, 255 deletions
diff --git a/.gitignore b/.gitignore index 1918a1b0ee..cb2bc5e743 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -test/tests.log
\ No newline at end of file +/gemfiles/*.lock +/test/tests.log diff --git a/.travis.yml b/.travis.yml index 99a95ae240..5e156e2b77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,19 @@ -sudo: false +language: ruby cache: bundler +sudo: false + rvm: - 2.2 - ruby-head + +gemfile: + - Gemfile + - gemfiles/rails_4-2-stable.gemfile + matrix: fast_finish: true + allow_failures: ruby-head + notifications: email: false irc: @@ -16,4 +25,4 @@ notifications: on_success: change on_failure: always rooms: - - secure: "EZmqsgjEQbWouCx6xL/30jslug7xcq+Dl09twDGjBs369GB5LiUm17/I7d6H1YQFY0Vu2LpiQ/zs+6ihlBjslRV/2RYM3AgAA9OOC3pn7uENFVTXaECi/io1wjvlbMNrf1YJSc3aUyiWKykRsdZnZSFszkDs4DMnZG1s/Oxf1JTYEGNWW3WcOFfYkzcS7NWlOW9OBf4RuzjtLYF05IO4t4FZI1aTWrNV3NNMZ+tqmiQHHNrQE/CzQE3ujqFiea2vVZ7PwvmjVWJgC29UZqS7HcNuq6cCMtMZZuubCZmyT85GjJ/SKTShxFqfV1oCpY3y6kyWcTAQsUoLtPEX0OxLeX+CgWNIJK0rY5+5/v5pZP1uwRsMfLerfp2a9g4fAnlcAKaZjalOc39rOkJl8FdvLQtqFIGWxpjWdJbMrCt3SrnnOccpDqDWpAL798LVBONcOuor71rEeNj1dZ6fCoHTKhLVy6UVm9eUI8zt1APM0xzHgTBI1KBVZi0ikqPcaW604rrNUSk8g/AFQk0pIKyDzV9qYMJD2wnr42cyPKg0gfk1tc9KRCNeH+My1HdZS6Zogpjkc3plAzJQ1DAPY0EBWUlEKghpkyCunjpxN3cw390iKgZUN52phtmGMRkyNnwI8+ELnT4I+Jata1mFyWiETM85q8Rqx+FeA0W/BBsEAp8="
\ No newline at end of file + - secure: "EZmqsgjEQbWouCx6xL/30jslug7xcq+Dl09twDGjBs369GB5LiUm17/I7d6H1YQFY0Vu2LpiQ/zs+6ihlBjslRV/2RYM3AgAA9OOC3pn7uENFVTXaECi/io1wjvlbMNrf1YJSc3aUyiWKykRsdZnZSFszkDs4DMnZG1s/Oxf1JTYEGNWW3WcOFfYkzcS7NWlOW9OBf4RuzjtLYF05IO4t4FZI1aTWrNV3NNMZ+tqmiQHHNrQE/CzQE3ujqFiea2vVZ7PwvmjVWJgC29UZqS7HcNuq6cCMtMZZuubCZmyT85GjJ/SKTShxFqfV1oCpY3y6kyWcTAQsUoLtPEX0OxLeX+CgWNIJK0rY5+5/v5pZP1uwRsMfLerfp2a9g4fAnlcAKaZjalOc39rOkJl8FdvLQtqFIGWxpjWdJbMrCt3SrnnOccpDqDWpAL798LVBONcOuor71rEeNj1dZ6fCoHTKhLVy6UVm9eUI8zt1APM0xzHgTBI1KBVZi0ikqPcaW604rrNUSk8g/AFQk0pIKyDzV9qYMJD2wnr42cyPKg0gfk1tc9KRCNeH+My1HdZS6Zogpjkc3plAzJQ1DAPY0EBWUlEKghpkyCunjpxN3cw390iKgZUN52phtmGMRkyNnwI8+ELnT4I+Jata1mFyWiETM85q8Rqx+FeA0W/BBsEAp8=" @@ -1,2 +1,8 @@ source 'https://rubygems.org' + +gem 'activesupport', github: 'rails/rails' +gem 'actionpack', github: 'rails/rails' +gem 'arel', github: 'rails/arel' +gem 'rack', github: 'rack/rack' + gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 5548531abe..7f128bbdd1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,47 @@ +GIT + remote: git://github.com/rack/rack.git + revision: 6216a3f8a3560639ee1ddadc1e0d6bf9e5f31830 + specs: + rack (2.0.0.alpha) + json + +GIT + remote: git://github.com/rails/arel.git + revision: 3c429c5d86e9e2201c2a35d934ca6a8911c18e69 + specs: + arel (7.0.0.alpha) + +GIT + remote: git://github.com/rails/rails.git + revision: 960de47f0eef79d234eb3cfc47fabb470fef1529 + specs: + actionpack (5.0.0.alpha) + actionview (= 5.0.0.alpha) + activesupport (= 5.0.0.alpha) + rack (~> 2.x) + rack-test (~> 0.6.3) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.0.alpha) + activesupport (= 5.0.0.alpha) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activesupport (5.0.0.alpha) + concurrent-ruby (~> 1.0.0.pre3, < 2.0.0) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + method_source + minitest (~> 5.1) + tzinfo (~> 1.1) + railties (5.0.0.alpha) + actionpack (= 5.0.0.alpha) + activesupport (= 5.0.0.alpha) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + PATH remote: . specs: @@ -14,27 +58,8 @@ PATH GEM remote: https://rubygems.org/ specs: - actionpack (4.2.3) - actionview (= 4.2.3) - activesupport (= 4.2.3) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.3) - activesupport (= 4.2.3) - builder (~> 3.1) - erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activesupport (4.2.3) - i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) builder (3.2.2) - celluloid (0.16.0) + celluloid (0.16.1) timers (~> 4.0.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) @@ -43,50 +68,46 @@ GEM coffee-script-source execjs coffee-script-source (1.9.1.1) + concurrent-ruby (1.0.0.pre4) em-hiredis (0.3.0) eventmachine (~> 1.0) hiredis (~> 0.5.0) erubis (2.7.0) - eventmachine (1.0.7) - execjs (2.5.2) + eventmachine (1.0.8) + execjs (2.6.0) faye-websocket (0.10.0) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) hiredis (0.5.2) - hitimes (1.2.2) + hitimes (1.2.3) i18n (0.7.0) json (1.8.3) - loofah (2.0.2) + loofah (2.0.3) nokogiri (>= 1.5.9) metaclass (0.0.4) + method_source (0.8.2) mini_portile (0.6.2) - minitest (5.7.0) + minitest (5.8.1) mocha (1.1.0) metaclass (~> 0.0.1) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) puma (2.12.2) - rack (1.6.4) rack-test (0.6.3) rack (>= 1.0) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.6) + rails-dom-testing (1.0.7) activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) - railties (4.2.3) - actionpack (= 4.2.3) - activesupport (= 4.2.3) - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) rake (10.4.2) redis (3.2.1) thor (0.19.1) thread_safe (0.3.5) - timers (4.0.1) + timers (4.0.4) hitimes tzinfo (1.2.2) thread_safe (~> 0.1) @@ -99,9 +120,13 @@ PLATFORMS DEPENDENCIES actioncable! + actionpack! + activesupport! + arel! mocha puma + rack! rake BUNDLED WITH - 1.10.5 + 1.10.6 @@ -1,7 +1,7 @@ -# Action Cable – Integrated websockets for Rails +# Action Cable – Integrated WebSockets for Rails [](https://travis-ci.org/rails/actioncable) -Action Cable seamlessly integrates websockets with the rest of your Rails application. +Action Cable seamlessly integrates WebSockets with the rest of your Rails application. It allows for real-time features to be written in Ruby in the same style and form as the rest of your Rails application, while still being performant and scalable. It's a full-stack offering that provides both a client-side @@ -12,9 +12,9 @@ domain model written with ActiveRecord or your ORM of choice. ## Terminology A single Action Cable server can handle multiple connection instances. It has one -connection instance per websocket connection. A single user may have multiple -websockets open to your application if they use multiple browser tabs or devices. -The client of a websocket connection is called the consumer. +connection instance per WebSocket connection. A single user may have multiple +WebSockets open to your application if they use multiple browser tabs or devices. +The client of a WebSocket connection is called the consumer. Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates a logical unit of work, similar to what a controller does in a regular MVC setup. For example, you could have a `ChatChannel` and a `AppearancesChannel`, and a consumer could be subscribed to either @@ -169,8 +169,8 @@ Turbolinks `page:change` callback and allowing the user to click a data-behavior ### Channel example 2: Receiving new web notifications -The appearance example was all about exposing server functionality to client-side invocation over the websocket connection. -But the great thing about websockets is that it's a two-way street. So now let's show an example where the server invokes +The appearance example was all about exposing server functionality to client-side invocation over the WebSocket connection. +But the great thing about WebSockets is that it's a two-way street. So now let's show an example where the server invokes action on the client. This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right @@ -336,7 +336,7 @@ The above will start a cable server on port 28080. Remember to point your client ### In app -If you are using a threaded server like Puma or Thin, the current implementation of ActionCable can run side-along with your Rails application. For example, to listen for websocket requests on `/websocket`, match requests on that path: +If you are using a threaded server like Puma or Thin, the current implementation of ActionCable can run side-along with your Rails application. For example, to listen for WebSocket requests on `/websocket`, match requests on that path: ```ruby # config/routes.rb @@ -359,7 +359,7 @@ We'll get all this abstracted properly when the framework is integrated into Rai ## Dependencies Action Cable is currently tied to Redis through its use of the pubsub feature to route -messages back and forth over the websocket cable connection. This dependency may well +messages back and forth over the WebSocket cable connection. This dependency may well be alleviated in the future, but for the moment that's what it is. So be sure to have Redis installed and running. @@ -7,5 +7,6 @@ Rake::TestTask.new(:test) do |t| t.libs << "test" t.pattern = 'test/**/*_test.rb' t.verbose = true + t.warning = false end Rake::Task['test'].comment = "Run tests" diff --git a/actioncable.gemspec b/actioncable.gemspec index 02350186db..2f4ae41dc1 100644 --- a/actioncable.gemspec +++ b/actioncable.gemspec @@ -1,11 +1,11 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.unshift File.expand_path("../lib", __FILE__) require 'action_cable/version' Gem::Specification.new do |s| s.name = 'actioncable' s.version = ActionCable::VERSION - s.summary = 'Websockets framework for Rails.' - s.description = 'Structure many real-time application concerns into channels over a single websockets connection.' + s.summary = 'WebSocket framework for Rails.' + s.description = 'Structure many real-time application concerns into channels over a single WebSocket connection.' s.license = 'MIT' s.author = ['Pratik Naik', 'David Heinemeier Hansson'] @@ -28,7 +28,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'puma' s.add_development_dependency 'mocha' - s.files = Dir['README', 'lib/**/*'] + s.files = Dir['README.md', 'lib/**/*'] s.has_rdoc = false s.require_path = 'lib' diff --git a/gemfiles/rails_42.gemfile b/gemfiles/rails_42.gemfile new file mode 100644 index 0000000000..8ca60d69db --- /dev/null +++ b/gemfiles/rails_42.gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 4.2.4' + +gemspec path: '..' diff --git a/lib/action_cable.rb b/lib/action_cable.rb index d2c5251634..89ffa1fda7 100644 --- a/lib/action_cable.rb +++ b/lib/action_cable.rb @@ -1,32 +1,21 @@ -require 'eventmachine' -EventMachine.epoll if EventMachine.epoll? -EventMachine.kqueue if EventMachine.kqueue? - -require 'set' - require 'active_support' -require 'active_support/json' -require 'active_support/concern' -require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/module/delegation' -require 'active_support/callbacks' - -require 'faye/websocket' -require 'celluloid' -require 'em-hiredis' -require 'redis' - -require 'action_cable/engine' if defined?(Rails) +require 'active_support/rails' require 'action_cable/version' module ActionCable - autoload :Server, 'action_cable/server' - autoload :Connection, 'action_cable/connection' - autoload :Channel, 'action_cable/channel' - autoload :RemoteConnections, 'action_cable/remote_connections' + extend ActiveSupport::Autoload # Singleton instance of the server module_function def server @server ||= ActionCable::Server::Base.new end + + eager_autoload do + autoload :Server + autoload :Connection + autoload :Channel + autoload :RemoteConnections + end end + +require 'action_cable/engine' if defined?(Rails) diff --git a/lib/action_cable/channel.rb b/lib/action_cable/channel.rb index 3b973ba0a7..7ae262ce5f 100644 --- a/lib/action_cable/channel.rb +++ b/lib/action_cable/channel.rb @@ -1,10 +1,14 @@ module ActionCable module Channel - autoload :Base, 'action_cable/channel/base' - autoload :Broadcasting, 'action_cable/channel/broadcasting' - autoload :Callbacks, 'action_cable/channel/callbacks' - autoload :Naming, 'action_cable/channel/naming' - autoload :PeriodicTimers, 'action_cable/channel/periodic_timers' - autoload :Streams, 'action_cable/channel/streams' + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Base + autoload :Broadcasting + autoload :Callbacks + autoload :Naming + autoload :PeriodicTimers + autoload :Streams + end end end diff --git a/lib/action_cable/channel/base.rb b/lib/action_cable/channel/base.rb index 2f1b4a187d..df87064195 100644 --- a/lib/action_cable/channel/base.rb +++ b/lib/action_cable/channel/base.rb @@ -1,6 +1,8 @@ +require 'set' + module ActionCable module Channel - # The channel provides the basic structure of grouping behavior into logical units when communicating over the websocket connection. + # The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection. # You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply # responding to the subscriber's direct requests. # @@ -139,7 +141,6 @@ module ActionCable # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback. def unsubscribe_from_channel run_unsubscribe_callbacks - logger.info "#{self.class.name} unsubscribed" end @@ -160,7 +161,7 @@ module ActionCable # the proper channel identifier marked as the recipient. def transmit(data, via: nil) logger.info "#{self.class.name} transmitting #{data.inspect}".tap { |m| m << " (via #{via})" if via } - connection.transmit({ identifier: @identifier, message: data }.to_json) + connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data) end @@ -175,7 +176,6 @@ module ActionCable def subscribe_to_channel - logger.info "#{self.class.name} subscribing" run_subscribe_callbacks end diff --git a/lib/action_cable/connection.rb b/lib/action_cable/connection.rb index 3d6ed6a6e8..b672e00682 100644 --- a/lib/action_cable/connection.rb +++ b/lib/action_cable/connection.rb @@ -1,12 +1,16 @@ module ActionCable module Connection - autoload :Authorization, 'action_cable/connection/authorization' - autoload :Base, 'action_cable/connection/base' - autoload :Identification, 'action_cable/connection/identification' - autoload :InternalChannel, 'action_cable/connection/internal_channel' - autoload :MessageBuffer, 'action_cable/connection/message_buffer' - autoload :WebSocket, 'action_cable/connection/web_socket' - autoload :Subscriptions, 'action_cable/connection/subscriptions' - autoload :TaggedLoggerProxy, 'action_cable/connection/tagged_logger_proxy' + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Authorization + autoload :Base + autoload :Identification + autoload :InternalChannel + autoload :MessageBuffer + autoload :WebSocket + autoload :Subscriptions + autoload :TaggedLoggerProxy + end end end diff --git a/lib/action_cable/connection/base.rb b/lib/action_cable/connection/base.rb index 2f2fa1fdec..9f74226f98 100644 --- a/lib/action_cable/connection/base.rb +++ b/lib/action_cable/connection/base.rb @@ -1,8 +1,8 @@ -require 'action_dispatch/http/request' +require 'action_dispatch' module ActionCable module Connection - # For every websocket the cable server is accepting, a Connection object will be instantiated. This instance becomes the parent + # For every WebSocket the cable server is accepting, a Connection object will be instantiated. This instance becomes the parent # of all the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions # based on an identifier sent by the cable consumer. The Connection itself does not deal with any specific application logic beyond # authentication and authorization. @@ -37,8 +37,8 @@ module ActionCable # established for that current_user (and potentially disconnect them if the user was removed from an account). You can declare as many # identification indexes as you like. Declaring an identification means that a attr_accessor is automatically set for that key. # - # Second, we rely on the fact that the websocket connection is established with the cookies from the domain being sent along. This makes - # it easy to use signed cookies that were set when logging in via a web interface to authorize the websocket connection. + # Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes + # it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection. # # Finally, we add a tag to the connection-specific logger with name of the current user to easily distinguish their messages in the log. # @@ -65,7 +65,7 @@ module ActionCable @started_at = Time.now end - # Called by the server when a new websocket connection is established. This configures the callbacks intended for overwriting by the user. + # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user. # This method should not be called directly. Rely on the #connect (and #disconnect) callback instead. def process logger.info started_request_message @@ -87,19 +87,18 @@ module ActionCable if websocket.alive? subscriptions.execute_command ActiveSupport::JSON.decode(data_in_json) else - logger.error "Received data without a live websocket (#{data.inspect})" + logger.error "Received data without a live WebSocket (#{data_in_json.inspect})" end end - # Send raw data straight back down the websocket. This is not intended to be called directly. Use the #transmit available on the + # Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the # Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON. def transmit(data) websocket.transmit data end - # Close the websocket connection. + # Close the WebSocket connection. def close - logger.error "Closing connection" websocket.close end @@ -120,12 +119,12 @@ module ActionCable end def beat - transmit({ identifier: '_ping', message: Time.now.to_i }.to_json) + transmit ActiveSupport::JSON.encode(identifier: '_ping', message: Time.now.to_i) end protected - # The request that initiated the websocket connection is available here. This gives access to the environment, cookies, etc. + # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc. def request @request ||= begin environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application @@ -133,7 +132,7 @@ module ActionCable end end - # The cookies of the request that initiated the websocket connection. Useful for performing authorization checks. + # The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks. def cookies request.cookie_jar end @@ -144,13 +143,12 @@ module ActionCable attr_reader :subscriptions, :message_buffer def on_open - server.add_connection(self) - connect if respond_to?(:connect) subscribe_to_internal_channel beat message_buffer.process! + server.add_connection(self) rescue ActionCable::Connection::Authorization::UnauthorizedError respond_to_invalid_request close @@ -203,17 +201,17 @@ module ActionCable 'Started %s "%s"%s for %s at %s' % [ request.request_method, request.filtered_path, - websocket.possible? ? ' [Websocket]' : '', + websocket.possible? ? ' [WebSocket]' : '', request.ip, - Time.now.to_default_s ] + Time.now.to_s ] end def finished_request_message 'Finished "%s"%s for %s at %s' % [ request.filtered_path, - websocket.possible? ? ' [Websocket]' : '', + websocket.possible? ? ' [WebSocket]' : '', request.ip, - Time.now.to_default_s ] + Time.now.to_s ] end end end diff --git a/lib/action_cable/connection/identification.rb b/lib/action_cable/connection/identification.rb index 4e9beac058..431493aa70 100644 --- a/lib/action_cable/connection/identification.rb +++ b/lib/action_cable/connection/identification.rb @@ -1,3 +1,5 @@ +require 'set' + module ActionCable module Connection module Identification @@ -22,7 +24,7 @@ module ActionCable # Return a single connection identifier that combines the value of all the registered identifiers into a single gid. def connection_identifier - if @connection_identifier.blank? + unless defined? @connection_identifier @connection_identifier = connection_gid identifiers.map { |id| instance_variable_get("@#{id}") }.compact end @@ -31,7 +33,13 @@ module ActionCable private def connection_gid(ids) - ids.map { |o| (o.try(:to_global_id) || o).to_s }.sort.join(":") + ids.map do |o| + if o.respond_to? :to_global_id + o.to_global_id + else + o.to_s + end + end.sort.join(":") end end end diff --git a/lib/action_cable/connection/internal_channel.rb b/lib/action_cable/connection/internal_channel.rb index b00e21824c..c065a24ab7 100644 --- a/lib/action_cable/connection/internal_channel.rb +++ b/lib/action_cable/connection/internal_channel.rb @@ -15,14 +15,14 @@ module ActionCable @_internal_redis_subscriptions ||= [] @_internal_redis_subscriptions << [ internal_redis_channel, callback ] - pubsub.subscribe(internal_redis_channel, &callback) + EM.next_tick { pubsub.subscribe(internal_redis_channel, &callback) } logger.info "Registered connection (#{connection_identifier})" end end def unsubscribe_from_internal_channel if @_internal_redis_subscriptions.present? - @_internal_redis_subscriptions.each { |channel, callback| pubsub.unsubscribe_proc(channel, callback) } + @_internal_redis_subscriptions.each { |channel, callback| EM.next_tick { pubsub.unsubscribe_proc(channel, callback) } } end end diff --git a/lib/action_cable/connection/message_buffer.rb b/lib/action_cable/connection/message_buffer.rb index d5a8e9eba9..25cff75b41 100644 --- a/lib/action_cable/connection/message_buffer.rb +++ b/lib/action_cable/connection/message_buffer.rb @@ -1,6 +1,6 @@ module ActionCable module Connection - # Allows us to buffer messages received from the websocket before the Connection has been fully initialized and is ready to receive them. + # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized and is ready to receive them. # Entirely internal operation and should not be used directly by the user. class MessageBuffer def initialize(connection) @@ -50,4 +50,4 @@ module ActionCable end end end -end
\ No newline at end of file +end diff --git a/lib/action_cable/connection/subscriptions.rb b/lib/action_cable/connection/subscriptions.rb index 69e3f60706..229be2a316 100644 --- a/lib/action_cable/connection/subscriptions.rb +++ b/lib/action_cable/connection/subscriptions.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/indifferent_access' + module ActionCable module Connection # Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on diff --git a/lib/action_cable/connection/web_socket.rb b/lib/action_cable/connection/web_socket.rb index 135a28cfe4..169b683b8c 100644 --- a/lib/action_cable/connection/web_socket.rb +++ b/lib/action_cable/connection/web_socket.rb @@ -1,3 +1,5 @@ +require 'faye/websocket' + module ActionCable module Connection # Decorate the Faye::WebSocket with helpers we need. diff --git a/lib/action_cable/engine.rb b/lib/action_cable/engine.rb index 6c943c7971..613a9b99f2 100644 --- a/lib/action_cable/engine.rb +++ b/lib/action_cable/engine.rb @@ -1,4 +1,22 @@ +require 'rails/engine' +require 'active_support/ordered_options' + module ActionCable class Engine < ::Rails::Engine + config.action_cable = ActiveSupport::OrderedOptions.new + + initializer "action_cable.logger" do + ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger } + end + + initializer "action_cable.set_configs" do |app| + options = app.config.action_cable + + options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development? + + ActiveSupport.on_load(:action_cable) do + options.each { |k,v| send("#{k}=", v) } + end + end end end diff --git a/lib/action_cable/server.rb b/lib/action_cable/server.rb index 2278509341..a2a89d5f1e 100644 --- a/lib/action_cable/server.rb +++ b/lib/action_cable/server.rb @@ -1,11 +1,19 @@ +require 'eventmachine' +EventMachine.epoll if EventMachine.epoll? +EventMachine.kqueue if EventMachine.kqueue? + module ActionCable module Server - autoload :Base, 'action_cable/server/base' - autoload :Broadcasting, 'action_cable/server/broadcasting' - autoload :Connections, 'action_cable/server/connections' - autoload :Configuration, 'action_cable/server/configuration' + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Base + autoload :Broadcasting + autoload :Connections + autoload :Configuration - autoload :Worker, 'action_cable/server/worker' - autoload :ActiveRecordConnectionManagement, 'action_cable/server/worker/active_record_connection_management' + autoload :Worker + autoload :ActiveRecordConnectionManagement, 'action_cable/server/worker/active_record_connection_management' + end end end diff --git a/lib/action_cable/server/base.rb b/lib/action_cable/server/base.rb index 9315a48f20..f1585dc776 100644 --- a/lib/action_cable/server/base.rb +++ b/lib/action_cable/server/base.rb @@ -1,3 +1,5 @@ +require 'em-hiredis' + module ActionCable module Server # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the rack process that starts the cable server, but @@ -66,5 +68,7 @@ module ActionCable config.connection_class.identifiers end end + + ActiveSupport.run_load_hooks(:action_cable, Base.config) end end diff --git a/lib/action_cable/server/broadcasting.rb b/lib/action_cable/server/broadcasting.rb index 037b98951e..6e0fbae387 100644 --- a/lib/action_cable/server/broadcasting.rb +++ b/lib/action_cable/server/broadcasting.rb @@ -1,3 +1,5 @@ +require 'redis' + module ActionCable module Server # Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these @@ -44,9 +46,9 @@ module ActionCable def broadcast(message) server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}" - server.broadcasting_redis.publish broadcasting, message.to_json + server.broadcasting_redis.publish broadcasting, ActiveSupport::JSON.encode(message) end end end end -end
\ No newline at end of file +end diff --git a/lib/action_cable/server/configuration.rb b/lib/action_cable/server/configuration.rb index 315782ec3e..b22de273b8 100644 --- a/lib/action_cable/server/configuration.rb +++ b/lib/action_cable/server/configuration.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/indifferent_access' + module ActionCable module Server # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak the configuration points diff --git a/lib/action_cable/server/connections.rb b/lib/action_cable/server/connections.rb index b3d1632cf7..47dcea8c20 100644 --- a/lib/action_cable/server/connections.rb +++ b/lib/action_cable/server/connections.rb @@ -18,13 +18,13 @@ module ActionCable connections.delete connection end - # Websocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you + # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you # then can't rely on being able to receive and send to it. So there's a 3 second heartbeat running on all connections. If the beat fails, we automatically # disconnect. def setup_heartbeat_timer EM.next_tick do @heartbeat_timer ||= EventMachine.add_periodic_timer(BEAT_INTERVAL) do - EM.next_tick { connections.map &:beat } + EM.next_tick { connections.map(&:beat) } end end end @@ -34,4 +34,4 @@ module ActionCable end end end -end
\ No newline at end of file +end diff --git a/lib/action_cable/server/worker.rb b/lib/action_cable/server/worker.rb index 91496775b8..e063b2a2e1 100644 --- a/lib/action_cable/server/worker.rb +++ b/lib/action_cable/server/worker.rb @@ -1,3 +1,6 @@ +require 'celluloid' +require 'active_support/callbacks' + module ActionCable module Server # Worker used by Server.send_async to do connection work in threads. Only for internal use. @@ -36,4 +39,4 @@ module ActionCable end end end -end
\ No newline at end of file +end diff --git a/test/channel/base_test.rb b/test/channel/base_test.rb index e7944ff06b..bac8569780 100644 --- a/test/channel/base_test.rb +++ b/test/channel/base_test.rb @@ -23,6 +23,11 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase on_subscribe :toggle_subscribed on_unsubscribe :toggle_subscribed + def initialize(*) + @subscribed = false + super + end + def subscribed @room = Room.new params[:id] @actions = [] diff --git a/test/channel/stream_test.rb b/test/channel/stream_test.rb index b0a6f49072..5914b39be0 100644 --- a/test/channel/stream_test.rb +++ b/test/channel/stream_test.rb @@ -2,7 +2,7 @@ require 'test_helper' require 'stubs/test_connection' require 'stubs/room' -class ActionCable::Channel::StreamTest < ActiveSupport::TestCase +class ActionCable::Channel::StreamTest < ActionCable::TestCase class ChatChannel < ActionCable::Channel::Base def subscribed if params[:id] @@ -17,16 +17,23 @@ class ActionCable::Channel::StreamTest < ActiveSupport::TestCase end test "streaming start and stop" do - @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1") } - channel = ChatChannel.new @connection, "{id: 1}", { id: 1 } + run_in_eventmachine do + @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1") } + channel = ChatChannel.new @connection, "{id: 1}", { id: 1 } - @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe_proc) } - channel.unsubscribe_from_channel + @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe_proc) } + channel.unsubscribe_from_channel + end end test "stream_for" do - @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:channel:stream_test:chat:Room#1-Campfire") } - channel = ChatChannel.new @connection, "" - channel.stream_for Room.new(1) + run_in_eventmachine do + EM.next_tick do + @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:channel:stream_test:chat:Room#1-Campfire") } + end + + channel = ChatChannel.new @connection, "" + channel.stream_for Room.new(1) + end end end diff --git a/test/connection/authorization_test.rb b/test/connection/authorization_test.rb index 09dfead8c8..762c90fbbc 100644 --- a/test/connection/authorization_test.rb +++ b/test/connection/authorization_test.rb @@ -1,7 +1,7 @@ require 'test_helper' require 'stubs/test_server' -class ActionCable::Connection::AuthorizationTest < ActiveSupport::TestCase +class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase class Connection < ActionCable::Connection::Base attr_reader :websocket @@ -10,17 +10,15 @@ class ActionCable::Connection::AuthorizationTest < ActiveSupport::TestCase end end - setup do - @server = TestServer.new - - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' - @connection = Connection.new(@server, env) - end - test "unauthorized connection" do - @connection.websocket.expects(:close) + run_in_eventmachine do + server = TestServer.new + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' - @connection.process - @connection.send :on_open + connection = Connection.new(server, env) + connection.websocket.expects(:close) + connection.process + connection.send :on_open + end end end diff --git a/test/connection/base_test.rb b/test/connection/base_test.rb index 7118c34d9e..da6041db4a 100644 --- a/test/connection/base_test.rb +++ b/test/connection/base_test.rb @@ -1,7 +1,7 @@ require 'test_helper' require 'stubs/test_server' -class ActionCable::Connection::BaseTest < ActiveSupport::TestCase +class ActionCable::Connection::BaseTest < ActionCable::TestCase class Connection < ActionCable::Connection::Base attr_reader :websocket, :subscriptions, :message_buffer, :connected @@ -12,69 +12,107 @@ class ActionCable::Connection::BaseTest < ActiveSupport::TestCase def disconnect @connected = false end + + def send_async(method, *args) + # Bypass Celluloid + send method, *args + end end setup do @server = TestServer.new @server.config.allowed_request_origins = %w( http://rubyonrails.com ) - - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', - 'HTTP_ORIGIN' => 'http://rubyonrails.com' - - @connection = Connection.new(@server, env) - @response = @connection.process end test "making a connection with invalid headers" do - connection = ActionCable::Connection::Base.new(@server, Rack::MockRequest.env_for("/test")) - response = connection.process - assert_equal 404, response[0] + run_in_eventmachine do + connection = ActionCable::Connection::Base.new(@server, Rack::MockRequest.env_for("/test")) + response = connection.process + assert_equal 404, response[0] + end end test "websocket connection" do - assert @connection.websocket.possible? - assert @connection.websocket.alive? + run_in_eventmachine do + connection = open_connection + connection.process + + assert connection.websocket.possible? + assert connection.websocket.alive? + end end test "rack response" do - assert_equal [ -1, {}, [] ], @response + run_in_eventmachine do + connection = open_connection + response = connection.process + + assert_equal [ -1, {}, [] ], response + end end test "on connection open" do - assert ! @connection.connected - - @connection.websocket.expects(:transmit).with(regexp_matches(/\_ping/)) - @connection.message_buffer.expects(:process!) - - @connection.send :on_open - - assert_equal [ @connection ], @server.connections - assert @connection.connected + run_in_eventmachine do + connection = open_connection + connection.process + + connection.websocket.expects(:transmit).with(regexp_matches(/\_ping/)) + connection.message_buffer.expects(:process!) + + # Allow EM to run on_open callback + EM.next_tick do + assert_equal [ connection ], @server.connections + assert connection.connected + end + end end test "on connection close" do - # Setup the connection - EventMachine.stubs(:add_periodic_timer).returns(true) - @connection.send :on_open - assert @connection.connected + run_in_eventmachine do + connection = open_connection + connection.process + + # Setup the connection + EventMachine.stubs(:add_periodic_timer).returns(true) + connection.send :on_open + assert connection.connected - @connection.subscriptions.expects(:unsubscribe_from_all) - @connection.send :on_close + connection.subscriptions.expects(:unsubscribe_from_all) + connection.send :on_close - assert ! @connection.connected - assert_equal [], @server.connections + assert ! connection.connected + assert_equal [], @server.connections + end end test "connection statistics" do - statistics = @connection.statistics + run_in_eventmachine do + connection = open_connection + connection.process + + statistics = connection.statistics - assert statistics[:identifier].blank? - assert_kind_of Time, statistics[:started_at] - assert_equal [], statistics[:subscriptions] + assert statistics[:identifier].blank? + assert_kind_of Time, statistics[:started_at] + assert_equal [], statistics[:subscriptions] + end end test "explicitly closing a connection" do - @connection.websocket.expects(:close) - @connection.close + run_in_eventmachine do + connection = open_connection + connection.process + + connection.websocket.expects(:close) + connection.close + end end + + private + def open_connection + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', + 'HTTP_ORIGIN' => 'http://rubyonrails.com' + + Connection.new(@server, env) + end end diff --git a/test/connection/cross_site_forgery_test.rb b/test/connection/cross_site_forgery_test.rb index 6073f89287..166abb7b38 100644 --- a/test/connection/cross_site_forgery_test.rb +++ b/test/connection/cross_site_forgery_test.rb @@ -1,9 +1,16 @@ require 'test_helper' require 'stubs/test_server' -class ActionCable::Connection::CrossSiteForgeryTest < ActiveSupport::TestCase +class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase HOST = 'rubyonrails.com' + class Connection < ActionCable::Connection::Base + def send_async(method, *args) + # Bypass Celluloid + send method, *args + end + end + setup do @server = TestServer.new @server.config.allowed_request_origins = %w( http://rubyonrails.com ) @@ -45,7 +52,13 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActiveSupport::TestCase end def connect_with_origin(origin) - ActionCable::Connection::Base.new(@server, env_for_origin(origin)).process + response = nil + + run_in_eventmachine do + response = Connection.new(@server, env_for_origin(origin)).process + end + + response end def env_for_origin(origin) diff --git a/test/connection/identifier_test.rb b/test/connection/identifier_test.rb index 745cf308d0..02e6b21845 100644 --- a/test/connection/identifier_test.rb +++ b/test/connection/identifier_test.rb @@ -2,7 +2,7 @@ require 'test_helper' require 'stubs/test_server' require 'stubs/user' -class ActionCable::Connection::IdentifierTest < ActiveSupport::TestCase +class ActionCable::Connection::IdentifierTest < ActionCable::TestCase class Connection < ActionCable::Connection::Base identified_by :current_user attr_reader :websocket @@ -14,59 +14,59 @@ class ActionCable::Connection::IdentifierTest < ActiveSupport::TestCase end end - setup do - @server = TestServer.new - - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' - @connection = Connection.new(@server, env) - end - test "connection identifier" do - open_connection_with_stubbed_pubsub - assert_equal "User#lifo", @connection.connection_identifier - end - - test "should subscribe to internal channel on open" do - pubsub = mock('pubsub') - pubsub.expects(:subscribe).with('action_cable/User#lifo') - @server.expects(:pubsub).returns(pubsub) - - open_connection + run_in_eventmachine do + open_connection_with_stubbed_pubsub + assert_equal "User#lifo", @connection.connection_identifier + end end - test "should unsubscribe from internal channel on close" do - open_connection_with_stubbed_pubsub + test "should subscribe to internal channel on open and unsubscribe on close" do + run_in_eventmachine do + pubsub = mock('pubsub') + pubsub.expects(:subscribe).with('action_cable/User#lifo') + pubsub.expects(:unsubscribe_proc).with('action_cable/User#lifo', kind_of(Proc)) - pubsub = mock('pubsub') - pubsub.expects(:unsubscribe_proc).with('action_cable/User#lifo', kind_of(Proc)) - @server.expects(:pubsub).returns(pubsub) + server = TestServer.new + server.stubs(:pubsub).returns(pubsub) - close_connection + open_connection server: server + close_connection + end end test "processing disconnect message" do - open_connection_with_stubbed_pubsub + run_in_eventmachine do + open_connection_with_stubbed_pubsub - @connection.websocket.expects(:close) - message = { 'type' => 'disconnect' }.to_json - @connection.process_internal_message message + @connection.websocket.expects(:close) + message = ActiveSupport::JSON.encode('type' => 'disconnect') + @connection.process_internal_message message + end end test "processing invalid message" do - open_connection_with_stubbed_pubsub + run_in_eventmachine do + open_connection_with_stubbed_pubsub - @connection.websocket.expects(:close).never - message = { 'type' => 'unknown' }.to_json - @connection.process_internal_message message + @connection.websocket.expects(:close).never + message = ActiveSupport::JSON.encode('type' => 'unknown') + @connection.process_internal_message message + end end protected def open_connection_with_stubbed_pubsub - @server.stubs(:pubsub).returns(stub_everything('pubsub')) - open_connection + server = TestServer.new + server.stubs(:pubsub).returns(stub_everything('pubsub')) + + open_connection server: server end - def open_connection + def open_connection(server:) + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(server, env) + @connection.process @connection.send :on_open end diff --git a/test/connection/string_identifier_test.rb b/test/connection/string_identifier_test.rb index 87a9025008..ab69df57b3 100644 --- a/test/connection/string_identifier_test.rb +++ b/test/connection/string_identifier_test.rb @@ -1,34 +1,39 @@ require 'test_helper' require 'stubs/test_server' -class ActionCable::Connection::StringIdentifierTest < ActiveSupport::TestCase +class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase class Connection < ActionCable::Connection::Base identified_by :current_token def connect self.current_token = "random-string" end - end - - setup do - @server = TestServer.new - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' - @connection = Connection.new(@server, env) + def send_async(method, *args) + # Bypass Celluloid + send method, *args + end end test "connection identifier" do - open_connection_with_stubbed_pubsub - assert_equal "random-string", @connection.connection_identifier + run_in_eventmachine do + open_connection_with_stubbed_pubsub + assert_equal "random-string", @connection.connection_identifier + end end protected def open_connection_with_stubbed_pubsub + @server = TestServer.new @server.stubs(:pubsub).returns(stub_everything('pubsub')) + open_connection end def open_connection + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(@server, env) + @connection.process @connection.send :on_open end diff --git a/test/connection/subscriptions_test.rb b/test/connection/subscriptions_test.rb index 24fe8f9300..4f6760827e 100644 --- a/test/connection/subscriptions_test.rb +++ b/test/connection/subscriptions_test.rb @@ -1,8 +1,13 @@ require 'test_helper' -class ActionCable::Connection::SubscriptionsTest < ActiveSupport::TestCase +class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase class Connection < ActionCable::Connection::Base attr_reader :websocket + + def send_async(method, *args) + # Bypass Celluloid + send method, *args + end end class ChatChannel < ActionCable::Channel::Base @@ -22,59 +27,76 @@ class ActionCable::Connection::SubscriptionsTest < ActiveSupport::TestCase @server = TestServer.new @server.stubs(:channel_classes).returns(ChatChannel.name => ChatChannel) - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' - @connection = Connection.new(@server, env) - - @subscriptions = ActionCable::Connection::Subscriptions.new(@connection) - @chat_identifier = { id: 1, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel' }.to_json + @chat_identifier = ActiveSupport::JSON.encode(id: 1, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel') end test "subscribe command" do - channel = subscribe_to_chat_channel + run_in_eventmachine do + setup_connection + channel = subscribe_to_chat_channel - assert_kind_of ChatChannel, channel - assert_equal 1, channel.room.id + assert_kind_of ChatChannel, channel + assert_equal 1, channel.room.id + end end test "subscribe command without an identifier" do - @subscriptions.execute_command 'command' => 'subscribe' - assert @subscriptions.identifiers.empty? + run_in_eventmachine do + setup_connection + + @subscriptions.execute_command 'command' => 'subscribe' + assert @subscriptions.identifiers.empty? + end end test "unsubscribe command" do - subscribe_to_chat_channel + run_in_eventmachine do + setup_connection + subscribe_to_chat_channel - channel = subscribe_to_chat_channel - channel.expects(:unsubscribe_from_channel) + channel = subscribe_to_chat_channel + channel.expects(:unsubscribe_from_channel) - @subscriptions.execute_command 'command' => 'unsubscribe', 'identifier' => @chat_identifier - assert @subscriptions.identifiers.empty? + @subscriptions.execute_command 'command' => 'unsubscribe', 'identifier' => @chat_identifier + assert @subscriptions.identifiers.empty? + end end test "unsubscribe command without an identifier" do - @subscriptions.execute_command 'command' => 'unsubscribe' - assert @subscriptions.identifiers.empty? + run_in_eventmachine do + setup_connection + + @subscriptions.execute_command 'command' => 'unsubscribe' + assert @subscriptions.identifiers.empty? + end end test "message command" do - channel = subscribe_to_chat_channel + run_in_eventmachine do + setup_connection + channel = subscribe_to_chat_channel - data = { 'content' => 'Hello World!', 'action' => 'speak' } - @subscriptions.execute_command 'command' => 'message', 'identifier' => @chat_identifier, 'data' => data.to_json + data = { 'content' => 'Hello World!', 'action' => 'speak' } + @subscriptions.execute_command 'command' => 'message', 'identifier' => @chat_identifier, 'data' => ActiveSupport::JSON.encode(data) - assert_equal [ data ], channel.lines + assert_equal [ data ], channel.lines + end end test "unsubscrib from all" do - channel1 = subscribe_to_chat_channel + run_in_eventmachine do + setup_connection - channel2_id = { id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel' }.to_json - channel2 = subscribe_to_chat_channel(channel2_id) + channel1 = subscribe_to_chat_channel - channel1.expects(:unsubscribe_from_channel) - channel2.expects(:unsubscribe_from_channel) + channel2_id = ActiveSupport::JSON.encode(id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel') + channel2 = subscribe_to_chat_channel(channel2_id) - @subscriptions.unsubscribe_from_all + channel1.expects(:unsubscribe_from_channel) + channel2.expects(:unsubscribe_from_channel) + + @subscriptions.unsubscribe_from_all + end end private @@ -84,4 +106,11 @@ class ActionCable::Connection::SubscriptionsTest < ActiveSupport::TestCase @subscriptions.send :find, 'identifier' => identifier end + + def setup_connection + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(@server, env) + + @subscriptions = ActionCable::Connection::Subscriptions.new(@connection) + end end diff --git a/test/stubs/test_server.rb b/test/stubs/test_server.rb index 2a7ac3e927..f9168f9b78 100644 --- a/test/stubs/test_server.rb +++ b/test/stubs/test_server.rb @@ -9,4 +9,7 @@ class TestServer @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) @config = OpenStruct.new(log_tags: []) end + + def send_async + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5640178f34..f8a9971077 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,4 +18,29 @@ ActiveSupport.test_order = :sorted # Require all the stubs and models Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file } +require 'celluloid' +$CELLULOID_DEBUG = false +$CELLULOID_TEST = false Celluloid.logger = Logger.new(StringIO.new) + +require 'faye/websocket' +class << Faye::WebSocket + remove_method :ensure_reactor_running + + # We don't want Faye to start the EM reactor in tests because it makes testing much harder. + # We want to be able to start and stop EM loop in tests to make things simpler. + def ensure_reactor_running + # no-op + end +end + +class ActionCable::TestCase < ActiveSupport::TestCase + def run_in_eventmachine + EM.run do + yield + + EM.run_deferred_callbacks + EM.stop + end + end +end diff --git a/test/worker_test.rb b/test/worker_test.rb index e1fa6f561b..69c4b6529d 100644 --- a/test/worker_test.rb +++ b/test/worker_test.rb @@ -11,6 +11,9 @@ class WorkerTest < ActiveSupport::TestCase def process(message) @last_action = [ :process, message ] end + + def connection + end end setup do |