bugsnag-maze-runner 6.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +76 -0
  4. data/bin/maze-runner +136 -0
  5. data/bin/upload-app +56 -0
  6. data/lib/features/scripts/await-android-emulator.sh +11 -0
  7. data/lib/features/scripts/clear-android-app-data.sh +8 -0
  8. data/lib/features/scripts/force-stop-android-app.sh +8 -0
  9. data/lib/features/scripts/install-android-app.sh +15 -0
  10. data/lib/features/scripts/launch-android-app.sh +38 -0
  11. data/lib/features/scripts/launch-android-emulator.sh +15 -0
  12. data/lib/features/steps/android_steps.rb +51 -0
  13. data/lib/features/steps/app_automator_steps.rb +228 -0
  14. data/lib/features/steps/aws_sam_steps.rb +212 -0
  15. data/lib/features/steps/breadcrumb_steps.rb +50 -0
  16. data/lib/features/steps/browser_steps.rb +93 -0
  17. data/lib/features/steps/build_api_steps.rb +25 -0
  18. data/lib/features/steps/document_server_steps.rb +7 -0
  19. data/lib/features/steps/error_reporting_steps.rb +342 -0
  20. data/lib/features/steps/feature_flag_steps.rb +190 -0
  21. data/lib/features/steps/header_steps.rb +72 -0
  22. data/lib/features/steps/log_steps.rb +29 -0
  23. data/lib/features/steps/multipart_request_steps.rb +142 -0
  24. data/lib/features/steps/network_steps.rb +75 -0
  25. data/lib/features/steps/payload_steps.rb +234 -0
  26. data/lib/features/steps/proxy_steps.rb +34 -0
  27. data/lib/features/steps/query_parameter_steps.rb +31 -0
  28. data/lib/features/steps/request_assertion_steps.rb +107 -0
  29. data/lib/features/steps/runner_steps.rb +406 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/value_steps.rb +119 -0
  32. data/lib/features/support/env.rb +7 -0
  33. data/lib/features/support/internal_hooks.rb +260 -0
  34. data/lib/maze/appium_server.rb +112 -0
  35. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  36. data/lib/maze/aws/sam.rb +112 -0
  37. data/lib/maze/bitbar_devices.rb +84 -0
  38. data/lib/maze/bitbar_utils.rb +112 -0
  39. data/lib/maze/browser_stack_devices.rb +160 -0
  40. data/lib/maze/browser_stack_utils.rb +164 -0
  41. data/lib/maze/browsers_bs.yml +220 -0
  42. data/lib/maze/browsers_cbt.yml +100 -0
  43. data/lib/maze/bugsnag_config.rb +42 -0
  44. data/lib/maze/capabilities.rb +126 -0
  45. data/lib/maze/checks/assert_check.rb +91 -0
  46. data/lib/maze/checks/noop_check.rb +34 -0
  47. data/lib/maze/compare.rb +161 -0
  48. data/lib/maze/configuration.rb +174 -0
  49. data/lib/maze/docker.rb +108 -0
  50. data/lib/maze/document_server.rb +46 -0
  51. data/lib/maze/driver/appium.rb +217 -0
  52. data/lib/maze/driver/browser.rb +138 -0
  53. data/lib/maze/driver/resilient_appium.rb +51 -0
  54. data/lib/maze/errors.rb +20 -0
  55. data/lib/maze/helper.rb +118 -0
  56. data/lib/maze/hooks/appium_hooks.rb +216 -0
  57. data/lib/maze/hooks/browser_hooks.rb +68 -0
  58. data/lib/maze/hooks/command_hooks.rb +9 -0
  59. data/lib/maze/hooks/hooks.rb +61 -0
  60. data/lib/maze/interactive_cli.rb +173 -0
  61. data/lib/maze/logger.rb +73 -0
  62. data/lib/maze/macos_utils.rb +14 -0
  63. data/lib/maze/network.rb +49 -0
  64. data/lib/maze/option/parser.rb +245 -0
  65. data/lib/maze/option/processor.rb +143 -0
  66. data/lib/maze/option/validator.rb +184 -0
  67. data/lib/maze/option.rb +64 -0
  68. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  69. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  70. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  71. data/lib/maze/proxy.rb +114 -0
  72. data/lib/maze/request_list.rb +82 -0
  73. data/lib/maze/retry_handler.rb +76 -0
  74. data/lib/maze/runner.rb +149 -0
  75. data/lib/maze/sauce_labs_utils.rb +96 -0
  76. data/lib/maze/server.rb +207 -0
  77. data/lib/maze/servlets/base_servlet.rb +22 -0
  78. data/lib/maze/servlets/command_servlet.rb +44 -0
  79. data/lib/maze/servlets/log_servlet.rb +64 -0
  80. data/lib/maze/servlets/reflective_servlet.rb +69 -0
  81. data/lib/maze/servlets/servlet.rb +160 -0
  82. data/lib/maze/smart_bear_utils.rb +71 -0
  83. data/lib/maze/store.rb +15 -0
  84. data/lib/maze/terminating_server.rb +129 -0
  85. data/lib/maze/timers.rb +51 -0
  86. data/lib/maze/wait.rb +35 -0
  87. data/lib/maze.rb +27 -0
  88. metadata +371 -0
@@ -0,0 +1,100 @@
1
+ # Selenium capabilities for browserNames available on CrossbrowserNameTesting
2
+ ---
3
+ ie_8:
4
+ browserName: "Internet Explorer"
5
+ version: "8"
6
+ platform: "Windows 7"
7
+
8
+ ie_9:
9
+ browserName: "Internet Explorer"
10
+ version: "9"
11
+ platform: "Windows 7"
12
+
13
+ ie_10:
14
+ browserName: "Internet Explorer"
15
+ version: "10"
16
+ os: "windows"
17
+ platform: "Windows 7 64-Bit"
18
+
19
+ ie_11:
20
+ browserName: "Internet Explorer"
21
+ version: "11"
22
+ platform: "Windows 7 64-Bit"
23
+
24
+ edge_14:
25
+ browserName: "MicrosoftEdge"
26
+ version: "14"
27
+ platform: "Windows 10"
28
+
29
+ edge_15:
30
+ browserName: "MicrosoftEdge"
31
+ version: "15"
32
+ platform: "Windows 10"
33
+
34
+ safari_8:
35
+ browserName: "Safari"
36
+ version: "8"
37
+ platform: "Mac OSX 10.10"
38
+
39
+ safari_10:
40
+ browserName: "Safari"
41
+ version: "10"
42
+ platform: "Mac OSX 10.12"
43
+
44
+ safari_13:
45
+ browserName: "Safari"
46
+ version: "13"
47
+ platform: "Mac OSX 10.15"
48
+
49
+ safari_14:
50
+ browserName: "Safari"
51
+ version: "14"
52
+ platform: "MacOS 11.0"
53
+
54
+ iphone_7_simulator:
55
+ browserName: "Safari"
56
+ deviceName: "iPhone 7 Simulator"
57
+ platformVersion': "10.0"
58
+ platformName': "iOS"
59
+
60
+ iphone_xr:
61
+ browserName: "Safari"
62
+ deviceName': "iPhone XR"
63
+ platformVersion': "12.1"
64
+ platformName': "iOS"
65
+
66
+ android_s7:
67
+ browserName: "Chrome"
68
+ deviceName: "Galaxy S7"
69
+ platformVersion: "7.0"
70
+ platformName: "Android"
71
+
72
+ firefox_30:
73
+ browserName: "Firefox"
74
+ version: "30"
75
+ platform: "Windows 7"
76
+
77
+ firefox_56:
78
+ browserName: "Firefox"
79
+ version: "56"
80
+ platform: "Mac OSX 10.13"
81
+
82
+ firefox_95:
83
+ browserName: "Firefox"
84
+ version: "95"
85
+ platform: "Windows 10"
86
+
87
+ chrome_43:
88
+ browserName: "Chrome"
89
+ version: "43"
90
+ platform: "Windows 7"
91
+
92
+ chrome_61:
93
+ browserName: "Chrome"
94
+ version: "61"
95
+ platform: "Windows 10"
96
+
97
+ chrome_96:
98
+ browserName: "Chrome"
99
+ version: "96"
100
+ platform: "Windows 10"
@@ -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,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # Appium capabilities for each target farm
5
+ class Capabilities
6
+ class << self
7
+ # @param device_type [String] A key from @see BrowserStackDevices::DEVICE_HASH
8
+ # @param local_id [String] unique key for the tunnel instance
9
+ # @param capabilities_option [String] extra capabilities provided on the command line
10
+ def for_browser_stack_device(device_type, local_id, appium_version, capabilities_option)
11
+ capabilities = {
12
+ 'browserstack.console' => 'errors',
13
+ 'browserstack.localIdentifier' => local_id,
14
+ 'browserstack.local' => 'true',
15
+ 'noReset' => 'true'
16
+ }
17
+ capabilities.merge! BrowserStackDevices::DEVICE_HASH[device_type]
18
+ capabilities.merge! JSON.parse(capabilities_option)
19
+ capabilities['browserstack.appium_version'] = appium_version unless appium_version.nil?
20
+ capabilities
21
+ end
22
+
23
+ # @param browser_type [String] A key from @see browsers_bs.yml
24
+ # @param local_id [String] unique key for the tunnel instance
25
+ # @param capabilities_option [String] extra capabilities provided on the command line
26
+ def for_browser_stack_browser(browser_type, local_id, capabilities_option)
27
+ capabilities = Selenium::WebDriver::Remote::Capabilities.new
28
+ capabilities['browserstack.local'] = 'true'
29
+ capabilities['browserstack.localIdentifier'] = local_id
30
+ capabilities['browserstack.console'] = 'errors'
31
+ browsers = YAML.safe_load(File.read("#{__dir__}/browsers_bs.yml"))
32
+ capabilities.merge! browsers[browser_type]
33
+ capabilities.merge! JSON.parse(capabilities_option)
34
+ capabilities
35
+ end
36
+
37
+ # @param browser_type [String] A key from @see browsers_cbt.yml
38
+ # @param local_id [String] unique key for the SB tunnel instance
39
+ # @param capabilities_option [String] extra capabilities provided on the command line
40
+ def for_cbt_browser(browser_type, local_id, capabilities_option)
41
+ capabilities = Selenium::WebDriver::Remote::Capabilities.new
42
+ capabilities['tunnel_name'] = local_id
43
+ browsers = YAML.safe_load(File.read("#{__dir__}/browsers_cbt.yml"))
44
+ capabilities.merge! browsers[browser_type]
45
+ capabilities.merge! JSON.parse(capabilities_option)
46
+ capabilities
47
+ end
48
+
49
+ # @param device_type [String]
50
+ def for_bitbar_device(bitbar_api_key, device_type, platform, platform_version, capabilities_option)
51
+ capabilities = {
52
+ 'bitbar_apiKey' => bitbar_api_key,
53
+ 'bitbar_testrun' => "#{platform} #{platform_version}",
54
+ 'bitbar_findDevice' => false,
55
+ 'bitbar_testTimeout' => 7200,
56
+ 'disabledAnimations' => 'true',
57
+ 'noReset' => 'true'
58
+ }
59
+ capabilities.merge! BitBarDevices.get_device(device_type, platform, platform_version, bitbar_api_key)
60
+ capabilities.merge! JSON.parse(capabilities_option)
61
+ capabilities
62
+ end
63
+
64
+ # Constructs Appium capabilities for running on a local Android or iOS device.
65
+ # @param platform [String] 'ios' or 'android'
66
+ # @param capabilities_option [String] extra capabilities provided on the command line
67
+ # @param team_id [String] Apple Team Id, for iOS only
68
+ # @param udid [String] device UDID, for iOS only
69
+ # noinspection RubyStringKeysInHashInspection
70
+ def for_local(platform, capabilities_option, team_id = nil, udid = nil)
71
+ capabilities = case platform.downcase
72
+ when 'android'
73
+ {
74
+ 'platformName' => 'Android',
75
+ 'automationName' => 'UiAutomator2',
76
+ 'autoGrantPermissions' => 'true',
77
+ 'noReset' => 'true'
78
+ }
79
+ when 'ios'
80
+ {
81
+ 'platformName' => 'iOS',
82
+ 'automationName' => 'XCUITest',
83
+ 'deviceName' => udid,
84
+ 'xcodeOrgId' => team_id,
85
+ 'xcodeSigningId' => 'iPhone Developer',
86
+ 'udid' => udid,
87
+ 'noReset' => 'true',
88
+ 'waitForQuiescence' => false,
89
+ 'newCommandTimeout' => 0
90
+ }
91
+ when 'macos'
92
+ {
93
+ 'platformName' => 'Mac'
94
+ }
95
+ else
96
+ raise "Unsupported platform: #{platform}"
97
+ end
98
+ common = {
99
+ 'os' => platform,
100
+ 'autoAcceptAlerts': 'true'
101
+ }
102
+ capabilities.merge! common
103
+ capabilities.merge! JSON.parse(capabilities_option)
104
+ end
105
+
106
+ def for_sauce_labs_device(device_name, os, os_version, tunnel_id, appium_version, capabilities_option)
107
+ capabilities = {
108
+ 'noReset' => true,
109
+ 'deviceOrientation' => 'portrait',
110
+ 'tunnelIdentifier' => tunnel_id,
111
+ 'browserName' => '',
112
+ 'autoAcceptAlerts' => true,
113
+ 'sendKeyStrategy' => 'setValue',
114
+ 'waitForQuiescence' => false,
115
+ 'newCommandTimeout' => 0
116
+ }
117
+ capabilities['deviceName'] = device_name unless device_name.nil?
118
+ capabilities['platformName'] = os unless os.nil?
119
+ capabilities['platformVersion'] = os_version unless os_version.nil?
120
+ capabilities.merge! JSON.parse(capabilities_option)
121
+ capabilities['appiumVersion'] = appium_version unless appium_version.nil?
122
+ capabilities
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,91 @@
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
+ assert_match(pattern, string, message)
28
+ end
29
+
30
+ def equal(expected, act, message = nil)
31
+ assert_equal(expected, act, message)
32
+ end
33
+
34
+ def not_equal(expected, act, message = nil)
35
+ assert_not_equal(expected, act, message)
36
+ end
37
+
38
+ def operator(operand1, operator, operand2, message = nil)
39
+ assert_operator(operand1, operator, operand2, message)
40
+ end
41
+
42
+ def kind_of(klass, object, message = nil)
43
+ assert_kind_of(klass, object, message)
44
+ end
45
+
46
+ def block(message = 'block failed', &block)
47
+ assert_block(message, &block)
48
+ end
49
+
50
+ def include(collection, object, message = nil)
51
+ assert_include(collection, object, message)
52
+ end
53
+ alias includes include
54
+
55
+ def not_include(collection, object, message = nil)
56
+ assert_not_include(collection, object, message)
57
+ end
58
+ alias not_includes not_include
59
+ end
60
+ end
61
+ end
62
+
63
+ # Wrapper for Maze.check.true to avoid making a breaking change.
64
+ # @deprecated TODO Remove in v7
65
+ def assert_true(value, message = nil)
66
+ Maze.check.true value, message
67
+ end
68
+
69
+ # Wrapper for Maze.check.false to avoid making a breaking change.
70
+ # @deprecated TODO Remove in v7
71
+ def assert_false(value, message = nil)
72
+ Maze.check.false value, message
73
+ end
74
+
75
+ # Wrapper for Maze.check.not_nil to avoid making a breaking change.
76
+ # @deprecated TODO Remove in v7
77
+ def assert_not_nil(value, message = nil)
78
+ Maze.check.not_nil value, message
79
+ end
80
+
81
+ # Wrapper for Maze.check.not_nil to avoid making a breaking change.
82
+ # @deprecated TODO Remove in v7
83
+ def assert_block(message = 'block failed', &block)
84
+ Maze.check.block(message, &block)
85
+ end
86
+
87
+ # Wrapper for Maze.check.true to avoid making a breaking change.
88
+ # @deprecated TODO Remove in v7
89
+ def assert_equal(value, message = nil)
90
+ Maze.check.equal value, message
91
+ 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,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # Routines for conducting comparisons
5
+ module Compare
6
+ # Provides a way to delivering results of element comparisons to test steps
7
+ class Result
8
+ # @!attribute [r] reasons
9
+ # @return [Array] An array of reasons for a comparison result
10
+ attr_reader :reasons
11
+
12
+ # @!attribute [w] equal
13
+ # @param [Boolean] Indicates if the comparison indicated the elements were equal
14
+ attr_writer :equal
15
+
16
+ # @!attribute [r] keys
17
+ # @return [Array] An array of keys checked
18
+ attr_reader :keys
19
+
20
+ # Creates the Result object
21
+ def initialize
22
+ @equal = true
23
+ @keys = []
24
+ @reasons = []
25
+ end
26
+
27
+ # Indicates if the values compared to produced this result were equal
28
+ #
29
+ # @return [Boolean] Whether the elements were equal
30
+ def equal?
31
+ @equal
32
+ end
33
+
34
+ # Returns the keys traversed in order to compare the end values
35
+ #
36
+ # @return [String] The keypath used in the test
37
+ def keypath
38
+ keys.length > 0 ? keys.reverse.join('.') : '<root>'
39
+ end
40
+
41
+ # Provides a standard assertion of equality, with standardised output
42
+ def assert_equal
43
+ Maze.check.true(@equal, "The compared fields do not match:\n #{result.reasons.join('\n')}")
44
+ end
45
+ end
46
+
47
+ class << self
48
+ # Compares two objects for value equality, traversing to compare each
49
+ # nested object.
50
+ #
51
+ # @param obj1 [Any] The first object to compare
52
+ # @param obj2 [Any] The second object to compare
53
+ # @param result [Result|nil] Optional. Used for comparing recursively
54
+ #
55
+ # @return [Result] The result of comparing the objects
56
+ def value(obj1, obj2, result = nil)
57
+ result ||= Result.new
58
+ return result if obj1 == 'IGNORE'
59
+
60
+ if obj1 == 'NUMBER'
61
+ if obj2.is_a?(Numeric)
62
+ return result
63
+ else
64
+ result.equal = false
65
+ result.reasons << "A Number was expected, '#{obj2.class} received"
66
+ return result
67
+ end
68
+ end
69
+
70
+ unless obj1.class == obj2.class
71
+ result.equal = false
72
+ result.reasons << "Object types differ - expected '#{obj1.class}', received '#{obj2.class}'"
73
+ return result
74
+ end
75
+
76
+ case obj1
77
+ when Array
78
+ array(obj1, obj2, result)
79
+ when Hash
80
+ hash(obj1, obj2, result)
81
+ when String
82
+ string(obj1, obj2, result)
83
+ else
84
+ result.reasons << "#{obj1} is not equal to #{obj2}" unless (result.equal = (obj1 == obj2))
85
+ end
86
+ result
87
+ end
88
+
89
+ # Compares two arrays for value equality, traversing and comparing each element.
90
+ # Results are written to the given Result object.
91
+ #
92
+ # @param array1 [Array] The first array to compare
93
+ # @param array2 [Array] The second array to compare
94
+ # @param result [Result] The Result to store the results
95
+ def array(array1, array2, result)
96
+ unless array1.length == array2.length
97
+ result.equal = false
98
+ result.reasons << "Expected #{array1.length} items in array, received #{array2.length}"
99
+ return
100
+ end
101
+
102
+ array1.each_with_index do |obj1, index|
103
+ value(obj1, array2[index], result)
104
+ unless result.equal?
105
+ result.keys << index.to_s
106
+ break
107
+ end
108
+ end
109
+ end
110
+
111
+ # Compares two hashes for value equality, traversing and comparing each key-value pair.
112
+ # Results are written to the given Result object.
113
+ #
114
+ # @param hash1 [Hash] The first hash to compare
115
+ # @param hash2 [Hash] The second hash to compare
116
+ # @param result [Result] The Result to store the results
117
+ def hash(hash1, hash2, result)
118
+ unless hash1.keys.length == hash2.keys.length
119
+ result.equal = false
120
+ missing = hash1.keys - hash2.keys
121
+ unexpected = hash2.keys - hash1.keys
122
+ result.reasons << "Missing keys from hash: #{missing.join(',')}" unless missing.empty?
123
+ result.reasons << "Unexpected keys in hash: #{unexpected.join(',')}" unless unexpected.empty?
124
+ return
125
+ end
126
+
127
+ hash1.each do |key, value|
128
+ value(value, hash2[key], result)
129
+ unless result.equal?
130
+ result.keys << key
131
+ break
132
+ end
133
+ end
134
+ end
135
+
136
+ # Compares two strings, writing the results to the given Result object.
137
+ #
138
+ # @param template [String] The expected string
139
+ # @param str2 [String] The string to compare
140
+ # @param result [Result] The Result to store the results
141
+ def string(template, str2, result)
142
+ return if template == str2 || regex_match(template, str2)
143
+
144
+ result.equal = false
145
+ result.reasons << "'#{str2}' does not match '#{template}'"
146
+ end
147
+
148
+ # Matches a string against a regex
149
+ #
150
+ # @param template [String] The regex to test with
151
+ # @param value [String] The value to test
152
+ #
153
+ # @return [Boolean] Whether the regex produced a match
154
+ def regex_match(template, value)
155
+ regex = template
156
+ regex = "^#{regex}$" unless regex.start_with?('^') || regex.end_with?('$')
157
+ value =~ /#{regex}/
158
+ end
159
+ end
160
+ end
161
+ end