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
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pty'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Maze
|
7
|
+
# Basic shell that runs an Appium server on a separate thread
|
8
|
+
class AppiumServer
|
9
|
+
class << self
|
10
|
+
# @return [string|nil] The PID of the appium process (if available)
|
11
|
+
attr_reader :pid
|
12
|
+
|
13
|
+
# @return [thread|nil] The thread running the appium process (if available)
|
14
|
+
attr_reader :appium_thread
|
15
|
+
|
16
|
+
# @return [Logger|nil] The logger used for creating the log file
|
17
|
+
attr_reader :appium_logger
|
18
|
+
|
19
|
+
# Starts a separate thread running the appium server so long as:
|
20
|
+
# - An instance of the appium server isn't already running
|
21
|
+
# - The port configured is available
|
22
|
+
# - The appium command is available via CLI
|
23
|
+
#
|
24
|
+
# @param address [String] The IP address on which to start the appium server
|
25
|
+
# @param port [String] The port on which to start the appium server
|
26
|
+
def start(address: '0.0.0.0', port: '4723')
|
27
|
+
return if running
|
28
|
+
|
29
|
+
# Check if the appium server appears to be running already, warning and carrying on if so
|
30
|
+
unless appium_port_available?(port)
|
31
|
+
$logger.warn "Requested appium port:#{port} is in use. Aborting built-in appium server launch"
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
# Check if appium is installed, warning if not
|
36
|
+
unless appium_available?
|
37
|
+
$logger.warn 'Appium is unavailable to be started from the command line. Install using `npm i -g appium`'
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
start_logger
|
42
|
+
|
43
|
+
command = "appium -a #{address} -p #{port}"
|
44
|
+
@appium_thread = Thread.new do
|
45
|
+
PTY.spawn(command) do |stdout, _stdin, pid|
|
46
|
+
@pid = pid
|
47
|
+
$logger.debug("Appium:#{@pid}") { 'Appium server started' }
|
48
|
+
stdout.each do |line|
|
49
|
+
log_line(line)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Temporary sleep to allow appium to start
|
55
|
+
sleep 2
|
56
|
+
end
|
57
|
+
|
58
|
+
# Checks whether the server is running, as indicated by the @pid and the appium thread being alive
|
59
|
+
#
|
60
|
+
# @return [Boolean] Whether the local appium server is running
|
61
|
+
def running
|
62
|
+
@appium_thread&.alive? ? true : false
|
63
|
+
end
|
64
|
+
|
65
|
+
# Stops the appium server, if running, using SIGINT for correct shutdown
|
66
|
+
def stop
|
67
|
+
return unless running
|
68
|
+
|
69
|
+
$logger.debug("Appium:#{@pid}") { 'Stopping appium server' }
|
70
|
+
Process.kill('INT', @pid)
|
71
|
+
@pid = nil
|
72
|
+
@appium_thread.join
|
73
|
+
@appium_thread = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Checks if the `appium` command is available on CI
|
79
|
+
#
|
80
|
+
# @return [Boolean] Whether the appium command is available
|
81
|
+
def appium_available?
|
82
|
+
`appium -v`
|
83
|
+
true
|
84
|
+
rescue Errno::ENOENT
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
# Starts the logger targeting a file defined by the APPIUM_LOGFILE config option
|
89
|
+
def start_logger
|
90
|
+
@appium_logger = ::Logger.new(Maze.config.appium_logfile)
|
91
|
+
@appium_logger.datetime_format = '%Y-%m-%d %H:%M:%S'
|
92
|
+
end
|
93
|
+
|
94
|
+
# Logs to a known file, creating the outstream if it isn't already present
|
95
|
+
#
|
96
|
+
# @param line [String] The line to log
|
97
|
+
def log_line(line)
|
98
|
+
return if @appium_logger.nil?
|
99
|
+
@appium_logger.info("Appium:#{@pid}") { line }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Checks if the given port is already in use
|
103
|
+
#
|
104
|
+
# @param port [String] The port that should be available
|
105
|
+
#
|
106
|
+
# @return [Boolean] Whether something is running on the given port
|
107
|
+
def appium_port_available?(port)
|
108
|
+
`netstat -vanp tcp | awk '{ print $4 }' | grep "\.#{port}$"`.empty?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require_relative '../helper'
|
5
|
+
|
6
|
+
module Maze
|
7
|
+
module Assertions
|
8
|
+
# Provides helper routines for checking sets of requests against values in a table.
|
9
|
+
class RequestSetAssertions
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Checks that a set of requests satisfy the properties expressed by the table given.
|
13
|
+
#
|
14
|
+
# @param requests [Hash[]] Requests to check
|
15
|
+
# @param table [Cucumber::MultilineArgument::DataTable] Table of expected values, where:
|
16
|
+
# - headings can be provided as key paths (e.g. events.0.breadcrumbs.0.name)
|
17
|
+
# - table values can be written as "null" for nil
|
18
|
+
def assert_requests_match(requests, table)
|
19
|
+
Maze.check.equal(table.hashes.length,
|
20
|
+
requests.length,
|
21
|
+
'Number of requests do not match number of entries in table.')
|
22
|
+
matches = matching_rows requests, table
|
23
|
+
return if matches.length == table.hashes.length
|
24
|
+
|
25
|
+
# Not all matched - log diagnostic before failing assertion
|
26
|
+
$logger.error "Only #{matches.length} of #{requests.length} matched:"
|
27
|
+
$logger.info matches.keys.sort
|
28
|
+
matches.sort.to_h.each do |row, request|
|
29
|
+
$logger.info "#{table.rows[row]} matched by request element #{matches[request]}"
|
30
|
+
end
|
31
|
+
Maze.check.equal(requests.length, matches.length, 'Not all requests matched a row in the table.')
|
32
|
+
end
|
33
|
+
|
34
|
+
# Given arrays of requests and table-based criteria, determines which rows of the table
|
35
|
+
# are satisfied by one of the requests. Where multiple rows in the table specify the same
|
36
|
+
# criteria, there must be multiple requests provided to satisfy each.
|
37
|
+
#
|
38
|
+
# @param requests [Hash[]] Requests to check
|
39
|
+
# @param table [Cucumber::MultilineArgument::DataTable] Table of expected values, where:
|
40
|
+
# - headings can be provided as key paths (e.g. events.0.breadcrumbs.0.name)
|
41
|
+
# - table values can be written as "null" for nil
|
42
|
+
# @return [Hash] A hash of row to request indexes, indicating the first request matching each row.
|
43
|
+
# E.g. {0 => 2} means that the first row was satisfied by the 3rd request.
|
44
|
+
def matching_rows(requests, table)
|
45
|
+
|
46
|
+
# iterate through each row in the table. exactly 1 request should match each row.
|
47
|
+
row_to_request_matches = {}
|
48
|
+
table.hashes.each_with_index do |row, row_index|
|
49
|
+
requests.each_with_index do |request, request_index|
|
50
|
+
# Skip if row already matched
|
51
|
+
next if row_to_request_matches.values.include? request_index
|
52
|
+
# Skip if no body in this request
|
53
|
+
next unless request.key?(:body)
|
54
|
+
next unless request_matches_row(request[:body], row)
|
55
|
+
|
56
|
+
# Record the match
|
57
|
+
row_to_request_matches[row_index] = request_index
|
58
|
+
end
|
59
|
+
end
|
60
|
+
row_to_request_matches
|
61
|
+
end
|
62
|
+
|
63
|
+
# Determines if a request body satisfies the criteria expressed by a row.
|
64
|
+
# The special string "null" is interpreted as nil in comparisons and
|
65
|
+
# regular expressions are assumed is the start and end of the string is a '/'.
|
66
|
+
#
|
67
|
+
# @param body [Hash] Request body to consider
|
68
|
+
# @param row [Hash] Hash of keys to expected value, where the keys given can
|
69
|
+
# be a Mongo-style dot notation path.
|
70
|
+
def request_matches_row(body, row)
|
71
|
+
row.each do |key, expected_value|
|
72
|
+
obs_val = Maze::Helper.read_key_path(body, key)
|
73
|
+
next if ('null'.eql? expected_value) && obs_val.nil? # Both are null/nil
|
74
|
+
next if ('@not_null'.eql? expected_value) && !obs_val.nil? # The value isn't null
|
75
|
+
|
76
|
+
unless obs_val.nil?
|
77
|
+
if expected_value[0] == '/' && expected_value[-1] == '/'
|
78
|
+
# Treat as regexp
|
79
|
+
regex = Regexp.new expected_value[1, expected_value.length - 2]
|
80
|
+
next if regex.match? obs_val.to_s # Value matches regex
|
81
|
+
elsif expected_value.eql? obs_val.to_s
|
82
|
+
# Values match
|
83
|
+
next
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Match not found - return false
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
# All matched - return true
|
91
|
+
true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
data/lib/maze/aws/sam.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module Maze
|
7
|
+
module Aws
|
8
|
+
# Interacts with the AWS SAM CLI to invoke Lambda functions
|
9
|
+
# Note that the SAM CLI must be installed on the host machine as it does not
|
10
|
+
# run in a Docker container! For this reason the "start-api" command is not
|
11
|
+
# supported as it could easily cause port clashes and zombie processes
|
12
|
+
class Sam
|
13
|
+
class << self
|
14
|
+
attr_reader :last_response, :last_exit_code
|
15
|
+
|
16
|
+
# Invoke the given lambda with an optional event
|
17
|
+
#
|
18
|
+
# This happens synchronously so there is no need to wait for a response
|
19
|
+
#
|
20
|
+
# @param directory [String] The directory containing the lambda
|
21
|
+
# @param lambda [String] The name of the lambda to invoke
|
22
|
+
# @param event [String, nil] An optional event file to invoke with
|
23
|
+
#
|
24
|
+
# @return [void]
|
25
|
+
def invoke(directory, lambda, event = nil)
|
26
|
+
command = build_invoke_command(lambda, event)
|
27
|
+
|
28
|
+
output, @last_exit_code = Maze::Runner.run_command("cd #{directory} && #{command}")
|
29
|
+
|
30
|
+
@last_response = parse(output)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Reset the last response and last exit code
|
34
|
+
#
|
35
|
+
# @return [void]
|
36
|
+
def reset!
|
37
|
+
@last_response = nil
|
38
|
+
@last_exit_code = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Build the command to invoke the given lambda with the given event
|
44
|
+
#
|
45
|
+
# @param lambda [String] The name of the lambda to invoke
|
46
|
+
# @param event [String, nil] An optional event file to invoke with
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
def build_invoke_command(lambda, event)
|
50
|
+
command = "sam local invoke #{Shellwords.escape(lambda)}"
|
51
|
+
command += " --event #{Shellwords.escape(event)}" unless event.nil?
|
52
|
+
command += " --docker-network #{Shellwords.escape(ENV['NETWORK_NAME'])}" if ENV.key?('NETWORK_NAME')
|
53
|
+
|
54
|
+
command
|
55
|
+
end
|
56
|
+
|
57
|
+
# The command output contains all stdout/stderr lines in an array. The
|
58
|
+
# Lambda response is the last line of output as JSON. The response body is
|
59
|
+
# also JSON, so we have to parse twice to get a Hash from the output
|
60
|
+
#
|
61
|
+
# @param output [Array<String>] The command's output as an array of lines
|
62
|
+
#
|
63
|
+
# @return [Hash]
|
64
|
+
def parse(output)
|
65
|
+
unless valid?(output)
|
66
|
+
raise <<~ERROR
|
67
|
+
Unable to parse Lambda output!
|
68
|
+
The likely cause is:
|
69
|
+
> #{output.last.chomp}
|
70
|
+
|
71
|
+
Full output:
|
72
|
+
> #{output.map(&:chomp).join("\n > ")}
|
73
|
+
ERROR
|
74
|
+
end
|
75
|
+
|
76
|
+
# Attempt to parse the last line of output as this is where a JSON
|
77
|
+
# response would be. It's possible for a Lambda to output nothing,
|
78
|
+
# e.g. if it forcefully exited, so we allow JSON parse failures here
|
79
|
+
begin
|
80
|
+
parsed_output = JSON.parse(output.last)
|
81
|
+
rescue JSON::ParserError
|
82
|
+
return {}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Error output has no "body" of additional JSON so we can stop here
|
86
|
+
return parsed_output unless parsed_output.key?('body')
|
87
|
+
|
88
|
+
# The body is _usually_ JSON but doesn't have to be. We attempt to
|
89
|
+
# parse it anyway because it allows us to assert against it easily,
|
90
|
+
# but if this fails then it may just be in another format, e.g. HTML
|
91
|
+
begin
|
92
|
+
parsed_output['body'] = JSON.parse(parsed_output['body'])
|
93
|
+
rescue JSON::ParserError
|
94
|
+
# Ignore
|
95
|
+
end
|
96
|
+
|
97
|
+
parsed_output
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check if the output looks valid. There should be a "END" marker with a
|
101
|
+
# request ID if the lambda invocation completed successfully
|
102
|
+
#
|
103
|
+
# @param output [Array<String>] The command's output as an array of lines
|
104
|
+
#
|
105
|
+
# @return [Boolean]
|
106
|
+
def valid?(output)
|
107
|
+
output.any? { |line| line =~ /^END RequestId:/ }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Maze
|
2
|
+
# Determines the public IP address and port when running on Buildkite with the Elastic CI Stack for AWS
|
3
|
+
class AwsPublicIp
|
4
|
+
attr_reader :address
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
# This class is only relevant on Buildkite
|
8
|
+
return unless ENV['BUILDKITE']
|
9
|
+
|
10
|
+
ip = determine_public_ip
|
11
|
+
port = determine_public_port
|
12
|
+
|
13
|
+
@address = "#{ip}:#{port}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Determines the public IP address of the running AWS instance
|
17
|
+
def determine_public_ip
|
18
|
+
# 169.254.169.254 is the address of the AWS instance metadata service
|
19
|
+
# See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
|
20
|
+
`curl --silent -XGET http://169.254.169.254/latest/meta-data/public-ipv4`
|
21
|
+
end
|
22
|
+
|
23
|
+
# Determines the external port of the running Docker container that's associated with the port of the mock server
|
24
|
+
def determine_public_port
|
25
|
+
port = 0
|
26
|
+
count = 0
|
27
|
+
max_attempts = 30
|
28
|
+
|
29
|
+
# Give up after 30 seconds
|
30
|
+
while port == 0 && count < max_attempts do
|
31
|
+
hostname = ENV['HOSTNAME']
|
32
|
+
command = "curl --silent -XGET --unix-socket /var/run/docker.sock http://localhost/containers/#{hostname}/json"
|
33
|
+
result = Maze::Runner.run_command(command)
|
34
|
+
if result[1] == 0
|
35
|
+
begin
|
36
|
+
json_string = result[0][0].strip
|
37
|
+
json_result = JSON.parse(json_string)
|
38
|
+
port = json_result['NetworkSettings']['Ports']["#{Maze.config.port}/tcp"][0]['HostPort']
|
39
|
+
rescue StandardError
|
40
|
+
$logger.error "Unable to parse public port from: #{json_string}"
|
41
|
+
return 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
count += 1
|
46
|
+
sleep 1 if port == 0 && count < max_attempts
|
47
|
+
end
|
48
|
+
$logger.error "Failed to determine public port within #{max_attempts} attempts" if port == 0 && count == max_attempts
|
49
|
+
|
50
|
+
port
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'bugsnag'
|
2
|
+
|
3
|
+
# Contains logic for running Bugsnag
|
4
|
+
module Maze
|
5
|
+
class BugsnagConfig
|
6
|
+
class << self
|
7
|
+
def start_bugsnag(cucumber_config)
|
8
|
+
# Use MAZE_BUGSNAG_API_KEY explicitly to avoid collisions with test env
|
9
|
+
return unless Maze.config.enable_bugsnag && ENV['MAZE_BUGSNAG_API_KEY']
|
10
|
+
|
11
|
+
Bugsnag.configure do |config|
|
12
|
+
config.api_key = ENV['MAZE_BUGSNAG_API_KEY']
|
13
|
+
config.discard_classes << 'Test::Unit::AssertionFailedError'
|
14
|
+
config.add_metadata(:'test driver', {
|
15
|
+
'driver type': Maze.driver.class,
|
16
|
+
'device farm': Maze.config.farm,
|
17
|
+
'capabilities': Maze.config.capabilities
|
18
|
+
}) if Maze.driver
|
19
|
+
config.add_metadata(:'buildkite', {
|
20
|
+
'pipeline': ENV['BUILDKITE_PIPELINE_NAME'],
|
21
|
+
'repo': ENV['BUILDKITE_REPO'],
|
22
|
+
'build url': ENV['BUILDKITE_BUILD_URL'],
|
23
|
+
'branch': ENV['BUILDKITE_BRANCH'],
|
24
|
+
'builder': ENV['BUILDKITE_BUILD_CREATOR'],
|
25
|
+
'message': ENV['BUILDKITE_MESSAGE'],
|
26
|
+
'step': ENV['BUILDKITE_LABEL']
|
27
|
+
}) if ENV['BUILDKITE']
|
28
|
+
config.project_root = Dir.pwd
|
29
|
+
end
|
30
|
+
|
31
|
+
Bugsnag.start_session
|
32
|
+
|
33
|
+
at_exit do
|
34
|
+
if $!
|
35
|
+
Bugsnag.notify($!)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
module Maze
|
5
|
+
module Checks
|
6
|
+
# Assertion-backed data verification checks
|
7
|
+
class AssertCheck
|
8
|
+
include Test::Unit::Assertions
|
9
|
+
|
10
|
+
def true(test, message = nil)
|
11
|
+
assert_true(test, message)
|
12
|
+
end
|
13
|
+
|
14
|
+
def false(test, message = nil)
|
15
|
+
assert_false(test, message)
|
16
|
+
end
|
17
|
+
|
18
|
+
def nil(test, message = nil)
|
19
|
+
assert_nil(test, message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def not_nil(test, message = nil)
|
23
|
+
assert_not_nil(test, message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def match(pattern, string, message = nil)
|
27
|
+
regexp = if pattern.class == Regexp
|
28
|
+
pattern
|
29
|
+
else
|
30
|
+
Regexp.new(pattern)
|
31
|
+
end
|
32
|
+
if message.nil?
|
33
|
+
message = "<#{string}> was not matched by regex <#{pattern}>"
|
34
|
+
end
|
35
|
+
assert_match(regexp, string, message)
|
36
|
+
end
|
37
|
+
|
38
|
+
def equal(expected, act, message = nil)
|
39
|
+
assert_equal(expected, act, message)
|
40
|
+
end
|
41
|
+
|
42
|
+
def not_equal(expected, act, message = nil)
|
43
|
+
assert_not_equal(expected, act, message)
|
44
|
+
end
|
45
|
+
|
46
|
+
def operator(operand1, operator, operand2, message = nil)
|
47
|
+
assert_operator(operand1, operator, operand2, message)
|
48
|
+
end
|
49
|
+
|
50
|
+
def kind_of(klass, object, message = nil)
|
51
|
+
assert_kind_of(klass, object, message)
|
52
|
+
end
|
53
|
+
|
54
|
+
def block(message = 'block failed', &block)
|
55
|
+
assert_block(message, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def include(collection, object, message = nil)
|
59
|
+
assert_include(collection, object, message)
|
60
|
+
end
|
61
|
+
alias includes include
|
62
|
+
|
63
|
+
def not_include(collection, object, message = nil)
|
64
|
+
assert_not_include(collection, object, message)
|
65
|
+
end
|
66
|
+
alias not_includes not_include
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Checks
|
5
|
+
# Assertion-backed data verification checks
|
6
|
+
class NoopCheck
|
7
|
+
def true(_test, _message = nil) end
|
8
|
+
|
9
|
+
def false(_test, _message = nil) end
|
10
|
+
|
11
|
+
def nil(_test, _message = nil) end
|
12
|
+
|
13
|
+
def not_nil(_test, _message = nil) end
|
14
|
+
|
15
|
+
def match(_pattern, _string, _message = nil) end
|
16
|
+
|
17
|
+
def equal(_expected, _actual, _message = nil) end
|
18
|
+
|
19
|
+
def not_equal(_expected, _actual, _message = nil) end
|
20
|
+
|
21
|
+
def operator(_operand1, _operator, _operand2, _message = nil) end
|
22
|
+
|
23
|
+
def kind_of(_klass, _object, _message = nil) end
|
24
|
+
|
25
|
+
def block(_message = 'block failed', &_block) end
|
26
|
+
|
27
|
+
def include(_collection, _object, _message = nil) end
|
28
|
+
alias includes include
|
29
|
+
|
30
|
+
def not_include(_collection, _object, _message = nil) end
|
31
|
+
alias not_includes not_include
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Client
|
5
|
+
module Appium
|
6
|
+
class BaseClient
|
7
|
+
FIXTURE_CONFIG = 'fixture_config.json'
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@session_ids = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def start_session
|
14
|
+
prepare_session
|
15
|
+
|
16
|
+
start_driver(Maze.config)
|
17
|
+
|
18
|
+
# Set bundle/app id for later use
|
19
|
+
Maze.driver.app_id = case Maze::Helper.get_current_platform
|
20
|
+
when 'android'
|
21
|
+
Maze.driver.session_capabilities['appPackage']
|
22
|
+
when 'ios'
|
23
|
+
Maze.driver.session_capabilities['CFBundleIdentifier'] # Present on BS and locally
|
24
|
+
end
|
25
|
+
# Ensure the device is unlocked
|
26
|
+
Maze.driver.unlock
|
27
|
+
|
28
|
+
log_run_intro
|
29
|
+
end
|
30
|
+
|
31
|
+
def prepare_session
|
32
|
+
raise 'Method not implemented by this class'
|
33
|
+
end
|
34
|
+
|
35
|
+
def maze_address
|
36
|
+
raise 'Method not implemented by this class'
|
37
|
+
end
|
38
|
+
|
39
|
+
def start_driver(config)
|
40
|
+
retry_failure = config.device_list.nil? || config.device_list.empty?
|
41
|
+
driver = nil
|
42
|
+
until Maze.driver
|
43
|
+
begin
|
44
|
+
start_driver_closure = Proc.new do
|
45
|
+
begin
|
46
|
+
config.capabilities = device_capabilities
|
47
|
+
driver = Maze::Driver::Appium.new config.appium_server_url,
|
48
|
+
config.capabilities,
|
49
|
+
config.locator
|
50
|
+
|
51
|
+
result = driver.start_driver
|
52
|
+
if result
|
53
|
+
# Log details of this session
|
54
|
+
$logger.info "Created Appium session: #{driver.session_id}"
|
55
|
+
@session_ids << driver.session_id
|
56
|
+
udid = driver.session_capabilities['udid']
|
57
|
+
$logger.info "Running on device: #{udid}" unless udid.nil?
|
58
|
+
end
|
59
|
+
result
|
60
|
+
rescue => start_error
|
61
|
+
$logger.error "Session creation failed: #{start_error}"
|
62
|
+
raise start_error unless retry_failure
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if retry_failure
|
68
|
+
wait = Maze::Wait.new(interval: 10, timeout: 60)
|
69
|
+
success = wait.until(&start_driver_closure)
|
70
|
+
|
71
|
+
unless success
|
72
|
+
$logger.error 'Appium driver failed to start after 6 attempts in 60 seconds'
|
73
|
+
raise RuntimeError.new('Appium driver failed to start in 60 seconds')
|
74
|
+
end
|
75
|
+
else
|
76
|
+
start_driver_closure.call
|
77
|
+
end
|
78
|
+
|
79
|
+
# Infer OS version if necessary when running locally
|
80
|
+
if Maze.config.farm == :local && Maze.config.os_version.nil?
|
81
|
+
version = case Maze.config.os
|
82
|
+
when 'android'
|
83
|
+
driver.session_capabilities['platformVersion'].to_f
|
84
|
+
when 'ios'
|
85
|
+
driver.session_capabilities['sdkVersion'].to_f
|
86
|
+
end
|
87
|
+
$logger.info "Inferred OS version to be #{version}"
|
88
|
+
Maze.config.os_version = version
|
89
|
+
end
|
90
|
+
|
91
|
+
Maze.driver = driver
|
92
|
+
rescue ::Selenium::WebDriver::Error::UnknownError => original_exception
|
93
|
+
$logger.warn "Attempt to acquire #{config.device} device from farm #{config.farm} failed"
|
94
|
+
$logger.warn "Exception: #{original_exception.message}"
|
95
|
+
if config.device_list.empty?
|
96
|
+
$logger.error 'No further devices to try - raising original exception'
|
97
|
+
raise original_exception
|
98
|
+
else
|
99
|
+
config.device = config.device_list.first
|
100
|
+
config.device_list = config.device_list.drop(1)
|
101
|
+
$logger.warn "Retrying driver initialisation using next device: #{config.device}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def start_scenario
|
108
|
+
# Launch the app on macOS
|
109
|
+
Maze.driver.get(Maze.config.app) if Maze.config.os == 'macos'
|
110
|
+
end
|
111
|
+
|
112
|
+
def device_capabilities
|
113
|
+
raise 'Method not implemented by this class'
|
114
|
+
end
|
115
|
+
|
116
|
+
def log_run_intro
|
117
|
+
raise 'Method not implemented by this class'
|
118
|
+
end
|
119
|
+
|
120
|
+
def log_run_outro
|
121
|
+
raise 'Method not implemented by this class'
|
122
|
+
end
|
123
|
+
|
124
|
+
def stop_session
|
125
|
+
Maze.driver&.driver_quit
|
126
|
+
Maze::AppiumServer.stop if Maze::AppiumServer.running
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|