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,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative '../option'
5
+ require_relative '../browser_stack_devices'
6
+
7
+ module Maze
8
+ module Option
9
+ # Validates command line options
10
+ class Validator
11
+ # Validates all provided options
12
+ # @param options [Hash] Parsed command line options
13
+ def validate(options)
14
+ errors = []
15
+
16
+ # Common options
17
+ farm = options[Option::FARM]
18
+ if farm && !%w[bs cbt sl local bb].include?(farm)
19
+ errors << "--#{Option::FARM} must be 'bs', 'cbt', 'sl', 'bb' or 'local' if provided"
20
+ end
21
+
22
+ begin
23
+ JSON.parse(options[Option::CAPABILITIES])
24
+ rescue JSON::ParserError
25
+ errors << "--#{Option::CAPABILITIES} must be valid JSON (given #{options[Option::CAPABILITIES]})"
26
+ end
27
+
28
+ # Farm specific options
29
+ validate_bs options, errors if farm == 'bs'
30
+ validate_sl options, errors if farm == 'sl'
31
+ validate_bitbar options, errors if farm == 'bb'
32
+ validate_local options, errors if farm == 'local'
33
+
34
+ errors
35
+ end
36
+
37
+ # Validates BrowserStack options
38
+ def validate_bs(options, errors)
39
+ # BS local binary
40
+ bs_local = Maze::Helper.expand_path options[Option::BS_LOCAL]
41
+ errors << "BrowserStack local binary '#{bs_local}' not found" unless File.exist? bs_local
42
+
43
+ # Device
44
+ browser = options[Option::BROWSER]
45
+ device = options[Option::DEVICE]
46
+ if browser.nil? && device.empty?
47
+ errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
48
+ elsif browser
49
+
50
+ browsers = YAML.safe_load(File.read("#{__dir__}/../browsers_bs.yml"))
51
+
52
+ unless browsers.include? browser
53
+ browser_list = browsers.keys.join ', '
54
+ errors << "Browser type '#{browser}' unknown on BrowserStack. Must be one of: #{browser_list}."
55
+ end
56
+ elsif device
57
+ device.each do |device_key|
58
+ next if Maze::BrowserStackDevices::DEVICE_HASH.key? device_key
59
+ errors << "Device type '#{device_key}' unknown on BrowserStack. Must be one of #{Maze::BrowserStackDevices::DEVICE_HASH.keys}"
60
+ end
61
+ # App
62
+ app = Maze::Helper.read_at_arg_file options[Option::APP]
63
+ if app.nil?
64
+ errors << "--#{Option::APP} must be provided when running on a device"
65
+ else
66
+ # TODO: What about Sauce Labs URLs?
67
+ unless app.start_with?('bs://')
68
+ app = Maze::Helper.expand_path app
69
+ errors << "app file '#{app}' not found" unless File.exist?(app)
70
+ end
71
+ end
72
+ end
73
+
74
+ # Credentials
75
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
76
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
77
+ end
78
+
79
+ # Validates Sauce Labs options
80
+ def validate_sl(options, errors)
81
+ # SL local binary
82
+ sl_local = Maze::Helper.expand_path options[Option::SL_LOCAL]
83
+ errors << "Sauce Connect binary '#{sl_local}' not found" unless File.exist? sl_local
84
+
85
+ # Device
86
+ browser = options[Option::BROWSER]
87
+ device = options[Option::DEVICE]
88
+ os = options[Option::OS]
89
+ os_version = options[Option::OS_VERSION]
90
+ if browser.nil? && device.nil? && os.nil? && os_version.nil?
91
+ errors << 'A device or browser option must be specified'
92
+ elsif browser
93
+ errors << 'Browsers not yet implemented on Sauce Labs'
94
+ else
95
+ # App
96
+ app = options[Option::APP]
97
+ if app.nil?
98
+ errors << "--#{Option::APP} must be provided when running on a device"
99
+ else
100
+ uuid_regex = /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/
101
+ unless uuid_regex.match? app
102
+ app = Maze::Helper.expand_path app
103
+ errors << "app file '#{app}' not found" unless File.exist?(app)
104
+ end
105
+ end
106
+
107
+ # OS
108
+ if options[Option::OS].nil?
109
+ errors << "--#{Option::OS} must be specified"
110
+ else
111
+ os = options[Option::OS].downcase
112
+ errors << 'os must be android or ios' unless %w[android ios].include? os
113
+ end
114
+
115
+ # OS Version
116
+ if options[Option::OS_VERSION].nil?
117
+ errors << "--#{Option::OS_VERSION} must be specified"
118
+ else
119
+ # Ensure OS version is a valid float so that notifier tests can perform numeric checks
120
+ # e.g 'Maze.config.os_version > 7'
121
+ unless /^[1-9][0-9]*(\.[0-9])?/.match? options[Option::OS_VERSION]
122
+ errors << "--#{Option::OS_VERSION} must be a valid version matching '/^[1-9][0-9]*(\\.[0-9])?/'"
123
+ end
124
+ end
125
+ end
126
+
127
+ # Credentials
128
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
129
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
130
+ end
131
+
132
+ # Validates BitBar device options
133
+ def validate_bitbar(options, errors)
134
+ if ENV['BUILDKITE']
135
+ errors << "--#{Option::TMS_URI} must be specified when running on Buildkite" if options[Option::TMS_URI].nil?
136
+ else
137
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
138
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
139
+ end
140
+
141
+ app = options[Option::APP]
142
+ if app.nil?
143
+ errors << "--#{Option::APP} must be provided when running on a device"
144
+ else
145
+ uuid_regex = /\A[0-9]+\z/
146
+ unless uuid_regex.match? app
147
+ app = Maze::Helper.expand_path app
148
+ errors << "app file '#{app}' not found" unless File.exist?(app)
149
+ end
150
+ end
151
+ end
152
+
153
+ # Validates Local device options
154
+ def validate_local(options, errors)
155
+ if options[Option::BROWSER].nil?
156
+ errors << "--#{Option::APP} must be specified" if options[Option::APP].nil?
157
+
158
+ # OS
159
+ if options[Option::OS].nil?
160
+ errors << "--#{Option::OS} must be specified"
161
+ else
162
+ os = options[Option::OS].downcase
163
+ errors << 'os must be android, ios, macos or windows' unless %w[android ios macos windows].include? os
164
+ if os == 'ios'
165
+ errors << "--#{Option::APPLE_TEAM_ID} must be specified for iOS" if options[Option::APPLE_TEAM_ID].nil?
166
+ errors << "--#{Option::UDID} must be specified for iOS" if options[Option::UDID].nil?
167
+ end
168
+ end
169
+
170
+ # OS Version
171
+ unless options[Option::OS_VERSION].nil?
172
+ # Ensure OS version is a valid float so that notifier tests can perform numeric checks
173
+ # e.g 'Maze.config.os_version > 7'
174
+ unless /^[1-9][0-9]*(\.[0-9])?/.match? options[Option::OS_VERSION]
175
+ errors << "--#{Option::OS_VERSION} must be a valid version matching '/^[1-9][0-9]*(\\.[0-9])?/'"
176
+ end
177
+ end
178
+ else
179
+ # TODO Validate browser options
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides the set of Maze Runner command line options
4
+ module Maze
5
+ module Option
6
+ # Document server options
7
+ DS_BIND_ADDRESS = 'document-server-bind-address'
8
+ DS_PORT = 'document-server-port'
9
+ DS_ROOT = 'document-server-root'
10
+
11
+ # Server options
12
+ BIND_ADDRESS = 'bind-address'
13
+ PORT = 'port'
14
+ NULL_PORT = 'null-port'
15
+
16
+ # Appium options
17
+ SEPARATE_SESSIONS = 'separate-sessions'
18
+ FARM = 'farm'
19
+ APP = 'app'
20
+ A11Y_LOCATOR = 'a11y-locator'
21
+ RESILIENT = 'resilient'
22
+ CAPABILITIES = 'capabilities'
23
+
24
+ # Generic device farm options
25
+ USERNAME = 'username'
26
+ ACCESS_KEY = 'access-key'
27
+ APPIUM_VERSION = 'appium-version'
28
+ DEVICE = 'device'
29
+ BROWSER = 'browser'
30
+ OS = 'os'
31
+ OS_VERSION = 'os-version'
32
+ LIST_DEVICES = 'list-devices'
33
+ APP_BUNDLE_ID = 'app-bundle-id'
34
+
35
+ # CrossBrowserTesting/Bitbar options
36
+ SB_LOCAL = 'sb-local'
37
+
38
+ # BrowserStack-only options
39
+ BS_LOCAL = 'bs-local'
40
+
41
+ # Sauce Labs-only options
42
+ SL_LOCAL = 'sl-local'
43
+
44
+ # BitBar-only options
45
+ TMS_URI = 'tms-uri'
46
+ TMS_TOKEN = 'tms-token'
47
+
48
+ # Local-only options
49
+ APPIUM_SERVER = 'appium-server'
50
+ START_APPIUM = 'start-appium'
51
+ APPIUM_LOGFILE = 'appium-logfile'
52
+ APPLE_TEAM_ID = 'apple-team-id'
53
+ UDID = 'udid'
54
+
55
+ # Logging options
56
+ FILE_LOG = 'file-log'
57
+ LOG_REQUESTS = 'log-requests'
58
+ ALWAYS_LOG = 'always-log'
59
+
60
+ # Runtime options
61
+ ENABLE_RETRIES = 'enable-retries'
62
+ ENABLE_BUGSNAG = 'enable-bugsnag'
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugsnag'
4
+ require 'cucumber/core/filter'
5
+
6
+ # Required to access the options
7
+ module Cucumber
8
+ class Configuration
9
+ attr_accessor :options
10
+ end
11
+ end
12
+
13
+ module Maze
14
+ module Plugins
15
+ class BugsnagReportingPlugin < Cucumber::Core::Filter.new(:configuration)
16
+
17
+ def test_case(test_case)
18
+ configuration.on_event(:test_step_finished) do |event|
19
+ @last_test_step = event.test_step if event.result.failed?
20
+ end
21
+
22
+ configuration.on_event(:test_case_finished) do |event|
23
+
24
+ # Ensure we're in the correct test case and that it's failed
25
+ next unless event.test_case.eql?(test_case) && event.result.failed?
26
+
27
+ Bugsnag.notify(event.result.exception) do |bsg_event|
28
+ unless @last_test_step.nil?
29
+ bsg_event.context = @last_test_step.location
30
+ bsg_event.grouping_hash = test_case.name + @last_test_step.location
31
+ bsg_event.add_metadata(:'scenario', {
32
+ 'failing step': @last_test_step.to_s,
33
+ 'failing step location': @last_test_step.location
34
+ })
35
+ end
36
+ bsg_event.add_metadata(:'scenario', {
37
+ 'scenario name': test_case.name,
38
+ 'scenario location': test_case.location,
39
+ 'scenario tags': test_case.tags,
40
+ 'scenario duration (mS)': event.result.duration.nanoseconds/1000000
41
+ })
42
+ end
43
+ end
44
+
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugsnag'
4
+ require 'cucumber/core/filter'
5
+ require 'json'
6
+
7
+ # Required to access the options
8
+ module Cucumber
9
+ class Configuration
10
+ attr_accessor :options
11
+ end
12
+ end
13
+
14
+ module Maze
15
+ module Plugins
16
+ class CucumberReportPlugin
17
+
18
+ def initialize
19
+ configured_data = {
20
+ driver_class: Maze.driver.class,
21
+ device_farm: Maze.config.farm,
22
+ device: Maze.config.device,
23
+ os: Maze.config.os,
24
+ os_version: Maze.config.os_version
25
+ }
26
+ buildkite_data = {
27
+ pipeline: ENV['BUILDKITE_PIPELINE_NAME'],
28
+ repo: ENV['BUILDKITE_REPO'],
29
+ build_url: ENV['BUILDKITE_BUILD_URL'],
30
+ branch: ENV['BUILDKITE_BRANCH'],
31
+ message: ENV['BUILDKITE_MESSAGE'],
32
+ step: ENV['BUILDKITE_LABEL'],
33
+ commit: ENV['BUILDKITE_COMMIT']
34
+ }
35
+ report['configuration'] = configured_data
36
+ report['build'] = buildkite_data
37
+ end
38
+
39
+ def install_plugin(cuc_config)
40
+ unless Maze.config.tms_uri && Maze.config.tms_token && ENV['BUILDKITE']
41
+ $logger.info 'No test report will be delivered for this run'
42
+ return
43
+ end
44
+ # Add installation hook
45
+ cuc_config.formats << ['json', {}, json_report_stream]
46
+
47
+ # Add exit hook
48
+ at_exit do
49
+ finish_report
50
+ end
51
+ end
52
+
53
+ def json_report_stream
54
+ @json_report_stream ||= StringIO.new
55
+ end
56
+
57
+ def report
58
+ @report ||= {}
59
+ end
60
+
61
+ private
62
+
63
+ def finish_report
64
+ session_hash = JSON.parse(json_report_stream.string)
65
+ report[:session] = session_hash
66
+ output_folder = File.join(Dir.pwd, 'maze_output')
67
+ filename = 'maze_report.json'
68
+ filepath = File.join(output_folder, filename)
69
+
70
+ begin
71
+ File.open(filepath, 'w') do |file|
72
+ file.puts JSON.pretty_generate(report)
73
+ end
74
+ rescue => e
75
+ $logger.warn 'Report could not be saved locally'
76
+ $logger.warn e.message
77
+ end
78
+
79
+ send_report
80
+ end
81
+
82
+ def send_report
83
+ uri = URI("#{Maze.config.tms_uri}/report")
84
+ request = Net::HTTP::Post.new(uri)
85
+ request['Content-Type'] = 'application/json'
86
+ request['Authorization'] = Maze.config.tms_token
87
+ request.body = JSON.generate(report)
88
+
89
+ begin
90
+ http = Net::HTTP.new(uri.hostname, uri.port)
91
+ response = http.request(request)
92
+ rescue => e
93
+ $logger.warn 'Report delivery attempt failed'
94
+ $logger.warn e.message
95
+ else
96
+ $logger.info 'Cucumber report delivered to test report server'
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/core/filter'
4
+
5
+ # Required to access the options
6
+ module Cucumber
7
+ class Configuration
8
+ attr_accessor :options
9
+ end
10
+ end
11
+
12
+ module Maze
13
+ module Plugins
14
+ class GlobalRetryPlugin < Cucumber::Core::Filter.new(:configuration)
15
+
16
+ def test_case(test_case)
17
+ configuration.on_event(:test_case_finished) do |event|
18
+
19
+ # Ensure we're in the correct test case
20
+ next unless event.test_case == test_case
21
+
22
+ # Set retry to 0
23
+ configuration.options[:retry] = 0
24
+
25
+ # Guard to check if the case should be retried
26
+ should_retry = event.result.failed? && Maze::RetryHandler.should_retry?(test_case, event)
27
+
28
+ next unless should_retry
29
+
30
+ # Set retry to 1
31
+ configuration.options[:retry] = 1
32
+ end
33
+
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/maze/proxy.rb ADDED
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'singleton'
5
+ require 'webrick'
6
+ require 'webrick/https'
7
+ require 'webrick/httpproxy'
8
+
9
+ module Maze
10
+ # Provides the ability to run a proxy server, using the WEBrick proxy server.
11
+ # Note that for an HTTPS proxy a self-signed certificate will be used. If using curl, for example, this
12
+ # means having to employ the --proxy-insecure option.
13
+ class Proxy
14
+ include Singleton
15
+
16
+ # There are some constraints on the port from driving remote browsers on BrowserStack.
17
+ # E.g. the ports/ranges that Safari will access on "localhost" urls are restricted to the following:
18
+ # 80, 3000, 4000, 5000, 8000, 8080 or 9000-9999 [ from https://stackoverflow.com/a/28678652 ]
19
+ PORT = 9000
20
+
21
+ def initialize
22
+ @hosts = []
23
+ end
24
+
25
+ # Whether the proxy handled a request for the given host
26
+ #
27
+ # @param host [String] The destination host to test for
28
+ def handled_host?(host)
29
+ @hosts.include? host
30
+ end
31
+
32
+ # Whether the proxy server thread is running
33
+ #
34
+ def running?
35
+ @thread&.alive?
36
+ end
37
+
38
+ # Starts the WEBrick proxy in a separate thread
39
+ # If authentication if requested, then the credentials used are simply 'user' with 'password'.
40
+ #
41
+ # @param protocol [Symbol] :Http or Https
42
+ # @param authenticated [Boolean] Whether basic authentication should be applied.
43
+ def start(protocol, authenticated = false)
44
+ @hosts.clear
45
+
46
+ attempts = 0
47
+ $logger.info 'Starting proxy server'
48
+ loop do
49
+ @thread = Thread.new do
50
+
51
+ handler = proc do |req, res|
52
+ req.header['host'].each { |host| @hosts.append(host) }
53
+ end
54
+ config = {
55
+ Logger: $logger,
56
+ Port: PORT,
57
+ ProxyContentHandler: handler
58
+ }
59
+
60
+ # Setup protocol
61
+ if protocol == :Http
62
+ $logger.info 'Starting HTTP proxy'
63
+ elsif protocol == :Https
64
+ $logger.info 'Starting HTTPS proxy'
65
+ cert_name = [
66
+ %w[CN localhost]
67
+ ]
68
+ config[:SSLCertName] = cert_name
69
+ config[:SSLEnable] = true
70
+ else
71
+ raise "Unsupported protocol #{protocol}: :Http and :Https are supported"
72
+ end
73
+
74
+ # Authentication required?
75
+ if authenticated
76
+ # Apache compatible Password manager
77
+ htpasswd = WEBrick::HTTPAuth::Htpasswd.new File.expand_path('htpasswd', __dir__)
78
+ htpasswd.set_passwd 'Proxy Realm', 'user', 'password'
79
+ htpasswd.flush
80
+ authenticator = WEBrick::HTTPAuth::ProxyBasicAuth.new Realm: 'Proxy Realm',
81
+ UserDB: htpasswd
82
+ config[:ProxyAuthProc] = authenticator.method(:authenticate).to_proc
83
+ end
84
+
85
+ # Crwate and start the proxy
86
+ proxy = WEBrick::HTTPProxyServer.new config
87
+ proxy.start
88
+ rescue StandardError => e
89
+ $logger.warn "Failed to start proxy server: #{e.message}"
90
+ ensure
91
+ proxy&.shutdown
92
+ end
93
+
94
+ # Need a short sleep here as a dying thread is still alive momentarily
95
+ sleep 1
96
+ break if running?
97
+
98
+ # Bail out after 3 attempts
99
+ attempts += 1
100
+ raise 'Too many failed attempts to start proxy server' if attempts == 3
101
+
102
+ # Failed to start - sleep before retrying
103
+ $logger.info 'Retrying in 5 seconds'
104
+ sleep 5
105
+ end
106
+ end
107
+
108
+ # Stops the WEBrick proxy thread if it's running
109
+ def stop
110
+ @thread&.kill if @thread&.alive?
111
+ @thread = nil
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # An abstraction for storing a list of requests (e.g. Errors, Sessions),
5
+ # keeping track of the "current" request (i.e. the one being inspected).
6
+ class RequestList
7
+ def initialize
8
+ @requests = []
9
+ @current = 0
10
+ @count = 0
11
+ end
12
+
13
+ def empty?
14
+ @requests.empty?
15
+ end
16
+
17
+ # The number of unprocessed/remaining requests in the list (not the total number actually held)
18
+ def size
19
+ @count
20
+ end
21
+
22
+ # The total number of requests received, including those already processed
23
+ def size_all
24
+ @requests.size
25
+ end
26
+
27
+ # Add a request to the list
28
+ #
29
+ # @param request The new request, from which a clone is made
30
+ def add(request)
31
+ @requests.append request.clone
32
+ @count += 1
33
+ end
34
+
35
+ # The current request
36
+ def current
37
+ @requests[@current] if @requests.size > @current
38
+ end
39
+
40
+ # Peek at requests yet to be processed - i.e. from current onwards. All requests are left visible in the list.
41
+ # Returns an empty array if there are no requests outstanding.
42
+ def remaining
43
+ return [] if current.nil?
44
+
45
+ @requests[@current..@requests.size]
46
+ end
47
+
48
+ # Moves to the next request, if there is one
49
+ def next
50
+ return if @current >= @requests.size
51
+
52
+ @current += 1
53
+ @count -= 1
54
+ end
55
+
56
+ # A frozen clone of all requests held, including those already processed
57
+ def all
58
+ @requests.clone.freeze
59
+ end
60
+
61
+ # Clears the list
62
+ def clear
63
+ @requests.clear
64
+ @current = 0
65
+ @count = 0
66
+ end
67
+
68
+ # Sorts the first `count` elements of the list by the Bugsnag-Sent-At header, if present in all of those elements
69
+ def sort_by_sent_at!(count)
70
+ return unless count > 1
71
+
72
+ header = 'Bugsnag-Sent-At'
73
+ sub_list = @requests[@current...@current + count]
74
+
75
+ return if sub_list.any? { |r| r[:request][header].nil? }
76
+
77
+ # Sort sublist by Bugsnag-Sent-At and overwrite in the main list
78
+ sub_list.sort_by! { |r| DateTime.parse(r[:request][header]) }
79
+ sub_list.each_with_index { |r, i| @requests[@current + i] = r }
80
+ end
81
+ end
82
+ end