bugsnag-maze-runner 7.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +74 -0
  4. data/bin/maze-runner +174 -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 +80 -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 +358 -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 +135 -0
  25. data/lib/features/steps/payload_steps.rb +257 -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 +186 -0
  29. data/lib/features/steps/runner_steps.rb +428 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/trace_steps.rb +206 -0
  32. data/lib/features/steps/value_steps.rb +119 -0
  33. data/lib/features/support/env.rb +7 -0
  34. data/lib/features/support/internal_hooks.rb +207 -0
  35. data/lib/maze/api/appium/file_manager.rb +29 -0
  36. data/lib/maze/appium_server.rb +112 -0
  37. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  38. data/lib/maze/aws/sam.rb +112 -0
  39. data/lib/maze/aws_public_ip.rb +53 -0
  40. data/lib/maze/bugsnag_config.rb +42 -0
  41. data/lib/maze/checks/assert_check.rb +69 -0
  42. data/lib/maze/checks/noop_check.rb +34 -0
  43. data/lib/maze/client/appium/base_client.rb +131 -0
  44. data/lib/maze/client/appium/bb_client.rb +102 -0
  45. data/lib/maze/client/appium/bb_devices.rb +127 -0
  46. data/lib/maze/client/appium/bs_client.rb +91 -0
  47. data/lib/maze/client/appium/bs_devices.rb +141 -0
  48. data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
  49. data/lib/maze/client/appium/local_client.rb +67 -0
  50. data/lib/maze/client/appium.rb +23 -0
  51. data/lib/maze/client/bb_api_client.rb +102 -0
  52. data/lib/maze/client/bb_client_utils.rb +181 -0
  53. data/lib/maze/client/bs_client_utils.rb +168 -0
  54. data/lib/maze/client/selenium/base_client.rb +15 -0
  55. data/lib/maze/client/selenium/bb_browsers.yml +188 -0
  56. data/lib/maze/client/selenium/bb_client.rb +38 -0
  57. data/lib/maze/client/selenium/bs_browsers.yml +257 -0
  58. data/lib/maze/client/selenium/bs_client.rb +89 -0
  59. data/lib/maze/client/selenium/local_client.rb +16 -0
  60. data/lib/maze/client/selenium.rb +16 -0
  61. data/lib/maze/compare.rb +161 -0
  62. data/lib/maze/configuration.rb +182 -0
  63. data/lib/maze/docker.rb +147 -0
  64. data/lib/maze/document_server.rb +46 -0
  65. data/lib/maze/driver/appium.rb +198 -0
  66. data/lib/maze/driver/browser.rb +124 -0
  67. data/lib/maze/errors.rb +52 -0
  68. data/lib/maze/generator.rb +55 -0
  69. data/lib/maze/helper.rb +122 -0
  70. data/lib/maze/hooks/appium_hooks.rb +55 -0
  71. data/lib/maze/hooks/browser_hooks.rb +15 -0
  72. data/lib/maze/hooks/command_hooks.rb +9 -0
  73. data/lib/maze/hooks/error_code_hook.rb +49 -0
  74. data/lib/maze/hooks/hooks.rb +61 -0
  75. data/lib/maze/http_request.rb +21 -0
  76. data/lib/maze/interactive_cli.rb +173 -0
  77. data/lib/maze/logger.rb +86 -0
  78. data/lib/maze/macos_utils.rb +14 -0
  79. data/lib/maze/maze_output.rb +88 -0
  80. data/lib/maze/network.rb +49 -0
  81. data/lib/maze/option/parser.rb +240 -0
  82. data/lib/maze/option/processor.rb +130 -0
  83. data/lib/maze/option/validator.rb +155 -0
  84. data/lib/maze/option.rb +62 -0
  85. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  86. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  87. data/lib/maze/plugins/error_code_plugin.rb +21 -0
  88. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  89. data/lib/maze/proxy.rb +114 -0
  90. data/lib/maze/request_list.rb +87 -0
  91. data/lib/maze/request_repeater.rb +49 -0
  92. data/lib/maze/retry_handler.rb +67 -0
  93. data/lib/maze/runner.rb +149 -0
  94. data/lib/maze/schemas/OtelTraceSchema.json +390 -0
  95. data/lib/maze/schemas/trace_schema.rb +7 -0
  96. data/lib/maze/schemas/trace_validator.rb +98 -0
  97. data/lib/maze/server.rb +251 -0
  98. data/lib/maze/servlets/base_servlet.rb +27 -0
  99. data/lib/maze/servlets/command_servlet.rb +47 -0
  100. data/lib/maze/servlets/log_servlet.rb +64 -0
  101. data/lib/maze/servlets/reflective_servlet.rb +70 -0
  102. data/lib/maze/servlets/servlet.rb +199 -0
  103. data/lib/maze/servlets/temp.rb +0 -0
  104. data/lib/maze/servlets/trace_servlet.rb +13 -0
  105. data/lib/maze/store.rb +15 -0
  106. data/lib/maze/terminating_server.rb +129 -0
  107. data/lib/maze/timers.rb +51 -0
  108. data/lib/maze/wait.rb +35 -0
  109. data/lib/maze.rb +27 -0
  110. data/lib/utils/deep_merge.rb +17 -0
  111. data/lib/utils/selenium_money_patch.rb +17 -0
  112. metadata +451 -0
@@ -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
@@ -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,9 @@
1
+ # Contains logic for the Cucumber hooks when in Command mode
2
+ module Maze
3
+ module Hooks
4
+ # Hooks for Command mode use
5
+ class CommandHooks < InternalHooks
6
+ # Nothing required at present
7
+ end
8
+ end
9
+ 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
@@ -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