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,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
+
@@ -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