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.
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