bugsnag-maze-runner 6.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,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
|
data/lib/maze/option.rb
ADDED
@@ -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
|