bugsnag-maze-runner 7.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +74 -0
  4. data/bin/maze-runner +174 -0
  5. data/bin/upload-app +56 -0
  6. data/lib/features/scripts/await-android-emulator.sh +11 -0
  7. data/lib/features/scripts/clear-android-app-data.sh +8 -0
  8. data/lib/features/scripts/force-stop-android-app.sh +8 -0
  9. data/lib/features/scripts/install-android-app.sh +15 -0
  10. data/lib/features/scripts/launch-android-app.sh +38 -0
  11. data/lib/features/scripts/launch-android-emulator.sh +15 -0
  12. data/lib/features/steps/android_steps.rb +51 -0
  13. data/lib/features/steps/app_automator_steps.rb +228 -0
  14. data/lib/features/steps/aws_sam_steps.rb +212 -0
  15. data/lib/features/steps/breadcrumb_steps.rb +80 -0
  16. data/lib/features/steps/browser_steps.rb +93 -0
  17. data/lib/features/steps/build_api_steps.rb +25 -0
  18. data/lib/features/steps/document_server_steps.rb +7 -0
  19. data/lib/features/steps/error_reporting_steps.rb +358 -0
  20. data/lib/features/steps/feature_flag_steps.rb +190 -0
  21. data/lib/features/steps/header_steps.rb +72 -0
  22. data/lib/features/steps/log_steps.rb +29 -0
  23. data/lib/features/steps/multipart_request_steps.rb +142 -0
  24. data/lib/features/steps/network_steps.rb +135 -0
  25. data/lib/features/steps/payload_steps.rb +257 -0
  26. data/lib/features/steps/proxy_steps.rb +34 -0
  27. data/lib/features/steps/query_parameter_steps.rb +31 -0
  28. data/lib/features/steps/request_assertion_steps.rb +186 -0
  29. data/lib/features/steps/runner_steps.rb +428 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/trace_steps.rb +206 -0
  32. data/lib/features/steps/value_steps.rb +119 -0
  33. data/lib/features/support/env.rb +7 -0
  34. data/lib/features/support/internal_hooks.rb +207 -0
  35. data/lib/maze/api/appium/file_manager.rb +29 -0
  36. data/lib/maze/appium_server.rb +112 -0
  37. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  38. data/lib/maze/aws/sam.rb +112 -0
  39. data/lib/maze/aws_public_ip.rb +53 -0
  40. data/lib/maze/bugsnag_config.rb +42 -0
  41. data/lib/maze/checks/assert_check.rb +69 -0
  42. data/lib/maze/checks/noop_check.rb +34 -0
  43. data/lib/maze/client/appium/base_client.rb +131 -0
  44. data/lib/maze/client/appium/bb_client.rb +102 -0
  45. data/lib/maze/client/appium/bb_devices.rb +127 -0
  46. data/lib/maze/client/appium/bs_client.rb +91 -0
  47. data/lib/maze/client/appium/bs_devices.rb +141 -0
  48. data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
  49. data/lib/maze/client/appium/local_client.rb +67 -0
  50. data/lib/maze/client/appium.rb +23 -0
  51. data/lib/maze/client/bb_api_client.rb +102 -0
  52. data/lib/maze/client/bb_client_utils.rb +181 -0
  53. data/lib/maze/client/bs_client_utils.rb +168 -0
  54. data/lib/maze/client/selenium/base_client.rb +15 -0
  55. data/lib/maze/client/selenium/bb_browsers.yml +188 -0
  56. data/lib/maze/client/selenium/bb_client.rb +38 -0
  57. data/lib/maze/client/selenium/bs_browsers.yml +257 -0
  58. data/lib/maze/client/selenium/bs_client.rb +89 -0
  59. data/lib/maze/client/selenium/local_client.rb +16 -0
  60. data/lib/maze/client/selenium.rb +16 -0
  61. data/lib/maze/compare.rb +161 -0
  62. data/lib/maze/configuration.rb +182 -0
  63. data/lib/maze/docker.rb +147 -0
  64. data/lib/maze/document_server.rb +46 -0
  65. data/lib/maze/driver/appium.rb +198 -0
  66. data/lib/maze/driver/browser.rb +124 -0
  67. data/lib/maze/errors.rb +52 -0
  68. data/lib/maze/generator.rb +55 -0
  69. data/lib/maze/helper.rb +122 -0
  70. data/lib/maze/hooks/appium_hooks.rb +55 -0
  71. data/lib/maze/hooks/browser_hooks.rb +15 -0
  72. data/lib/maze/hooks/command_hooks.rb +9 -0
  73. data/lib/maze/hooks/error_code_hook.rb +49 -0
  74. data/lib/maze/hooks/hooks.rb +61 -0
  75. data/lib/maze/http_request.rb +21 -0
  76. data/lib/maze/interactive_cli.rb +173 -0
  77. data/lib/maze/logger.rb +86 -0
  78. data/lib/maze/macos_utils.rb +14 -0
  79. data/lib/maze/maze_output.rb +88 -0
  80. data/lib/maze/network.rb +49 -0
  81. data/lib/maze/option/parser.rb +240 -0
  82. data/lib/maze/option/processor.rb +130 -0
  83. data/lib/maze/option/validator.rb +155 -0
  84. data/lib/maze/option.rb +62 -0
  85. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  86. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  87. data/lib/maze/plugins/error_code_plugin.rb +21 -0
  88. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  89. data/lib/maze/proxy.rb +114 -0
  90. data/lib/maze/request_list.rb +87 -0
  91. data/lib/maze/request_repeater.rb +49 -0
  92. data/lib/maze/retry_handler.rb +67 -0
  93. data/lib/maze/runner.rb +149 -0
  94. data/lib/maze/schemas/OtelTraceSchema.json +390 -0
  95. data/lib/maze/schemas/trace_schema.rb +7 -0
  96. data/lib/maze/schemas/trace_validator.rb +98 -0
  97. data/lib/maze/server.rb +251 -0
  98. data/lib/maze/servlets/base_servlet.rb +27 -0
  99. data/lib/maze/servlets/command_servlet.rb +47 -0
  100. data/lib/maze/servlets/log_servlet.rb +64 -0
  101. data/lib/maze/servlets/reflective_servlet.rb +70 -0
  102. data/lib/maze/servlets/servlet.rb +199 -0
  103. data/lib/maze/servlets/temp.rb +0 -0
  104. data/lib/maze/servlets/trace_servlet.rb +13 -0
  105. data/lib/maze/store.rb +15 -0
  106. data/lib/maze/terminating_server.rb +129 -0
  107. data/lib/maze/timers.rb +51 -0
  108. data/lib/maze/wait.rb +35 -0
  109. data/lib/maze.rb +27 -0
  110. data/lib/utils/deep_merge.rb +17 -0
  111. data/lib/utils/selenium_money_patch.rb +17 -0
  112. metadata +451 -0
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ class MacosUtils
5
+ class << self
6
+ def capture_screen(scenario)
7
+ path = File.join(File.join(Dir.pwd, 'maze_output'), 'failed', Maze::Helper.to_friendly_filename(scenario.name))
8
+ FileUtils.makedirs(path)
9
+
10
+ system("/usr/sbin/screencapture #{path}/#{Maze::Helper.to_friendly_filename(scenario.name)}-screenshot.jpg")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,88 @@
1
+ module Maze
2
+ # Responsible for writing files to the maze_output directory
3
+ class MazeOutput
4
+
5
+ # @param scenario The Cucumber scenario
6
+ def initialize(scenario)
7
+ @scenario = scenario
8
+ end
9
+
10
+ # Writes each list of requests to a separate file under, e.g:
11
+ # maze_output/failed/scenario_name/errors.log
12
+ def write_requests
13
+ path = output_folder
14
+ FileUtils.makedirs(path)
15
+
16
+ request_types = %w[errors sessions builds uploads logs sourcemaps traces invalid]
17
+
18
+ request_types.each do |request_type|
19
+ list = Maze::Server.list_for(request_type).all
20
+ next if list.empty?
21
+
22
+ filename = "#{request_type}.log"
23
+ filepath = File.join(path, filename)
24
+
25
+ counter = 1
26
+ File.open(filepath, 'w+') do |file|
27
+ list.each do |request|
28
+ file.puts "=== Request #{counter} of #{list.size} ==="
29
+ if request[:invalid]
30
+ invalid_request = true
31
+ uri = request[:request][:request_uri]
32
+ headers = request[:request][:header]
33
+ body = request[:request][:body]
34
+ else
35
+ invalid_request = false
36
+ uri = request[:request].request_uri
37
+ headers = request[:request].header
38
+ body = request[:body]
39
+ end
40
+ file.puts "URI: #{uri}"
41
+ file.puts "HEADERS:"
42
+ headers.each do |key, values|
43
+ file.puts " #{key}: #{values.map {|v| "'#{v}'"}.join(' ')}"
44
+ end
45
+ file.puts
46
+ file.puts "BODY:"
47
+ if !invalid_request && headers["content-type"].first == 'application/json'
48
+ file.puts JSON.pretty_generate(body)
49
+ else
50
+ file.puts body
51
+ end
52
+ file.puts
53
+ if request.include?(:reason)
54
+ file.puts "REASON:"
55
+ file.puts request[:reason]
56
+ file.puts
57
+ end
58
+ counter += 1
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # Pulls the logs from the device if the scenario fails
65
+ # @param logs [Array<String>] The lines of log to be written
66
+ def write_device_logs(logs)
67
+
68
+ dir = output_folder
69
+ FileUtils.makedirs(dir)
70
+ filepath = File.join(dir, 'device.log')
71
+
72
+ File.open(filepath, 'w+') do |file|
73
+ logs.each { |line| file.puts line }
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # Determines the output folder for the scenario
80
+ def output_folder
81
+ folder1 = File.join(Dir.pwd, 'maze_output')
82
+ folder2 = @scenario.failed? ? 'failed' : 'passed'
83
+ folder3 = Maze::Helper.to_friendly_filename(@scenario.name)
84
+
85
+ File.join(folder1, folder2, folder3)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ module Maze
6
+ # Sets the maximum number of times Maze runner will ping to see if a port is open, defaulting to 100
7
+ MAX_MAZE_CONNECT_ATTEMPTS = ENV.fetch('MAX_MAZE_CONNECT_ATTEMPTS', 100).to_i
8
+
9
+ # Provides network utility functionality
10
+ class Network
11
+ class << self
12
+ # Repeatedly pings a port to see if the host is ready for a connection.
13
+ # The maximum amount of attempts is determined by the MAX_MAZE_CONNECT_ATTEMPTS variable.
14
+ #
15
+ # @param host [String] The name of the host to connect to
16
+ # @param port [String] The port to attempt to connect to
17
+ #
18
+ # @raise [StandardError] When the port is not available for a connection
19
+ def wait_for_port(host, port)
20
+ attempts = 0
21
+ up = false
22
+ until (attempts >= MAX_MAZE_CONNECT_ATTEMPTS) || up
23
+ attempts += 1
24
+ up = port_open?(host, port)
25
+ sleep 0.1 unless up
26
+ end
27
+ raise "Port not ready in time!" unless up
28
+ end
29
+
30
+ # Attempts to connect to a port, timing out after a time.
31
+ #
32
+ # @param host [String] The name of the host to connect to
33
+ # @param port [String] The port to attempt to connect to
34
+ # @param seconds [Float] Optional. The length of time to wait before timing out.
35
+ def port_open?(host, port, seconds=0.1)
36
+ Timeout::timeout(seconds) do
37
+ begin
38
+ TCPSocket.new(host, port).close
39
+ true
40
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
41
+ false
42
+ end
43
+ end
44
+ rescue Timeout::Error
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/cli/main'
4
+ require 'optimist'
5
+ require_relative '../option'
6
+ require_relative '../../maze'
7
+
8
+ module Maze
9
+ module Option
10
+ # Parses the command line options
11
+ class Parser
12
+ class << self
13
+ def parse(args)
14
+ parser = Optimist::Parser.new do
15
+ text 'Maze Runner extends the functionality of Cucumber, ' \
16
+ 'providing all of the command line arguments that it provides.'
17
+ text ''
18
+ text 'Usage [OPTIONS] <filenames>'
19
+ text ''
20
+ text 'Overridden Cucumber options:'
21
+ opt :help,
22
+ 'Print this help.'
23
+ opt :version,
24
+ 'Display Maze Runner and Cucumber versions'
25
+
26
+ text ''
27
+ text 'General options:'
28
+
29
+ opt Option::AWS_PUBLIC_IP,
30
+ 'Intended for use on Buildkite with the Elastic CI Stack for CI. Enables awareness of being run with a public IP address.',
31
+ type: :boolean,
32
+ default: false
33
+
34
+ opt Option::ENABLE_RETRIES,
35
+ 'Enables retrying failed scenarios when tagged',
36
+ type: :boolean,
37
+ default: true
38
+
39
+ opt Option::ENABLE_BUGSNAG,
40
+ 'Enables reporting to Bugsnag on scenario failure (requires MAZE_BUGSNAG_API_KEY)',
41
+ type: :boolean,
42
+ default: true
43
+
44
+ opt Option::REPEATER_API_KEY,
45
+ 'Enables forwarding of all received POST requests to Bugsnag, using the API key provided. MAZE_REPEATER_API_KEY may also be set.',
46
+ type: :string
47
+
48
+ text ''
49
+ text 'Server options:'
50
+
51
+ opt Option::BIND_ADDRESS,
52
+ 'Mock server bind address',
53
+ type: :string
54
+ opt Option::PORT,
55
+ 'Mock server port',
56
+ default: 9339
57
+ opt Option::NULL_PORT,
58
+ 'Terminating connection port',
59
+ default: 9341
60
+
61
+ text ''
62
+ text 'Document server options:'
63
+
64
+ opt Option::DS_ROOT,
65
+ 'Document server root',
66
+ type: :string
67
+ opt Option::DS_BIND_ADDRESS,
68
+ 'Document server bind address',
69
+ type: :string
70
+ opt Option::DS_PORT,
71
+ 'Document server port',
72
+ default: 9340
73
+
74
+ text ''
75
+ text 'Appium options:'
76
+
77
+ opt Option::FARM,
78
+ 'Device farm to use: "bs" (BrowserStack) or "local"',
79
+ type: :string
80
+ opt Option::APP,
81
+ 'The app to be installed and run against. Assumed to be contained in a file if prefixed with @.',
82
+ type: :string
83
+ opt Option::A11Y_LOCATOR,
84
+ 'Locate elements by accessibility id rather than id',
85
+ type: :boolean,
86
+ default: false
87
+ opt Option::CAPABILITIES,
88
+ 'Additional desired Appium capabilities as a JSON string',
89
+ default: '{}'
90
+
91
+ text ''
92
+ text 'Device farm options:'
93
+
94
+ opt Option::DEVICE,
95
+ 'Device to use. Can be listed multiple times to have a prioritised list of devices',
96
+ short: :none,
97
+ type: :string,
98
+ multi: true
99
+ opt Option::BROWSER,
100
+ 'Browser to use (an entry in <farm>_browsers.yml)',
101
+ short: :none,
102
+ type: :string
103
+ opt Option::USERNAME,
104
+ 'Device farm username. Consumes env var from environment based on farm set',
105
+ type: :string
106
+ opt Option::ACCESS_KEY,
107
+ 'Device farm access key. Consumes env var from environment based on farm set',
108
+ type: :string
109
+ opt Option::APPIUM_VERSION,
110
+ 'The Appium version to use',
111
+ type: :string
112
+ opt Option::LIST_DEVICES,
113
+ 'Lists the devices available for the configured device farm, or all devices if none are specified',
114
+ default: false
115
+ opt Option::APP_BUNDLE_ID,
116
+ 'The bundle identifier of the test application',
117
+ type: :string
118
+ opt Option::TUNNEL,
119
+ 'Start the device farm secure tunnel',
120
+ default: true
121
+
122
+ # SmartBear-only options
123
+ opt Option::SB_LOCAL,
124
+ '(SB only) Path to the SBSecureTunnel binary. MAZE_SB_LOCAL env var or "/SBSecureTunnel" by default',
125
+ type: :string
126
+
127
+ # BrowserStack-only options
128
+ opt Option::BS_LOCAL,
129
+ '(BS only) Path to the BrowserStackLocal binary. MAZE_BS_LOCAL env var or "/BrowserStackLocal" by default',
130
+ type: :string
131
+
132
+ # TMS options
133
+ opt Option::TMS_URI,
134
+ 'URI of the test management server root. MAZE_TMS_URI env var',
135
+ type: :string
136
+
137
+ opt Option::TMS_TOKEN,
138
+ 'Token used to access the test management server. MAZE_TMS_TOKEN env var',
139
+ type: :string
140
+
141
+ text ''
142
+ text 'Local device options:'
143
+
144
+ opt Option::OS,
145
+ 'OS type to use ("ios", "android")',
146
+ type: :string
147
+ opt Option::OS_VERSION,
148
+ 'The intended OS version when running on a local device',
149
+ type: :string
150
+ opt Option::APPIUM_SERVER,
151
+ 'Appium server URL, only used for --farm=local. MAZE_APPIUM_SERVER env var or "http://localhost:4723/wd/hub" by default',
152
+ type: :string
153
+ opt Option::START_APPIUM,
154
+ 'Whether a local Appium server should be start. Only used for --farm=local.',
155
+ default: true
156
+ opt Option::APPIUM_LOGFILE,
157
+ 'The file local appium server output is logged to, defaulting to "appium_server.log"',
158
+ default: 'appium_server.log'
159
+ opt Option::APPLE_TEAM_ID,
160
+ 'Apple Team Id, required for local iOS testing. MAZE_APPLE_TEAM_ID env var by default',
161
+ type: :string
162
+ opt Option::UDID,
163
+ 'Apple UDID, required for local iOS testing. MAZE_UDID env var by default',
164
+ type: :string
165
+
166
+ text ''
167
+ text 'Logging options:'
168
+
169
+ opt Option::FILE_LOG,
170
+ "Writes lists of received requests to the maze_output folder for all scenarios",
171
+ type: :boolean,
172
+ default: true
173
+
174
+ opt Option::LOG_REQUESTS,
175
+ "Log lists of received requests to the console in the event of scenario failure. Defaults to true if the BUILDKITE environment variable is set",
176
+ type: :boolean,
177
+ default: false
178
+
179
+ opt Option::ALWAYS_LOG,
180
+ "Always log all received requests at the end of a scenario, whether is passes or fails",
181
+ type: :boolean,
182
+ default: false
183
+
184
+ version "Maze Runner v#{Maze::VERSION} " \
185
+ "(Cucumber v#{Cucumber::VERSION.strip})"
186
+ text ''
187
+ text 'The Cucumber help follows:'
188
+ text ''
189
+ end
190
+
191
+ # Allow for options destined for Cucumber
192
+ parser.ignore_invalid_options = true
193
+ options = parser.parse args
194
+ populate_environmental_defaults(options)
195
+
196
+ rescue Optimist::HelpNeeded
197
+ parser.educate
198
+ Cucumber::Cli::Main.new(['--help']).execute!
199
+ exit
200
+ rescue Optimist::VersionNeeded
201
+ puts parser.version
202
+ exit
203
+ end
204
+
205
+ # Populates unset options with appropriate environment variables or default values if necessary
206
+ #
207
+ # @param options [Hash] The hash of already-parsed options
208
+ #
209
+ # @returns [Hash] The options hash with environment vars added
210
+ def populate_environmental_defaults(options)
211
+ case options.farm
212
+ when 'bs'
213
+ # Allow browser/device credentials to exist in separate accounts
214
+ if options[Option::BROWSER]
215
+ options[Option::USERNAME] ||= ENV['BROWSER_STACK_BROWSERS_USERNAME'] || ENV['BROWSER_STACK_USERNAME']
216
+ options[Option::ACCESS_KEY] ||= ENV['BROWSER_STACK_BROWSERS_ACCESS_KEY'] ||ENV['BROWSER_STACK_ACCESS_KEY']
217
+ else
218
+ options[Option::USERNAME] ||= ENV['BROWSER_STACK_DEVICES_USERNAME'] || ENV['BROWSER_STACK_USERNAME']
219
+ options[Option::ACCESS_KEY] ||= ENV['BROWSER_STACK_DEVICES_ACCESS_KEY'] ||ENV['BROWSER_STACK_ACCESS_KEY']
220
+ end
221
+ when 'bb'
222
+ options[Option::USERNAME] ||= ENV['BITBAR_USERNAME']
223
+ options[Option::ACCESS_KEY] ||= ENV['BITBAR_ACCESS_KEY']
224
+ options[Option::TMS_URI] ||= ENV['MAZE_TMS_URI']
225
+ end
226
+
227
+ options[Option::REPEATER_API_KEY] ||= ENV['MAZE_REPEATER_API_KEY']
228
+ options[Option::SB_LOCAL] ||= ENV['MAZE_SB_LOCAL'] || '/SBSecureTunnel'
229
+ options[Option::TMS_URI] ||= ENV['MAZE_TMS_URI']
230
+ options[Option::TMS_TOKEN] ||= ENV['MAZE_TMS_TOKEN']
231
+ options[Option::BS_LOCAL] ||= ENV['MAZE_BS_LOCAL'] || '/BrowserStackLocal'
232
+ options[Option::APPIUM_SERVER] ||= ENV['MAZE_APPIUM_SERVER'] || 'http://localhost:4723/wd/hub'
233
+ options[Option::APPLE_TEAM_ID] ||= ENV['MAZE_APPLE_TEAM_ID']
234
+ options[Option::UDID] ||= ENV['MAZE_UDID']
235
+ options
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../option'
4
+
5
+ module Maze
6
+ module Option
7
+ # Processes the parsed command line options
8
+ class Processor
9
+ class << self
10
+ # Populates config from the parsed options given
11
+ # @param config [Configuration] MazeRunner configuration to populate
12
+ # @param options [Hash] Parsed command line options
13
+ def populate(config, options)
14
+
15
+ # Server options
16
+ config.bind_address = options[Maze::Option::BIND_ADDRESS]
17
+ config.port = options[Maze::Option::PORT]
18
+ config.null_port = options[Maze::Option::NULL_PORT]
19
+
20
+ # General options
21
+ config.aws_public_ip = options[Maze::Option::AWS_PUBLIC_IP]
22
+ config.enable_retries = options[Maze::Option::ENABLE_RETRIES]
23
+ config.enable_bugsnag = options[Maze::Option::ENABLE_BUGSNAG]
24
+ config.tms_uri = options[Maze::Option::TMS_URI]
25
+ config.tms_token = options[Maze::Option::TMS_TOKEN]
26
+ config.repeater_api_key = options[Maze::Option::REPEATER_API_KEY]
27
+
28
+ # Document server options
29
+ config.document_server_root = options[Maze::Option::DS_ROOT]
30
+ config.document_server_bind_address = options[Maze::Option::DS_BIND_ADDRESS]
31
+ config.document_server_port = options[Maze::Option::DS_PORT]
32
+
33
+ # Logger options
34
+ config.file_log = options[Maze::Option::FILE_LOG]
35
+ config.log_requests = options[Maze::Option::LOG_REQUESTS] || !ENV['BUILDKITE'].nil?
36
+ config.always_log = options[Maze::Option::ALWAYS_LOG]
37
+
38
+ # General appium options
39
+ config.app = Maze::Helper.read_at_arg_file options[Maze::Option::APP]
40
+ farm = options[Maze::Option::FARM]
41
+ config.farm = case farm
42
+ when nil then :none
43
+ when 'bs' then :bs
44
+ when 'bb' then :bb
45
+ when 'local' then :local
46
+ else
47
+ raise "Unknown farm '#{farm}'"
48
+ end
49
+ config.locator = options[Maze::Option::A11Y_LOCATOR] ? :accessibility_id : :id
50
+ config.capabilities_option = options[Maze::Option::CAPABILITIES]
51
+ config.legacy_driver = !ENV['USE_LEGACY_DRIVER'].nil?
52
+ config.start_tunnel = options[Maze::Option::TUNNEL]
53
+
54
+ # Farm specific options
55
+ case config.farm
56
+ when :bs
57
+ device_option = options[Maze::Option::DEVICE]
58
+ if device_option.nil? || device_option.empty?
59
+ config.browser = options[Maze::Option::BROWSER]
60
+ else
61
+ if device_option.is_a?(Array)
62
+ config.device = device_option.first
63
+ config.device_list = device_option.drop(1)
64
+ else
65
+ config.device = device_option
66
+ config.device_list = []
67
+ end
68
+ if config.legacy_driver?
69
+ config.os_version = Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH[config.device]['os_version'].to_f
70
+ else
71
+ config.os_version = Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH[config.device]['platformVersion'].to_f
72
+ end
73
+ end
74
+ config.bs_local = Maze::Helper.expand_path(options[Maze::Option::BS_LOCAL])
75
+ config.appium_version = options[Maze::Option::APPIUM_VERSION]
76
+ username = config.username = options[Maze::Option::USERNAME]
77
+ access_key = config.access_key = options[Maze::Option::ACCESS_KEY]
78
+ config.appium_server_url = "http://#{username}:#{access_key}@hub-cloud.browserstack.com/wd/hub"
79
+ when :bb then
80
+ config.username = options[Maze::Option::USERNAME]
81
+ config.access_key = options[Maze::Option::ACCESS_KEY]
82
+ config.tms_uri = options[Maze::Option::TMS_URI]
83
+ device_option = options[Maze::Option::DEVICE]
84
+ if device_option.nil? || device_option.empty?
85
+ # BitBar Web
86
+ config.browser = options[Maze::Option::BROWSER]
87
+ else
88
+ # BitBar Devices
89
+ if device_option.is_a?(Array)
90
+ config.device = device_option.first
91
+ config.device_list = device_option.drop(1)
92
+ else
93
+ config.device = device_option
94
+ config.device_list = []
95
+ end
96
+ end
97
+ config.os = options[Maze::Option::OS]
98
+ config.os_version = options[Maze::Option::OS_VERSION]
99
+ config.sb_local = Maze::Helper.expand_path(options[Maze::Option::SB_LOCAL])
100
+ config.appium_server_url = 'https://us-west-mobile-hub.bitbar.com/wd/hub'
101
+ config.app_bundle_id = options[Maze::Option::APP_BUNDLE_ID]
102
+ when :local then
103
+ if options[Maze::Option::BROWSER]
104
+ config.browser = options[Maze::Option::BROWSER]
105
+ else
106
+ os = config.os = options[Maze::Option::OS].downcase
107
+ config.os_version = options[Maze::Option::OS_VERSION].to_f unless options[Maze::Option::OS_VERSION].nil?
108
+ config.appium_server_url = options[Maze::Option::APPIUM_SERVER]
109
+ config.start_appium = options[Maze::Option::START_APPIUM]
110
+ config.appium_logfile = options[Maze::Option::APPIUM_LOGFILE]
111
+ if os == 'ios'
112
+ config.apple_team_id = options[Maze::Option::APPLE_TEAM_ID]
113
+ config.device_id = options[Maze::Option::UDID]
114
+ end
115
+ end
116
+ when :none
117
+ if options[Maze::Option::OS]
118
+ config.os = options[Maze::Option::OS].downcase
119
+ end
120
+ if options[Maze::Option::OS_VERSION]
121
+ config.os_version = options[Maze::Option::OS_VERSION].to_f
122
+ end
123
+ else
124
+ raise "Unexpected farm option #{config.farm}"
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative '../option'
5
+
6
+ module Maze
7
+ module Option
8
+ # Validates command line options
9
+ class Validator
10
+ # Validates all provided options
11
+ # @param options [Hash] Parsed command line options
12
+ def validate(options)
13
+ errors = []
14
+
15
+ #
16
+ # Common options
17
+ #
18
+
19
+ # --farm
20
+ farm = options[Option::FARM]
21
+ if farm && !%w[bs local bb].include?(farm)
22
+ errors << "--#{Option::FARM} must be 'bs', 'bb' or 'local' if provided"
23
+ end
24
+
25
+ # --capabilities
26
+ begin
27
+ JSON.parse(options[Option::CAPABILITIES])
28
+ rescue JSON::ParserError
29
+ errors << "--#{Option::CAPABILITIES} must be valid JSON (given #{options[Option::CAPABILITIES]})"
30
+ end
31
+
32
+ # --repeater-api-key
33
+ key = options[Option::REPEATER_API_KEY]
34
+ key_regex = /^[0-9a-fA-F]{32}$/
35
+ if key && !key_regex.match?(key)
36
+ errors << "--#{Option::REPEATER_API_KEY} must be set to a 32-character hex value"
37
+ end
38
+
39
+ # Farm specific options
40
+ validate_bs options, errors if farm == 'bs'
41
+ validate_bitbar options, errors if farm == 'bb'
42
+ validate_local options, errors if farm == 'local'
43
+
44
+ errors
45
+ end
46
+
47
+ # Validates BrowserStack options
48
+ def validate_bs(options, errors)
49
+ # BS local binary
50
+ bs_local = Maze::Helper.expand_path options[Option::BS_LOCAL]
51
+ errors << "BrowserStack local binary '#{bs_local}' not found" unless File.exist? bs_local
52
+
53
+ # Device
54
+ browser = options[Option::BROWSER]
55
+ device = options[Option::DEVICE]
56
+ if browser.nil? && device.empty?
57
+ errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
58
+ elsif browser
59
+
60
+ browsers = YAML.safe_load(File.read("#{__dir__}/../client/selenium/bs_browsers.yml"))
61
+
62
+ unless browsers.include? browser
63
+ browser_list = browsers.keys.join ', '
64
+ errors << "Browser type '#{browser}' unknown on BrowserStack. Must be one of: #{browser_list}."
65
+ end
66
+ elsif device
67
+ device.each do |device_key|
68
+ next if Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH.key? device_key
69
+ errors << "Device type '#{device_key}' unknown on BrowserStack. Must be one of #{Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH.keys}"
70
+ end
71
+ # App
72
+ app = Maze::Helper.read_at_arg_file options[Option::APP]
73
+ if app.nil?
74
+ errors << "--#{Option::APP} must be provided when running on a device"
75
+ else
76
+ unless app.start_with?('bs://')
77
+ app = Maze::Helper.expand_path app
78
+ errors << "app file '#{app}' not found" unless File.exist?(app)
79
+ end
80
+ end
81
+ end
82
+
83
+ # Credentials
84
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
85
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
86
+ end
87
+
88
+ # Validates BitBar device options
89
+ def validate_bitbar(options, errors)
90
+ browser = options[Option::BROWSER]
91
+ device = options[Option::DEVICE]
92
+
93
+ if ENV['BUILDKITE'] && browser
94
+ errors << "--#{Option::TMS_URI} must be specified when running on Buildkite" if options[Option::TMS_URI].nil?
95
+ else
96
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
97
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
98
+ end
99
+
100
+ # Device
101
+ if browser.nil? && device.empty?
102
+ errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
103
+ elsif browser
104
+ browsers = YAML.safe_load(File.read("#{__dir__}/../client/selenium/bb_browsers.yml"))
105
+
106
+ unless browsers.include? browser
107
+ browser_list = browsers.keys.join ', '
108
+ errors << "Browser type '#{browser}' unknown on BitBar. Must be one of: #{browser_list}."
109
+ end
110
+ elsif device
111
+ app = options[Option::APP]
112
+ if app.nil?
113
+ errors << "--#{Option::APP} must be provided when running on a device"
114
+ else
115
+ uuid_regex = /\A[0-9]+\z/
116
+ unless uuid_regex.match? app
117
+ app = Maze::Helper.expand_path app
118
+ errors << "app file '#{app}' not found" unless File.exist?(app)
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # Validates Local device options
125
+ def validate_local(options, errors)
126
+ if options[Option::BROWSER].nil?
127
+ errors << "--#{Option::APP} must be specified" if options[Option::APP].nil?
128
+
129
+ # OS
130
+ if options[Option::OS].nil?
131
+ errors << "--#{Option::OS} must be specified"
132
+ else
133
+ os = options[Option::OS].downcase
134
+ errors << 'os must be android, ios, macos or windows' unless %w[android ios macos windows].include? os
135
+ if os == 'ios'
136
+ errors << "--#{Option::APPLE_TEAM_ID} must be specified for iOS" if options[Option::APPLE_TEAM_ID].nil?
137
+ errors << "--#{Option::UDID} must be specified for iOS" if options[Option::UDID].nil?
138
+ end
139
+ end
140
+
141
+ # OS Version
142
+ unless options[Option::OS_VERSION].nil?
143
+ # Ensure OS version is a valid float so that notifier tests can perform numeric checks
144
+ # e.g 'Maze.config.os_version > 7'
145
+ unless /^[1-9][0-9]*(\.[0-9])?/.match? options[Option::OS_VERSION]
146
+ errors << "--#{Option::OS_VERSION} must be a valid version matching '/^[1-9][0-9]*(\\.[0-9])?/'"
147
+ end
148
+ end
149
+ else
150
+ # TODO Validate browser options
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end