mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Run connection tests in EM loop
This commit is contained in:
parent
db56e8bf3b
commit
ee16ca8990
10 changed files with 240 additions and 126 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
run_in_eventmachine do
|
||||
connection = open_connection
|
||||
connection.process
|
||||
|
||||
@connection.websocket.expects(:transmit).with(regexp_matches(/\_ping/))
|
||||
@connection.message_buffer.expects(:process!)
|
||||
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
|
||||
# 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
|
||||
|
||||
@connection.subscriptions.expects(:unsubscribe_from_all)
|
||||
@connection.send :on_close
|
||||
# Setup the connection
|
||||
EventMachine.stubs(:add_periodic_timer).returns(true)
|
||||
connection.send :on_open
|
||||
assert connection.connected
|
||||
|
||||
assert ! @connection.connected
|
||||
assert_equal [], @server.connections
|
||||
connection.subscriptions.expects(:unsubscribe_from_all)
|
||||
connection.send :on_close
|
||||
|
||||
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
|
||||
|
||||
assert statistics[:identifier].blank?
|
||||
assert_kind_of Time, statistics[:started_at]
|
||||
assert_equal [], statistics[:subscriptions]
|
||||
statistics = connection.statistics
|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
run_in_eventmachine do
|
||||
open_connection_with_stubbed_pubsub
|
||||
assert_equal "User#lifo", @connection.connection_identifier
|
||||
end
|
||||
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)
|
||||
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))
|
||||
|
||||
open_connection
|
||||
end
|
||||
server = TestServer.new
|
||||
server.stubs(:pubsub).returns(pubsub)
|
||||
|
||||
test "should unsubscribe from internal channel on close" do
|
||||
open_connection_with_stubbed_pubsub
|
||||
|
||||
pubsub = mock('pubsub')
|
||||
pubsub.expects(:unsubscribe_proc).with('action_cable/User#lifo', kind_of(Proc))
|
||||
@server.expects(: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 = { 'type' => 'disconnect' }.to_json
|
||||
@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 = { 'type' => 'unknown' }.to_json
|
||||
@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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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' => data.to_json
|
||||
|
||||
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 = { id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel' }.to_json
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -19,3 +19,21 @@ ActiveSupport.test_order = :sorted
|
|||
Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file }
|
||||
|
||||
Celluloid.logger = Logger.new(StringIO.new)
|
||||
|
||||
class Faye::WebSocket
|
||||
# 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 EW loop in tests to make things simpler.
|
||||
def self.ensure_reactor_running
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
||||
class ActionCable::TestCase < ActiveSupport::TestCase
|
||||
def run_in_eventmachine
|
||||
EM.run do
|
||||
yield
|
||||
|
||||
EM::Timer.new(0.1) { EM.stop }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,9 @@ class WorkerTest < ActiveSupport::TestCase
|
|||
def process(message)
|
||||
@last_action = [ :process, message ]
|
||||
end
|
||||
|
||||
def connection
|
||||
end
|
||||
end
|
||||
|
||||
setup do
|
||||
|
|
Loading…
Reference in a new issue