bugsnag-maze-runner 7.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +74 -0
  4. data/bin/maze-runner +174 -0
  5. data/bin/upload-app +56 -0
  6. data/lib/features/scripts/await-android-emulator.sh +11 -0
  7. data/lib/features/scripts/clear-android-app-data.sh +8 -0
  8. data/lib/features/scripts/force-stop-android-app.sh +8 -0
  9. data/lib/features/scripts/install-android-app.sh +15 -0
  10. data/lib/features/scripts/launch-android-app.sh +38 -0
  11. data/lib/features/scripts/launch-android-emulator.sh +15 -0
  12. data/lib/features/steps/android_steps.rb +51 -0
  13. data/lib/features/steps/app_automator_steps.rb +228 -0
  14. data/lib/features/steps/aws_sam_steps.rb +212 -0
  15. data/lib/features/steps/breadcrumb_steps.rb +80 -0
  16. data/lib/features/steps/browser_steps.rb +93 -0
  17. data/lib/features/steps/build_api_steps.rb +25 -0
  18. data/lib/features/steps/document_server_steps.rb +7 -0
  19. data/lib/features/steps/error_reporting_steps.rb +358 -0
  20. data/lib/features/steps/feature_flag_steps.rb +190 -0
  21. data/lib/features/steps/header_steps.rb +72 -0
  22. data/lib/features/steps/log_steps.rb +29 -0
  23. data/lib/features/steps/multipart_request_steps.rb +142 -0
  24. data/lib/features/steps/network_steps.rb +135 -0
  25. data/lib/features/steps/payload_steps.rb +257 -0
  26. data/lib/features/steps/proxy_steps.rb +34 -0
  27. data/lib/features/steps/query_parameter_steps.rb +31 -0
  28. data/lib/features/steps/request_assertion_steps.rb +186 -0
  29. data/lib/features/steps/runner_steps.rb +428 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/trace_steps.rb +206 -0
  32. data/lib/features/steps/value_steps.rb +119 -0
  33. data/lib/features/support/env.rb +7 -0
  34. data/lib/features/support/internal_hooks.rb +207 -0
  35. data/lib/maze/api/appium/file_manager.rb +29 -0
  36. data/lib/maze/appium_server.rb +112 -0
  37. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  38. data/lib/maze/aws/sam.rb +112 -0
  39. data/lib/maze/aws_public_ip.rb +53 -0
  40. data/lib/maze/bugsnag_config.rb +42 -0
  41. data/lib/maze/checks/assert_check.rb +69 -0
  42. data/lib/maze/checks/noop_check.rb +34 -0
  43. data/lib/maze/client/appium/base_client.rb +131 -0
  44. data/lib/maze/client/appium/bb_client.rb +102 -0
  45. data/lib/maze/client/appium/bb_devices.rb +127 -0
  46. data/lib/maze/client/appium/bs_client.rb +91 -0
  47. data/lib/maze/client/appium/bs_devices.rb +141 -0
  48. data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
  49. data/lib/maze/client/appium/local_client.rb +67 -0
  50. data/lib/maze/client/appium.rb +23 -0
  51. data/lib/maze/client/bb_api_client.rb +102 -0
  52. data/lib/maze/client/bb_client_utils.rb +181 -0
  53. data/lib/maze/client/bs_client_utils.rb +168 -0
  54. data/lib/maze/client/selenium/base_client.rb +15 -0
  55. data/lib/maze/client/selenium/bb_browsers.yml +188 -0
  56. data/lib/maze/client/selenium/bb_client.rb +38 -0
  57. data/lib/maze/client/selenium/bs_browsers.yml +257 -0
  58. data/lib/maze/client/selenium/bs_client.rb +89 -0
  59. data/lib/maze/client/selenium/local_client.rb +16 -0
  60. data/lib/maze/client/selenium.rb +16 -0
  61. data/lib/maze/compare.rb +161 -0
  62. data/lib/maze/configuration.rb +182 -0
  63. data/lib/maze/docker.rb +147 -0
  64. data/lib/maze/document_server.rb +46 -0
  65. data/lib/maze/driver/appium.rb +198 -0
  66. data/lib/maze/driver/browser.rb +124 -0
  67. data/lib/maze/errors.rb +52 -0
  68. data/lib/maze/generator.rb +55 -0
  69. data/lib/maze/helper.rb +122 -0
  70. data/lib/maze/hooks/appium_hooks.rb +55 -0
  71. data/lib/maze/hooks/browser_hooks.rb +15 -0
  72. data/lib/maze/hooks/command_hooks.rb +9 -0
  73. data/lib/maze/hooks/error_code_hook.rb +49 -0
  74. data/lib/maze/hooks/hooks.rb +61 -0
  75. data/lib/maze/http_request.rb +21 -0
  76. data/lib/maze/interactive_cli.rb +173 -0
  77. data/lib/maze/logger.rb +86 -0
  78. data/lib/maze/macos_utils.rb +14 -0
  79. data/lib/maze/maze_output.rb +88 -0
  80. data/lib/maze/network.rb +49 -0
  81. data/lib/maze/option/parser.rb +240 -0
  82. data/lib/maze/option/processor.rb +130 -0
  83. data/lib/maze/option/validator.rb +155 -0
  84. data/lib/maze/option.rb +62 -0
  85. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  86. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  87. data/lib/maze/plugins/error_code_plugin.rb +21 -0
  88. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  89. data/lib/maze/proxy.rb +114 -0
  90. data/lib/maze/request_list.rb +87 -0
  91. data/lib/maze/request_repeater.rb +49 -0
  92. data/lib/maze/retry_handler.rb +67 -0
  93. data/lib/maze/runner.rb +149 -0
  94. data/lib/maze/schemas/OtelTraceSchema.json +390 -0
  95. data/lib/maze/schemas/trace_schema.rb +7 -0
  96. data/lib/maze/schemas/trace_validator.rb +98 -0
  97. data/lib/maze/server.rb +251 -0
  98. data/lib/maze/servlets/base_servlet.rb +27 -0
  99. data/lib/maze/servlets/command_servlet.rb +47 -0
  100. data/lib/maze/servlets/log_servlet.rb +64 -0
  101. data/lib/maze/servlets/reflective_servlet.rb +70 -0
  102. data/lib/maze/servlets/servlet.rb +199 -0
  103. data/lib/maze/servlets/temp.rb +0 -0
  104. data/lib/maze/servlets/trace_servlet.rb +13 -0
  105. data/lib/maze/store.rb +15 -0
  106. data/lib/maze/terminating_server.rb +129 -0
  107. data/lib/maze/timers.rb +51 -0
  108. data/lib/maze/wait.rb +35 -0
  109. data/lib/maze.rb +27 -0
  110. data/lib/utils/deep_merge.rb +17 -0
  111. data/lib/utils/selenium_money_patch.rb +17 -0
  112. metadata +451 -0
@@ -0,0 +1,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