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.
- checksums.yaml +4 -4
- data/bin/download-logs +14 -16
- data/bin/maze-runner +53 -15
- data/bin/upload-app +6 -6
- data/lib/features/steps/breadcrumb_steps.rb +44 -14
- data/lib/features/steps/error_reporting_steps.rb +16 -0
- data/lib/features/steps/network_steps.rb +66 -6
- data/lib/features/steps/payload_steps.rb +23 -0
- data/lib/features/steps/request_assertion_steps.rb +87 -8
- data/lib/features/steps/runner_steps.rb +22 -0
- data/lib/features/steps/session_tracking_steps.rb +1 -1
- data/lib/features/steps/trace_steps.rb +206 -0
- data/lib/features/support/internal_hooks.rb +31 -84
- data/lib/maze/api/appium/file_manager.rb +29 -0
- data/lib/maze/aws_public_ip.rb +53 -0
- data/lib/maze/checks/assert_check.rb +9 -31
- data/lib/maze/client/appium/base_client.rb +131 -0
- data/lib/maze/client/appium/bb_client.rb +102 -0
- data/lib/maze/client/appium/bb_devices.rb +127 -0
- data/lib/maze/client/appium/bs_client.rb +91 -0
- data/lib/maze/client/appium/bs_devices.rb +141 -0
- data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
- data/lib/maze/client/appium/local_client.rb +67 -0
- data/lib/maze/client/appium.rb +23 -0
- data/lib/maze/client/bb_api_client.rb +102 -0
- data/lib/maze/client/bb_client_utils.rb +181 -0
- data/lib/maze/client/bs_client_utils.rb +168 -0
- data/lib/maze/client/selenium/base_client.rb +15 -0
- data/lib/maze/client/selenium/bb_browsers.yml +188 -0
- data/lib/maze/client/selenium/bb_client.rb +38 -0
- data/lib/maze/client/selenium/bs_browsers.yml +257 -0
- data/lib/maze/client/selenium/bs_client.rb +89 -0
- data/lib/maze/client/selenium/local_client.rb +16 -0
- data/lib/maze/client/selenium.rb +16 -0
- data/lib/maze/configuration.rb +18 -10
- data/lib/maze/docker.rb +40 -1
- data/lib/maze/driver/appium.rb +5 -24
- data/lib/maze/driver/browser.rb +12 -26
- data/lib/maze/errors.rb +32 -0
- data/lib/maze/generator.rb +55 -0
- data/lib/maze/helper.rb +7 -3
- data/lib/maze/hooks/appium_hooks.rb +29 -190
- data/lib/maze/hooks/browser_hooks.rb +2 -55
- data/lib/maze/hooks/error_code_hook.rb +49 -0
- data/lib/maze/hooks/hooks.rb +2 -2
- data/lib/maze/http_request.rb +21 -0
- data/lib/maze/logger.rb +16 -3
- data/lib/maze/maze_output.rb +88 -0
- data/lib/maze/option/parser.rb +17 -22
- data/lib/maze/option/processor.rb +21 -34
- data/lib/maze/option/validator.rb +38 -67
- data/lib/maze/option.rb +16 -18
- data/lib/maze/plugins/cucumber_report_plugin.rb +1 -1
- data/lib/maze/plugins/error_code_plugin.rb +21 -0
- data/lib/maze/request_list.rb +10 -5
- data/lib/maze/request_repeater.rb +49 -0
- data/lib/maze/retry_handler.rb +4 -13
- data/lib/maze/schemas/OtelTraceSchema.json +390 -0
- data/lib/maze/schemas/trace_schema.rb +7 -0
- data/lib/maze/schemas/trace_validator.rb +98 -0
- data/lib/maze/server.rb +74 -30
- data/lib/maze/servlets/base_servlet.rb +10 -5
- data/lib/maze/servlets/command_servlet.rb +10 -7
- data/lib/maze/servlets/log_servlet.rb +2 -2
- data/lib/maze/servlets/reflective_servlet.rb +12 -11
- data/lib/maze/servlets/servlet.rb +47 -8
- data/lib/maze/servlets/temp.rb +0 -0
- data/lib/maze/servlets/trace_servlet.rb +13 -0
- data/lib/maze.rb +2 -2
- data/lib/utils/deep_merge.rb +17 -0
- data/lib/utils/selenium_money_patch.rb +17 -0
- metadata +101 -21
- data/lib/maze/bitbar_devices.rb +0 -84
- data/lib/maze/bitbar_utils.rb +0 -112
- data/lib/maze/browser_stack_devices.rb +0 -160
- data/lib/maze/browser_stack_utils.rb +0 -164
- data/lib/maze/browsers_bs.yml +0 -220
- data/lib/maze/browsers_cbt.yml +0 -100
- data/lib/maze/capabilities.rb +0 -126
- data/lib/maze/driver/resilient_appium.rb +0 -51
- data/lib/maze/sauce_labs_utils.rb +0 -96
- 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, :
|
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
|
-
#
|
44
|
-
Maze
|
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
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|