bugsnag-maze-runner 9.21.0 → 9.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b00232685620344397218ed5ccf143f583dea9cb7a94c454dcbc7f8d5ea00c3
4
- data.tar.gz: 44d839ae61d043e63c01b4462359d6b8ccef54a73eac1cc679c1813099aeafab
3
+ metadata.gz: 8884abca15e25562492a3411949b061374130f0975c39da3635939c31ab94ccb
4
+ data.tar.gz: fe80b49925c6579219f0e8a307d31be1915e6cfacabe30293d3c24898e663a29
5
5
  SHA512:
6
- metadata.gz: 0a8ac5cdcb8da3b8cfc80b54f8318989eef4df1f8b92dcff63884544bb25f6dde895aeeac0aa4c9091f385d029e7fd6a00ff6b04772bbdb71699686c6620eeda
7
- data.tar.gz: 8e88e848cf6599fb944ce054a0fda6e65bc873fc3b6bf7b91c62f5a55f06332d00096c9ef060f69dccabe670475371d3d59e8d88036a870dd41fe157ee2621b0
6
+ metadata.gz: 748f7b6a945799d375b4b7d18d3183b3627f696fa229c0572e21ba5074b8d23f4c7833605b470deaaf3ef00acef2b6f54f48a3d3a4eb8f105f62de592903a20e
7
+ data.tar.gz: a06ea26f1055c4837fe9ccf22703bdcad6bab92dc58caa2c04edfa5aaefec0950c82bc39805dc5dd47fb7a8ce754dc6a6888cda11ff04f9ff9c428ec354a94a3
data/bin/maze-runner CHANGED
@@ -11,6 +11,9 @@ require_relative '../lib/utils/deep_merge'
11
11
  require_relative '../lib/maze'
12
12
 
13
13
  require_relative '../lib/maze/appium_server'
14
+ require_relative '../lib/maze/api/appium/manager'
15
+ require_relative '../lib/maze/api/appium/app_manager'
16
+ require_relative '../lib/maze/api/appium/device_manager'
14
17
  require_relative '../lib/maze/api/appium/file_manager'
15
18
  require_relative '../lib/maze/api/cucumber/scenario'
16
19
  require_relative '../lib/maze/api/exit_code'
@@ -8,7 +8,6 @@ require 'selenium-webdriver'
8
8
  require 'uri'
9
9
 
10
10
  BeforeAll do
11
-
12
11
  Maze.check = Maze::Checks::AssertCheck.new
13
12
 
14
13
  # Infer mode of operation from config, one of:
@@ -101,9 +100,16 @@ end
101
100
 
102
101
  # Before each scenario
103
102
  Before do |scenario|
103
+ next if scenario.status == :skipped
104
+
104
105
  Maze.scenario = Maze::Api::Cucumber::Scenario.new(scenario)
105
106
 
106
- # Default to no dynamic try
107
+ # Skip scenario if the driver it needs has failed
108
+ if (Maze.mode == :appium || Maze.mode == :browser) && Maze.driver.failed?
109
+ skip_this_scenario
110
+ end
111
+
112
+ # Default to no dynamic retry
107
113
  Maze.dynamic_retry = false
108
114
 
109
115
  if ENV['BUILDKITE']
@@ -128,6 +134,8 @@ end
128
134
 
129
135
  # General processing to be run after each scenario
130
136
  After do |scenario|
137
+ next if scenario.status == :skipped
138
+
131
139
  # If we're running on macos, take a screenshot if the scenario fails
132
140
  if Maze.config.os == "macos" && scenario.status == :failed
133
141
  Maze::MacosUtils.capture_screen(scenario)
@@ -220,6 +228,8 @@ end
220
228
  #
221
229
  # Furthermore, this hook should appear after the general hook as they are executed in reverse order by Cucumber.
222
230
  After do |scenario|
231
+ next if scenario.status == :skipped
232
+
223
233
  # Call any pre_complete hooks registered by the client
224
234
  Maze.hooks.call_pre_complete scenario
225
235
 
@@ -234,6 +244,8 @@ end
234
244
  # Test all requests against schemas or extra validation rules. These will only run if the schema/validation is
235
245
  # specified for the specific endpoint
236
246
  After do |scenario|
247
+ next if scenario.status == :skipped
248
+
237
249
  ['error', 'session', 'build', 'trace'].each do |endpoint|
238
250
  Maze::Schemas::Validator.validate_payload_elements(Maze::Server.list_for(endpoint), endpoint)
239
251
  end
@@ -0,0 +1,91 @@
1
+ require_relative '../../helper'
2
+ require_relative './manager'
3
+
4
+ module Maze
5
+ module Api
6
+ module Appium
7
+ # Provides operations for working with the app.
8
+ class AppManager < Maze::Api::Appium::Manager
9
+
10
+ # Activates the app
11
+ # @returns [Boolean] Whether the app was successfully launched
12
+ def activate
13
+ if failed_driver?
14
+ $logger.error 'Cannot activate the app - Appium driver failed.'
15
+ return false
16
+ end
17
+
18
+ @driver.activate_app(@driver.app_id)
19
+ true
20
+ rescue Selenium::WebDriver::Error::ServerError => e
21
+ # Assume the remote appium session has stopped, so crash out of the session
22
+ fail_driver
23
+ raise e
24
+ end
25
+
26
+ # Terminates the app
27
+ # @returns [Boolean] Whether the app was successfully closed
28
+ def terminate
29
+ if failed_driver?
30
+ $logger.error 'Cannot terminate the app - Appium driver failed.'
31
+ return false
32
+ end
33
+
34
+ @driver.terminate_app(@driver.app_id)
35
+ true
36
+ rescue Selenium::WebDriver::Error::ServerError => e
37
+ # Assume the remote appium session has stopped, so crash out of the session
38
+ fail_driver
39
+ raise e
40
+ end
41
+
42
+ # Launches the app (legacy method).
43
+ # @returns [Boolean] Whether the app was successfully launched
44
+ def launch
45
+ if failed_driver?
46
+ $logger.error 'Cannot launch the app - Appium driver failed.'
47
+ return false
48
+ end
49
+
50
+ @driver.launch_app
51
+ true
52
+ rescue Selenium::WebDriver::Error::ServerError => e
53
+ # Assume the remote appium session has stopped, so crash out of the session
54
+ fail_driver
55
+ raise e
56
+ end
57
+
58
+ # Closes the app (legacy method).
59
+ # @returns [Boolean] Whether the app was successfully closed
60
+ def close
61
+ if failed_driver?
62
+ $logger.error 'Cannot close the app - Appium driver failed.'
63
+ return false
64
+ end
65
+
66
+ @driver.close_app
67
+ true
68
+ rescue Selenium::WebDriver::Error::ServerError => e
69
+ # Assume the remote appium session has stopped, so crash out of the session
70
+ fail_driver
71
+ raise e
72
+ end
73
+
74
+ # Gets the app state.
75
+ # @returns [Symbol, nil] The app state, such as :not_running, :running_in_foreground, :running_in_background - of nil if the driver has failed.
76
+ def state
77
+ if failed_driver?
78
+ $logger.error('Cannot get the app state - Appium driver failed.')
79
+ return nil
80
+ end
81
+
82
+ @driver.app_state(@driver.app_id)
83
+ rescue Selenium::WebDriver::Error::ServerError => e
84
+ # Assume the remote appium session has stopped, so crash out of the session
85
+ fail_driver
86
+ raise e
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,61 @@
1
+ require 'json'
2
+ require_relative '../../helper'
3
+ require_relative './manager'
4
+
5
+ module Maze
6
+ module Api
7
+ module Appium
8
+ # Provides operations for working with the app.
9
+ class DeviceManager < Maze::Api::Appium::Manager
10
+
11
+ # Unlocks the device.
12
+ # @returns [Boolean] Success status
13
+ def unlock
14
+ if failed_driver?
15
+ $logger.error 'Cannot unlock the device - Appium driver failed.'
16
+ return false
17
+ end
18
+
19
+ @driver.unlock
20
+ true
21
+ rescue Selenium::WebDriver::Error::ServerError => e
22
+ # Assume the remote appium session has stopped, so crash out of the session
23
+ fail_driver
24
+ raise e
25
+ end
26
+
27
+ # Sets the rotation of the device.
28
+ # @param orientation [Symbol] The orientation to set the device to, :portrait or :landscape
29
+ # @returns [Boolean] Success status
30
+ def set_rotation(orientation)
31
+ if failed_driver?
32
+ $logger.error 'Cannot set the device rotation - Appium driver failed.'
33
+ return false
34
+ end
35
+
36
+ @driver.set_rotation(orientation)
37
+ true
38
+ rescue Selenium::WebDriver::Error::ServerError => e
39
+ # Assume the remote appium session has stopped, so crash out of the session
40
+ fail_driver
41
+ raise e
42
+ end
43
+
44
+ # Gets the device info, in JSON format
45
+ # @returns [String, nil] Device info or nil
46
+ def info
47
+ if failed_driver?
48
+ $logger.error 'Cannot get the device info - Appium driver failed.'
49
+ return nil
50
+ end
51
+
52
+ JSON.generate(@driver.device_info)
53
+ rescue Selenium::WebDriver::Error::ServerError => e
54
+ # Assume the remote appium session has stopped, so crash out of the session
55
+ fail_driver
56
+ raise e
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,29 +1,43 @@
1
+ require_relative '../../helper'
2
+ require_relative './manager'
3
+
1
4
  module Maze
2
5
  module Api
3
6
  module Appium
4
7
  # Provides operations for working with files during Appium runs.
5
- class FileManager
6
- # param driver
7
- def initialize
8
- @driver = Maze.driver
9
- end
10
-
8
+ class FileManager < Maze::Api::Appium::Manager
11
9
  # Creates a file with the given contents on the device (using Appium). The file will be located in the app's
12
10
  # documents directory for iOS. On Android, it will be /sdcard/Android/data/<app-id>/files unless
13
11
  # Maze.config.android_app_files_directory has been set.
14
12
  # @param contents [String] Content of the file to be written
15
13
  # @param filename [String] Name (with no path) of the file to be written on the device
14
+ # @return [Boolean] Whether the file was successfully written to the device
16
15
  def write_app_file(contents, filename)
16
+ if failed_driver?
17
+ $logger.error 'Cannot write file to device - Appium driver failed.'
18
+ return false
19
+ end
20
+
17
21
  path = case Maze::Helper.get_current_platform
18
22
  when 'ios'
19
23
  "@#{@driver.app_id}/Documents/#{filename}"
20
24
  when 'android'
21
25
  directory = Maze.config.android_app_files_directory || "/sdcard/Android/data/#{@driver.app_id}/files"
22
26
  "#{directory}/#{filename}"
27
+ else
28
+ raise 'write_app_file is not supported on this platform'
23
29
  end
24
30
 
25
31
  $logger.trace "Pushing file to '#{path}' with contents: #{contents}"
26
32
  @driver.push_file(path, contents)
33
+ true
34
+ rescue Selenium::WebDriver::Error::UnknownError => e
35
+ $logger.error "Error writing file to device: #{e.message}"
36
+ false
37
+ rescue Selenium::WebDriver::Error::ServerError => e
38
+ # Assume the remote appium session has stopped, so crash out of the session
39
+ fail_driver
40
+ raise e
27
41
  end
28
42
 
29
43
  # Attempts to retrieve a given file from the device (using Appium). The default location for the file will be
@@ -31,9 +45,15 @@ module Maze
31
45
  # Maze.config.android_app_files_directory has been set.
32
46
  # @param filename [String] Name (with no path) of the file to be retrieved from the device
33
47
  # @param directory [String] Directory on the device where the file is located (optional)
48
+ # @return [String, nil] The content of the file read, or nil
34
49
  def read_app_file(filename, directory = nil)
50
+ if failed_driver?
51
+ $logger.error 'Cannot read file from device - Appium driver failed.'
52
+ return nil
53
+ end
54
+
35
55
  if directory
36
- path = directory
56
+ path = "#{directory}/#{filename}"
37
57
  else
38
58
  path = case Maze::Helper.get_current_platform
39
59
  when 'ios'
@@ -41,11 +61,20 @@ module Maze
41
61
  when 'android'
42
62
  dir = Maze.config.android_app_files_directory || "/sdcard/Android/data/#{@driver.app_id}/files"
43
63
  "#{dir}/#{filename}"
64
+ else
65
+ raise 'read_app_file is not supported on this platform'
44
66
  end
45
67
  end
46
68
 
47
69
  $logger.trace "Attempting to read file from '#{path}'"
48
- file = @driver.pull_file(path)
70
+ @driver.pull_file(path)
71
+ rescue Selenium::WebDriver::Error::UnknownError => e
72
+ $logger.error "Error reading file from device: #{e.message}"
73
+ nil
74
+ rescue Selenium::WebDriver::Error::ServerError => e
75
+ # Assume the remote appium session has stopped, so crash out of the session
76
+ fail_driver
77
+ raise e
49
78
  end
50
79
  end
51
80
  end
@@ -0,0 +1,20 @@
1
+ module Maze
2
+ module Api
3
+ module Appium
4
+ # Base class for all Appium managers.
5
+ class Manager
6
+ def initialize
7
+ @driver = Maze.driver
8
+ end
9
+
10
+ def failed_driver?
11
+ @driver.failed?
12
+ end
13
+
14
+ def fail_driver
15
+ @driver.fail_driver
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/maze/aws/sam.rb CHANGED
@@ -65,7 +65,7 @@ module Maze
65
65
  unless valid?(output)
66
66
  message = <<-WARNING
67
67
  The lambda function did not successfully complete.
68
- This may be expected and a normal result of the text execution.
68
+ This may be expected and a normal result of the test execution.
69
69
  The listed cause is:
70
70
  > #{output.last.chomp}
71
71
 
@@ -75,11 +75,12 @@ module Maze
75
75
  $logger.warn message
76
76
  end
77
77
 
78
- # Attempt to parse the last line of output as this is where a JSON
79
- # response would be. It's possible for a Lambda to output nothing,
78
+ # Attempt to parse response line of the output.
79
+ # It's possible for a Lambda to output nothing,
80
80
  # e.g. if it forcefully exited, so we allow JSON parse failures here
81
81
  begin
82
- parsed_output = JSON.parse(output.last)
82
+ response_line = output.find { |line| /{.*}/.match(line.strip) }
83
+ parsed_output = JSON.parse(response_line)
83
84
  rescue JSON::ParserError
84
85
  return {}
85
86
  end
@@ -33,6 +33,9 @@ module Maze
33
33
  $logger.debug "session_capabilities: #{Maze.driver.session_capabilities.inspect}"
34
34
  end
35
35
 
36
+ # Log the device information after it's started
37
+ write_device_info
38
+
36
39
  # Ensure the device is unlocked
37
40
  begin
38
41
  Maze.driver.unlock
@@ -56,6 +59,16 @@ module Maze
56
59
  raise 'Method not implemented by this class'
57
60
  end
58
61
 
62
+ def write_device_info
63
+ info = Maze.driver.device_info
64
+ filepath = File.join(Dir.pwd, 'maze_output', 'device_info.json')
65
+ File.open(filepath, 'w+') do |file|
66
+ file.puts(JSON.pretty_generate(info))
67
+ end
68
+ rescue => error
69
+ $logger.warn "Could not write device information file, #{error.message}"
70
+ end
71
+
59
72
  def attempt_start_driver(config)
60
73
  config.capabilities = device_capabilities
61
74
  driver = Maze::Driver::Appium.new config.appium_server_url,
@@ -134,7 +147,7 @@ module Maze
134
147
  end
135
148
 
136
149
  def stop_session
137
- Maze.driver&.driver_quit
150
+ Maze.driver.driver_quit unless Maze.driver.failed?
138
151
  Maze::AppiumServer.stop if Maze::AppiumServer.running
139
152
  end
140
153
  end
@@ -67,7 +67,7 @@ module Maze
67
67
 
68
68
  def log_run_intro
69
69
  # Log a link to the BrowserStack session search dashboard
70
- url = "https://app-automate.browserstack.com/dashboard/v2/search?query=#{Maze.run_uuid}&type=builds"
70
+ url = "https://app-automate.browserstack.com/projects/#{project_name_capabilities[:project]}/builds/#{Maze.run_uuid}/1?tab=tests"
71
71
  $logger.info Maze::Loggers::LogUtil.linkify(url, 'BrowserStack session(s)')
72
72
  end
73
73
 
@@ -6,12 +6,68 @@ module Maze
6
6
  raise 'Method not implemented by this class'
7
7
  end
8
8
 
9
+ def start_driver(config, selenium_url, max_attempts = 5)
10
+ attempts = 0
11
+
12
+ while attempts < max_attempts && Maze.driver.nil?
13
+ attempts += 1
14
+ start_error = nil
15
+
16
+ $logger.trace "Attempting to start Selenium driver with capabilities: #{config.capabilities.to_json}"
17
+ $logger.trace "Attempt #{attempts}"
18
+ begin
19
+ Maze.driver = Maze::Driver::Browser.new(:remote, selenium_url, config.capabilities)
20
+ Maze.driver.start_driver
21
+ rescue => error
22
+ Maze.driver = nil
23
+ $logger.error "Session creation failed: #{error}"
24
+ start_error = error
25
+ end
26
+
27
+ unless Maze.driver
28
+ interval = handle_start_error(config, start_error)
29
+ if interval.nil? || attempts >= max_attempts
30
+ $logger.error 'Failed to create Selenium driver, exiting'
31
+ Kernel.exit(::Maze::Api::ExitCode::SESSION_CREATION_FAILURE)
32
+ else
33
+ $logger.warn "Failed to create Selenium driver, retrying in #{interval} seconds"
34
+ $logger.info "Error: #{start_error.message}" if start_error
35
+ Kernel.sleep(interval)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def handle_start_error(config, error)
42
+ notify = true
43
+ interval = nil
44
+
45
+ # Used if we have a want to determine fatal errors later
46
+ case error.class.to_s
47
+ when 'Selenium::WebDriver::Error::WebDriverError'
48
+ interval = 5
49
+ notify = false
50
+ else
51
+ interval = 10
52
+ end
53
+
54
+ Bugsnag.notify error if notify
55
+
56
+ unless config.browser_list.empty?
57
+ # If the list is empty we have only one browser to continue with
58
+ config.browser = config.browser_list.shift
59
+ config.capabilities = create_capabilities(config)
60
+ end
61
+
62
+ interval
63
+ end
64
+
9
65
  def log_run_outro
10
66
  raise 'Method not implemented by this class'
11
67
  end
12
68
 
13
69
  def stop_session
14
- Maze.driver&.driver_quit
70
+ Maze.driver.driver_quit unless Maze.driver.failed?
15
71
  end
16
72
  end
17
73
  end
@@ -4,6 +4,19 @@ module Maze
4
4
  class BitBarClient < BaseClient
5
5
  def start_session
6
6
  config = Maze.config
7
+ if Maze::Client::BitBarClientUtils.use_local_tunnel?
8
+ Maze::Client::BitBarClientUtils.start_local_tunnel config.sb_local,
9
+ config.username,
10
+ config.access_key
11
+ end
12
+
13
+ create_capabilities(config)
14
+
15
+ selenium_url = Maze.config.selenium_server_url
16
+ start_driver(config, selenium_url)
17
+ end
18
+
19
+ def create_capabilities(config)
7
20
  capabilities = ::Selenium::WebDriver::Remote::Capabilities.new
8
21
  capabilities['bitbar_apiKey'] = config.access_key
9
22
  browsers = YAML.safe_load(File.read("#{__dir__}/bb_browsers.yml"))
@@ -13,20 +26,8 @@ module Maze
13
26
  capabilities.merge! JSON.parse(config.capabilities_option)
14
27
  capabilities['bitbar:options']['testTimeout'] = 900
15
28
  capabilities['acceptInsecureCerts'] = true unless Maze.config.browser.include? 'ie_'
29
+ capabilities['bitbar_apiKey'] = config.access_key if Maze::Client::BitBarClientUtils.use_local_tunnel?
16
30
  config.capabilities = capabilities
17
-
18
- if Maze::Client::BitBarClientUtils.use_local_tunnel?
19
- capabilities['bitbar_apiKey'] = config.access_key
20
- Maze::Client::BitBarClientUtils.start_local_tunnel config.sb_local,
21
- config.username,
22
- config.access_key
23
- end
24
-
25
- selenium_url = Maze.config.selenium_server_url
26
-
27
- $logger.trace "Starting Selenium driver with capabilities: #{config.capabilities.to_json}"
28
- Maze.driver = Maze::Driver::Browser.new :remote, selenium_url, config.capabilities
29
- Maze.driver.start_driver
30
31
  end
31
32
 
32
33
  def log_run_outro
@@ -5,55 +5,63 @@ module Maze
5
5
  module Selenium
6
6
  class BrowserStackClient < BaseClient
7
7
  def start_session
8
- # Set up the capabilities
9
- browsers = YAML.safe_load(File.read("#{__dir__}/bs_browsers.yml"))
10
-
11
8
  config = Maze.config
12
- browser = browsers[config.browser]
13
9
 
10
+ # Start the tunnel
11
+ Maze::Client::BrowserStackClientUtils.start_local_tunnel config.bs_local,
12
+ Maze.run_uuid,
13
+ config.access_key
14
+
15
+ create_capabilities(config)
16
+
17
+ # Start the driver
18
+ selenium_url = "https://#{config.username}:#{config.access_key}@hub.browserstack.com/wd/hub"
19
+ start_driver(config, selenium_url)
20
+
21
+ # Log details for the session
22
+ log_session_info
23
+ end
24
+
25
+ def create_capabilities(config)
14
26
  if config.legacy_driver?
15
27
  capabilities = ::Selenium::WebDriver::Remote::Capabilities.new
16
28
  capabilities['browserstack.local'] = 'true'
17
29
  capabilities['browserstack.localIdentifier'] = Maze.run_uuid
18
30
  capabilities['browserstack.console'] = 'errors'
19
31
  capabilities['acceptInsecureCerts'] = 'true'
20
-
21
- # Convert W3S capabilities to JSON-WP
22
- capabilities['browser'] = browser['browserName']
23
- capabilities['browser_version'] = browser['browserVersion']
24
- capabilities['device'] = browser['device']
25
- capabilities['os'] = browser['os']
26
- capabilities['os_version'] = browser['osVersion']
27
-
28
32
  capabilities.merge! JSON.parse(config.capabilities_option)
29
33
  capabilities.merge! project_name_capabilities
30
- config.capabilities = capabilities
34
+ add_browser_capabilities(config, capabilities)
31
35
  else
32
- capabilities = {
36
+ raw_capabilities = {
33
37
  'acceptInsecureCerts' => true,
34
38
  'bstack:options' => {
35
39
  'local' => 'true',
36
40
  'localIdentifier' => Maze.run_uuid
37
41
  }
38
42
  }
39
- capabilities.deep_merge! browser
40
- capabilities.deep_merge! JSON.parse(config.capabilities_option)
41
- capabilities.merge! project_name_capabilities
42
- config.capabilities = ::Selenium::WebDriver::Remote::Capabilities.new capabilities
43
+ raw_capabilities.deep_merge! JSON.parse(config.capabilities_option)
44
+ raw_capabilities.merge! project_name_capabilities
45
+ add_browser_capabilities(config, raw_capabilities)
46
+ capabilities = ::Selenium::WebDriver::Remote::Capabilities.new raw_capabilities
43
47
  end
48
+ config.capabilities = capabilities
49
+ end
44
50
 
45
- # Start the tunnel
46
- Maze::Client::BrowserStackClientUtils.start_local_tunnel config.bs_local,
47
- Maze.run_uuid,
48
- config.access_key
49
-
50
- # Start the driver
51
- selenium_url = "https://#{config.username}:#{config.access_key}@hub.browserstack.com/wd/hub"
52
- Maze.driver = Maze::Driver::Browser.new :remote, selenium_url, config.capabilities
53
- Maze.driver.start_driver
51
+ def add_browser_capabilities(config, capabilities)
52
+ browsers = YAML.safe_load(File.read("#{__dir__}/bs_browsers.yml"))
53
+ browser = browsers[config.browser]
54
54
 
55
- # Log details for the session
56
- log_session_info
55
+ if config.legacy_driver?
56
+ # Convert W3S capabilities to JSON-WP
57
+ capabilities['browser'] = browser['browserName']
58
+ capabilities['browser_version'] = browser['browserVersion']
59
+ capabilities['device'] = browser['device']
60
+ capabilities['os'] = browser['os']
61
+ capabilities['os_version'] = browser['osVersion']
62
+ else
63
+ capabilities.deep_merge! browser
64
+ end
57
65
  end
58
66
 
59
67
  def stop_session
@@ -86,7 +94,7 @@ module Maze
86
94
 
87
95
  def log_session_info
88
96
  # Log a link to the BrowserStack session search dashboard
89
- url = "https://automate.browserstack.com/dashboard/v2/search?query=#{Maze.run_uuid}&type=builds"
97
+ url = "https://automate.browserstack.com/projects/#{project_name_capabilities[:project]}/builds/#{Maze.run_uuid}/1?tab=tests"
90
98
  $logger.info Maze::Loggers::LogUtil.linkify url, 'BrowserStack session(s)'
91
99
  end
92
100
  end
@@ -173,6 +173,9 @@ module Maze
173
173
  # Test browser type
174
174
  attr_accessor :browser
175
175
 
176
+ # A list of browsers to attempt to connect to, in order
177
+ attr_accessor :browser_list
178
+
176
179
  # Test browser version
177
180
  attr_accessor :browser_version
178
181
 
@@ -20,7 +20,7 @@ module Maze
20
20
  attr_reader :device_type
21
21
 
22
22
  # @!attribute [r] capabilities
23
- # @return [Hash] The capabilities used to launch the BrowserStack instance
23
+ # @return [Hash] The capabilities used to launch the Appium session
24
24
  attr_reader :capabilities
25
25
 
26
26
  # Creates the Appium driver
@@ -32,6 +32,7 @@ module Maze
32
32
  # Sets up identifiers for ease of connecting jobs
33
33
  capabilities ||= {}
34
34
 
35
+ @failed = false
35
36
  @element_locator = locator
36
37
  @capabilities = capabilities
37
38
 
@@ -64,6 +65,17 @@ module Maze
64
65
  end
65
66
  end
66
67
 
68
+ # Whether the driver has known to have failed (it may still have failed and us not know yet)
69
+ def failed?
70
+ @failed
71
+ end
72
+
73
+ # Marks the driver as failed
74
+ def fail_driver
75
+ $logger.error 'Appium driver failed, remaining scenarios will be skipped'
76
+ @failed = true
77
+ end
78
+
67
79
  # Checks for an element, waiting until it is present or the method times out
68
80
  #
69
81
  # @param element_id [String] the element to search for
@@ -83,7 +95,7 @@ module Maze
83
95
  end
84
96
  rescue Selenium::WebDriver::Error::ServerError => e
85
97
  # Assume the remote appium session has stopped, so crash out of the session
86
- Maze.driver = nil
98
+ fail_driver
87
99
  raise e
88
100
  else
89
101
  true
@@ -94,7 +106,7 @@ module Maze
94
106
  super
95
107
  rescue Selenium::WebDriver::Error::ServerError => e
96
108
  # Assume the remote appium session has stopped, so crash out of the session
97
- Maze.driver = nil
109
+ fail_driver
98
110
  raise e
99
111
  end
100
112
 
@@ -103,7 +115,7 @@ module Maze
103
115
  super
104
116
  rescue Selenium::WebDriver::Error::ServerError => e
105
117
  # Assume the remote appium session has stopped, so crash out of the session
106
- Maze.driver = nil
118
+ fail_driver
107
119
  raise e
108
120
  end
109
121
 
@@ -114,7 +126,7 @@ module Maze
114
126
  end
115
127
  rescue Selenium::WebDriver::Error::ServerError => e
116
128
  # Assume the remote appium session has stopped, so crash out of the session
117
- Maze.driver = nil
129
+ fail_driver
118
130
  raise e
119
131
  end
120
132
 
@@ -128,7 +140,7 @@ module Maze
128
140
  end
129
141
  rescue Selenium::WebDriver::Error::ServerError => e
130
142
  # Assume the remote appium session has stopped, so crash out of the session
131
- Maze.driver = nil
143
+ fail_driver
132
144
  raise e
133
145
  end
134
146
 
@@ -146,7 +158,7 @@ module Maze
146
158
  false
147
159
  rescue Selenium::WebDriver::Error::ServerError => e
148
160
  # Assume the remote appium session has stopped, so crash out of the session
149
- Maze.driver = nil
161
+ fail_driver
150
162
  raise e
151
163
  end
152
164
 
@@ -160,7 +172,7 @@ module Maze
160
172
  end
161
173
  rescue Selenium::WebDriver::Error::ServerError => e
162
174
  # Assume the remote appium session has stopped, so crash out of the session
163
- Maze.driver = nil
175
+ fail_driver
164
176
  raise e
165
177
  end
166
178
 
@@ -185,7 +197,7 @@ module Maze
185
197
  end
186
198
  rescue Selenium::WebDriver::Error::ServerError => e
187
199
  # Assume the remote appium session has stopped, so crash out of the session
188
- Maze.driver = nil
200
+ fail_driver
189
201
  raise e
190
202
  end
191
203
 
@@ -222,7 +234,7 @@ module Maze
222
234
  end
223
235
  rescue Selenium::WebDriver::Error::ServerError => e
224
236
  # Assume the remote appium session has stopped, so crash out of the session
225
- Maze.driver = nil
237
+ fail_driver
226
238
  raise e
227
239
  end
228
240
 
@@ -13,15 +13,41 @@ module Maze
13
13
 
14
14
  def initialize(driver_for, selenium_url=nil, capabilities=nil)
15
15
  capabilities ||= {}
16
+ @failed = false
16
17
  @capabilities = capabilities
17
18
  @driver_for = driver_for
18
19
  @selenium_url = selenium_url
19
20
  end
20
21
 
22
+ # Whether the driver has known to have failed (it may still have failed and us not know yet)
23
+ def failed?
24
+ @failed
25
+ end
26
+
27
+ # Marks the driver as failed
28
+ def fail_driver
29
+ $logger.error 'Selenium driver failed, remaining scenarios will be skipped'
30
+ @failed = true
31
+ end
32
+
21
33
  def find_element(*args)
22
34
  @driver.find_element(*args)
23
35
  end
24
36
 
37
+ def wait_for_element(id)
38
+ @driver.find_element(id: id)
39
+ end
40
+
41
+ def click_element(id)
42
+ element = @driver.find_element(id: id)
43
+
44
+ if $browser.mobile?
45
+ element.click
46
+ else
47
+ @driver.action.move_to(element).click.perform
48
+ end
49
+ end
50
+
25
51
  def navigate
26
52
  @driver.navigate
27
53
  end
@@ -128,6 +154,7 @@ module Maze
128
154
  end
129
155
  $logger.info "Selenium driver started in #{(Time.now - time).to_i}s"
130
156
  @driver = driver
157
+ @failed = false
131
158
  rescue => error
132
159
  Bugsnag.notify error
133
160
  $logger.warn "Selenium driver failed to start in #{(Time.now - time).to_i}s"
@@ -42,11 +42,11 @@ module Maze
42
42
  # Reset the server to ensure that test fixtures cannot fetch
43
43
  # commands from the previous scenario (in idempotent mode).
44
44
  begin
45
- Maze.driver.terminate_app Maze.driver&.app_id
45
+ Maze.driver.terminate_app Maze.driver.app_id
46
46
  rescue Selenium::WebDriver::Error::UnknownError, Selenium::WebDriver::Error::InvalidSessionIdError
47
47
  if Maze.config.appium_version && Maze.config.appium_version.to_f < 2.0
48
48
  $logger.warn 'terminate_app failed, using the slower but more forceful close_app instead'
49
- Maze.driver&.close_app
49
+ Maze.driver.close_app
50
50
  else
51
51
  $logger.warn 'terminate_app failed, future errors may occur if the application did not close remotely'
52
52
  end
@@ -124,9 +124,10 @@ module Maze
124
124
  type: :string,
125
125
  multi: true
126
126
  opt Option::BROWSER,
127
- 'Browser to use (an entry in <farm>_browsers.yml)',
127
+ 'Browser to use (an entry in <farm>_browsers.yml). Can be listed multiple times to have a prioritised list of browsers',
128
128
  short: :none,
129
- type: :string
129
+ type: :string,
130
+ multi: true
130
131
  opt Option::BROWSER_VERSION,
131
132
  'Browser version to use (applies to entries in <farm>_browsers.yml that do not include a version)',
132
133
  short: :none,
@@ -57,21 +57,18 @@ module Maze
57
57
  case config.farm
58
58
  when :bs
59
59
  device_option = options[Maze::Option::DEVICE]
60
- if device_option.nil? || device_option.empty?
61
- config.browser = options[Maze::Option::BROWSER]
62
- else
63
- if device_option.is_a?(Array)
64
- config.device = device_option.first
65
- config.device_list = device_option.drop(1)
66
- else
67
- config.device = device_option
68
- config.device_list = []
69
- end
60
+ browser_option = options[Maze::Option::BROWSER]
61
+ if !device_option.empty?
62
+ config.device = device_option.first
63
+ config.device_list = device_option.drop(1)
70
64
  if config.legacy_driver?
71
65
  config.os_version = Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH[config.device]['os_version'].to_f
72
66
  else
73
67
  config.os_version = Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH[config.device]['platformVersion'].to_f
74
68
  end
69
+ elsif !browser_option.empty?
70
+ config.browser = browser_option.first
71
+ config.browser_list = browser_option.drop(1)
75
72
  end
76
73
  config.bs_local = Maze::Helper.expand_path(options[Maze::Option::BS_LOCAL])
77
74
  config.appium_version = options[Maze::Option::APPIUM_VERSION]
@@ -83,18 +80,20 @@ module Maze
83
80
  config.access_key = options[Maze::Option::ACCESS_KEY]
84
81
  config.appium_version = options[Maze::Option::APPIUM_VERSION]
85
82
  device_option = options[Maze::Option::DEVICE]
86
- if device_option.nil? || device_option.empty?
87
- # BitBar Web
88
- config.browser = options[Maze::Option::BROWSER]
89
- config.browser_version = options[Maze::Option::BROWSER_VERSION]
90
- else
91
- # BitBar Devices
92
- if device_option.is_a?(Array)
93
- config.device = device_option.first
94
- config.device_list = device_option.drop(1)
83
+ browser_option = options[Maze::Option::BROWSER]
84
+ browser_version = options[Maze::Option::BROWSER_VERSION]
85
+ if !device_option.empty?
86
+ config.device = device_option.first
87
+ config.device_list = device_option.drop(1)
88
+ elsif !browser_option.empty?
89
+ if browser_version.nil?
90
+ config.browser = browser_option.first
91
+ config.browser_list = browser_option.drop(1)
95
92
  else
96
- config.device = device_option
97
- config.device_list = []
93
+ # Dropping all but the first browser as the version is specified
94
+ config.browser = browser_option.first
95
+ config.browser_list = []
96
+ config.browser_version = browser_version
98
97
  end
99
98
  end
100
99
  config.os = options[Maze::Option::OS]
@@ -104,9 +103,7 @@ module Maze
104
103
  config.selenium_server_url = options[Maze::Option::SELENIUM_SERVER]
105
104
  config.app_bundle_id = options[Maze::Option::APP_BUNDLE_ID]
106
105
  when :local then
107
- if options[Maze::Option::BROWSER]
108
- config.browser = options[Maze::Option::BROWSER]
109
- else
106
+ if options[Maze::Option::BROWSER].empty?
110
107
  os = config.os = options[Maze::Option::OS].downcase
111
108
  config.os_version = options[Maze::Option::OS_VERSION].to_f unless options[Maze::Option::OS_VERSION].nil?
112
109
  config.appium_server_url = options[Maze::Option::APPIUM_SERVER]
@@ -116,6 +113,8 @@ module Maze
116
113
  config.apple_team_id = options[Maze::Option::APPLE_TEAM_ID]
117
114
  config.device_id = options[Maze::Option::UDID]
118
115
  end
116
+ else
117
+ config.browser = options[Maze::Option::BROWSER].first
119
118
  end
120
119
  when :none
121
120
  if options[Maze::Option::OS]
@@ -53,17 +53,17 @@ module Maze
53
53
  # Device
54
54
  browser = options[Option::BROWSER]
55
55
  device = options[Option::DEVICE]
56
- if browser.nil? && device.empty?
56
+ if browser.empty? && device.empty?
57
57
  errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
58
- elsif browser
59
-
58
+ elsif !browser.empty?
60
59
  browsers = YAML.safe_load(File.read("#{__dir__}/../client/selenium/bs_browsers.yml"))
61
60
 
62
- unless browsers.include? browser
61
+ rejected_browsers = browser.reject { |br| browsers.include? br }
62
+ unless rejected_browsers.empty?
63
63
  browser_list = browsers.keys.join ', '
64
- errors << "Browser type '#{browser}' unknown on BrowserStack. Must be one of: #{browser_list}."
64
+ errors << "Browser types '#{rejected_browsers.join(', ')}' unknown on BrowserStack. Must be one of: #{browser_list}."
65
65
  end
66
- elsif device
66
+ elsif !device.empty?
67
67
  device.each do |device_key|
68
68
  next if Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH.key? device_key
69
69
  errors << "Device type '#{device_key}' unknown on BrowserStack. Must be one of #{Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH.keys}"
@@ -89,25 +89,30 @@ module Maze
89
89
  def validate_bitbar(options, errors)
90
90
  browser = options[Option::BROWSER]
91
91
  device = options[Option::DEVICE]
92
-
92
+
93
93
  errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
94
94
  errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
95
95
 
96
96
  # Device
97
- if browser.nil? && device.empty?
97
+ if browser.empty? && device.empty?
98
98
  errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
99
- elsif browser
99
+ elsif !browser.empty?
100
100
  browsers = YAML.safe_load(File.read("#{__dir__}/../client/selenium/bb_browsers.yml"))
101
101
 
102
- if browsers.include? browser
103
- if options[Option::BROWSER_VERSION].nil? && !browsers[browser].include?('version')
104
- errors << "--#{Option::BROWSER_VERSION} must be specified for browser '#{browser}'"
102
+ rejected_browsers = browser.reject { |br| browsers.include? br }
103
+ if rejected_browsers.empty?
104
+ if options[Option::BROWSER_VERSION].nil?
105
+ browser.each do |br|
106
+ next if browsers[br].include?('version')
107
+ errors << "--#{Option::BROWSER_VERSION} must be specified for browser '#{br}'"
108
+ end
105
109
  end
106
110
  else
107
111
  browser_list = browsers.keys.join ', '
108
- errors << "Browser type '#{browser}' unknown on BitBar. Must be one of: #{browser_list}."
112
+ errors << "Browser types '#{rejected_browsers.join(', ')}' unknown on BitBar. Must be one of: #{browser_list}."
109
113
  end
110
- elsif device
114
+
115
+ elsif !device.empty?
111
116
  app = Maze::Helper.read_at_arg_file options[Option::APP]
112
117
  if app.nil?
113
118
  errors << "--#{Option::APP} must be provided when running on a device"
@@ -123,7 +128,7 @@ module Maze
123
128
 
124
129
  # Validates Local device options
125
130
  def validate_local(options, errors)
126
- if options[Option::BROWSER].nil?
131
+ if options[Option::BROWSER].empty?
127
132
  errors << "--#{Option::APP} must be specified" if options[Option::APP].nil?
128
133
 
129
134
  # OS
data/lib/maze.rb CHANGED
@@ -8,7 +8,7 @@ require_relative 'maze/timers'
8
8
  # providing an alternative to the proliferation of global variables or singletons.
9
9
  module Maze
10
10
 
11
- VERSION = '9.21.0'
11
+ VERSION = '9.23.0'
12
12
 
13
13
  class << self
14
14
  attr_accessor :check, :driver, :internal_hooks, :mode, :start_time, :dynamic_retry, :public_address,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bugsnag-maze-runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.21.0
4
+ version: 9.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Kirkland
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-28 00:00:00.000000000 Z
11
+ date: 2025-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cucumber
@@ -440,7 +440,10 @@ files:
440
440
  - lib/features/support/env.rb
441
441
  - lib/features/support/internal_hooks.rb
442
442
  - lib/maze.rb
443
+ - lib/maze/api/appium/app_manager.rb
444
+ - lib/maze/api/appium/device_manager.rb
443
445
  - lib/maze/api/appium/file_manager.rb
446
+ - lib/maze/api/appium/manager.rb
444
447
  - lib/maze/api/cucumber/scenario.rb
445
448
  - lib/maze/api/exit_code.rb
446
449
  - lib/maze/appium_server.rb