bugsnag-maze-runner 6.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +76 -0
  4. data/bin/maze-runner +136 -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 +50 -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 +342 -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 +75 -0
  25. data/lib/features/steps/payload_steps.rb +234 -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 +107 -0
  29. data/lib/features/steps/runner_steps.rb +406 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/value_steps.rb +119 -0
  32. data/lib/features/support/env.rb +7 -0
  33. data/lib/features/support/internal_hooks.rb +260 -0
  34. data/lib/maze/appium_server.rb +112 -0
  35. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  36. data/lib/maze/aws/sam.rb +112 -0
  37. data/lib/maze/bitbar_devices.rb +84 -0
  38. data/lib/maze/bitbar_utils.rb +112 -0
  39. data/lib/maze/browser_stack_devices.rb +160 -0
  40. data/lib/maze/browser_stack_utils.rb +164 -0
  41. data/lib/maze/browsers_bs.yml +220 -0
  42. data/lib/maze/browsers_cbt.yml +100 -0
  43. data/lib/maze/bugsnag_config.rb +42 -0
  44. data/lib/maze/capabilities.rb +126 -0
  45. data/lib/maze/checks/assert_check.rb +91 -0
  46. data/lib/maze/checks/noop_check.rb +34 -0
  47. data/lib/maze/compare.rb +161 -0
  48. data/lib/maze/configuration.rb +174 -0
  49. data/lib/maze/docker.rb +108 -0
  50. data/lib/maze/document_server.rb +46 -0
  51. data/lib/maze/driver/appium.rb +217 -0
  52. data/lib/maze/driver/browser.rb +138 -0
  53. data/lib/maze/driver/resilient_appium.rb +51 -0
  54. data/lib/maze/errors.rb +20 -0
  55. data/lib/maze/helper.rb +118 -0
  56. data/lib/maze/hooks/appium_hooks.rb +216 -0
  57. data/lib/maze/hooks/browser_hooks.rb +68 -0
  58. data/lib/maze/hooks/command_hooks.rb +9 -0
  59. data/lib/maze/hooks/hooks.rb +61 -0
  60. data/lib/maze/interactive_cli.rb +173 -0
  61. data/lib/maze/logger.rb +73 -0
  62. data/lib/maze/macos_utils.rb +14 -0
  63. data/lib/maze/network.rb +49 -0
  64. data/lib/maze/option/parser.rb +245 -0
  65. data/lib/maze/option/processor.rb +143 -0
  66. data/lib/maze/option/validator.rb +184 -0
  67. data/lib/maze/option.rb +64 -0
  68. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  69. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  70. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  71. data/lib/maze/proxy.rb +114 -0
  72. data/lib/maze/request_list.rb +82 -0
  73. data/lib/maze/retry_handler.rb +76 -0
  74. data/lib/maze/runner.rb +149 -0
  75. data/lib/maze/sauce_labs_utils.rb +96 -0
  76. data/lib/maze/server.rb +207 -0
  77. data/lib/maze/servlets/base_servlet.rb +22 -0
  78. data/lib/maze/servlets/command_servlet.rb +44 -0
  79. data/lib/maze/servlets/log_servlet.rb +64 -0
  80. data/lib/maze/servlets/reflective_servlet.rb +69 -0
  81. data/lib/maze/servlets/servlet.rb +160 -0
  82. data/lib/maze/smart_bear_utils.rb +71 -0
  83. data/lib/maze/store.rb +15 -0
  84. data/lib/maze/terminating_server.rb +129 -0
  85. data/lib/maze/timers.rb +51 -0
  86. data/lib/maze/wait.rb +35 -0
  87. data/lib/maze.rb +27 -0
  88. metadata +371 -0
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appium_lib'
4
+
5
+ module Maze
6
+ # Handles the logic of when a test should be retried after a failure.
7
+ # Note: This class expects a failed test. For repeating a single test see RepeatHandler
8
+ class RetryHandler
9
+ class << self
10
+
11
+ # Errors which indicate a selenium/appium driver has crashed and needs to be restarted
12
+ DRIVER_ERRORS = [
13
+ Maze::Error::AppiumElementNotFoundError,
14
+
15
+ Selenium::WebDriver::Error::NoSuchElementError,
16
+ Selenium::WebDriver::Error::StaleElementReferenceError,
17
+ Selenium::WebDriver::Error::TimeoutError,
18
+ Selenium::WebDriver::Error::UnknownError,
19
+ Selenium::WebDriver::Error::WebDriverError
20
+ ].freeze
21
+
22
+ # Acceptable tags to indicate a test should be restarted
23
+ RETRY_TAGS = %w[@retry @retryable @retriable].freeze
24
+
25
+ # Determines whether a failed test_case should be restarted
26
+ #
27
+ # @param test_case [Cucumber::RunningTestCase] The current test_case or scenario
28
+ # @param event [Cucumber::Core::Event] The triggering event
29
+ def should_retry?(test_case, event)
30
+ # Only retry if the option is set and we haven't already retried
31
+ return false if !Maze.config.enable_retries || retried_previously?(test_case)
32
+
33
+ if retry_on_driver_error?(event)
34
+ $logger.warn "Retrying #{test_case.name} due to driver error: #{event.result.exception}"
35
+ if Maze.driver.is_a?(Maze::Driver::Appium) || Maze.driver.is_a?(Maze::Driver::ResilientAppium)
36
+ Maze.driver.restart
37
+ elsif Maze.driver.is_a?(Maze::Driver::Browser)
38
+ Maze.driver.refresh
39
+ end
40
+ elsif retry_on_tag?(test_case)
41
+ $logger.warn "Retrying #{test_case.name} due to retry tag"
42
+ elsif Maze.dynamic_retry
43
+ $logger.warn "Retrying #{test_case.name} due to dynamic retry set"
44
+ else
45
+ return false
46
+ end
47
+ increment_retry_count(test_case)
48
+ true
49
+ end
50
+
51
+ def retried_previously?(test_case)
52
+ global_retried[test_case] > 0
53
+ end
54
+
55
+ private
56
+
57
+ def increment_retry_count(test_case)
58
+ global_retried[test_case] += 1
59
+ end
60
+
61
+ def retry_on_driver_error?(event)
62
+ Maze.driver && DRIVER_ERRORS.include?(event.result.exception.class)
63
+ end
64
+
65
+ def retry_on_tag?(test_case)
66
+ test_case.tags.any? do |tag|
67
+ RETRY_TAGS.include?(tag.name)
68
+ end
69
+ end
70
+
71
+ def global_retried
72
+ @global_retried ||= Hash.new { |h, k| h[k] = 0 }
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require_relative './interactive_cli'
5
+
6
+ module Maze
7
+ # Runs scripts and commands, applying relevant environment variables as necessary
8
+ class Runner
9
+ # Determines the default path to the `scripts` directory. Can be overwritten in the env.rb file.
10
+ SCRIPT_PATH ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'features', 'scripts')) unless defined? SCRIPT_PATH
11
+
12
+ class << self
13
+ # Runs a command, applying previously set environment variables.
14
+ # The output from the command is always printed in debug mode -
15
+ # just so the caller can verify something about the output.
16
+ #
17
+ # @param cmd [String] The command to run
18
+ # @param blocking [Boolean] Optional. Whether to wait for a return code before proceeding
19
+ # @param success_codes [Array] Optional. An array of integer codes which indicate the run was successful
20
+ #
21
+ # @return [Array, Thread] If blocking, the output and exit_status are returned
22
+ def run_command(cmd, blocking: true, success_codes: [0])
23
+ executor = lambda do
24
+ $logger.debug "Executing: #{cmd}"
25
+
26
+ Open3.popen2e(environment, cmd) do |_stdin, stdout_and_stderr, wait_thr|
27
+ # Add the pid to the list of pids to kill at the end
28
+ pids << wait_thr.pid unless blocking
29
+
30
+ output = []
31
+ stdout_and_stderr.each do |line|
32
+ output << line
33
+ $logger.debug line.chomp
34
+ end
35
+
36
+ exit_status = wait_thr.value.to_i
37
+ $logger.debug "Exit status: #{exit_status}"
38
+
39
+ # if the command fails we log the output at warn level too
40
+ if !success_codes.nil? && !success_codes.include?(exit_status) && $logger.level != Logger::DEBUG
41
+ output.each { |line| $logger.warn(cmd) { line.chomp } }
42
+ end
43
+
44
+ return [output, exit_status]
45
+ end
46
+ end
47
+
48
+ if blocking
49
+ executor.call
50
+ else
51
+ Thread.new(&executor)
52
+ end
53
+ end
54
+
55
+ # Runs a script in the script directory indicated by the SCRIPT_PATH environment variable.
56
+ #
57
+ # @param script_name [String] The name of the script to run
58
+ # @param blocking [Boolean] Optional. Whether to wait for a return code before proceeding
59
+ # @param success_codes [Array] Optional. An array of integer codes which indicate the run was successful
60
+ # @param command [String] Optional. Command to run the script with, e.g. 'ruby'.
61
+ #
62
+ # @return [Array] If blocking, the output and exit_status are returned
63
+ def run_script(script_name, blocking: false, success_codes: [0], command: nil)
64
+ script_path = File.join(SCRIPT_PATH, script_name)
65
+ script_path = File.join(Dir.pwd, script_name) unless File.exist? script_path
66
+ if command
67
+ script_path = "#{command} #{script_path}"
68
+ elsif Gem.win_platform?
69
+ # Windows does not support the shebang that we use in the scripts so it
70
+ # needs to know how to execute the script. Passing `cmd /c` tells windows
71
+ # to use its known file associations to execute this path. If Ruby is
72
+ # installed on Windows then it will know that `rb` files should be executed
73
+ # using Ruby etc.
74
+ script_path = "cmd /c #{script_path}"
75
+ end
76
+ run_command(script_path, blocking: blocking, success_codes: success_codes)
77
+ end
78
+
79
+ # Creates a new interactive session. Can only be called if no session already
80
+ # exists. Check with {interactive_session?} if necessary.
81
+ #
82
+ # @return [InteractiveCLI] The interactiveCLI instance
83
+ def start_interactive_session(*args)
84
+ raise 'An interactive session is already running!' if interactive_session?
85
+
86
+ wait = Maze::Wait.new(interval: 0.3, timeout: 3)
87
+
88
+ interactive_session = InteractiveCLI.new(*args)
89
+
90
+ success = wait.until { interactive_session.running? }
91
+ raise 'Shell session did not start in time!' unless success
92
+
93
+ @interactive_session = interactive_session
94
+ end
95
+
96
+ def interactive_session?
97
+ !@interactive_session.nil?
98
+ end
99
+
100
+ def interactive_session
101
+ raise 'No interactive session is running!' unless interactive_session?
102
+
103
+ @interactive_session
104
+ end
105
+
106
+ # Stops the interactive session, allowing a new one to be started
107
+ #
108
+ # @return [Boolean] True if the interactive session exited successfully
109
+ def stop_interactive_session
110
+ raise 'No interactive session is running!' unless interactive_session?
111
+
112
+ success = @interactive_session.stop
113
+
114
+ # Make sure the process is killed if it did not stop
115
+ pids << @interactive_session.pid if @interactive_session.running?
116
+
117
+ @interactive_session = nil
118
+
119
+ success
120
+ end
121
+
122
+ # Stops all script processes previously started by this class.
123
+ def kill_running_scripts
124
+ stop_interactive_session if interactive_session?
125
+
126
+ pids.each do |p|
127
+ Process.kill('KILL', p)
128
+ rescue Errno::ESRCH
129
+ # ignored
130
+ end
131
+ pids.clear
132
+ end
133
+
134
+ # Allows access to a hash of environment variables applied to command and script runs.
135
+ #
136
+ # @return [Hash] The hash of currently set environment variables and their values
137
+ def environment
138
+ @environment ||= {}
139
+ end
140
+
141
+ # Allows access to the process ids created by the runner.
142
+ #
143
+ # @return [Array] pids created by the runner
144
+ def pids
145
+ @pids ||= []
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # Utils supporting the SauceLabs device farm integration
5
+ class SauceLabsUtils
6
+ PID_FILE = 'sc.pid'
7
+ READY_FILE = 'sc.ready'
8
+
9
+ class << self
10
+ attr_accessor :connect_shell
11
+
12
+ # Uploads an app to Sauce Labs for later consumption
13
+ # @param username [String] the Sauce Labs username
14
+ # @param access_key [String] the Sauce Labs access key
15
+ def upload_app(username, access_key, app)
16
+ uuid_regex = /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/
17
+
18
+ if uuid_regex.match? app
19
+ $logger.info "Using pre-uploaded app with UUID #{app}"
20
+ app_uuid = app
21
+ else
22
+ expanded_app = Maze::Helper.expand_path(app)
23
+ $logger.info "Uploading app: #{expanded_app}"
24
+
25
+ # Upload the app tp Sauce Labs
26
+ uri = URI('https://api.us-west-1.saucelabs.com/v1/storage/upload')
27
+ request = Net::HTTP::Post.new(uri)
28
+ request.basic_auth(username, access_key)
29
+ request.set_form({ 'payload' => File.new(expanded_app, 'rb') }, 'multipart/form-data')
30
+
31
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
32
+ http.request(request)
33
+ end
34
+
35
+ # Pull the UUID from the response
36
+ begin
37
+ response = JSON.parse res.body
38
+
39
+ if response.key?('item') && response['item'].key?('id')
40
+ app_uuid = response['item']['id']
41
+ $logger.info "Uploaded app UUID: #{app_uuid}"
42
+ $logger.info 'You can use this UUID to avoid uploading the same app more than once.'
43
+ else
44
+ $logger.error "Unexpected response body: #{}"
45
+ raise 'App upload failed'
46
+ end
47
+ rescue JSON::ParserError
48
+ $logger.error "Expected JSON response, received: #{body}"
49
+ raise
50
+ end
51
+ end
52
+ app_uuid
53
+ end
54
+
55
+ # Sauce Connect
56
+ # @param sc_local [String] path to the Sauce Connect binary
57
+ # @param tunnel_id [String] unique key for the tunnel instance
58
+ # @param username [String] Sauce Labs username
59
+ # @param access_key [String] Sauce Labs access key
60
+ def start_sauce_connect(sc_local, tunnel_id, username, access_key)
61
+ $logger.info 'Starting Sauce Connect tunnel'
62
+ endpoint = 'https://saucelabs.com/rest/v1'
63
+ wait = Maze::Wait.new(interval: 0.3, timeout: 3)
64
+ connect_shell = Maze::InteractiveCLI.new
65
+ success = wait.until { connect_shell.running? }
66
+ raise 'Shell session did not start in time!' unless success
67
+
68
+ command = "#{sc_local} -u #{username} -k #{access_key} -x #{endpoint} " \
69
+ "-i #{tunnel_id} -d #{PID_FILE} -l sc.log -f #{READY_FILE}"
70
+
71
+ # TODO Handle the case where the command fails, providing suitable diagnostics
72
+ File.delete(READY_FILE) if File.exist?(READY_FILE)
73
+ File.delete(PID_FILE) if File.exist?(PID_FILE)
74
+ connect_shell.run_command command
75
+ success = Maze::Wait.new(timeout: 30).until do
76
+ File.exist? READY_FILE
77
+ end
78
+ unless success
79
+ $logger.info "Failed: #{connect_shell.stdout_lines}"
80
+ end
81
+ end
82
+
83
+ def stop_sauce_connect
84
+ pid = nil
85
+ File.open(PID_FILE, 'r') do |file|
86
+ pid = file.read.to_i
87
+ end
88
+ Process.kill('INT', pid)
89
+ Maze::Wait.new(timeout: 30).until do
90
+ `ps aux | awk '{print $2 }' | grep #{pid}`.empty?
91
+ end
92
+ File.delete(PID_FILE) if File.exist?(PID_FILE)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'webrick'
6
+ require_relative './logger'
7
+ require_relative './request_list'
8
+
9
+ module Maze
10
+ # Receives and stores requests through a WEBrick HTTPServer
11
+ class Server
12
+
13
+ class << self
14
+ # Allows overwriting of the server status code
15
+ attr_writer :status_code
16
+
17
+ # Dictates if the status code should be reset after use
18
+ attr_writer :reset_status_code
19
+
20
+ # Allows a delay in milliseconds before responding to HTTP requests to be set
21
+ attr_writer :response_delay_ms
22
+
23
+ # Dictates if the response delay should be reset after use
24
+ attr_writer :reset_response_delay
25
+
26
+ # @return [String] The UUID attached to all command requests for this session
27
+ attr_reader :command_uuid
28
+
29
+ # The intended HTTP status code on a successful request
30
+ #
31
+ # @return [Integer] The HTTP status code, defaults to 200
32
+ def status_code
33
+ code = @status_code ||= 200
34
+ @status_code = 200 if reset_status_code
35
+ code
36
+ end
37
+
38
+ def reset_status_code
39
+ @reset_status_code ||= false
40
+ end
41
+
42
+ def response_delay_ms
43
+ delay = @response_delay_ms ||= 0
44
+ @response_delay_ms = 0 if reset_response_delay
45
+ delay
46
+ end
47
+
48
+ def reset_response_delay
49
+ @reset_response_delay ||= false
50
+ end
51
+
52
+ # Provides dynamic access to request lists by name
53
+ #
54
+ # @param type [String, Symbol] Request type
55
+ # @return Request list for the type given
56
+ def list_for(type)
57
+ type = type.to_s
58
+ case type
59
+ when 'error', 'errors'
60
+ errors
61
+ when 'session', 'sessions'
62
+ sessions
63
+ when 'build', 'builds'
64
+ builds
65
+ when 'log', 'logs'
66
+ logs
67
+ when 'upload', 'uploads'
68
+ uploads
69
+ when 'sourcemap', 'sourcemaps'
70
+ sourcemaps
71
+ when 'invalid', 'invalid requests'
72
+ invalid_requests
73
+ else
74
+ raise "Invalid request type '#{type}'"
75
+ end
76
+ end
77
+
78
+ # A list of error requests received
79
+ #
80
+ # @return [RequestList] Received error requests
81
+ def errors
82
+ @errors ||= RequestList.new
83
+ end
84
+
85
+ # A list of session requests received
86
+ #
87
+ # @return [RequestList] Received error requests
88
+ def sessions
89
+ @sessions ||= RequestList.new
90
+ end
91
+
92
+ # A list of build requests received
93
+ #
94
+ # @return [RequestList] Received build requests
95
+ def builds
96
+ @builds ||= RequestList.new
97
+ end
98
+
99
+ # A list of upload requests received
100
+ #
101
+ # @return [RequestList] Received upload requests
102
+ def uploads
103
+ @uploads ||= RequestList.new
104
+ end
105
+
106
+ # A list of sourcemap requests received
107
+ #
108
+ # @return [RequestList] Received sourcemap requests
109
+ def sourcemaps
110
+ @sourcemaps ||= RequestList.new
111
+ end
112
+
113
+ # A list of log requests received
114
+ #
115
+ # @return [RequestList] Received log requests
116
+ def logs
117
+ @logs ||= RequestList.new
118
+ end
119
+
120
+ # A list of commands for a test fixture to perform. Strictly speaking these are responses to HTTP
121
+ # requests, but the list behavior is all we need.
122
+ #
123
+ # @return [RequestList] Commands to be performed
124
+ def commands
125
+ @commands ||= RequestList.new
126
+ end
127
+
128
+ # Whether the server thread is running
129
+ # An array of any invalid requests received.
130
+ # Each request is hash consisting of:
131
+ # request: The original HTTPRequest object
132
+ # reason: Reason for being considered invalid. Examples include invalid JSON and missing/invalid digest.
133
+ # @return [RequestList] An array of received requests
134
+ def invalid_requests
135
+ @invalid_requests ||= RequestList.new
136
+ end
137
+
138
+ # Whether the server thread is running
139
+ #
140
+ # @return [Boolean] If the server is running
141
+ def running?
142
+ @thread&.alive?
143
+ end
144
+
145
+ # Starts the WEBrick server in a separate thread
146
+ def start
147
+ attempts = 0
148
+ $logger.info 'Starting mock server'
149
+ @command_uuid = SecureRandom.uuid
150
+ $logger.info "Fixture commands UUID: #{@command_uuid}"
151
+ loop do
152
+
153
+ @thread = Thread.new do
154
+ options = {
155
+ Port: Maze.config.port,
156
+ Logger: $logger,
157
+ AccessLog: []
158
+ }
159
+ options[:BindAddress] = Maze.config.bind_address unless Maze.config.bind_address.nil?
160
+ server = WEBrick::HTTPServer.new(options)
161
+
162
+ # Mount a block to respond to all requests with status:200
163
+ server.mount_proc '/' do |_request, response|
164
+ $logger.info 'Received request on server root, responding with 200'
165
+ response.header['Access-Control-Allow-Origin'] = '*'
166
+ response.body = 'Maze runner received request'
167
+ response.status = 200
168
+ end
169
+
170
+ # When adding more endpoints, be sure to update the 'I should receive no requests' step
171
+ server.mount '/notify', Servlets::Servlet, :errors
172
+ server.mount '/sessions', Servlets::Servlet, :sessions
173
+ server.mount '/builds', Servlets::Servlet, :builds
174
+ server.mount '/uploads', Servlets::Servlet, :uploads
175
+ server.mount '/sourcemap', Servlets::Servlet, :sourcemaps
176
+ server.mount '/react-native-source-map', Servlets::Servlet, :sourcemaps
177
+ server.mount '/command', Servlets::CommandServlet
178
+ server.mount '/logs', Servlets::LogServlet
179
+ server.start
180
+ rescue StandardError => e
181
+ $logger.warn "Failed to start mock server: #{e.message}"
182
+ ensure
183
+ server&.shutdown
184
+ end
185
+
186
+ # Need a short sleep here as a dying thread is still alive momentarily
187
+ sleep 1
188
+ break if running?
189
+
190
+ # Bail out after 3 attempts
191
+ attempts += 1
192
+ raise 'Too many failed attempts to start mock server' if attempts == 3
193
+
194
+ # Failed to start - sleep before retrying
195
+ $logger.info 'Retrying in 5 seconds'
196
+ sleep 5
197
+ end
198
+ end
199
+
200
+ # Stops the WEBrick server thread if it's running
201
+ def stop
202
+ @thread&.kill if @thread&.alive?
203
+ @thread = nil
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Servlets
5
+
6
+ # Base servlet to avoid duplication of common code
7
+ class BaseServlet < WEBrick::HTTPServlet::AbstractServlet
8
+ # Logs and returns a set of valid headers for this servlet.
9
+ #
10
+ # @param request [HTTPRequest] The incoming GET request
11
+ # @param response [HTTPResponse] The response to return
12
+ def do_OPTIONS(request, response)
13
+ response.header['Access-Control-Allow-Origin'] = '*'
14
+ response.header['Access-Control-Allow-Headers'] = %w[Accept
15
+ Bugsnag-Api-Key Bugsnag-Integrity
16
+ Bugsnag-Payload-Version
17
+ Bugsnag-Sent-At Content-Type
18
+ Origin].join(',')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Maze
6
+ module Servlets
7
+
8
+ # Allows clients to queue up "commands", in the form of Ruby hashes, using Maze::Server.commands.add. GET
9
+ # requests made to the /command endpoint will then respond with each queued command in turn.
10
+ class CommandServlet < BaseServlet
11
+ # Serves the next command, if these is one.
12
+ #
13
+ # @param _request [HTTPRequest] The incoming GET request
14
+ # @param response [HTTPResponse] The response to return
15
+ def do_GET(_request, response)
16
+ response.header['Access-Control-Allow-Origin'] = '*'
17
+
18
+ commands = Maze::Server.commands
19
+ # Note that empty? is not the same as size == 0 (design bug to be corrected in v7)
20
+ if commands.size == 0
21
+ response.body = 'No commands to provide'
22
+ response.status = 400
23
+ else
24
+ command = commands.current
25
+ command[:uuid] = Maze::Server.command_uuid
26
+ response.body = JSON.pretty_generate(command)
27
+ response.status = 200
28
+ commands.next
29
+ end
30
+ end
31
+
32
+ # Logs and returns a set of valid headers for this servlet.
33
+ #
34
+ # @param request [HTTPRequest] The incoming GET request
35
+ # @param response [HTTPResponse] The response to return
36
+ def do_OPTIONS(request, response)
37
+ super
38
+
39
+ response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
40
+ response.status = Server.status_code
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Servlets
5
+
6
+ # Receives log requests sent from the test fixture
7
+ class LogServlet < BaseServlet
8
+ # Constructor
9
+ #
10
+ # @param server [HTTPServer] WEBrick HTTPServer
11
+ def initialize(server)
12
+ super server
13
+ @requests = Server.logs
14
+ end
15
+
16
+ # Logs and parses an incoming POST request.
17
+ # Parses `multipart/form-data` and `application/json` content-types.
18
+ # Parsed requests are added to the requests list.
19
+ #
20
+ # @param request [HTTPRequest] The incoming GET request
21
+ # @param response [HTTPResponse] The response to return
22
+ def do_POST(request, response)
23
+ hash = {
24
+ body: JSON.parse(request.body),
25
+ request: request
26
+ }
27
+ @requests.add(hash)
28
+
29
+ response.header['Access-Control-Allow-Origin'] = '*'
30
+ response.status = Server.status_code
31
+ rescue JSON::ParserError => e
32
+ msg = "Unable to parse request as JSON: #{e.message}"
33
+ $logger.error msg
34
+ Server.invalid_requests.add({
35
+ reason: msg,
36
+ request: request,
37
+ body: request.body
38
+ })
39
+ rescue StandardError => e
40
+ $logger.error "Invalid request: #{e.message}"
41
+ Server.invalid_requests.add({
42
+ invalid: true,
43
+ reason: e.message,
44
+ request: {
45
+ request_uri: request.request_uri,
46
+ header: request.header.to_h,
47
+ body: request.inspect
48
+ }
49
+ })
50
+ end
51
+
52
+ # Logs and returns a set of valid headers for this servlet.
53
+ #
54
+ # @param request [HTTPRequest] The incoming GET request
55
+ # @param response [HTTPResponse] The response to return
56
+ def do_OPTIONS(request, response)
57
+ super
58
+
59
+ response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
60
+ response.status = Server.status_code
61
+ end
62
+ end
63
+ end
64
+ end