bugsnag-maze-runner 7.22.1
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 +74 -0
- data/bin/maze-runner +174 -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 +80 -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 +358 -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 +135 -0
- data/lib/features/steps/payload_steps.rb +257 -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 +186 -0
- data/lib/features/steps/runner_steps.rb +428 -0
- data/lib/features/steps/session_tracking_steps.rb +116 -0
- data/lib/features/steps/trace_steps.rb +206 -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 +207 -0
- data/lib/maze/api/appium/file_manager.rb +29 -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/aws_public_ip.rb +53 -0
- data/lib/maze/bugsnag_config.rb +42 -0
- data/lib/maze/checks/assert_check.rb +69 -0
- data/lib/maze/checks/noop_check.rb +34 -0
- data/lib/maze/client/appium/base_client.rb +131 -0
- data/lib/maze/client/appium/bb_client.rb +102 -0
- data/lib/maze/client/appium/bb_devices.rb +127 -0
- data/lib/maze/client/appium/bs_client.rb +91 -0
- data/lib/maze/client/appium/bs_devices.rb +141 -0
- data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
- data/lib/maze/client/appium/local_client.rb +67 -0
- data/lib/maze/client/appium.rb +23 -0
- data/lib/maze/client/bb_api_client.rb +102 -0
- data/lib/maze/client/bb_client_utils.rb +181 -0
- data/lib/maze/client/bs_client_utils.rb +168 -0
- data/lib/maze/client/selenium/base_client.rb +15 -0
- data/lib/maze/client/selenium/bb_browsers.yml +188 -0
- data/lib/maze/client/selenium/bb_client.rb +38 -0
- data/lib/maze/client/selenium/bs_browsers.yml +257 -0
- data/lib/maze/client/selenium/bs_client.rb +89 -0
- data/lib/maze/client/selenium/local_client.rb +16 -0
- data/lib/maze/client/selenium.rb +16 -0
- data/lib/maze/compare.rb +161 -0
- data/lib/maze/configuration.rb +182 -0
- data/lib/maze/docker.rb +147 -0
- data/lib/maze/document_server.rb +46 -0
- data/lib/maze/driver/appium.rb +198 -0
- data/lib/maze/driver/browser.rb +124 -0
- data/lib/maze/errors.rb +52 -0
- data/lib/maze/generator.rb +55 -0
- data/lib/maze/helper.rb +122 -0
- data/lib/maze/hooks/appium_hooks.rb +55 -0
- data/lib/maze/hooks/browser_hooks.rb +15 -0
- data/lib/maze/hooks/command_hooks.rb +9 -0
- data/lib/maze/hooks/error_code_hook.rb +49 -0
- data/lib/maze/hooks/hooks.rb +61 -0
- data/lib/maze/http_request.rb +21 -0
- data/lib/maze/interactive_cli.rb +173 -0
- data/lib/maze/logger.rb +86 -0
- data/lib/maze/macos_utils.rb +14 -0
- data/lib/maze/maze_output.rb +88 -0
- data/lib/maze/network.rb +49 -0
- data/lib/maze/option/parser.rb +240 -0
- data/lib/maze/option/processor.rb +130 -0
- data/lib/maze/option/validator.rb +155 -0
- data/lib/maze/option.rb +62 -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/error_code_plugin.rb +21 -0
- data/lib/maze/plugins/global_retry_plugin.rb +38 -0
- data/lib/maze/proxy.rb +114 -0
- data/lib/maze/request_list.rb +87 -0
- data/lib/maze/request_repeater.rb +49 -0
- data/lib/maze/retry_handler.rb +67 -0
- data/lib/maze/runner.rb +149 -0
- data/lib/maze/schemas/OtelTraceSchema.json +390 -0
- data/lib/maze/schemas/trace_schema.rb +7 -0
- data/lib/maze/schemas/trace_validator.rb +98 -0
- data/lib/maze/server.rb +251 -0
- data/lib/maze/servlets/base_servlet.rb +27 -0
- data/lib/maze/servlets/command_servlet.rb +47 -0
- data/lib/maze/servlets/log_servlet.rb +64 -0
- data/lib/maze/servlets/reflective_servlet.rb +70 -0
- data/lib/maze/servlets/servlet.rb +199 -0
- data/lib/maze/servlets/temp.rb +0 -0
- data/lib/maze/servlets/trace_servlet.rb +13 -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
- data/lib/utils/deep_merge.rb +17 -0
- data/lib/utils/selenium_money_patch.rb +17 -0
- metadata +451 -0
data/lib/maze/errors.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appium_lib'
|
4
|
+
|
5
|
+
module Maze
|
6
|
+
module Error
|
7
|
+
# An error raised when an appium element cannot be found
|
8
|
+
class AppiumElementNotFoundError < StandardError
|
9
|
+
|
10
|
+
# @# @!attribute [r] element
|
11
|
+
# @return [String] The named element that could not be found
|
12
|
+
attr_reader :element
|
13
|
+
|
14
|
+
# Creates the error
|
15
|
+
#
|
16
|
+
# @param message [String] The error to display
|
17
|
+
# @param element [String] The name of the element that could not be located
|
18
|
+
def initialize(message='Element not found', element='No element specified')
|
19
|
+
@element = element
|
20
|
+
super(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ERROR_CODES = {
|
25
|
+
::Selenium::WebDriver::Error::UnknownError => {
|
26
|
+
retry: true,
|
27
|
+
error_code: 10
|
28
|
+
},
|
29
|
+
::Selenium::WebDriver::Error::WebDriverError => {
|
30
|
+
retry: true,
|
31
|
+
error_code: 10
|
32
|
+
},
|
33
|
+
Maze::Error::AppiumElementNotFoundError => {
|
34
|
+
retry: true,
|
35
|
+
error_code: 11
|
36
|
+
},
|
37
|
+
::Selenium::WebDriver::Error::NoSuchElementError => {
|
38
|
+
retry: true,
|
39
|
+
error_code: 12
|
40
|
+
},
|
41
|
+
::Selenium::WebDriver::Error::TimeoutError => {
|
42
|
+
retry: true,
|
43
|
+
error_code: 13
|
44
|
+
},
|
45
|
+
::Selenium::WebDriver::Error::StaleElementReferenceError => {
|
46
|
+
retry: true,
|
47
|
+
error_code: 14
|
48
|
+
},
|
49
|
+
}.freeze
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# A thread-safe generator of values, backed by an Enumerator.
|
2
|
+
module Maze
|
3
|
+
class Generator
|
4
|
+
def initialize(enumerator)
|
5
|
+
# A SizedQueue allows a set number of values to always be ready for clients
|
6
|
+
# and will be automatically topped up from the enumerator.
|
7
|
+
@queue = SizedQueue.new(10)
|
8
|
+
|
9
|
+
# The queue filler continually adds to the queue (when there is room), taking
|
10
|
+
# values from the Enumerator. This ensure the enumerator is always run inside
|
11
|
+
# the same thread
|
12
|
+
@queue_filler = create_queue_filler(enumerator)
|
13
|
+
|
14
|
+
while @queue.empty? && enumerator.size != 0
|
15
|
+
# Wait for the queue to start filling
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return The next value
|
20
|
+
def next
|
21
|
+
@queue.pop
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return Whether the generator has been closed
|
25
|
+
def closed?
|
26
|
+
@queue.closed?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Cleans up resources used by the generator
|
30
|
+
def close
|
31
|
+
@queue_filler.exit
|
32
|
+
@queue.close
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Create a thread that will constantly append to @queue with values from the
|
38
|
+
# given enumerator
|
39
|
+
#
|
40
|
+
# @param enumerator [Enumerator]
|
41
|
+
# @return [Thread]
|
42
|
+
def create_queue_filler(enumerator)
|
43
|
+
# By passing the enumerator as an argument to Thread.new, it creates a copy
|
44
|
+
# local to that thread and therefore we're not sharing an enumerator across
|
45
|
+
# threads
|
46
|
+
Thread.new(enumerator) do |inner|
|
47
|
+
loop do
|
48
|
+
# Add to the queue until it fills up, this will then block until there's
|
49
|
+
# room in the queue again
|
50
|
+
@queue << inner.next
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/maze/helper.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# A collection of helper routines
|
7
|
+
module Maze
|
8
|
+
# Miscellaneous helper functions.
|
9
|
+
module Helper
|
10
|
+
class << self
|
11
|
+
# Parses a request's query string, because WEBrick doesn't in POST requests
|
12
|
+
#
|
13
|
+
# @param request [Hash] The received request
|
14
|
+
#
|
15
|
+
# @return [Hash] The parsed query string.
|
16
|
+
def parse_querystring(request)
|
17
|
+
CGI.parse(request[:request].query_string)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Enables traversal of a hash using Mongo-style dot notation.
|
21
|
+
#
|
22
|
+
# @example hash["array"][0]["item"] becomes "hash.array.0.item"
|
23
|
+
#
|
24
|
+
# @param hash [Hash] The hash to traverse
|
25
|
+
# @param key_path [String] The dot notation path within the hash
|
26
|
+
#
|
27
|
+
# @return [Any] The value found by the key path
|
28
|
+
def read_key_path(hash, key_path)
|
29
|
+
value = hash
|
30
|
+
key_path.split('.').each do |key|
|
31
|
+
if key =~ /^(\d+)$/
|
32
|
+
key = key.to_i
|
33
|
+
if value.length > key
|
34
|
+
value = value[key]
|
35
|
+
else
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
else
|
39
|
+
if value.key? key
|
40
|
+
value = value[key]
|
41
|
+
else
|
42
|
+
return nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Determines if the Bugsnag-Integrity header is valid.
|
50
|
+
# Whether a missing header is deemed valid depends on @see Maze.config.enforce_bugsnag_integrity.
|
51
|
+
#
|
52
|
+
# @return [Boolean] True if the header is present and valid, or not present and not enforced. False otherwise.
|
53
|
+
def valid_bugsnag_integrity_header(request)
|
54
|
+
header = request[:request]['Bugsnag-Integrity']
|
55
|
+
return !Maze.config.enforce_bugsnag_integrity if header.nil?
|
56
|
+
|
57
|
+
digests = request[:digests]
|
58
|
+
if header.start_with?('sha1')
|
59
|
+
computed_digest = "sha1 #{digests[:sha1]}"
|
60
|
+
elsif header.start_with?('simple')
|
61
|
+
computed_digest = "simple #{digests[:simple]}"
|
62
|
+
else
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
header == computed_digest
|
66
|
+
end
|
67
|
+
|
68
|
+
# Nil-safe version of File.expand_path
|
69
|
+
#
|
70
|
+
# @param path [String] Path to expand
|
71
|
+
#
|
72
|
+
# @return [String] Expanded path, or nil if path is nil.
|
73
|
+
def expand_path(path)
|
74
|
+
return nil unless path
|
75
|
+
|
76
|
+
File.expand_path path
|
77
|
+
end
|
78
|
+
|
79
|
+
# Helps interpret "@file" arguments. I.e. if the argument starts with an "@",
|
80
|
+
# read the contents of the filename given.
|
81
|
+
def read_at_arg_file(argument)
|
82
|
+
return nil if argument.nil?
|
83
|
+
return argument unless argument.start_with? '@'
|
84
|
+
|
85
|
+
file = argument[1..argument.size]
|
86
|
+
File.read file
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the current platform all lower-case.
|
90
|
+
# @return Typically 'ios', 'android', 'mac' or 'browser'
|
91
|
+
def get_current_platform
|
92
|
+
if Maze.mode == :browser
|
93
|
+
os = 'browser'
|
94
|
+
else
|
95
|
+
os = case Maze.config.farm
|
96
|
+
when :bs
|
97
|
+
Maze.config.capabilities['platformName']
|
98
|
+
else
|
99
|
+
Maze.config.os
|
100
|
+
end
|
101
|
+
os = os&.downcase
|
102
|
+
end
|
103
|
+
|
104
|
+
raise('Unable to determine the current platform') if os.nil?
|
105
|
+
|
106
|
+
os
|
107
|
+
end
|
108
|
+
|
109
|
+
# Logs the given message and exits the program with a failure status
|
110
|
+
def error_exit(message)
|
111
|
+
$logger.error message
|
112
|
+
exit false
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the name of the scenario to
|
116
|
+
# @param string [String] a string to convert to a file name
|
117
|
+
def to_friendly_filename(string)
|
118
|
+
string.gsub(/[:"& ]/, "_").gsub(/_+/, "_")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Contains logic for the Cucumber hooks when in Appium mode
|
2
|
+
module Maze
|
3
|
+
module Hooks
|
4
|
+
# Hooks for Appium mode use
|
5
|
+
class AppiumHooks < InternalHooks
|
6
|
+
@client
|
7
|
+
|
8
|
+
def before_all
|
9
|
+
@client = Maze::Client::Appium.start
|
10
|
+
end
|
11
|
+
|
12
|
+
def before(scenario)
|
13
|
+
@client.start_scenario
|
14
|
+
end
|
15
|
+
|
16
|
+
def after(scenario)
|
17
|
+
|
18
|
+
if Maze.config.os == 'macos'
|
19
|
+
# Close the app - without the sleep, launching the app for the next scenario intermittently fails
|
20
|
+
system("killall -KILL #{Maze.config.app} && sleep 1")
|
21
|
+
elsif [:bb, :bs, :local].include? Maze.config.farm
|
22
|
+
write_device_logs(scenario) if scenario.failed?
|
23
|
+
|
24
|
+
# appium_lib 12 says that reset is deprecated and activate_app/terminate_app should be used
|
25
|
+
# instead. However, they do not clear out app data, which we need between scenarios.
|
26
|
+
# install_app/remove_app might also be an option to consider.
|
27
|
+
Maze.driver.reset
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def after_all
|
32
|
+
@client&.log_run_outro
|
33
|
+
end
|
34
|
+
|
35
|
+
def at_exit
|
36
|
+
@client&.stop_session
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Pulls the device logs using Appium and writes them to file in the maze_output folder
|
42
|
+
def write_device_logs(scenario)
|
43
|
+
log_name = case Maze::Helper.get_current_platform
|
44
|
+
when 'android'
|
45
|
+
'logcat'
|
46
|
+
when 'ios'
|
47
|
+
'syslog'
|
48
|
+
end
|
49
|
+
logs = Maze.driver.get_log(log_name)
|
50
|
+
|
51
|
+
Maze::MazeOutput.new(scenario).write_device_logs(logs)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Contains logic for the Cucumber hooks when in Browser mode
|
2
|
+
module Maze
|
3
|
+
module Hooks
|
4
|
+
# Hooks for Browser mode use
|
5
|
+
class BrowserHooks < InternalHooks
|
6
|
+
def before_all
|
7
|
+
@client = Maze::Client::Selenium.start
|
8
|
+
end
|
9
|
+
|
10
|
+
def at_exit
|
11
|
+
@client.stop_session
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Hooks
|
5
|
+
# Registers an exit hook that will process the reason for an early exit and provide a suitable error code.
|
6
|
+
# Error code sets and specific code meanings are as follows:
|
7
|
+
# 1*: An error has occurred within browser or device drivers:
|
8
|
+
# 10: An unknown error has occurred
|
9
|
+
# 11: An expected UI element was missing
|
10
|
+
# 12: A UI element was missing at time of interaction
|
11
|
+
# 13: A command sent to the remote server timed out
|
12
|
+
# 14: An element was present but did not accept interaction
|
13
|
+
# 2*: Errors relating to potential network, test server, or payload issues
|
14
|
+
# 21: Expected payload(s) was not received
|
15
|
+
# 22: A command was not read by the test fixture
|
16
|
+
class ErrorCodeHook
|
17
|
+
class << self
|
18
|
+
|
19
|
+
attr_accessor :exit_code
|
20
|
+
attr_accessor :last_test_error_class
|
21
|
+
|
22
|
+
def register_exit_code_hook
|
23
|
+
return if @registered
|
24
|
+
at_exit do
|
25
|
+
exit_hook
|
26
|
+
end
|
27
|
+
@registered = true
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def exit_hook
|
33
|
+
override_exit_code = nil
|
34
|
+
|
35
|
+
maze_errors = Maze::Error::ERROR_CODES
|
36
|
+
if maze_errors.include?(last_test_error_class)
|
37
|
+
override_exit_code = maze_errors[last_test_error_class][:error_code]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if a specific error code has been registered elsewhere
|
41
|
+
override_exit_code = @exit_code if @exit_code
|
42
|
+
|
43
|
+
# If an override code is specified, use it, otherwise we'll use the native exit code
|
44
|
+
exit(override_exit_code) unless override_exit_code.nil?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Hooks
|
5
|
+
# Provides the ability for callbacks to be provided as part of running Cucumber.
|
6
|
+
# These are akin to Cucumber's BeforeAll, Before, After and AfterAll hooks, but are invoked in such a way that
|
7
|
+
# Maze Runner's hooks do not interfere with callbacks registered by clients.
|
8
|
+
class Hooks
|
9
|
+
def initialize
|
10
|
+
@before_all = []
|
11
|
+
@before = []
|
12
|
+
@after = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Register blocks to be called from a Cucumber BeforeAll hook (after MazeRunner does everything it needs to)
|
16
|
+
def before_all(&block)
|
17
|
+
@before_all << block
|
18
|
+
end
|
19
|
+
|
20
|
+
# Register blocks to be called from a Cucumber Before hook (after MazeRunner does everything it needs to)
|
21
|
+
def before(&block)
|
22
|
+
@before << block
|
23
|
+
end
|
24
|
+
|
25
|
+
# Register blocks to be called from a Cucumber After hook (before MazeRunner does everything it needs to)
|
26
|
+
def after(&block)
|
27
|
+
@after << block
|
28
|
+
end
|
29
|
+
|
30
|
+
# For MazeRunner use only, call the registered BeforeAll blocks
|
31
|
+
def call_before_all
|
32
|
+
@before_all.each(&:call)
|
33
|
+
end
|
34
|
+
|
35
|
+
# For MazeRunner use only, call the registered Before blocks
|
36
|
+
# @param scenario The current Cucumber scenario
|
37
|
+
def call_before(scenario)
|
38
|
+
@before.each { |block| block.call(scenario) }
|
39
|
+
end
|
40
|
+
|
41
|
+
# For MazeRunner use only, call the registered After blocks
|
42
|
+
# @param scenario The current Cucumber scenario
|
43
|
+
def call_after(scenario)
|
44
|
+
@after.each { |block| block.call(scenario) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Base class for hooks internal to Maze Runner
|
49
|
+
class InternalHooks
|
50
|
+
def before_all; end
|
51
|
+
|
52
|
+
def before(_scenario); end
|
53
|
+
|
54
|
+
def after(_scenario); end
|
55
|
+
|
56
|
+
def after_all; end
|
57
|
+
|
58
|
+
def at_exit; end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
class HttpRequest < SimpleDelegator
|
5
|
+
def body
|
6
|
+
@body ||= decode_body
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def decode_body
|
12
|
+
delegate = __getobj__
|
13
|
+
if %r{^gzip$}.match(delegate['Content-Encoding'])
|
14
|
+
gz_element = Zlib::GzipReader.new(StringIO.new(delegate.body))
|
15
|
+
gz_element.read
|
16
|
+
else
|
17
|
+
delegate.body
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'pty'
|
2
|
+
# TODO: Removed pending PLAT-6322
|
3
|
+
# require 'boring'
|
4
|
+
|
5
|
+
module Maze
|
6
|
+
# Encapsulates a shell session, retaining state and input streams for interactive tests
|
7
|
+
class InteractiveCLI
|
8
|
+
# @!attribute [r] stdout_lines
|
9
|
+
# @return [Array] An array of output strings received from the terminals STDOUT pipe
|
10
|
+
attr_reader :stdout_lines
|
11
|
+
|
12
|
+
# @!attribute [r] stderr_lines
|
13
|
+
# @return [Array] An array of error strings received from the terminals STDERR pipe
|
14
|
+
attr_reader :stderr_lines
|
15
|
+
|
16
|
+
# @!attribute [r] pid
|
17
|
+
# @return [Number, nil] The PID of the running terminal
|
18
|
+
attr_reader :pid
|
19
|
+
|
20
|
+
# @!attribute [r] current_buffer
|
21
|
+
# @return [String] A string representation of the current output present in the terminal
|
22
|
+
attr_reader :current_buffer
|
23
|
+
|
24
|
+
# Creates an InteractiveCLI instance
|
25
|
+
#
|
26
|
+
# @param shell [String] A path to the shell to run, defaults to `/bin/sh`
|
27
|
+
# @param stop_command [String] The stop command, defaults to `exit`
|
28
|
+
def initialize(shell = '/bin/sh', stop_command = 'exit')
|
29
|
+
@shell = shell
|
30
|
+
@stop_command = stop_command
|
31
|
+
@stdout_lines = []
|
32
|
+
@stderr_lines = []
|
33
|
+
@on_exit_blocks = []
|
34
|
+
@current_buffer = ''
|
35
|
+
# TODO: Removed pending PLAT-6322
|
36
|
+
# @boring = Boring.new
|
37
|
+
|
38
|
+
start_threaded_shell(shell)
|
39
|
+
end
|
40
|
+
|
41
|
+
def start(threaded: true)
|
42
|
+
threaded ? start_threaded_shell(@shell) : start_shell(@shell)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Attempts to stop the shell using the preset command and wait for it to exit
|
46
|
+
#
|
47
|
+
# @return [Boolean] If the shell stopped successfully
|
48
|
+
def stop
|
49
|
+
run_command(@stop_command)
|
50
|
+
|
51
|
+
@in_stream.close
|
52
|
+
|
53
|
+
maybe_thread = @thread.join(15)
|
54
|
+
|
55
|
+
# The thread did not exit!
|
56
|
+
return false if maybe_thread.nil?
|
57
|
+
|
58
|
+
@pid = nil
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Boolean] Whether the shell is currently running
|
63
|
+
def running?
|
64
|
+
!@pid.nil?
|
65
|
+
end
|
66
|
+
|
67
|
+
# Runs the given command if the shell is running
|
68
|
+
#
|
69
|
+
# @param command [String] The command to run
|
70
|
+
#
|
71
|
+
# @return [Boolean] true if the command is executed, false otherwise
|
72
|
+
def run_command(command)
|
73
|
+
return false unless running?
|
74
|
+
|
75
|
+
@in_stream.puts(command)
|
76
|
+
|
77
|
+
true
|
78
|
+
rescue ::Errno::EIO => err
|
79
|
+
$logger.debug(pid) { "EIO error: #{err}" }
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
def on_exit(&block)
|
84
|
+
@on_exit_blocks << block
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Starts a shell on another thread
|
90
|
+
#
|
91
|
+
# @param shell [String] A path to the shell to run
|
92
|
+
def start_threaded_shell(shell)
|
93
|
+
@thread = Thread.new do
|
94
|
+
start_shell(shell)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Starts a shell
|
99
|
+
#
|
100
|
+
# @param shell [String] A path to the shell to run
|
101
|
+
def start_shell(shell)
|
102
|
+
stderr_reader, stderr_writer = IO.pipe
|
103
|
+
|
104
|
+
PTY.spawn(shell, err: stderr_writer.fileno) do |stdout, stdin, pid|
|
105
|
+
# We don't need to write to stderr so close it ASAP
|
106
|
+
stderr_writer.close
|
107
|
+
|
108
|
+
$logger.debug(pid) { 'PTY spawned!' }
|
109
|
+
@pid = pid
|
110
|
+
@in_stream = stdin
|
111
|
+
|
112
|
+
stdout_thread = Thread.new do
|
113
|
+
stdout.each_char do |char|
|
114
|
+
if char == "\n"
|
115
|
+
line = format_line(@current_buffer)
|
116
|
+
|
117
|
+
$logger.debug("#{pid} STDOUT") { line.dump }
|
118
|
+
@stdout_lines << line
|
119
|
+
@current_buffer.clear
|
120
|
+
else
|
121
|
+
@current_buffer << char
|
122
|
+
end
|
123
|
+
end
|
124
|
+
rescue ::Errno::EIO => err
|
125
|
+
$logger.debug(pid) { "EIO error: #{err}" }
|
126
|
+
end
|
127
|
+
|
128
|
+
stderr_thread = Thread.new do
|
129
|
+
buffer = ''
|
130
|
+
|
131
|
+
stderr_reader.each_char do |char|
|
132
|
+
if char == "\n"
|
133
|
+
line = format_line(buffer)
|
134
|
+
|
135
|
+
$logger.debug("#{pid} STDERR") { line.dump }
|
136
|
+
@stderr_lines << line
|
137
|
+
buffer.clear
|
138
|
+
else
|
139
|
+
buffer << char
|
140
|
+
end
|
141
|
+
end
|
142
|
+
rescue ::Errno::EIO => err
|
143
|
+
$logger.debug(pid) { "EIO error: #{err}" }
|
144
|
+
end
|
145
|
+
|
146
|
+
_, status = Process.wait2(@pid)
|
147
|
+
@pid = nil
|
148
|
+
|
149
|
+
# Stop the thread that's reading from stdout
|
150
|
+
failed = stdout_thread.join(5).nil?
|
151
|
+
raise 'stdout is blocked!' if failed
|
152
|
+
|
153
|
+
# Stop the thread that's reading from stderr
|
154
|
+
failed = stderr_thread.join(5).nil?
|
155
|
+
raise 'stderr is blocked!' if failed
|
156
|
+
|
157
|
+
$logger.debug(pid) { "PTY exit status: #{status.exitstatus}" }
|
158
|
+
@on_exit_blocks.each do |block|
|
159
|
+
block.call(status.exitstatus)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
ensure
|
163
|
+
stderr_reader.close unless stderr_reader.closed?
|
164
|
+
stderr_writer.close unless stderr_writer.closed?
|
165
|
+
end
|
166
|
+
|
167
|
+
def format_line(line)
|
168
|
+
# TODO: Removed pending PLAT-6322
|
169
|
+
# @boring.scrub(line.strip)
|
170
|
+
line.strip
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/maze/logger.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
# Logger classes
|
7
|
+
module Maze
|
8
|
+
# A logger, with level configured according to the environment
|
9
|
+
class Logger < Logger
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
attr_accessor :datetime_format
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
if ENV['VERBOSE'] || ENV['DEBUG']
|
16
|
+
super(STDOUT, level: Logger::DEBUG)
|
17
|
+
elsif ENV['QUIET']
|
18
|
+
super(STDOUT, level: Logger::ERROR)
|
19
|
+
else
|
20
|
+
super(STDOUT, level: Logger::INFO)
|
21
|
+
end
|
22
|
+
|
23
|
+
@datetime_format = '%H:%M:%S'
|
24
|
+
|
25
|
+
@formatter = proc do |severity, time, _name, message|
|
26
|
+
formatted_time = time.strftime(@datetime_format)
|
27
|
+
|
28
|
+
"\e[2m[#{formatted_time}]\e[0m #{severity.rjust(5)}: #{message}\n"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
$logger = Maze::Logger.instance
|
34
|
+
|
35
|
+
# A collection of logging utilities
|
36
|
+
class LogUtil
|
37
|
+
class << self
|
38
|
+
# Logs Hash-based data, accounting for things like file upload requests that are too big to log meaningfully.
|
39
|
+
#
|
40
|
+
# @param severity [Integer] A constant from Logger::Severity
|
41
|
+
# @param data [Hash] The data to log (currently needs to be a Hash)
|
42
|
+
def log_hash(severity, data)
|
43
|
+
return unless data.is_a? Hash
|
44
|
+
|
45
|
+
# Try to pretty print as JSON, if not too big
|
46
|
+
begin
|
47
|
+
json = JSON.pretty_generate data
|
48
|
+
if json.length < 128 * 1024
|
49
|
+
$logger.add severity, json
|
50
|
+
else
|
51
|
+
log_hash_by_field severity, data
|
52
|
+
end
|
53
|
+
rescue Encoding::UndefinedConversionError
|
54
|
+
log_hash_by_field severity, data
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Logs a hash field by field,
|
59
|
+
#
|
60
|
+
# @param severity [Integer] A Logger::Severity
|
61
|
+
# @param hash [Hash] The Hash
|
62
|
+
def log_hash_by_field(severity, hash)
|
63
|
+
hash.keys.each do |key|
|
64
|
+
value = hash[key].to_s
|
65
|
+
if value.length < 1024
|
66
|
+
$logger.add severity, " #{key}: #{value}"
|
67
|
+
else
|
68
|
+
$logger.add severity, " #{key} (length): #{value.length}"
|
69
|
+
$logger.add severity, " #{key} (start): #{value[0, 1024]}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Produces a clickable link when logged in Buildkite
|
75
|
+
# @param url [String] Link URL
|
76
|
+
# @param text [String] Link text
|
77
|
+
def linkify(url, text)
|
78
|
+
if ENV['BUILDKITE']
|
79
|
+
"\033]1339;url='#{url}';content='#{text}'\a"
|
80
|
+
else
|
81
|
+
"#{text}: #{url}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|