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,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Servlets
5
+ # Receives HTTP requests and responds according to the parameters given, which are:
6
+ # - delay_ms - milliseconds to wait before responding
7
+ # - status - HTTP response code
8
+ # For GET requests these are expected to passed as GET parameters,
9
+ # for POST requests they are expected to be given as JSON fields.
10
+ class ReflectiveServlet < WEBrick::HTTPServlet::AbstractServlet
11
+
12
+ # Accepts a GET request to provide a reflective response to.
13
+ #
14
+ # @param request [HTTPRequest] The incoming GET request
15
+ # @param response [HTTPResponse] The response to return
16
+ def do_GET(request, response)
17
+ delay_ms = request.query['delay_ms']
18
+ status = request.query['status']
19
+ reflect response, delay_ms, status
20
+ end
21
+
22
+ # Accepts a POST request to provide a reflective response to.
23
+ #
24
+ # @param request [HTTPRequest] The incoming GET request
25
+ # @param response [HTTPResponse] The response to return
26
+ def do_POST(request, response)
27
+
28
+ content_type = request['Content-Type']
29
+ unless content_type == 'application/json'
30
+ msg = "Content-Type '#{content_type}' not supported - only application/json is supported at present"
31
+ $logger.error msg
32
+ response.status = 415
33
+ response.body = msg
34
+ return
35
+ end
36
+
37
+ body = JSON.parse(request.body)
38
+ delay_ms = body['delay_ms']
39
+ status = body['status']
40
+
41
+ reflect response, delay_ms, status
42
+ rescue JSON::ParserError => e
43
+ msg = "Unable to parse request as JSON: #{e.message}"
44
+ $logger.error msg
45
+ response.status = 418
46
+ rescue StandardError => e
47
+ $logger.error "Invalid request: #{e.message}"
48
+ response.status = 500
49
+ end
50
+
51
+ def reflect(response, delay_ms, status)
52
+ sleep delay_ms.to_i / 1000 unless delay_ms.nil?
53
+ response.status = status || 200
54
+ response.header['Access-Control-Allow-Origin'] = '*'
55
+ response.body = "Returned status #{status} after waiting #{delay_ms} ms"
56
+ end
57
+
58
+ # Logs and returns a set of valid headers for this servlet.
59
+ #
60
+ # @param request [HTTPRequest] The incoming GET request
61
+ # @param response [HTTPResponse] The response to return
62
+ def do_OPTIONS(request, response)
63
+ super
64
+
65
+ response.header['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Servlets
5
+
6
+ # Receives and parses the requests and payloads sent from the test fixture
7
+ class Servlet < BaseServlet
8
+ # Constructor
9
+ #
10
+ # @param server [HTTPServer] WEBrick HTTPServer
11
+ # @param request_type [Symbol] Request type that the servlet will receive
12
+ def initialize(server, request_type)
13
+ super server
14
+ @request_type = request_type
15
+ @requests = Server.list_for request_type
16
+ end
17
+
18
+ # Logs an incoming GET WEBrick request.
19
+ #
20
+ # @param request [HTTPRequest] The incoming GET request
21
+ # @param _response [HTTPResponse] The response to return
22
+ def do_GET(request, _response)
23
+ log_request(request)
24
+ end
25
+
26
+ # Logs and parses an incoming POST request.
27
+ # Parses `multipart/form-data` and `application/json` content-types.
28
+ # Parsed requests are added to the requests list.
29
+ #
30
+ # @param request [HTTPRequest] The incoming GET request
31
+ # @param response [HTTPResponse] The response to return
32
+ def do_POST(request, response)
33
+ log_request(request)
34
+ case request['Content-Type']
35
+ when %r{^multipart/form-data; boundary=([^;]+)}
36
+ boundary = WEBrick::HTTPUtils::dequote($1)
37
+ body = WEBrick::HTTPUtils.parse_form_data(request.body, boundary)
38
+ hash = {
39
+ body: body,
40
+ request: request
41
+ }
42
+ else
43
+ # "content-type" is assumed to be JSON (which mimics the behaviour of
44
+ # the actual API). This supports browsers that can't set this header for
45
+ # cross-domain requests (IE8/9)
46
+ digests = check_digest request
47
+ hash = {
48
+ body: JSON.parse(request.body),
49
+ request: request,
50
+ digests: digests
51
+ }
52
+ end
53
+ @requests.add(hash)
54
+
55
+ # For the response, delaying if configured to do so
56
+ response_delay_ms = Server.response_delay_ms
57
+ if response_delay_ms.positive?
58
+ $logger.info "Waiting #{response_delay_ms} milliseconds before responding"
59
+ sleep response_delay_ms / 1000.0
60
+ end
61
+ response.header['Access-Control-Allow-Origin'] = '*'
62
+ response.status = Server.status_code
63
+ rescue JSON::ParserError => e
64
+ msg = "Unable to parse request as JSON: #{e.message}"
65
+ if Maze.config.captured_invalid_requests.include? @request_type
66
+ $logger.error msg
67
+ Server.invalid_requests.add({
68
+ reason: msg,
69
+ request: request,
70
+ body: request.body
71
+ })
72
+ else
73
+ $logger.warn msg
74
+ end
75
+ rescue StandardError => e
76
+ if Maze.config.captured_invalid_requests.include? @request_type
77
+ $logger.error "Invalid request: #{e.message}"
78
+ Server.invalid_requests.add({
79
+ invalid: true,
80
+ reason: e.message,
81
+ request: {
82
+ request_uri: request.request_uri,
83
+ header: request.header,
84
+ body: request.inspect
85
+ }
86
+ })
87
+ else
88
+ $logger.warn "Invalid request: #{e.message}"
89
+ end
90
+ end
91
+
92
+ # Logs and returns a set of valid headers for this servlet.
93
+ #
94
+ # @param request [HTTPRequest] The incoming GET request
95
+ # @param response [HTTPResponse] The response to return
96
+ def do_OPTIONS(request, response)
97
+ super
98
+
99
+ response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
100
+ response.status = Server.status_code
101
+ end
102
+
103
+ private
104
+
105
+ def log_request(request)
106
+ $logger.debug "#{request.request_method} request received"
107
+ $logger.debug "URI: #{request.unparsed_uri}"
108
+ $logger.debug "HEADERS: #{request.raw_header}"
109
+ return if request.body.nil?
110
+
111
+ case request['Content-Type']
112
+ when nil
113
+ nil
114
+ when %r{^multipart/form-data; boundary=([^;]+)}
115
+ boundary = WEBrick::HTTPUtils.dequote(Regexp.last_match(1))
116
+ body = WEBrick::HTTPUtils.parse_form_data(request.body, boundary)
117
+ $logger.debug 'BODY:'
118
+ LogUtil.log_hash(Logger::Severity::DEBUG, body)
119
+ else
120
+ $logger.debug "BODY: #{JSON.pretty_generate(JSON.parse(request.body))}"
121
+ end
122
+ end
123
+
124
+ # Checks the Bugsnag-Integrity header, if present, against the request and based on configuration.
125
+ # If the header is present, if the digest must be correct. However, the header need only be present
126
+ # if configuration says so.
127
+ def check_digest(request)
128
+ header = request['Bugsnag-Integrity']
129
+ if header.nil? && Maze.config.enforce_bugsnag_integrity
130
+ raise 'Bugsnag-Integrity header must be present according to Maze.config.enforce_bugsnag_integrity'
131
+ end
132
+ return if header.nil?
133
+
134
+ # Header must have type and digest
135
+ parts = header.split ' '
136
+ raise "Invalid Bugsnag-Integrity header: #{header}" unless parts.size == 2
137
+
138
+ # Both digest types are stored whatever
139
+ sha1 = Digest::SHA1.hexdigest(request.body)
140
+ simple = request.body.bytesize
141
+ $logger.debug "DIGESTS computed: sha1=#{sha1} simple=#{simple}"
142
+
143
+ # Check digests match
144
+ case parts[0]
145
+ when 'sha1'
146
+ raise "Given sha1 #{parts[1]} does not match the computed #{sha1}" unless parts[1] == sha1
147
+ when 'simple'
148
+ raise "Given simple digest #{parts[1].inspect} does not match the computed #{simple.inspect}" unless parts[1].to_i == simple
149
+ else
150
+ raise "Invalid Bugsnag-Integrity digest type: #{parts[0]}"
151
+ end
152
+
153
+ {
154
+ sha1: sha1,
155
+ simple: simple
156
+ }
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # Utils supporting the BrowserStack device farm integration
5
+ class SmartBearUtils
6
+ class << self
7
+ SB_READY_FILE = 'sb.ready'
8
+ SB_KILL_FILE = 'sb.kill'
9
+
10
+ # Starts the SmartBear local tunnel
11
+ #
12
+ # @param sb_local [String] path to the SBSecureTunnel binary
13
+ # @param username [String] Username to start the tunnel with
14
+ # @param access_key [String] CBT access key
15
+ # @param tunnel_name [String] Tunnel name
16
+ def start_local_tunnel(sb_local, username, access_key, tunnel_name=nil)
17
+ # Make sure the ready/kill files are already deleted
18
+ File.delete(SB_READY_FILE) if File.exist?(SB_READY_FILE)
19
+ File.delete(SB_KILL_FILE) if File.exist?(SB_KILL_FILE)
20
+
21
+ $logger.info 'Starting CBT SBSecureTunnel'
22
+ command = "#{sb_local} --username #{username} --authkey #{access_key} --acceptAllCerts " \
23
+ "--ready #{SB_READY_FILE} --kill #{SB_KILL_FILE}"
24
+ command << " --tunnelname #{tunnel_name}" unless tunnel_name.nil?
25
+
26
+ output = start_tunnel_thread(command)
27
+
28
+ success = Maze::Wait.new(timeout: 30).until do
29
+ File.exist?(SB_READY_FILE)
30
+ end
31
+ unless success
32
+ $logger.error "Failed: #{output}"
33
+ end
34
+ end
35
+
36
+ # Stops the local tunnel
37
+ def stop_local_tunnel
38
+ FileUtils.touch(SB_KILL_FILE)
39
+ Maze::Wait.new(timeout: 30).until do
40
+ !File.exist?(SB_READY_FILE)
41
+ end
42
+ File.delete(SB_READY_FILE) if File.exist?(SB_READY_FILE)
43
+ File.delete(SB_KILL_FILE) if File.exist?(SB_KILL_FILE)
44
+ end
45
+
46
+ private
47
+
48
+ def start_tunnel_thread(cmd)
49
+ executor = lambda do
50
+ Open3.popen2e(Maze::Runner.environment, cmd) do |_stdin, stdout_and_stderr, wait_thr|
51
+
52
+ output = []
53
+ stdout_and_stderr.each do |line|
54
+ output << line
55
+ $logger.debug('SBSecureTunnel') {line.chomp}
56
+ end
57
+
58
+ exit_status = wait_thr.value.to_i
59
+ $logger.debug "Exit status: #{exit_status}"
60
+
61
+ output.each { |line| $logger.warn('SBSecureTunnel') {line.chomp} } unless exit_status == 0
62
+
63
+ return [output, exit_status]
64
+ end
65
+ end
66
+
67
+ Thread.new(&executor)
68
+ end
69
+ end
70
+ end
71
+ end
data/lib/maze/store.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # Provides a hash to store values for cross-request comparisons
5
+ class Store
6
+ class << self
7
+ # Returns the hash of stored values for the current scenario
8
+ #
9
+ # @return [Hash] The stored value hash
10
+ def values
11
+ @values ||= {}
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ module Maze
6
+ # Receives and terminates network connections without reading any data
7
+ class TerminatingServer
8
+ CONTINUE_RESPONSE = "HTTP/1.1 100 CONTINUE\n\r"
9
+ BAD_REQUEST_RESPONSE = "HTTP/1.1 400 BAD REQUEST\n\r"
10
+
11
+ class << self
12
+
13
+ # Starts the socket accept loop in a separate thread
14
+ def start
15
+ # Only run a single server thread
16
+ return if running?
17
+
18
+ attempts = 0
19
+ loop do
20
+
21
+ @thread = Thread.new do
22
+ # Reset the received count
23
+ @received_requests = 0
24
+
25
+ Socket.tcp_server_loop(Maze.config.null_port) {|socket, _client_addrinfo|
26
+ $logger.info 'Terminating server received request'
27
+ @received_requests += 1
28
+ headers = receive_headers(socket)
29
+
30
+ body_length = headers['Content-Length']
31
+ receive_data(socket, body_length) unless body_length.nil?
32
+
33
+ end_connection(socket)
34
+ }
35
+ rescue StandardError => e
36
+ $logger.warn "Terminating server error: #{e.message}"
37
+ end
38
+
39
+ break if running?
40
+
41
+ # Bail out after 3 attempts
42
+ attempts += 1
43
+ raise 'Too many failed attempts to start terminating server' if attempts == 3
44
+
45
+ # Failed to start - sleep before retrying
46
+ $logger.info 'Retrying in 3 seconds'
47
+ sleep 1
48
+ end
49
+ end
50
+
51
+ # The maximum string length to be received before disconnecting
52
+ #
53
+ # @return [Integer] The string length, defaults to 1MB
54
+ def max_received_size
55
+ @max_received_size ||= 1048576
56
+ end
57
+
58
+ # Set the maximum string length to be received before disconnecting
59
+ #
60
+ # @param new_max_size [Integer] The new maximum size
61
+ def max_received_size=(new_max_size)
62
+ @max_received_size = new_max_size
63
+ end
64
+
65
+ # The response string sent to a connected client
66
+ #
67
+ # @return [String] The response string, defaults to "400/BAD REQUEST"
68
+ def response
69
+ @response ||= BAD_REQUEST_RESPONSE
70
+ end
71
+
72
+ # Set the response string to an arbitrary value
73
+ #
74
+ # @param new_response [String] The new response
75
+ def response=(new_response)
76
+ @response = new_response
77
+ end
78
+
79
+ # Resets the response string to "400/BAD REQUEST" and the read size to 1MB
80
+ def reset_elements
81
+ @response = BAD_REQUEST_RESPONSE
82
+ @max_received_size = 1048576
83
+ end
84
+
85
+ # Whether the server thread is running
86
+ #
87
+ # @return [Boolean] If the server is running
88
+ def running?
89
+ @thread&.alive?
90
+ end
91
+
92
+ # Outputs the amount of times the server has received a connection on the last run
93
+ def received_request_count
94
+ @received_requests ||= 0
95
+ end
96
+
97
+ # Stops the socket accept loop if alive
98
+ def stop
99
+ @thread&.kill if @thread&.alive?
100
+ @thread = nil
101
+ end
102
+
103
+ private
104
+
105
+ def receive_headers(socket)
106
+ headers = {}
107
+ while (request = socket.gets) && (request.chomp.length > 0)
108
+ key, val = request.chomp.split(': ')
109
+ headers[key] = val
110
+ $logger.debug "Received #{headers.size} headers"
111
+ end
112
+ headers
113
+ end
114
+
115
+ def receive_data(socket, body_length)
116
+ read_length = body_length.to_i < max_received_size ? body_length.to_i : max_received_size
117
+ $logger.info "Reading #{read_length} bytes"
118
+ socket.read(read_length)
119
+ end
120
+
121
+ def end_connection(socket)
122
+ $logger.info "Responding with: #{response}"
123
+ # Unlikely to be used, but replicates pipeline response
124
+ socket.print response
125
+ socket.close
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,51 @@
1
+ module Maze
2
+
3
+ # A simple run/stop timer
4
+ class Timer
5
+ attr_accessor :total
6
+
7
+ def initialize
8
+ @total = 0
9
+ end
10
+
11
+ def time(&block)
12
+ start = Time.now
13
+
14
+ block.call
15
+ ensure
16
+ @total += Time.now - start
17
+ end
18
+
19
+ def reset
20
+ @total = 0
21
+ end
22
+ end
23
+
24
+ # Stores a collection of timers
25
+ class Timers
26
+ def initialize
27
+ @timers = {}
28
+ end
29
+
30
+ def add(name)
31
+ timer = Timer.new
32
+ @timers[name] = timer
33
+ timer
34
+ end
35
+
36
+ def get(name)
37
+ @timers[name]
38
+ end
39
+
40
+ def size
41
+ @timers.size
42
+ end
43
+
44
+ def report
45
+ $logger.info 'Timer totals:'
46
+ @timers.sort.each do |name, timer|
47
+ $logger.info " #{name}: #{timer.total}"
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/maze/wait.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # Allows repeated attempts at something, until it is successful or the timeout
5
+ # is exceed
6
+ class Wait
7
+ # @param interval [Numeric] Optional. The time to sleep between attempts
8
+ # @param timeout [Numeric] The amount of time to spend on attempts before giving up
9
+ def initialize(interval: 0.1, timeout:)
10
+ raise "Interval must be greater than zero, got '#{interval}'" unless interval > 0
11
+ raise "Timeout (#{timeout}) must be greater than interval (#{interval})" unless timeout > interval
12
+
13
+ @interval = interval
14
+ @max_attempts = timeout / interval
15
+ end
16
+
17
+ # Wait until the given block succeeds (returns a truthy value) or the
18
+ # timeout is exceeded
19
+ #
20
+ # @return [Object] The last value returned by the block
21
+ def until(&block)
22
+ success = false
23
+ attempts = 0
24
+
25
+ until success || attempts >= @max_attempts do
26
+ attempts += 1
27
+ success = block.call
28
+
29
+ sleep @interval unless success
30
+ end
31
+
32
+ success
33
+ end
34
+ end
35
+ end
data/lib/maze.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'maze/configuration'
4
+ require_relative 'maze/hooks/hooks'
5
+ require_relative 'maze/timers'
6
+
7
+ # Glues the various parts of MazeRunner together that need to be accessed globally,
8
+ # providing an alternative to the proliferation of global variables or singletons.
9
+ module Maze
10
+ VERSION = '6.27.0'
11
+
12
+ class << self
13
+ attr_accessor :check, :driver, :internal_hooks, :mode, :start_time, :dynamic_retry
14
+
15
+ def config
16
+ @config ||= Maze::Configuration.new
17
+ end
18
+
19
+ def hooks
20
+ @hooks ||= Maze::Hooks::Hooks.new
21
+ end
22
+
23
+ def timers
24
+ @timers ||= Maze::Timers.new
25
+ end
26
+ end
27
+ end