bugsnag-maze-runner 6.27.0 → 7.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/bin/download-logs +14 -16
  3. data/bin/maze-runner +53 -15
  4. data/bin/upload-app +6 -6
  5. data/lib/features/steps/breadcrumb_steps.rb +44 -14
  6. data/lib/features/steps/error_reporting_steps.rb +16 -0
  7. data/lib/features/steps/network_steps.rb +66 -6
  8. data/lib/features/steps/payload_steps.rb +23 -0
  9. data/lib/features/steps/request_assertion_steps.rb +87 -8
  10. data/lib/features/steps/runner_steps.rb +22 -0
  11. data/lib/features/steps/session_tracking_steps.rb +1 -1
  12. data/lib/features/steps/trace_steps.rb +206 -0
  13. data/lib/features/support/internal_hooks.rb +31 -84
  14. data/lib/maze/api/appium/file_manager.rb +29 -0
  15. data/lib/maze/aws_public_ip.rb +53 -0
  16. data/lib/maze/checks/assert_check.rb +9 -31
  17. data/lib/maze/client/appium/base_client.rb +131 -0
  18. data/lib/maze/client/appium/bb_client.rb +102 -0
  19. data/lib/maze/client/appium/bb_devices.rb +127 -0
  20. data/lib/maze/client/appium/bs_client.rb +91 -0
  21. data/lib/maze/client/appium/bs_devices.rb +141 -0
  22. data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
  23. data/lib/maze/client/appium/local_client.rb +67 -0
  24. data/lib/maze/client/appium.rb +23 -0
  25. data/lib/maze/client/bb_api_client.rb +102 -0
  26. data/lib/maze/client/bb_client_utils.rb +181 -0
  27. data/lib/maze/client/bs_client_utils.rb +168 -0
  28. data/lib/maze/client/selenium/base_client.rb +15 -0
  29. data/lib/maze/client/selenium/bb_browsers.yml +188 -0
  30. data/lib/maze/client/selenium/bb_client.rb +38 -0
  31. data/lib/maze/client/selenium/bs_browsers.yml +257 -0
  32. data/lib/maze/client/selenium/bs_client.rb +89 -0
  33. data/lib/maze/client/selenium/local_client.rb +16 -0
  34. data/lib/maze/client/selenium.rb +16 -0
  35. data/lib/maze/configuration.rb +18 -10
  36. data/lib/maze/docker.rb +40 -1
  37. data/lib/maze/driver/appium.rb +5 -24
  38. data/lib/maze/driver/browser.rb +12 -26
  39. data/lib/maze/errors.rb +32 -0
  40. data/lib/maze/generator.rb +55 -0
  41. data/lib/maze/helper.rb +7 -3
  42. data/lib/maze/hooks/appium_hooks.rb +29 -190
  43. data/lib/maze/hooks/browser_hooks.rb +2 -55
  44. data/lib/maze/hooks/error_code_hook.rb +49 -0
  45. data/lib/maze/hooks/hooks.rb +2 -2
  46. data/lib/maze/http_request.rb +21 -0
  47. data/lib/maze/logger.rb +16 -3
  48. data/lib/maze/maze_output.rb +88 -0
  49. data/lib/maze/option/parser.rb +17 -22
  50. data/lib/maze/option/processor.rb +21 -34
  51. data/lib/maze/option/validator.rb +38 -67
  52. data/lib/maze/option.rb +16 -18
  53. data/lib/maze/plugins/cucumber_report_plugin.rb +1 -1
  54. data/lib/maze/plugins/error_code_plugin.rb +21 -0
  55. data/lib/maze/request_list.rb +10 -5
  56. data/lib/maze/request_repeater.rb +49 -0
  57. data/lib/maze/retry_handler.rb +4 -13
  58. data/lib/maze/schemas/OtelTraceSchema.json +390 -0
  59. data/lib/maze/schemas/trace_schema.rb +7 -0
  60. data/lib/maze/schemas/trace_validator.rb +98 -0
  61. data/lib/maze/server.rb +74 -30
  62. data/lib/maze/servlets/base_servlet.rb +10 -5
  63. data/lib/maze/servlets/command_servlet.rb +10 -7
  64. data/lib/maze/servlets/log_servlet.rb +2 -2
  65. data/lib/maze/servlets/reflective_servlet.rb +12 -11
  66. data/lib/maze/servlets/servlet.rb +47 -8
  67. data/lib/maze/servlets/temp.rb +0 -0
  68. data/lib/maze/servlets/trace_servlet.rb +13 -0
  69. data/lib/maze.rb +2 -2
  70. data/lib/utils/deep_merge.rb +17 -0
  71. data/lib/utils/selenium_money_patch.rb +17 -0
  72. metadata +101 -21
  73. data/lib/maze/bitbar_devices.rb +0 -84
  74. data/lib/maze/bitbar_utils.rb +0 -112
  75. data/lib/maze/browser_stack_devices.rb +0 -160
  76. data/lib/maze/browser_stack_utils.rb +0 -164
  77. data/lib/maze/browsers_bs.yml +0 -220
  78. data/lib/maze/browsers_cbt.yml +0 -100
  79. data/lib/maze/capabilities.rb +0 -126
  80. data/lib/maze/driver/resilient_appium.rb +0 -51
  81. data/lib/maze/sauce_labs_utils.rb +0 -96
  82. data/lib/maze/smart_bear_utils.rb +0 -71
@@ -0,0 +1,206 @@
1
+ # @!group Trace steps
2
+
3
+ # Waits for a given number of spans to be received, which may be spread across one or more trace requests.
4
+ #
5
+ # @step_input span_count [Integer] The number of spans to wait for
6
+ When('I wait for {int} span(s)') do |span_count|
7
+ assert_received_spans span_count, Maze::Server.list_for('traces')
8
+ end
9
+
10
+ When('I receive and discard the initial p-value request') do
11
+ steps %Q{
12
+ And I wait to receive at least 1 trace
13
+ And the trace payload field "resourceSpans" is an array with 0 elements
14
+ And I discard the oldest trace
15
+ }
16
+ end
17
+
18
+ Then('I should have received no spans') do
19
+ sleep Maze.config.receive_no_requests_wait
20
+ Maze.check.equal spans_from_request_list(Maze::Server.list_for('traces')).size, 0
21
+ end
22
+
23
+ Then('the trace payload field {string} bool attribute {string} is true') do |field, attribute|
24
+ check_attribute_equal field, attribute, 'boolValue', true
25
+ end
26
+
27
+ Then('the trace payload field {string} bool attribute {string} is false') do |field, attribute|
28
+ check_attribute_equal field, attribute, 'boolValue', false
29
+ end
30
+
31
+ Then('the trace payload field {string} integer attribute {string} equals {int}') do |field, attribute, expected|
32
+ check_attribute_equal field, attribute, 'intValue', expected
33
+ end
34
+
35
+ Then('the trace payload field {string} integer attribute {string} is greater than {int}') do |field, attribute, expected|
36
+ value = get_attribute_value field, attribute, 'intValue'
37
+ Maze.check.operator value, :>, expected,
38
+ "The payload field '#{field}' attribute '#{attribute}' (#{value}) is not greater than '#{expected}'"
39
+ end
40
+
41
+ Then('the trace payload field {string} string attribute {string} equals {string}') do |field, attribute, expected|
42
+ check_attribute_equal field, attribute, 'stringValue', expected
43
+ end
44
+
45
+ Then('the trace payload field {string} string attribute {string} equals the stored value {string}') do |field, attribute, stored_key|
46
+ value = get_attribute_value field, attribute, 'stringValue'
47
+ stored = Maze::Store.values[stored_key]
48
+ result = Maze::Compare.value value, stored
49
+ Maze.check.true result.equal?, "Payload value: #{value} does not equal stored value: #{stored}"
50
+ end
51
+
52
+ Then('the trace payload field {string} string attribute {string} matches the regex {string}') do |field, attribute, pattern|
53
+ value = get_attribute_value field, attribute, 'stringValue'
54
+ regex = Regexp.new pattern
55
+ Maze.check.match regex, value
56
+ end
57
+
58
+ Then('the trace payload field {string} integer attribute {string} matches the regex {string}') do |field, attribute, pattern|
59
+ regex = Regexp.new(pattern)
60
+ list = Maze::Server.traces
61
+ attributes = Maze::Helper.read_key_path(list.current[:body], "#{field}.attributes")
62
+ attribute = attributes.find { |a| a['key'] == attribute }
63
+ value = attribute["value"]["intValue"]
64
+ Maze.check.match(regex, value)
65
+ end
66
+
67
+ Then('the trace payload field {string} string attribute {string} exists') do |field, attribute|
68
+ value = get_attribute_value field, attribute, 'stringValue'
69
+ Maze.check.not_nil value
70
+ end
71
+
72
+ Then('the trace payload field {string} string attribute {string} is one of:') do |field, key, possible_values|
73
+ list = Maze::Server.traces
74
+ attributes = Maze::Helper.read_key_path(list.current[:body], "#{field}.attributes")
75
+ attribute = attributes.find { |a| a['key'] == key }
76
+
77
+ possible_attributes = possible_values.raw.flatten.map { |v| { 'key' => key, 'value' => { 'stringValue' => v } } }
78
+ Maze.check.not_nil(attribute, "The attribute #{key} is nil")
79
+ Maze.check.include(possible_attributes, attribute)
80
+ end
81
+
82
+ Then('the trace payload field {string} boolean attribute {string} is true') do |field, key|
83
+ assert_attribute field, key, { 'boolValue' => true }
84
+ end
85
+
86
+ Then('the trace payload field {string} boolean attribute {string} is false') do |field, key|
87
+ assert_attribute field, key, { 'boolValue' => false }
88
+ end
89
+
90
+ # @!group Span steps
91
+ Then('a span {word} equals {string}') do |attribute, expected|
92
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
93
+ selected_attributes = spans.map { |span| span[attribute] }
94
+ Maze.check.includes selected_attributes, expected
95
+ end
96
+
97
+ Then('every span field {string} equals {string}') do |key, expected|
98
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
99
+ selected_keys = spans.map { |span| span[key] == expected }
100
+ Maze.check.not_includes selected_keys, false
101
+ end
102
+
103
+ Then('every span field {string} matches the regex {string}') do |key, pattern|
104
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
105
+ spans.map { |span| Maze.check.match pattern, span[key] }
106
+ end
107
+
108
+ Then('every span string attribute {string} exists') do |attribute|
109
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
110
+ spans.map { |span| Maze.check.not_nil span['attributes'].find { |a| a['key'] == attribute }['value']['stringValue'] }
111
+ end
112
+
113
+ Then('every span string attribute {string} equals {string}') do |attribute, expected|
114
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
115
+ spans.map { |span| Maze.check.equal expected, span['attributes'].find { |a| a['key'] == attribute }['value']['stringValue'] }
116
+ end
117
+
118
+ Then('every span string attribute {string} matches the regex {string}') do |attribute, pattern|
119
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
120
+ spans.map { |span| Maze.check.match pattern, span['attributes'].find { |a| a['key'] == attribute }['value']['stringValue'] }
121
+ end
122
+
123
+ Then('every span integer attribute {string} is greater than {int}') do |attribute, expected|
124
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
125
+ spans.map { |span| Maze::check.true span['attributes'].find { |a| a['key'] == attribute }['value']['intValue'].to_i > expected }
126
+ end
127
+
128
+ Then('every span bool attribute {string} is true') do |attribute|
129
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
130
+ spans.map { |span| Maze::check.true span['attributes'].find { |a| a['key'] == attribute }['value']['boolValue'] }
131
+ end
132
+
133
+ Then('a span string attribute {string} exists') do |attribute|
134
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
135
+ selected_attributes = spans.map { |span| span['attributes'].find { |a| a['key'] == attribute }['value']['stringValue'] }
136
+ Maze.check.false(selected_attributes.empty?)
137
+ end
138
+
139
+ Then('a span string attribute {string} equals {string}') do |attribute, expected|
140
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
141
+ selected_attributes = spans.map { |span| span['attributes'].find { |a| a['key'] == attribute }['value']['stringValue'] }
142
+ Maze.check.includes selected_attributes, expected
143
+ end
144
+
145
+ Then('a span field {string} equals {string}') do |key, expected|
146
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
147
+ selected_keys = spans.map { |span| span[key] }
148
+ Maze.check.includes selected_keys, expected
149
+ end
150
+
151
+ Then('a span field {string} matches the regex {string}') do |attribute, pattern|
152
+ regex = Regexp.new pattern
153
+ spans = spans_from_request_list(Maze::Server.list_for('traces'))
154
+ selected_attributes = spans.select { |span| regex.match? span[attribute] }
155
+
156
+ Maze.check.false(selected_attributes.empty?)
157
+ end
158
+
159
+ def spans_from_request_list list
160
+ return list.remaining
161
+ .flat_map { |req| req[:body]['resourceSpans'] }
162
+ .flat_map { |r| r['scopeSpans'] }
163
+ .flat_map { |s| s['spans'] }
164
+ .select { |s| !s.nil? }
165
+ end
166
+
167
+ def assert_received_spans(span_count, list)
168
+ timeout = Maze.config.receive_requests_wait
169
+ wait = Maze::Wait.new(timeout: timeout)
170
+
171
+ received = wait.until { spans_from_request_list(list).size >= span_count }
172
+ received_count = spans_from_request_list(list).size
173
+
174
+ unless received
175
+ raise Test::Unit::AssertionFailedError.new <<-MESSAGE
176
+ Expected #{span_count} spans but received #{received_count} within the #{timeout}s timeout.
177
+ This could indicate that:
178
+ - Bugsnag crashed with a fatal error.
179
+ - Bugsnag did not make the requests that it should have done.
180
+ - The requests were made, but not deemed to be valid (e.g. missing integrity header).
181
+ - The requests made were prevented from being received due to a network or other infrastructure issue.
182
+ Please check the Maze Runner and device logs to confirm.)
183
+ MESSAGE
184
+ end
185
+
186
+ Maze.check.operator(span_count, :<=, received_count, "#{received_count} spans received")
187
+ end
188
+
189
+ def get_attribute_value(field, attribute, attr_type)
190
+ list = Maze::Server.list_for 'trace'
191
+ attributes = Maze::Helper.read_key_path list.current[:body], "#{field}.attributes"
192
+ attribute = attributes.find { |a| a['key'] == attribute }
193
+ value = attribute&.dig 'value', attr_type
194
+ attr_type == 'intValue' && value.is_a?(String) ? value.to_i : value
195
+ end
196
+
197
+ def check_attribute_equal(field, attribute, attr_type, expected)
198
+ value = get_attribute_value field, attribute, attr_type
199
+ Maze.check.equal value, expected
200
+ end
201
+
202
+ def assert_attribute(field, key, expected)
203
+ list = Maze::Server.traces
204
+ attributes = Maze::Helper.read_key_path(list.current[:body], "#{field}.attributes")
205
+ Maze.check.equal attributes.find { |a| a['key'] == key }, { 'key' => key, 'value' => expected }
206
+ end
@@ -16,7 +16,7 @@ BeforeAll do
16
16
  # - Browser (Selenium with local or remote browsers)
17
17
  # - Command (the software under test is invoked with a system call)
18
18
  # TODO Consider making this a specific command line option defaulting to Appium
19
- is_appium = [:bs, :sl, :bb, :local].include?(Maze.config.farm) && !Maze.config.app.nil?
19
+ is_appium = [:bs, :bb, :local].include?(Maze.config.farm) && !Maze.config.app.nil?
20
20
  is_browser = !Maze.config.browser.nil?
21
21
  if is_appium
22
22
  Maze.mode = :appium
@@ -40,17 +40,31 @@ BeforeAll do
40
40
  # Record the local server starting time
41
41
  Maze.start_time = Time.now.strftime('%Y-%m-%d %H:%M:%S')
42
42
 
43
- # Start document server, if asked for
44
- Maze::DocumentServer.start unless Maze.config.document_server_root.nil?
43
+ # Give each run of the tool a unique id
44
+ Maze.run_uuid = SecureRandom.uuid
45
+ $logger.info "UUID for this run: #{Maze.run_uuid}"
46
+
47
+ # Determine public IP if enabled
48
+ if Maze.config.aws_public_ip
49
+ Maze.public_address = Maze::AwsPublicIp.new.address
50
+ $logger.info "Public address: #{Maze.public_address}"
51
+ end
45
52
 
46
53
  # Start mock server
47
54
  Maze::Server.start
55
+ Maze::Server.set_response_delay_generator(Maze::Generator.new [Maze::Server::DEFAULT_RESPONSE_DELAY].cycle)
56
+ Maze::Server.set_sampling_probability_generator(Maze::Generator.new [Maze::Server::DEFAULT_SAMPLING_PROBABILITY].cycle)
57
+ Maze::Server.set_status_code_generator(Maze::Generator.new [Maze::Server::DEFAULT_STATUS_CODE].cycle)
48
58
 
49
59
  # Invoke the internal hook for the mode of operation
50
60
  Maze.internal_hooks.before_all
51
61
 
52
62
  # Call any blocks registered by the client
53
63
  Maze.hooks.call_before_all
64
+
65
+ # Start document server, if asked for
66
+ # This must happen after any client hooks have run, so that they can set the server root
67
+ Maze::DocumentServer.start unless Maze.config.document_server_root.nil?
54
68
  end
55
69
 
56
70
  # @param config The Cucumber config
@@ -58,6 +72,12 @@ InstallPlugin do |config|
58
72
  # Start Bugsnag
59
73
  Maze::BugsnagConfig.start_bugsnag(config)
60
74
 
75
+ if config.fail_fast?
76
+ # Register exit code handler
77
+ Maze::Hooks::ErrorCodeHook.register_exit_code_hook
78
+ config.filters << Maze::Plugins::ErrorCodePlugin.new(config)
79
+ end
80
+
61
81
  # Only add the retry plugin if --retry is not used on the command line
62
82
  config.filters << Maze::Plugins::GlobalRetryPlugin.new(config) if config.options[:retry].zero?
63
83
  config.filters << Maze::Plugins::BugsnagReportingPlugin.new(config)
@@ -76,7 +96,7 @@ Before do |scenario|
76
96
  end
77
97
 
78
98
  # Invoke the internal hook for the mode of operation
79
- Maze.internal_hooks.before
99
+ Maze.internal_hooks.before scenario
80
100
 
81
101
  # Call any blocks registered by the client
82
102
  Maze.hooks.call_before scenario
@@ -92,14 +112,6 @@ After do |scenario|
92
112
  # Call any blocks registered by the client
93
113
  Maze.hooks.call_after scenario
94
114
 
95
- # Make sure we reset to HTTP 200 return status after each scenario
96
- Maze::Server.status_code = 200
97
- Maze::Server.reset_status_code = false
98
-
99
- # Similarly for the response delay
100
- Maze::Server.response_delay_ms = 0
101
- Maze::Server.reset_response_delay = false
102
-
103
115
  # Stop document server if started by the Cucumber step
104
116
  Maze::DocumentServer.manual_stop
105
117
 
@@ -119,7 +131,7 @@ After do |scenario|
119
131
 
120
132
  Maze::Proxy.instance.stop
121
133
 
122
- # Log unprocessed requests on Buildkite if the scenario fails
134
+ # Log all received requests to the console if the scenario fails and/or config says to
123
135
  if (scenario.failed? && Maze.config.log_requests) || Maze.config.always_log
124
136
  $stdout.puts '^^^ +++'
125
137
  output_received_requests('errors')
@@ -130,21 +142,15 @@ After do |scenario|
130
142
  end
131
143
 
132
144
  # Log all received requests to file
133
- write_requests(scenario) if Maze.config.file_log
145
+ Maze::MazeOutput.new(scenario).write_requests if Maze.config.file_log
134
146
 
135
147
  # Invoke the internal hook for the mode of operation
136
- Maze.internal_hooks.after
148
+ Maze.internal_hooks.after scenario
137
149
 
138
150
  ensure
139
151
  # Request arrays in particular are cleared here, rather than in the Before hook, to allow requests to be registered
140
152
  # when a test fixture starts (which can be before the first Before scenario hook fires).
141
- Maze::Server.errors.clear
142
- Maze::Server.sessions.clear
143
- Maze::Server.builds.clear
144
- Maze::Server.uploads.clear
145
- Maze::Server.sourcemaps.clear
146
- Maze::Server.logs.clear
147
- Maze::Server.invalid_requests.clear
153
+ Maze::Server.reset!
148
154
  Maze::Runner.environment.clear
149
155
  Maze::Store.values.clear
150
156
  Maze::Aws::Sam.reset!
@@ -152,10 +158,10 @@ end
152
158
 
153
159
  def output_received_requests(request_type)
154
160
  request_queue = Maze::Server.list_for(request_type)
155
- if request_queue.empty?
161
+ count = request_queue.size_all
162
+ if count == 0
156
163
  $logger.info "No #{request_type} received"
157
164
  else
158
- count = request_queue.size_all
159
165
  $logger.info "#{count} #{request_type} were received:"
160
166
  request_queue.all.each.with_index(1) do |request, number|
161
167
  $stdout.puts "--- #{request_type} #{number} of #{count}"
@@ -164,70 +170,11 @@ def output_received_requests(request_type)
164
170
  end
165
171
  end
166
172
 
167
- # Writes each list of requests to a separate file under, e.g:
168
- # maze_output/failed/scenario_name/errors.log
169
- def write_requests(scenario)
170
- folder1 = File.join(Dir.pwd, 'maze_output')
171
- folder2 = scenario.failed? ? 'failed' : 'passed'
172
- folder3 = Maze::Helper.to_friendly_filename(scenario.name)
173
-
174
- path = File.join(folder1, folder2, folder3)
175
-
176
- FileUtils.makedirs(path)
177
-
178
- request_types = %w[errors sessions builds uploads logs sourcemaps invalid]
179
-
180
- request_types.each do |request_type|
181
- list = Maze::Server.list_for(request_type).all
182
- next if list.empty?
183
-
184
- filename = "#{request_type}.log"
185
- filepath = File.join(path, filename)
186
-
187
- counter = 1
188
- File.open(filepath, 'w+') do |file|
189
- list.each do |request|
190
- file.puts "=== Request #{counter} of #{list.size} ==="
191
- if request[:invalid]
192
- invalid_request = true
193
- uri = request[:request][:request_uri]
194
- headers = request[:request][:header]
195
- body = request[:request][:body]
196
- else
197
- invalid_request = false
198
- uri = request[:request].request_uri
199
- headers = request[:request].header
200
- body = request[:body]
201
- end
202
- file.puts "URI: #{uri}"
203
- file.puts "HEADERS:"
204
- headers.each do |key, values|
205
- file.puts " #{key}: #{values.map {|v| "'#{v}'"}.join(' ')}"
206
- end
207
- file.puts
208
- file.puts "BODY:"
209
- if !invalid_request && headers["content-type"].first == 'application/json'
210
- file.puts JSON.pretty_generate(body)
211
- else
212
- file.puts body
213
- end
214
- file.puts
215
- if request.include?(:reason)
216
- file.puts "REASON:"
217
- file.puts request[:reason]
218
- file.puts
219
- end
220
- counter += 1
221
- end
222
- end
223
- end
224
- end
225
-
226
173
  # Check for invalid requests after each scenario. This is its own hook as failing a scenario raises an exception
227
174
  # and we need the logic in the other After hook to be performed.
228
175
  # Furthermore, this hook should appear after the general hook as they are executed in reverse order by Cucumber.
229
176
  After do |scenario|
230
- unless Maze::Server.invalid_requests.empty?
177
+ unless Maze::Server.invalid_requests.size_all == 0
231
178
  msg = "#{Maze::Server.invalid_requests.size_all} invalid request(s) received during scenario"
232
179
  scenario.fail msg
233
180
  end
@@ -0,0 +1,29 @@
1
+ module Maze
2
+ module Api
3
+ module Appium
4
+ # 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
+
11
+ # Creates a file with the given contents on the device (using Appium). The file will be located in the app's
12
+ # Documents directory for iOS and /sdcard/Android/data/<app-id>/ for Android.
13
+ # @param contents [String] Content of the file to be written
14
+ # @param filename [String] Name (with no path) of the file to be written on the device
15
+ def write_app_file(contents, filename)
16
+ path = case Maze::Helper.get_current_platform
17
+ when 'ios'
18
+ "@#{@driver.app_id}/Documents/#{filename}"
19
+ when 'android'
20
+ "/sdcard/Android/data/#{@driver.app_id}/files/#{filename}"
21
+ end
22
+
23
+ $logger.debug "Pushing file to '#{path}' with contents: #{contents}"
24
+ @driver.push_file(path, contents)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ module Maze
2
+ # Determines the public IP address and port when running on Buildkite with the Elastic CI Stack for AWS
3
+ class AwsPublicIp
4
+ attr_reader :address
5
+
6
+ def initialize
7
+ # This class is only relevant on Buildkite
8
+ return unless ENV['BUILDKITE']
9
+
10
+ ip = determine_public_ip
11
+ port = determine_public_port
12
+
13
+ @address = "#{ip}:#{port}"
14
+ end
15
+
16
+ # Determines the public IP address of the running AWS instance
17
+ def determine_public_ip
18
+ # 169.254.169.254 is the address of the AWS instance metadata service
19
+ # See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
20
+ `curl --silent -XGET http://169.254.169.254/latest/meta-data/public-ipv4`
21
+ end
22
+
23
+ # Determines the external port of the running Docker container that's associated with the port of the mock server
24
+ def determine_public_port
25
+ port = 0
26
+ count = 0
27
+ max_attempts = 30
28
+
29
+ # Give up after 30 seconds
30
+ while port == 0 && count < max_attempts do
31
+ hostname = ENV['HOSTNAME']
32
+ command = "curl --silent -XGET --unix-socket /var/run/docker.sock http://localhost/containers/#{hostname}/json"
33
+ result = Maze::Runner.run_command(command)
34
+ if result[1] == 0
35
+ begin
36
+ json_string = result[0][0].strip
37
+ json_result = JSON.parse(json_string)
38
+ port = json_result['NetworkSettings']['Ports']["#{Maze.config.port}/tcp"][0]['HostPort']
39
+ rescue StandardError
40
+ $logger.error "Unable to parse public port from: #{json_string}"
41
+ return 0
42
+ end
43
+ end
44
+
45
+ count += 1
46
+ sleep 1 if port == 0 && count < max_attempts
47
+ end
48
+ $logger.error "Failed to determine public port within #{max_attempts} attempts" if port == 0 && count == max_attempts
49
+
50
+ port
51
+ end
52
+ end
53
+ end
@@ -24,7 +24,15 @@ module Maze
24
24
  end
25
25
 
26
26
  def match(pattern, string, message = nil)
27
- assert_match(pattern, string, message)
27
+ regexp = if pattern.class == Regexp
28
+ pattern
29
+ else
30
+ Regexp.new(pattern)
31
+ end
32
+ if message.nil?
33
+ message = "<#{string}> was not matched by regex <#{pattern}>"
34
+ end
35
+ assert_match(regexp, string, message)
28
36
  end
29
37
 
30
38
  def equal(expected, act, message = nil)
@@ -59,33 +67,3 @@ module Maze
59
67
  end
60
68
  end
61
69
  end
62
-
63
- # Wrapper for Maze.check.true to avoid making a breaking change.
64
- # @deprecated TODO Remove in v7
65
- def assert_true(value, message = nil)
66
- Maze.check.true value, message
67
- end
68
-
69
- # Wrapper for Maze.check.false to avoid making a breaking change.
70
- # @deprecated TODO Remove in v7
71
- def assert_false(value, message = nil)
72
- Maze.check.false value, message
73
- end
74
-
75
- # Wrapper for Maze.check.not_nil to avoid making a breaking change.
76
- # @deprecated TODO Remove in v7
77
- def assert_not_nil(value, message = nil)
78
- Maze.check.not_nil value, message
79
- end
80
-
81
- # Wrapper for Maze.check.not_nil to avoid making a breaking change.
82
- # @deprecated TODO Remove in v7
83
- def assert_block(message = 'block failed', &block)
84
- Maze.check.block(message, &block)
85
- end
86
-
87
- # Wrapper for Maze.check.true to avoid making a breaking change.
88
- # @deprecated TODO Remove in v7
89
- def assert_equal(value, message = nil)
90
- Maze.check.equal value, message
91
- end
@@ -0,0 +1,131 @@
1
+ require 'json'
2
+
3
+ module Maze
4
+ module Client
5
+ module Appium
6
+ class BaseClient
7
+ FIXTURE_CONFIG = 'fixture_config.json'
8
+
9
+ def initialize
10
+ @session_ids = []
11
+ end
12
+
13
+ def start_session
14
+ prepare_session
15
+
16
+ start_driver(Maze.config)
17
+
18
+ # Set bundle/app id for later use
19
+ Maze.driver.app_id = case Maze::Helper.get_current_platform
20
+ when 'android'
21
+ Maze.driver.session_capabilities['appPackage']
22
+ when 'ios'
23
+ Maze.driver.session_capabilities['CFBundleIdentifier'] # Present on BS and locally
24
+ end
25
+ # Ensure the device is unlocked
26
+ Maze.driver.unlock
27
+
28
+ log_run_intro
29
+ end
30
+
31
+ def prepare_session
32
+ raise 'Method not implemented by this class'
33
+ end
34
+
35
+ def maze_address
36
+ raise 'Method not implemented by this class'
37
+ end
38
+
39
+ def start_driver(config)
40
+ retry_failure = config.device_list.nil? || config.device_list.empty?
41
+ driver = nil
42
+ until Maze.driver
43
+ begin
44
+ start_driver_closure = Proc.new do
45
+ begin
46
+ config.capabilities = device_capabilities
47
+ driver = Maze::Driver::Appium.new config.appium_server_url,
48
+ config.capabilities,
49
+ config.locator
50
+
51
+ result = driver.start_driver
52
+ if result
53
+ # Log details of this session
54
+ $logger.info "Created Appium session: #{driver.session_id}"
55
+ @session_ids << driver.session_id
56
+ udid = driver.session_capabilities['udid']
57
+ $logger.info "Running on device: #{udid}" unless udid.nil?
58
+ end
59
+ result
60
+ rescue => start_error
61
+ $logger.error "Session creation failed: #{start_error}"
62
+ raise start_error unless retry_failure
63
+ false
64
+ end
65
+ end
66
+
67
+ if retry_failure
68
+ wait = Maze::Wait.new(interval: 10, timeout: 60)
69
+ success = wait.until(&start_driver_closure)
70
+
71
+ unless success
72
+ $logger.error 'Appium driver failed to start after 6 attempts in 60 seconds'
73
+ raise RuntimeError.new('Appium driver failed to start in 60 seconds')
74
+ end
75
+ else
76
+ start_driver_closure.call
77
+ end
78
+
79
+ # Infer OS version if necessary when running locally
80
+ if Maze.config.farm == :local && Maze.config.os_version.nil?
81
+ version = case Maze.config.os
82
+ when 'android'
83
+ driver.session_capabilities['platformVersion'].to_f
84
+ when 'ios'
85
+ driver.session_capabilities['sdkVersion'].to_f
86
+ end
87
+ $logger.info "Inferred OS version to be #{version}"
88
+ Maze.config.os_version = version
89
+ end
90
+
91
+ Maze.driver = driver
92
+ rescue ::Selenium::WebDriver::Error::UnknownError => original_exception
93
+ $logger.warn "Attempt to acquire #{config.device} device from farm #{config.farm} failed"
94
+ $logger.warn "Exception: #{original_exception.message}"
95
+ if config.device_list.empty?
96
+ $logger.error 'No further devices to try - raising original exception'
97
+ raise original_exception
98
+ else
99
+ config.device = config.device_list.first
100
+ config.device_list = config.device_list.drop(1)
101
+ $logger.warn "Retrying driver initialisation using next device: #{config.device}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def start_scenario
108
+ # Launch the app on macOS
109
+ Maze.driver.get(Maze.config.app) if Maze.config.os == 'macos'
110
+ end
111
+
112
+ def device_capabilities
113
+ raise 'Method not implemented by this class'
114
+ end
115
+
116
+ def log_run_intro
117
+ raise 'Method not implemented by this class'
118
+ end
119
+
120
+ def log_run_outro
121
+ raise 'Method not implemented by this class'
122
+ end
123
+
124
+ def stop_session
125
+ Maze.driver&.driver_quit
126
+ Maze::AppiumServer.stop if Maze::AppiumServer.running
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end