Skip to content

Commit 4621549

Browse files
authored
feat: add attach to an existing session (#337)
Added a method to attach to an existing session into the driver class so that users can attach to an existing session for debugging. The capabilities only has the given automationName and platformName for the internal use. W3C does not define getting a session info, so we need to give them.
1 parent ae52976 commit 4621549

File tree

7 files changed

+208
-19
lines changed

7 files changed

+208
-19
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Read `release_notes.md` for commit level details.
55
## [Unreleased]
66

77
### Enhancements
8+
- Add `::Appium::Core::Driver#attach_to` to generate a driver instance which has the given session id.
9+
- The primary usage is for debugging to attach to an existing session.
810

911
### Bug fixes
1012

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ $ IGNORE_VERSION_SKIP=true CI=true bundle exec rake test:func:android
104104

105105
opts = {
106106
capabilities: { # Append capabilities
107-
platformName: :ios,
107+
platformName: 'ios',
108108
platformVersion: '11.0',
109109
deviceName: 'iPhone Simulator',
110110
automationName: 'XCUITest',
@@ -133,6 +133,15 @@ $ IGNORE_VERSION_SKIP=true CI=true bundle exec rake test:func:android
133133

134134
More examples are in [test/functional](test/functional)
135135

136+
As of version 5.8.0, the client can attach to an existing session. The main purpose is for debugging.
137+
138+
```ruby
139+
# @driver is the driver instance of an existing session
140+
attached_driver = ::Appium::Core::Driver.attach_to @driver.session_id, url: 'http://127.0.0.1:4723/wd/hub', automation_name: 'XCUITest', platform_name: 'ios'
141+
assert attached_driver.session_id == @driver.session_id
142+
attached_driver.page_source
143+
```
144+
136145
### Capabilities
137146

138147
Read [Appium/Core/Driver](https://www.rubydoc.info/github/appium/ruby_lib_core/Appium/Core/Driver) to catch up with available capabilities.

lib/appium_lib_core/common/base/bridge.rb

+27-1
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,39 @@ class Bridge < ::Selenium::WebDriver::Remote::Bridge
4343

4444
def browser
4545
@browser ||= begin
46-
name = @capabilities.browser_name
46+
name = @capabilities&.browser_name
4747
name ? name.tr(' ', '_').downcase.to_sym : 'unknown'
4848
rescue KeyError
4949
APPIUM_NATIVE_BROWSER_NAME
5050
end
5151
end
5252

53+
# Appium only.
54+
# Attach to an existing session.
55+
#
56+
# @param [String] The session id to attach to.
57+
# @param [String] platform_name The platform name to keep in the dummy capabilities
58+
# @param [String] platform_name The automation name to keep in the dummy capabilities
59+
# @return [::Appium::Core::Base::Capabilities]
60+
#
61+
# @example
62+
#
63+
# new_driver = ::Appium::Core::Driver.attach_to(
64+
# driver.session_id,
65+
# url: 'http://127.0.0.1:4723/wd/hub', automation_name: 'UiAutomator2', platform_name: 'Android'
66+
# )
67+
#
68+
def attach_to(session_id, platform_name, automation_name)
69+
@available_commands = ::Appium::Core::Commands::COMMANDS.dup
70+
@session_id = session_id
71+
72+
# generate a dummy capabilities instance which only has the given platformName and automationName
73+
@capabilities = ::Appium::Core::Base::Capabilities.new(
74+
'platformName' => platform_name,
75+
'automationName' => automation_name
76+
)
77+
end
78+
5379
# Override
5480
# Creates session handling.
5581
#

lib/appium_lib_core/common/base/driver.rb

+14-1
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,26 @@ def initialize(bridge: nil, listener: nil, **opts)
5959
# @return [::Appium::Core::Base::Bridge]
6060
#
6161
def create_bridge(**opts)
62+
# for a new session request
6263
capabilities = opts.delete(:capabilities)
6364
bridge_opts = { http_client: opts.delete(:http_client), url: opts.delete(:url) }
65+
66+
# for attaching to an existing session
67+
session_id = opts.delete(:existing_session_id)
68+
automation_name = opts.delete(:automation_name)
69+
platform_name = opts.delete(:platform_name)
70+
6471
raise ::Appium::Core::Error::ArgumentError, "Unable to create a driver with parameters: #{opts}" unless opts.empty?
6572

6673
bridge = ::Appium::Core::Base::Bridge.new(**bridge_opts)
6774

68-
bridge.create_session(capabilities)
75+
if session_id.nil?
76+
bridge.create_session(capabilities)
77+
else
78+
# attach to the existing session id
79+
bridge.attach_to(session_id, platform_name, automation_name)
80+
end
81+
6982
bridge
7083
end
7184

lib/appium_lib_core/driver.rb

+97-15
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,40 @@ class Driver
275275
# @core.start_driver # start driver with 'url'. Connect to 'http://custom-host:8080/wd/hub.com'
276276
#
277277
def self.for(opts = {})
278-
new(opts)
278+
new.setup_for_new_session(opts)
279+
end
280+
281+
# Attach to an existing session. The main usage of this method is to attach to
282+
# an existing session for debugging. The generated driver instance has the capabilities which
283+
# has the given automationName and platformName only since the W3C WebDriver spec does not provide
284+
# an endpoint to get running session's capabilities.
285+
#
286+
#
287+
# @param [String] The session id to attach to.
288+
# @param [String] url The WebDriver URL to attach to with the session_id.
289+
# @param [String] automation_name The platform name to keep in the dummy capabilities
290+
# @param [String] platform_name The automation name to keep in the dummy capabilities
291+
# @return [Selenium::WebDriver] A new driver instance with the given session id.
292+
#
293+
# @example
294+
#
295+
# new_driver = ::Appium::Core::Driver.attach_to(
296+
# driver.session_id, # The 'driver' has an existing session id
297+
# url: 'http://127.0.0.1:4723/wd/hub', automation_name: 'UiAutomator2', platform_name: 'Android'
298+
# )
299+
# new_driver.page_source # for example
300+
#
301+
def self.attach_to(
302+
session_id, url: nil, automation_name: nil, platform_name: nil,
303+
http_client_ops: { http_client: nil, open_timeout: 999_999, read_timeout: 999_999 }
304+
)
305+
new.attach_to(
306+
session_id,
307+
automation_name: automation_name,
308+
platform_name: platform_name,
309+
url: url,
310+
http_client_ops: http_client_ops
311+
)
279312
end
280313

281314
private
@@ -286,20 +319,25 @@ def delegated_target_for_test
286319
@delegate_target
287320
end
288321

289-
public
290-
291322
# @private
292-
def initialize(opts = {})
323+
def initialize
293324
@delegate_target = self # for testing purpose
294325
@automation_name = nil # initialise before 'set_automation_name'
326+
end
327+
328+
public
329+
330+
# @private
331+
# Set up for a neww session
332+
def setup_for_new_session(opts = {})
333+
@custom_url = opts.delete :url # to set the custom url as :url
295334

296335
# TODO: Remove when we implement Options
297336
# The symbolize_keys is to keep compatiility for the legacy code, which allows capabilities to give 'string' as the key.
298337
# The toplevel `caps`, `capabilities` and `appium_lib` are expected to be symbol.
299338
# FIXME: First, please try to remove `nested: true` to `nested: false`.
300339
opts = Appium.symbolize_keys(opts, nested: true)
301340

302-
@custom_url = opts.delete :url
303341
@caps = get_caps(opts)
304342

305343
set_appium_lib_specific_values(get_appium_lib_opts(opts))
@@ -308,8 +346,7 @@ def initialize(opts = {})
308346
set_automation_name
309347

310348
extend_for(device: @device, automation_name: @automation_name)
311-
312-
self # rubocop:disable Lint/Void
349+
self
313350
end
314351

315352
# Creates a new global driver and quits the old one if it exists.
@@ -320,7 +357,7 @@ def initialize(opts = {})
320357
# @option http_client_ops [Hash] :http_client Custom HTTP Client
321358
# @option http_client_ops [Hash] :open_timeout Custom open timeout for http client.
322359
# @option http_client_ops [Hash] :read_timeout Custom read timeout for http client.
323-
# @return [Selenium::WebDriver] the new global driver
360+
# @return [Selenium::WebDriver] A new driver instance
324361
#
325362
# @example
326363
#
@@ -406,7 +443,47 @@ def start_driver(server_url: nil,
406443
@driver
407444
end
408445

409-
private
446+
# @privvate
447+
# Attach to an existing session
448+
def attach_to(session_id, url: nil, automation_name: nil, platform_name: nil,
449+
http_client_ops: { http_client: nil, open_timeout: 999_999, read_timeout: 999_999 })
450+
451+
raise ::Appium::Core::Error::ArgumentError, 'The :url must not be nil' if url.nil?
452+
raise ::Appium::Core::Error::ArgumentError, 'The :automation_name must not be nil' if automation_name.nil?
453+
raise ::Appium::Core::Error::ArgumentError, 'The :platform_name must not be nil' if platform_name.nil?
454+
455+
@custom_url = url
456+
457+
# use lowercase internally
458+
@automation_name = convert_downcase(automation_name)
459+
@device = convert_downcase(platform_name)
460+
461+
extend_for(device: @device, automation_name: @automation_name)
462+
463+
@http_client = get_http_client http_client: http_client_ops.delete(:http_client),
464+
open_timeout: http_client_ops.delete(:open_timeout),
465+
read_timeout: http_client_ops.delete(:read_timeout)
466+
467+
# Note that 'enable_idempotency_header' works only a new session reqeust. The attach_to method skips
468+
# the new session request, this it does not needed.
469+
470+
begin
471+
# included https://github.com/SeleniumHQ/selenium/blob/43f8b3f66e7e01124eff6a5805269ee441f65707/rb/lib/selenium/webdriver/remote/driver.rb#L29
472+
@driver = ::Appium::Core::Base::Driver.new(http_client: @http_client,
473+
url: @custom_url,
474+
listener: @listener,
475+
existing_session_id: session_id,
476+
automation_name: automation_name,
477+
platform_name: platform_name)
478+
479+
# export session
480+
write_session_id(@driver.session_id, @export_session_path) if @export_session
481+
rescue Errno::ECONNREFUSED
482+
raise "ERROR: Unable to connect to Appium. Is the server running on #{@custom_url}?"
483+
end
484+
485+
@driver
486+
end
410487

411488
def get_http_client(http_client: nil, open_timeout: nil, read_timeout: nil)
412489
client = http_client || Appium::Core::Base::Http::Default.new
@@ -432,8 +509,6 @@ def set_implicit_wait_by_default(wait)
432509
{}
433510
end
434511

435-
public
436-
437512
# Quits the driver
438513
# @return [void]
439514
#
@@ -627,17 +702,20 @@ def set_appium_device
627702
@device = @caps[:platformName] || @caps['platformName']
628703
return @device unless @device
629704

630-
@device = @device.is_a?(Symbol) ? @device.downcase : @device.downcase.strip.intern
705+
@device = convert_downcase @device
631706
end
632707

633708
# @private
634709
def set_automation_name
635710
# TODO: check if the Appium.symbolize_keys(opts, nested: false) enoug with this
636711
candidate = @caps[:automationName] || @caps['automationName']
637712
@automation_name = candidate if candidate
638-
@automation_name = if @automation_name
639-
@automation_name.is_a?(Symbol) ? @automation_name.downcase : @automation_name.downcase.strip.intern
640-
end
713+
@automation_name = convert_downcase @automation_name if @automation_name
714+
end
715+
716+
# @private
717+
def convert_downcase(value)
718+
value.is_a?(Symbol) ? value.downcase : value.downcase.strip.intern
641719
end
642720

643721
# @private
@@ -651,6 +729,10 @@ def set_automation_name_if_nil
651729

652730
# @private
653731
def write_session_id(session_id, export_path = '/tmp/appium_lib_session')
732+
::Appium::Logger.warn(
733+
'[DEPRECATION] export_session option will be removed. ' \
734+
'Please save the session id by yourself with #session_id method like @driver.session_id.'
735+
)
654736
export_path = export_path.tr('/', '\\') if ::Appium::Core::Base.platform.windows?
655737
File.write(export_path, session_id)
656738
rescue IOError => e

test/unit/common_test.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_add_appium_prefix_already_have_appium_prefix
158158
base_caps = Appium::Core::Base::Capabilities.new cap
159159

160160
assert_equal base_caps[:platformName], :ios
161-
assert_equal base_caps['platformName'], nil
161+
assert_nil base_caps['platformName']
162162

163163
expected = {
164164
'platformName' => :ios,

test/unit/driver_test.rb

+57
Original file line numberDiff line numberDiff line change
@@ -485,5 +485,62 @@ def test_search_context_in_element_class
485485
windows_uiautomation: '-windows uiautomation',
486486
tizen_uiautomation: '-tizen uiautomation' }, ::Appium::Core::Element::FINDERS)
487487
end
488+
489+
def test_attach_to_an_existing_session
490+
android_mock_create_session_w3c_direct = lambda do |core|
491+
response = {
492+
value: {
493+
sessionId: '1234567890',
494+
capabilities: {
495+
platformName: :android,
496+
automationName: ENV['APPIUM_DRIVER'] || 'uiautomator2',
497+
app: 'test/functional/app/api.apk.zip',
498+
platformVersion: '7.1.1',
499+
deviceName: 'Android Emulator',
500+
appPackage: 'io.appium.android.apis',
501+
appActivity: 'io.appium.android.apis.ApiDemos',
502+
someCapability: 'some_capability',
503+
unicodeKeyboard: true,
504+
resetKeyboard: true,
505+
directConnectProtocol: 'http',
506+
directConnectHost: 'localhost',
507+
directConnectPort: '8888',
508+
directConnectPath: '/wd/hub'
509+
}
510+
}
511+
}.to_json
512+
513+
stub_request(:post, 'http://127.0.0.1:4723/wd/hub/session')
514+
.to_return(headers: HEADER, status: 200, body: response)
515+
516+
stub_request(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts')
517+
.with(body: { implicit: 30_000 }.to_json)
518+
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)
519+
520+
driver = core.start_driver
521+
522+
assert_requested(:post, 'http://127.0.0.1:4723/wd/hub/session', times: 1)
523+
assert_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts',
524+
body: { implicit: 30_000 }.to_json, times: 1)
525+
driver
526+
end
527+
528+
core = ::Appium::Core.for(Caps.android_direct)
529+
driver = android_mock_create_session_w3c_direct.call(core)
530+
531+
attached_driver = ::Appium::Core::Driver.attach_to(
532+
driver.session_id,
533+
url: 'http://127.0.0.1:4723/wd/hub', automation_name: 'UiAutomator2', platform_name: 'Android'
534+
)
535+
536+
assert_equal driver.session_id, attached_driver.session_id
537+
# base session
538+
assert driver.respond_to?(:current_activity)
539+
assert_equal driver.capabilities['automationName'], ENV['APPIUM_DRIVER'] || 'uiautomator2'
540+
541+
# to check the extend_for if the new driver instance also has the expected method for Android
542+
assert attached_driver.respond_to?(:current_activity)
543+
assert_equal attached_driver.capabilities['automationName'], 'UiAutomator2'
544+
end
488545
end
489546
end

0 commit comments

Comments
 (0)