bugsnag-maze-runner 6.27.0
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 +76 -0
- data/bin/maze-runner +136 -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 +50 -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 +342 -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 +75 -0
- data/lib/features/steps/payload_steps.rb +234 -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 +107 -0
- data/lib/features/steps/runner_steps.rb +406 -0
- data/lib/features/steps/session_tracking_steps.rb +116 -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 +260 -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/bitbar_devices.rb +84 -0
- data/lib/maze/bitbar_utils.rb +112 -0
- data/lib/maze/browser_stack_devices.rb +160 -0
- data/lib/maze/browser_stack_utils.rb +164 -0
- data/lib/maze/browsers_bs.yml +220 -0
- data/lib/maze/browsers_cbt.yml +100 -0
- data/lib/maze/bugsnag_config.rb +42 -0
- data/lib/maze/capabilities.rb +126 -0
- data/lib/maze/checks/assert_check.rb +91 -0
- data/lib/maze/checks/noop_check.rb +34 -0
- data/lib/maze/compare.rb +161 -0
- data/lib/maze/configuration.rb +174 -0
- data/lib/maze/docker.rb +108 -0
- data/lib/maze/document_server.rb +46 -0
- data/lib/maze/driver/appium.rb +217 -0
- data/lib/maze/driver/browser.rb +138 -0
- data/lib/maze/driver/resilient_appium.rb +51 -0
- data/lib/maze/errors.rb +20 -0
- data/lib/maze/helper.rb +118 -0
- data/lib/maze/hooks/appium_hooks.rb +216 -0
- data/lib/maze/hooks/browser_hooks.rb +68 -0
- data/lib/maze/hooks/command_hooks.rb +9 -0
- data/lib/maze/hooks/hooks.rb +61 -0
- data/lib/maze/interactive_cli.rb +173 -0
- data/lib/maze/logger.rb +73 -0
- data/lib/maze/macos_utils.rb +14 -0
- data/lib/maze/network.rb +49 -0
- data/lib/maze/option/parser.rb +245 -0
- data/lib/maze/option/processor.rb +143 -0
- data/lib/maze/option/validator.rb +184 -0
- data/lib/maze/option.rb +64 -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/global_retry_plugin.rb +38 -0
- data/lib/maze/proxy.rb +114 -0
- data/lib/maze/request_list.rb +82 -0
- data/lib/maze/retry_handler.rb +76 -0
- data/lib/maze/runner.rb +149 -0
- data/lib/maze/sauce_labs_utils.rb +96 -0
- data/lib/maze/server.rb +207 -0
- data/lib/maze/servlets/base_servlet.rb +22 -0
- data/lib/maze/servlets/command_servlet.rb +44 -0
- data/lib/maze/servlets/log_servlet.rb +64 -0
- data/lib/maze/servlets/reflective_servlet.rb +69 -0
- data/lib/maze/servlets/servlet.rb +160 -0
- data/lib/maze/smart_bear_utils.rb +71 -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
- 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
|
data/lib/maze/runner.rb
ADDED
@@ -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
|
data/lib/maze/server.rb
ADDED
@@ -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
|