bugsnag-maze-runner 7.22.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/bugsnag-print-load-paths +6 -0
- data/bin/download-logs +74 -0
- data/bin/maze-runner +174 -0
- data/bin/upload-app +56 -0
- data/lib/features/scripts/await-android-emulator.sh +11 -0
- data/lib/features/scripts/clear-android-app-data.sh +8 -0
- data/lib/features/scripts/force-stop-android-app.sh +8 -0
- data/lib/features/scripts/install-android-app.sh +15 -0
- data/lib/features/scripts/launch-android-app.sh +38 -0
- data/lib/features/scripts/launch-android-emulator.sh +15 -0
- data/lib/features/steps/android_steps.rb +51 -0
- data/lib/features/steps/app_automator_steps.rb +228 -0
- data/lib/features/steps/aws_sam_steps.rb +212 -0
- data/lib/features/steps/breadcrumb_steps.rb +80 -0
- data/lib/features/steps/browser_steps.rb +93 -0
- data/lib/features/steps/build_api_steps.rb +25 -0
- data/lib/features/steps/document_server_steps.rb +7 -0
- data/lib/features/steps/error_reporting_steps.rb +358 -0
- data/lib/features/steps/feature_flag_steps.rb +190 -0
- data/lib/features/steps/header_steps.rb +72 -0
- data/lib/features/steps/log_steps.rb +29 -0
- data/lib/features/steps/multipart_request_steps.rb +142 -0
- data/lib/features/steps/network_steps.rb +135 -0
- data/lib/features/steps/payload_steps.rb +257 -0
- data/lib/features/steps/proxy_steps.rb +34 -0
- data/lib/features/steps/query_parameter_steps.rb +31 -0
- data/lib/features/steps/request_assertion_steps.rb +186 -0
- data/lib/features/steps/runner_steps.rb +428 -0
- data/lib/features/steps/session_tracking_steps.rb +116 -0
- data/lib/features/steps/trace_steps.rb +206 -0
- data/lib/features/steps/value_steps.rb +119 -0
- data/lib/features/support/env.rb +7 -0
- data/lib/features/support/internal_hooks.rb +207 -0
- data/lib/maze/api/appium/file_manager.rb +29 -0
- data/lib/maze/appium_server.rb +112 -0
- data/lib/maze/assertions/request_set_assertions.rb +97 -0
- data/lib/maze/aws/sam.rb +112 -0
- data/lib/maze/aws_public_ip.rb +53 -0
- data/lib/maze/bugsnag_config.rb +42 -0
- data/lib/maze/checks/assert_check.rb +69 -0
- data/lib/maze/checks/noop_check.rb +34 -0
- 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/compare.rb +161 -0
- data/lib/maze/configuration.rb +182 -0
- data/lib/maze/docker.rb +147 -0
- data/lib/maze/document_server.rb +46 -0
- data/lib/maze/driver/appium.rb +198 -0
- data/lib/maze/driver/browser.rb +124 -0
- data/lib/maze/errors.rb +52 -0
- data/lib/maze/generator.rb +55 -0
- data/lib/maze/helper.rb +122 -0
- data/lib/maze/hooks/appium_hooks.rb +55 -0
- data/lib/maze/hooks/browser_hooks.rb +15 -0
- data/lib/maze/hooks/command_hooks.rb +9 -0
- data/lib/maze/hooks/error_code_hook.rb +49 -0
- data/lib/maze/hooks/hooks.rb +61 -0
- data/lib/maze/http_request.rb +21 -0
- data/lib/maze/interactive_cli.rb +173 -0
- data/lib/maze/logger.rb +86 -0
- data/lib/maze/macos_utils.rb +14 -0
- data/lib/maze/maze_output.rb +88 -0
- data/lib/maze/network.rb +49 -0
- data/lib/maze/option/parser.rb +240 -0
- data/lib/maze/option/processor.rb +130 -0
- data/lib/maze/option/validator.rb +155 -0
- data/lib/maze/option.rb +62 -0
- data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
- data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
- data/lib/maze/plugins/error_code_plugin.rb +21 -0
- data/lib/maze/plugins/global_retry_plugin.rb +38 -0
- data/lib/maze/proxy.rb +114 -0
- data/lib/maze/request_list.rb +87 -0
- data/lib/maze/request_repeater.rb +49 -0
- data/lib/maze/retry_handler.rb +67 -0
- data/lib/maze/runner.rb +149 -0
- 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 +251 -0
- data/lib/maze/servlets/base_servlet.rb +27 -0
- data/lib/maze/servlets/command_servlet.rb +47 -0
- data/lib/maze/servlets/log_servlet.rb +64 -0
- data/lib/maze/servlets/reflective_servlet.rb +70 -0
- data/lib/maze/servlets/servlet.rb +199 -0
- data/lib/maze/servlets/temp.rb +0 -0
- data/lib/maze/servlets/trace_servlet.rb +13 -0
- data/lib/maze/store.rb +15 -0
- data/lib/maze/terminating_server.rb +129 -0
- data/lib/maze/timers.rb +51 -0
- data/lib/maze/wait.rb +35 -0
- data/lib/maze.rb +27 -0
- data/lib/utils/deep_merge.rb +17 -0
- data/lib/utils/selenium_money_patch.rb +17 -0
- metadata +451 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
# @!group Session tracking steps
|
2
|
+
|
3
|
+
# Verifies that generic elements of a session payload are present.
|
4
|
+
# APIKey fields and headers are tested against the '$api_key' global variable.
|
5
|
+
#
|
6
|
+
# @step_input payload_version [String] The payload version expected
|
7
|
+
# @step_input notifier_name [String] The expected name of the notifier
|
8
|
+
Then('the session is valid for the session reporting API version {string} for the {string} notifier') \
|
9
|
+
do |payload_version, notifier_name|
|
10
|
+
steps %(
|
11
|
+
Then the session "bugsnag-api-key" header equals "#{$api_key}"
|
12
|
+
And the session "bugsnag-payload-version" header equals "#{payload_version}"
|
13
|
+
And the session "Content-Type" header equals "application/json"
|
14
|
+
And the session "Bugsnag-Sent-At" header is a timestamp
|
15
|
+
|
16
|
+
And the session payload field "notifier.name" equals "#{notifier_name}"
|
17
|
+
And the session payload field "notifier.url" is not null
|
18
|
+
And the session payload field "notifier.version" is not null
|
19
|
+
|
20
|
+
And the session payload field "app" is not null
|
21
|
+
And the session payload field "device" is not null
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Verifies that generic elements of a session payload are present for the React Native notifier
|
26
|
+
# APIKey fields and headers are tested against the '$api_key' global variable.
|
27
|
+
#
|
28
|
+
# @step_input payload_version [String] The payload version expected
|
29
|
+
# @step_input notifier_name [String] The expected name of the notifier
|
30
|
+
# TODO: I'm reluctant to risk changing the previous step implementation right now, but we should consider
|
31
|
+
# refactoring the two at some point to avoid duplication.
|
32
|
+
Then('the session is valid for the session reporting API version {string} for the React Native notifier') do |payload_version|
|
33
|
+
steps %{
|
34
|
+
Then the session "bugsnag-api-key" header equals "#{$api_key}"
|
35
|
+
And the session "bugsnag-payload-version" header equals "#{payload_version}"
|
36
|
+
And the session "Content-Type" header equals "application/json"
|
37
|
+
And the session "Bugsnag-Sent-At" header is a timestamp
|
38
|
+
|
39
|
+
And the session payload field "notifier.name" matches the regex "(Bugsnag React Native|(Android|iOS) Bugsnag Notifier)"
|
40
|
+
And the session payload field "notifier.url" is not null
|
41
|
+
And the session payload field "notifier.version" is not null
|
42
|
+
|
43
|
+
And the session payload field "app" is not null
|
44
|
+
And the session payload field "device" is not null
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Tests whether a value in the first session entry matches a literal.
|
49
|
+
#
|
50
|
+
# @step_input field [String] The relative location of the value to test
|
51
|
+
# @step_input literal [Enum] The literal to test against, one of: true, false, null, not null
|
52
|
+
Then(/^the session "(.+)" is (true|false|null|not null)$/) do |field, literal|
|
53
|
+
step "the session payload field \"sessions.0.#{field}\" is #{literal}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Tests whether a value in the first session entry matches a string.
|
57
|
+
#
|
58
|
+
# @step_input field [String] The relative location of the value to test
|
59
|
+
# @step_input string_value [String] The string to match against
|
60
|
+
Then('the session {string} equals {string}') do |field, string_value|
|
61
|
+
step "the session payload field \"sessions.0.#{field}\" equals \"#{string_value}\""
|
62
|
+
end
|
63
|
+
|
64
|
+
# Tests whether a value in the first session entry is a timestamp.
|
65
|
+
#
|
66
|
+
# @step_input field [String] The relative location of the value to test
|
67
|
+
Then('the session {string} is a timestamp') do |field|
|
68
|
+
step "the session payload field \"sessions.0.#{field}\" matches the regex \"#{TIMESTAMP_REGEX}\""
|
69
|
+
end
|
70
|
+
|
71
|
+
# Tests whether a value in the first sessionCount entry matches a literal.
|
72
|
+
#
|
73
|
+
# @step_input field [String] The relative location of the value to test
|
74
|
+
# @step_input literal [Enum] The literal to test against, one of: true, false, null, not null
|
75
|
+
Then(/^the sessionCount "(.+)" is (true|false|null|not null)$/) do |field, literal|
|
76
|
+
step "the session payload field \"sessionCounts.0.#{field}\" is #{literal}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Tests whether a value in the first sessionCount entry matches a string.
|
80
|
+
#
|
81
|
+
# @step_input field [String] The relative location of the value to test
|
82
|
+
# @step_input string_value [String] The string to match against
|
83
|
+
Then('the sessionCount {string} equals {string}') do |field, string_value|
|
84
|
+
step "the session payload field \"sessionCounts.0.#{field}\" equals \"#{string_value}\""
|
85
|
+
end
|
86
|
+
|
87
|
+
# Tests whether a value in the first sessionCount entry equals an integer.
|
88
|
+
#
|
89
|
+
# @step_input field [String] The relative location of the value to test
|
90
|
+
# @step_input int_value [Integer] The integer to test against
|
91
|
+
Then('the sessionCount {string} equals {int}') do |field, int_value|
|
92
|
+
step "the session payload field \"sessionCounts.0.#{field}\" equals #{int_value}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Tests whether a value in the first sessionCount entry is a timestamp.
|
96
|
+
#
|
97
|
+
# @step_input field [String] The relative location of the value to test
|
98
|
+
Then('the sessionCount {string} is a timestamp') do |field|
|
99
|
+
step "the session payload field \"sessionCounts.0.#{field}\" matches the regex \"#{TIMESTAMP_REGEX}\""
|
100
|
+
end
|
101
|
+
|
102
|
+
# Tests that a payload has an appropriately structured session array
|
103
|
+
Then('the session payload has a valid sessions array') do
|
104
|
+
if sessions = Maze::Server.sessions.current[:body]['sessions']
|
105
|
+
steps %(
|
106
|
+
Then the session "id" is not null
|
107
|
+
And the session "startedAt" is a timestamp
|
108
|
+
)
|
109
|
+
else
|
110
|
+
steps %(
|
111
|
+
Then the sessionCount "sessionsStarted" is not null
|
112
|
+
And the sessionCount "startedAt" is a timestamp
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -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
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
# @!group Value steps
|
4
|
+
|
5
|
+
# Stores a payload value against a key for cross-request comparisons.
|
6
|
+
#
|
7
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
8
|
+
# @step_input field [String] The payload field to store
|
9
|
+
# @step_input key [String] The key to store the value against
|
10
|
+
Then('the {word} payload field {string} is stored as the value {string}') do |request_type, field, key|
|
11
|
+
list = Maze::Server.list_for request_type
|
12
|
+
value = Maze::Helper.read_key_path(list.current[:body], field)
|
13
|
+
Maze::Store.values[key] = value.dup
|
14
|
+
end
|
15
|
+
|
16
|
+
# Tests whether a payload field matches a previously stored payload value
|
17
|
+
#
|
18
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
19
|
+
# @step_input field [String] The payload field to test
|
20
|
+
# @step_input key [String] The key indicating a previously stored value
|
21
|
+
Then('the {word} payload field {string} equals the stored value {string}') do |request_type, field, key|
|
22
|
+
list = Maze::Server.list_for request_type
|
23
|
+
payload_value = Maze::Helper.read_key_path(list.current[:body], field)
|
24
|
+
stored_value = Maze::Store.values[key]
|
25
|
+
result = Maze::Compare.value(payload_value, stored_value)
|
26
|
+
Maze.check.true(result.equal?, "Payload value: #{payload_value} does not equal stored value: #{stored_value}")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Tests whether a payload field is distinct from a previously stored payload value
|
30
|
+
#
|
31
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
32
|
+
# @step_input field [String] The payload field to test
|
33
|
+
# @step_input key [String] The key indicating a previously stored value
|
34
|
+
Then('the {word} payload field {string} does not equal the stored value {string}') do |request_type, field, key|
|
35
|
+
list = Maze::Server.list_for request_type
|
36
|
+
payload_value = Maze::Helper.read_key_path(list.current[:body], field)
|
37
|
+
stored_value = Maze::Store.values[key]
|
38
|
+
result = Maze::Compare.value(payload_value, stored_value)
|
39
|
+
Maze.check.false(result.equal?, "Payload value: #{payload_value} equals stored value: #{stored_value}")
|
40
|
+
end
|
41
|
+
|
42
|
+
# Tests whether a payload field matches a previously stored payload value, ignoring case
|
43
|
+
#
|
44
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
45
|
+
# @step_input field [String] The payload field to test
|
46
|
+
# @step_input key [String] The key indicating a previously stored value
|
47
|
+
Then('the {word} payload field {string} equals the stored value {string} ignoring case') do |request_type, field, key|
|
48
|
+
list = Maze::Server.list_for request_type
|
49
|
+
payload_value = Maze::Helper.read_key_path(list.current[:body], field)
|
50
|
+
stored_value = Maze::Store.values[key]
|
51
|
+
payload_value.downcase!
|
52
|
+
stored_value.downcase!
|
53
|
+
result = Maze::Compare.value(payload_value, stored_value)
|
54
|
+
Maze.check.true(result.equal?, "Payload value: #{payload_value} does not equal stored value: #{stored_value}")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Tests whether a payload field is distinct from a previously stored payload value, ignoring case
|
58
|
+
#
|
59
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
60
|
+
# @step_input field [String] The payload field to test
|
61
|
+
# @step_input key [String] The key indicating a previously stored value
|
62
|
+
Then('the {word} payload field {string} does not equal the stored value {string} ignoring case') do |request_type, field, key|
|
63
|
+
list = Maze::Server.list_for request_type
|
64
|
+
payload_value = Maze::Helper.read_key_path(list.current[:body], field)
|
65
|
+
stored_value = Maze::Store.values[key]
|
66
|
+
payload_value.downcase!
|
67
|
+
stored_value.downcase!
|
68
|
+
result = Maze::Compare.value(payload_value, stored_value)
|
69
|
+
Maze.check.false(result.equal?, "Payload value: #{payload_value} equals stored value: #{stored_value}")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Tests whether a payload field is a number (Numeric according to Ruby)
|
73
|
+
#
|
74
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
75
|
+
# @step_input field [String] The payload field to test
|
76
|
+
Then('the {word} payload field {string} is a number') do |request_type, field|
|
77
|
+
list = Maze::Server.list_for request_type
|
78
|
+
value = Maze::Helper.read_key_path(list.current[:body], field)
|
79
|
+
Maze.check.kind_of Numeric, value
|
80
|
+
end
|
81
|
+
|
82
|
+
# Tests whether a payload field is an integer (Integer according to Ruby)
|
83
|
+
#
|
84
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
85
|
+
# @step_input field [String] The payload field to test
|
86
|
+
Then('the {word} payload field {string} is an integer') do |request_type, field|
|
87
|
+
list = Maze::Server.list_for request_type
|
88
|
+
value = Maze::Helper.read_key_path(list.current[:body], field)
|
89
|
+
Maze.check.kind_of Integer, value
|
90
|
+
end
|
91
|
+
|
92
|
+
# Tests whether a payload field is a date (parseable as a Date, according to Ruby)
|
93
|
+
#
|
94
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
95
|
+
# @step_input field [String] The payload field to test
|
96
|
+
Then('the {word} payload field {string} is a date') do |request_type, field|
|
97
|
+
list = Maze::Server.list_for request_type
|
98
|
+
value = Maze::Helper.read_key_path(list.current[:body], field)
|
99
|
+
date = begin
|
100
|
+
Date.parse(value)
|
101
|
+
rescue StandardError
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
Maze.check.kind_of Date, date
|
105
|
+
end
|
106
|
+
|
107
|
+
# Tests whether a payload field (loosely) matches a UUID regex (/[a-fA-F0-9-]!{36}/)
|
108
|
+
#
|
109
|
+
# @step_input request_type [String] The type of request (error, session, build, etc)
|
110
|
+
# @step_input field [String] The payload field to test
|
111
|
+
Then('the {word} payload field {string} is a UUID') do |request_type, field|
|
112
|
+
list = Maze::Server.list_for request_type
|
113
|
+
value = Maze::Helper.read_key_path(list.current[:body], field)
|
114
|
+
Maze.check.not_nil(value, "Expected UUID, got nil for #{field}")
|
115
|
+
match = /[a-fA-F0-9-]{36}/.match(value).size > 0
|
116
|
+
Maze.check.true(match, "Field #{field} is not a UUID, received #{value}")
|
117
|
+
end
|
118
|
+
|
119
|
+
# @!endgroup
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
# The apikey that should be used on this test run
|
4
|
+
$api_key = SecureRandom.hex(16).tr('+/=', 'xyz')
|
5
|
+
|
6
|
+
# A regex providing the pattern expected from timestamps
|
7
|
+
TIMESTAMP_REGEX = /^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:[\d\.]+(Z|[+-]\d{2}:\d{2})?$/
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cucumber'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'json'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'selenium-webdriver'
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
BeforeAll do
|
11
|
+
|
12
|
+
Maze.check = Maze::Checks::AssertCheck.new
|
13
|
+
|
14
|
+
# Infer mode of operation from config, one of:
|
15
|
+
# - Appium (using either remote or local devices)
|
16
|
+
# - Browser (Selenium with local or remote browsers)
|
17
|
+
# - Command (the software under test is invoked with a system call)
|
18
|
+
# TODO Consider making this a specific command line option defaulting to Appium
|
19
|
+
is_appium = [:bs, :bb, :local].include?(Maze.config.farm) && !Maze.config.app.nil?
|
20
|
+
is_browser = !Maze.config.browser.nil?
|
21
|
+
if is_appium
|
22
|
+
Maze.mode = :appium
|
23
|
+
Maze.internal_hooks = Maze::Hooks::AppiumHooks.new
|
24
|
+
elsif is_browser
|
25
|
+
Maze.mode = :browser
|
26
|
+
Maze.internal_hooks = Maze::Hooks::BrowserHooks.new
|
27
|
+
else
|
28
|
+
Maze.mode = :command
|
29
|
+
Maze.internal_hooks = Maze::Hooks::CommandHooks.new
|
30
|
+
end
|
31
|
+
$logger.info "Running in #{Maze.mode.to_s} mode"
|
32
|
+
|
33
|
+
# Clear out maze_output folder
|
34
|
+
maze_output = Dir.glob(File.join(Dir.pwd, 'maze_output', '*'))
|
35
|
+
if Maze.config.file_log && !maze_output.empty?
|
36
|
+
maze_output.each { |path| $logger.info "Clearing contents of #{path}" }
|
37
|
+
FileUtils.rm_rf(maze_output)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Record the local server starting time
|
41
|
+
Maze.start_time = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
42
|
+
|
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
|
52
|
+
|
53
|
+
# Start mock server
|
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)
|
58
|
+
|
59
|
+
# Invoke the internal hook for the mode of operation
|
60
|
+
Maze.internal_hooks.before_all
|
61
|
+
|
62
|
+
# Call any blocks registered by the client
|
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?
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param config The Cucumber config
|
71
|
+
InstallPlugin do |config|
|
72
|
+
# Start Bugsnag
|
73
|
+
Maze::BugsnagConfig.start_bugsnag(config)
|
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
|
+
|
81
|
+
# Only add the retry plugin if --retry is not used on the command line
|
82
|
+
config.filters << Maze::Plugins::GlobalRetryPlugin.new(config) if config.options[:retry].zero?
|
83
|
+
config.filters << Maze::Plugins::BugsnagReportingPlugin.new(config)
|
84
|
+
cucumber_report_plugin = Maze::Plugins::CucumberReportPlugin.new
|
85
|
+
cucumber_report_plugin.install_plugin(config)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Before each scenario
|
89
|
+
Before do |scenario|
|
90
|
+
# Default to no dynamic try
|
91
|
+
Maze.dynamic_retry = false
|
92
|
+
|
93
|
+
if ENV['BUILDKITE']
|
94
|
+
location = "\e[90m\t# #{scenario.location}\e[0m"
|
95
|
+
$stdout.puts "--- Scenario: #{scenario.name} #{location}"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Invoke the internal hook for the mode of operation
|
99
|
+
Maze.internal_hooks.before scenario
|
100
|
+
|
101
|
+
# Call any blocks registered by the client
|
102
|
+
Maze.hooks.call_before scenario
|
103
|
+
end
|
104
|
+
|
105
|
+
# General processing to be run after each scenario
|
106
|
+
After do |scenario|
|
107
|
+
# If we're running on macos, take a screenshot if the scenario fails
|
108
|
+
if Maze.config.os == "macos" && scenario.status == :failed
|
109
|
+
Maze::MacosUtils.capture_screen(scenario)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Call any blocks registered by the client
|
113
|
+
Maze.hooks.call_after scenario
|
114
|
+
|
115
|
+
# Stop document server if started by the Cucumber step
|
116
|
+
Maze::DocumentServer.manual_stop
|
117
|
+
|
118
|
+
# Stop terminating server if started by the Cucumber step
|
119
|
+
Maze::TerminatingServer.stop
|
120
|
+
|
121
|
+
# This is here to stop sessions from one test hitting another.
|
122
|
+
# However this does mean that tests take longer.
|
123
|
+
# In addition, reset the last captured exit code
|
124
|
+
# TODO:SM We could try and fix this by generating unique endpoints
|
125
|
+
# for each test.
|
126
|
+
Maze::Docker.reset
|
127
|
+
|
128
|
+
# Make sure that any scripts are killed between test runs
|
129
|
+
# so future tests are run from a clean slate.
|
130
|
+
Maze::Runner.kill_running_scripts
|
131
|
+
|
132
|
+
Maze::Proxy.instance.stop
|
133
|
+
|
134
|
+
# Log all received requests to the console if the scenario fails and/or config says to
|
135
|
+
if (scenario.failed? && Maze.config.log_requests) || Maze.config.always_log
|
136
|
+
$stdout.puts '^^^ +++'
|
137
|
+
output_received_requests('errors')
|
138
|
+
output_received_requests('sessions')
|
139
|
+
output_received_requests('builds')
|
140
|
+
output_received_requests('logs')
|
141
|
+
output_received_requests('invalid requests')
|
142
|
+
end
|
143
|
+
|
144
|
+
# Log all received requests to file
|
145
|
+
Maze::MazeOutput.new(scenario).write_requests if Maze.config.file_log
|
146
|
+
|
147
|
+
# Invoke the internal hook for the mode of operation
|
148
|
+
Maze.internal_hooks.after scenario
|
149
|
+
|
150
|
+
ensure
|
151
|
+
# Request arrays in particular are cleared here, rather than in the Before hook, to allow requests to be registered
|
152
|
+
# when a test fixture starts (which can be before the first Before scenario hook fires).
|
153
|
+
Maze::Server.reset!
|
154
|
+
Maze::Runner.environment.clear
|
155
|
+
Maze::Store.values.clear
|
156
|
+
Maze::Aws::Sam.reset!
|
157
|
+
end
|
158
|
+
|
159
|
+
def output_received_requests(request_type)
|
160
|
+
request_queue = Maze::Server.list_for(request_type)
|
161
|
+
count = request_queue.size_all
|
162
|
+
if count == 0
|
163
|
+
$logger.info "No #{request_type} received"
|
164
|
+
else
|
165
|
+
$logger.info "#{count} #{request_type} were received:"
|
166
|
+
request_queue.all.each.with_index(1) do |request, number|
|
167
|
+
$stdout.puts "--- #{request_type} #{number} of #{count}"
|
168
|
+
Maze::LogUtil.log_hash(Logger::Severity::INFO, request)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Check for invalid requests after each scenario. This is its own hook as failing a scenario raises an exception
|
174
|
+
# and we need the logic in the other After hook to be performed.
|
175
|
+
# Furthermore, this hook should appear after the general hook as they are executed in reverse order by Cucumber.
|
176
|
+
After do |scenario|
|
177
|
+
unless Maze::Server.invalid_requests.size_all == 0
|
178
|
+
msg = "#{Maze::Server.invalid_requests.size_all} invalid request(s) received during scenario"
|
179
|
+
scenario.fail msg
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# After all tests
|
184
|
+
AfterAll do
|
185
|
+
|
186
|
+
if Maze.timers.size.positive?
|
187
|
+
$stdout.puts '--- Timer summary'
|
188
|
+
Maze.timers.report
|
189
|
+
end
|
190
|
+
|
191
|
+
$stdout.puts '+++ All scenarios complete'
|
192
|
+
|
193
|
+
# Stop the mock server
|
194
|
+
Maze::Server.stop
|
195
|
+
|
196
|
+
# In order to not impact future test runs, we down
|
197
|
+
# all services (which removes networks etc) so that
|
198
|
+
# future test runs are from a clean slate.
|
199
|
+
Maze::Docker.down_all_services
|
200
|
+
|
201
|
+
# Invoke the internal hook for the mode of operation
|
202
|
+
Maze.internal_hooks.after_all
|
203
|
+
end
|
204
|
+
|
205
|
+
at_exit do
|
206
|
+
Maze.internal_hooks.at_exit
|
207
|
+
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
|