mini_autobot 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +26 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +191 -0
- data/LICENSE +22 -0
- data/README.md +632 -0
- data/bin/mini_autobot +5 -0
- data/lib/mini_autobot.rb +44 -0
- data/lib/mini_autobot/connector.rb +288 -0
- data/lib/mini_autobot/console.rb +15 -0
- data/lib/mini_autobot/emails.rb +5 -0
- data/lib/mini_autobot/emails/mailbox.rb +15 -0
- data/lib/mini_autobot/endeca/base.rb +6 -0
- data/lib/mini_autobot/init.rb +63 -0
- data/lib/mini_autobot/logger.rb +12 -0
- data/lib/mini_autobot/page_objects.rb +22 -0
- data/lib/mini_autobot/page_objects/base.rb +264 -0
- data/lib/mini_autobot/page_objects/overlay/base.rb +76 -0
- data/lib/mini_autobot/page_objects/widgets/base.rb +47 -0
- data/lib/mini_autobot/parallel.rb +197 -0
- data/lib/mini_autobot/runner.rb +91 -0
- data/lib/mini_autobot/settings.rb +78 -0
- data/lib/mini_autobot/test_case.rb +233 -0
- data/lib/mini_autobot/test_cases.rb +7 -0
- data/lib/mini_autobot/utils.rb +10 -0
- data/lib/mini_autobot/utils/assertion_helper.rb +35 -0
- data/lib/mini_autobot/utils/castable.rb +103 -0
- data/lib/mini_autobot/utils/data_generator_helper.rb +145 -0
- data/lib/mini_autobot/utils/endeca_helper.rb +46 -0
- data/lib/mini_autobot/utils/loggable.rb +16 -0
- data/lib/mini_autobot/utils/overlay_and_widget_helper.rb +78 -0
- data/lib/mini_autobot/utils/page_object_helper.rb +209 -0
- data/lib/mini_autobot/version.rb +3 -0
- data/lib/minitap/minitest5_rent.rb +22 -0
- data/lib/minitest/autobot_settings_plugin.rb +77 -0
- data/lib/tapout/custom_reporters/fancy_tap_reporter.rb +94 -0
- data/lib/yard/tagged_test_case_handler.rb +61 -0
- data/mini_autobot.gemspec +38 -0
- metadata +299 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
module MiniAutobot
|
2
|
+
class Logger < ActiveSupport::Logger
|
3
|
+
|
4
|
+
LOG_FILE_MODE = File::WRONLY | File::APPEND | File::CREAT
|
5
|
+
|
6
|
+
def initialize(file, *args)
|
7
|
+
file = File.open(MiniAutobot.root.join('logs', file), LOG_FILE_MODE) unless file.respond_to?(:write)
|
8
|
+
super(file, *args)
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module MiniAutobot
|
2
|
+
|
3
|
+
# This is the overarching module that contains page objects, modules, and
|
4
|
+
# widgets.
|
5
|
+
#
|
6
|
+
# When new modules or classes are added, an `autoload` clause must be added
|
7
|
+
# into this module so that requires are taken care of automatically.
|
8
|
+
module PageObjects
|
9
|
+
|
10
|
+
# Exception to capture validation problems when instantiating a new page
|
11
|
+
# object. The message contains the page object being instantiated as well
|
12
|
+
# as the original, underlying error message if any.
|
13
|
+
class InvalidePageState < Exception; end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
# Major classes and modules
|
20
|
+
require_relative 'page_objects/base'
|
21
|
+
require_relative 'page_objects/overlay/base'
|
22
|
+
require_relative 'page_objects/widgets/base'
|
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'minitest/assertions'
|
2
|
+
|
3
|
+
module MiniAutobot
|
4
|
+
module PageObjects
|
5
|
+
|
6
|
+
# The base page object. All page objects should be a subclass of this.
|
7
|
+
# Every subclass must implement the following class methods:
|
8
|
+
#
|
9
|
+
# expected_path
|
10
|
+
#
|
11
|
+
# All methods added here will be available to all subclasses, so do so
|
12
|
+
# sparingly. This class has access to assertions, which should only be
|
13
|
+
# used to validate the page.
|
14
|
+
class Base
|
15
|
+
include Minitest::Assertions
|
16
|
+
include Utils::Castable
|
17
|
+
include Utils::Loggable
|
18
|
+
include Utils::PageObjectHelper
|
19
|
+
include Utils::OverlayAndWidgetHelper
|
20
|
+
|
21
|
+
attr_accessor :assertions
|
22
|
+
attr_accessor :failures
|
23
|
+
attr_reader :driver
|
24
|
+
|
25
|
+
# Given a set of arguments (no arguments by default), return the expected
|
26
|
+
# path to the page, which must only have file path and query-string.
|
27
|
+
#
|
28
|
+
# @param args [String] one or more arguments to be used in calculating
|
29
|
+
# the expected path, if any.
|
30
|
+
# @return [String] the expected path.
|
31
|
+
def self.expected_path(*args)
|
32
|
+
raise NotImplementedError, "expected_path is not defined for #{self}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Initializes a new page object from the driver. When a page is initialized,
|
36
|
+
# no validation occurs. As such, do not call this method directly. Rather,
|
37
|
+
# use PageObjectHelper#page in a test case, or #cast in another page object.
|
38
|
+
#
|
39
|
+
# @param driver [Selenium::WebDriver] The WebDriver instance.
|
40
|
+
def initialize(driver)
|
41
|
+
@driver = driver
|
42
|
+
|
43
|
+
@assertions = 0
|
44
|
+
@failures = []
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the current path loaded in the driver.
|
48
|
+
#
|
49
|
+
# @return [String] The current path, without hostname.
|
50
|
+
def current_path
|
51
|
+
current_url.path
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the current URL loaded in the driver.
|
55
|
+
#
|
56
|
+
# @return [String] The current URL, including hostname.
|
57
|
+
def current_url
|
58
|
+
URI.parse(driver.current_url)
|
59
|
+
end
|
60
|
+
|
61
|
+
## interface for Overlay And Widget Helper version of get_widgets! and get_overlay!
|
62
|
+
def page_object
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
# Instructs the driver to visit the {expected_path}.
|
67
|
+
#
|
68
|
+
# @param args [*Object] optional parameters to pass into {expected_path}.
|
69
|
+
def go!(*args)
|
70
|
+
driver.get(driver.url_for(self.class.expected_path(*args)))
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check that the page includes a certain string.
|
74
|
+
#
|
75
|
+
# @param value [String] the string to search
|
76
|
+
# @return [Boolean]
|
77
|
+
def include?(value)
|
78
|
+
driver.page_source.include?(value)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Retrieves all META tags with a `name` attribute on the current page.
|
82
|
+
def meta
|
83
|
+
tags = driver.all(:css, 'meta[name]')
|
84
|
+
tags.inject(Hash.new) do |vals, tag|
|
85
|
+
vals[tag.attribute(:name)] = tag.attribute(:content) if tag.attribute(:name)
|
86
|
+
vals
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def headline
|
91
|
+
driver.find_element(:css, 'body div.site-content h1').text
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get page title from any page
|
95
|
+
def title
|
96
|
+
driver.title
|
97
|
+
end
|
98
|
+
|
99
|
+
# By default, any driver state is accepted for any page. This method
|
100
|
+
# should be overridden in subclasses.
|
101
|
+
def validate!
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
# Wait for all dom events to load
|
106
|
+
def wait_for_dom(timeout = 15)
|
107
|
+
uuid = SecureRandom.uuid
|
108
|
+
# make sure body is loaded before appending anything to it
|
109
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for body to load").until do
|
110
|
+
is_element_present?(:css, 'body')
|
111
|
+
end
|
112
|
+
driver.execute_script <<-EOS
|
113
|
+
_.defer(function() {
|
114
|
+
$('body').append("<div id='#{uuid}'></div>");
|
115
|
+
});
|
116
|
+
EOS
|
117
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all dom events to finish").until do
|
118
|
+
is_element_present?(:css, "div[id='#{uuid}']")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Wait on all AJAX requests to finish
|
123
|
+
def wait_for_ajax(timeout = 15)
|
124
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all ajax requests to finish").until do
|
125
|
+
driver.execute_script 'return window.jQuery != undefined && jQuery.active == 0'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Explicitly wait for a certain condition to be true:
|
130
|
+
# wait.until { driver.find_element(:css, 'body.tmpl-srp') }
|
131
|
+
# when timeout is not specified, default timeout 5 sec will be used
|
132
|
+
# when timeout is larger than 15, max timeout 15 sec will be used
|
133
|
+
def wait(opts = {})
|
134
|
+
if !opts[:timeout].nil? && opts[:timeout] > 15
|
135
|
+
puts "WARNING: #{opts[:timeout]} sec timeout is NOT supported by wait method,
|
136
|
+
max timeout 15 sec will be used instead"
|
137
|
+
opts[:timeout] = 15
|
138
|
+
end
|
139
|
+
Selenium::WebDriver::Wait.new(opts)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Wrap blocks acting on Selenium elements and catch errors they
|
143
|
+
# raise. This probably qualifies as a Dumb LISPer Trick. If there's a
|
144
|
+
# better Ruby-ish way to do this, I welcome it. [~jacord]
|
145
|
+
def with_rescue(lbl, &blk)
|
146
|
+
yield ## run the block
|
147
|
+
## rescue errors. Rerunning may help, but we can also test for specific
|
148
|
+
## problems.
|
149
|
+
rescue Selenium::WebDriver::Error::ElementNotVisibleError => e
|
150
|
+
## The element is in the DOM but e.visible? is 'false'. Retry may help.
|
151
|
+
logger.debug "Retrying #{lbl}: #{e.class}"
|
152
|
+
yield
|
153
|
+
rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
|
154
|
+
## The page has changed and invalidated your element. Retry may help.
|
155
|
+
logger.debug "Retrying #{lbl}: #{e.class}"
|
156
|
+
yield
|
157
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
158
|
+
## Raised by get_element(s). Retry MAY help, but check first for HTTP
|
159
|
+
## 500, which may be best handled higher up the stack.
|
160
|
+
logger.debug "Recovering from NoSuchElementError during #{lbl}"
|
161
|
+
raise_on_error_page
|
162
|
+
## If we got past the above, retry the block.
|
163
|
+
logger.debug "Retrying #{lbl}: #{e.class}"
|
164
|
+
yield
|
165
|
+
end
|
166
|
+
|
167
|
+
## Wrap an action, wait for page title change. This function eliminates
|
168
|
+
## some error-prone boilerplate around fetching page titles
|
169
|
+
def with_page_title_wait(&blk)
|
170
|
+
title = driver.title
|
171
|
+
yield
|
172
|
+
wait_for_title_change(title)
|
173
|
+
end
|
174
|
+
|
175
|
+
# returns the all the page source of a page, useful for debugging
|
176
|
+
#
|
177
|
+
def page_source
|
178
|
+
driver.page_source
|
179
|
+
end
|
180
|
+
|
181
|
+
## PageObject validate! helper. Raises RuntimeError if one of our error
|
182
|
+
## pages is displaying. This can prevent a test from taking the entire
|
183
|
+
## implicit_wait before announcing error. [~jacord]
|
184
|
+
def raise_on_error_page
|
185
|
+
logger.debug "raise_on_error_page"
|
186
|
+
title = ''
|
187
|
+
begin
|
188
|
+
title = driver.title
|
189
|
+
rescue ReadTimeout
|
190
|
+
logger.debug 'ReadTimeout exception was thrown while trying to execute driver.title'
|
191
|
+
logger.debug 'ignore exception and proceed'
|
192
|
+
end
|
193
|
+
title = driver.title
|
194
|
+
logger.debug "Page Title: '#{title}'"
|
195
|
+
raise "HTTP 500 Error" if %r/Internal Server Error/ =~ title
|
196
|
+
raise "HTTP 503 Error" if %r/503 Service Temporarily Unavailable/ =~ title
|
197
|
+
raise "HTTP 404 Error" if %r/Error 404: Page Not Found/ =~ title
|
198
|
+
|
199
|
+
header = driver.find_element('body h1') rescue nil
|
200
|
+
|
201
|
+
unless header.nil?
|
202
|
+
raise "HTTP 500 Error" if header.text == 'Internal Server Error'
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
# click on a link on any page, cast a new page and return it
|
208
|
+
def click_on_link!(link_text, page_name)
|
209
|
+
driver.find_element(:link, link_text).location_once_scrolled_into_view
|
210
|
+
driver.find_element(:link, link_text).click
|
211
|
+
# check user angent, if it's on IE, wait 2sec for the title change
|
212
|
+
sleep 2 if driver.browser == :ie # todo remove this if every page has wait for title change in validate!
|
213
|
+
#sleep 5 #wait for 5 secs
|
214
|
+
logger.debug "click_on_link '#{link_text}'"
|
215
|
+
cast(page_name)
|
216
|
+
end
|
217
|
+
|
218
|
+
def wait_for_title_change(title)
|
219
|
+
title = driver.title if title.nil?
|
220
|
+
logger.debug("Waiting for title change from '#{title}'")
|
221
|
+
wait(timeout: 15, message: "Waited 15 sec for page transition")
|
222
|
+
.until { driver.title != title }
|
223
|
+
logger.debug("Arrived at #{driver.title}")
|
224
|
+
end
|
225
|
+
|
226
|
+
def wait_for_link(link_text)
|
227
|
+
message = "waited 15 sec, can't find link #{link_text} on page"
|
228
|
+
wait(timeout: 15, message: message).until{ driver.find_element(:link, link_text) }
|
229
|
+
|
230
|
+
unless driver.find_element(:link, link_text).displayed?
|
231
|
+
driver.navigate.refresh
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# example usage:
|
236
|
+
# original_url = driver.current_url
|
237
|
+
# driver.find_element(*LINK_REGISTER).click # do some action that should cause url to change
|
238
|
+
# wait_for_url_change(original_url)
|
239
|
+
def wait_for_url_change(original_url)
|
240
|
+
time = 15
|
241
|
+
message = "waited #{time} sec, url is still #{original_url}, was expecting it to change"
|
242
|
+
wait(timeout: time, message: message).until { driver.current_url != original_url }
|
243
|
+
end
|
244
|
+
|
245
|
+
def go_to_page!(url, page_type = :base)
|
246
|
+
driver.navigate.to(url)
|
247
|
+
cast(page_type)
|
248
|
+
end
|
249
|
+
|
250
|
+
def go_to_subpage!(url_path, page_type = :base)
|
251
|
+
# build url string
|
252
|
+
base_url = driver.current_url
|
253
|
+
|
254
|
+
# This is to safeguard ie, but we should handle this more intelligently in the future
|
255
|
+
base_url.slice!('qateam:wap88@')
|
256
|
+
|
257
|
+
url = base_url + url_path
|
258
|
+
driver.navigate.to(url)
|
259
|
+
cast(page_type)
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module MiniAutobot
|
2
|
+
module PageObjects
|
3
|
+
module Overlay
|
4
|
+
|
5
|
+
# A Overlay represents a portion (an element) of a page that is repeated
|
6
|
+
# or reproduced multiple times, either on the same page, or across multiple
|
7
|
+
# page objects or page modules.
|
8
|
+
class Base
|
9
|
+
include Utils::Castable
|
10
|
+
include Utils::PageObjectHelper
|
11
|
+
include Utils::OverlayAndWidgetHelper
|
12
|
+
|
13
|
+
attr_reader :driver
|
14
|
+
|
15
|
+
def initialize(page)
|
16
|
+
@driver = page.driver
|
17
|
+
@page = page
|
18
|
+
|
19
|
+
# works here but not in initialize of base of page objects
|
20
|
+
# because a page instance is already present when opening an overlay
|
21
|
+
end
|
22
|
+
|
23
|
+
## for overlay that include Utils::OverlayAndWidgetHelper
|
24
|
+
def page_object
|
25
|
+
@page
|
26
|
+
end
|
27
|
+
|
28
|
+
# By default, any driver state is accepted for any page. This method
|
29
|
+
# should be overridden in subclasses.
|
30
|
+
def validate!
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Wait for all dom events to load
|
35
|
+
def wait_for_dom(timeout = 15)
|
36
|
+
uuid = SecureRandom.uuid
|
37
|
+
# make sure body is loaded before appending anything to it
|
38
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for body to load").until do
|
39
|
+
is_element_present?(:css, 'body')
|
40
|
+
end
|
41
|
+
driver.execute_script <<-EOS
|
42
|
+
_.defer(function() {
|
43
|
+
$('body').append("<div id='#{uuid}'></div>");
|
44
|
+
});
|
45
|
+
EOS
|
46
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all dom events to finish").until do
|
47
|
+
is_element_present?(:css, "div[id='#{uuid}']")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Wait on all AJAX requests to finish
|
52
|
+
def wait_for_ajax(timeout = 15)
|
53
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all ajax requests to finish").until do
|
54
|
+
driver.execute_script 'return window.jQuery != undefined && jQuery.active == 0'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Explicitly wait for a certain condition to be true:
|
59
|
+
# wait.until { driver.find_element(:css, 'body.tmpl-srp') }
|
60
|
+
# when timeout is not specified, default timeout 5 sec will be used
|
61
|
+
# when timeout is larger than 15, max timeout 15 sec will be used
|
62
|
+
def wait(opts = {})
|
63
|
+
if !opts[:timeout].nil? && opts[:timeout] > 15
|
64
|
+
puts "WARNING: #{opts[:timeout]} sec timeout is NOT supported by wait method,
|
65
|
+
max timeout 15 sec will be used instead"
|
66
|
+
opts[:timeout] = 15
|
67
|
+
end
|
68
|
+
Selenium::WebDriver::Wait.new(opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module MiniAutobot
|
2
|
+
module PageObjects
|
3
|
+
module Widgets
|
4
|
+
|
5
|
+
# A widget represents a portion (an element) of a page that is repeated
|
6
|
+
# or reproduced multiple times, either on the same page, or across multiple
|
7
|
+
# page objects or page modules.
|
8
|
+
class Base
|
9
|
+
include Utils::Castable
|
10
|
+
include Utils::PageObjectHelper
|
11
|
+
include Utils::OverlayAndWidgetHelper
|
12
|
+
|
13
|
+
attr_reader :driver, :element, :page
|
14
|
+
|
15
|
+
def initialize(page, element)
|
16
|
+
@driver = page.driver
|
17
|
+
@page = page
|
18
|
+
@element = element
|
19
|
+
end
|
20
|
+
|
21
|
+
## for widgets that include Utils::OverlayAndWidgetHelper
|
22
|
+
def page_object
|
23
|
+
@page
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :driver
|
27
|
+
attr_reader :element
|
28
|
+
|
29
|
+
# Explicitly wait for a certain condition to be true:
|
30
|
+
# wait.until { driver.find_element(:css, 'body.tmpl-srp') }
|
31
|
+
# when timeout is not specified, default timeout 5 sec will be used
|
32
|
+
# when timeout is larger than 15, max timeout 15 sec will be used
|
33
|
+
def wait(opts = {})
|
34
|
+
if !opts[:timeout].nil? && opts[:timeout] > 15
|
35
|
+
puts "WARNING: #{opts[:timeout]} sec timeout is NOT supported by wait method,
|
36
|
+
max timeout 15 sec will be used instead"
|
37
|
+
opts[:timeout] = 15
|
38
|
+
end
|
39
|
+
Selenium::WebDriver::Wait.new(opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module MiniAutobot
|
2
|
+
class Parallel
|
3
|
+
|
4
|
+
attr_reader :all_tests, :simultaneous_jobs
|
5
|
+
|
6
|
+
def initialize(simultaneous_jobs, all_tests)
|
7
|
+
@start_time = Time.now
|
8
|
+
clean_result!
|
9
|
+
|
10
|
+
@simultaneous_jobs = simultaneous_jobs
|
11
|
+
@all_tests = all_tests
|
12
|
+
|
13
|
+
connector = MiniAutobot.settings.connector
|
14
|
+
@on_sauce = true if connector.include? 'saucelabs'
|
15
|
+
@platform = connector.split(':')[2] || ''
|
16
|
+
|
17
|
+
@pids = []
|
18
|
+
@static_run_command = "mini_autobot -c #{MiniAutobot.settings.connector} -e #{MiniAutobot.settings.env}"
|
19
|
+
tap_reporter_path = MiniAutobot.gem_root.join('lib/tapout/custom_reporters/fancy_tap_reporter.rb')
|
20
|
+
@pipe_tap = "--tapy | tapout --no-color -r #{tap_reporter_path.to_s} fancytap"
|
21
|
+
end
|
22
|
+
|
23
|
+
# return true only if specified to run on mac in connector
|
24
|
+
# @return [boolean]
|
25
|
+
def run_on_mac?
|
26
|
+
return true if @platform.include?('osx')
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
|
30
|
+
# remove all results files under logs/tap_results/
|
31
|
+
def clean_result!
|
32
|
+
IO.popen 'rm logs/tap_results/*'
|
33
|
+
puts "Cleaning result files.\n"
|
34
|
+
end
|
35
|
+
|
36
|
+
def count_autobot_process
|
37
|
+
counting_process = IO.popen "ps -ef | grep '#{@static_run_command}' -c"
|
38
|
+
count_of_processes = counting_process.readlines[0].to_i
|
39
|
+
count_of_processes
|
40
|
+
end
|
41
|
+
|
42
|
+
# run multiple commands with logging to start multiple tests in parallel
|
43
|
+
# @param [Integer, Array]
|
44
|
+
# n = number of tests will be running in parallel
|
45
|
+
def run_in_parallel!
|
46
|
+
# set number of tests to be running in parallel
|
47
|
+
if simultaneous_jobs.nil?
|
48
|
+
if run_on_mac?
|
49
|
+
@simultaneous_jobs = 10 # saucelabs account limit for parallel is 10 for mac
|
50
|
+
else
|
51
|
+
@simultaneous_jobs = 15 # saucelabs account limit for parallel is 15 for non-mac
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
size = all_tests.size
|
56
|
+
if size <= simultaneous_jobs
|
57
|
+
run_test_set(all_tests)
|
58
|
+
puts "CAUTION! All #{size} tests are starting at the same time!"
|
59
|
+
puts "will not really run it since computer will die" if size > 30
|
60
|
+
sleep 20
|
61
|
+
else
|
62
|
+
first_test_set = all_tests[0, simultaneous_jobs]
|
63
|
+
all_to_run = all_tests[(simultaneous_jobs + 1)...(all_tests.size - 1)]
|
64
|
+
run_test_set(first_test_set)
|
65
|
+
keep_running_full(all_to_run)
|
66
|
+
end
|
67
|
+
|
68
|
+
wait_all_done_saucelabs if @on_sauce
|
69
|
+
wait_for_pids(@pids) unless ENV['JENKINS_HOME']
|
70
|
+
puts "\nAll Complete! Started at #{@start_time} and finished at #{Time.now}\n"
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
|
74
|
+
def wait_for_pids(pids)
|
75
|
+
running_pids = pids # assume all pids are running at this moment
|
76
|
+
while running_pids.size > 1
|
77
|
+
sleep 5
|
78
|
+
puts "running_pids = #{running_pids}"
|
79
|
+
running_pids.each do |pid|
|
80
|
+
unless process_running?(pid)
|
81
|
+
puts "#{pid} is not running, removing it from pool"
|
82
|
+
running_pids.delete(pid)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def process_running?(pid)
|
89
|
+
begin
|
90
|
+
Process.getpgid(pid)
|
91
|
+
true
|
92
|
+
rescue Errno::ESRCH
|
93
|
+
false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# runs each test from a test set in a separate child process
|
98
|
+
def run_test_set(test_set)
|
99
|
+
test_set.each do |test|
|
100
|
+
run_command = "#{@static_run_command} -n #{test} #{@pipe_tap} > logs/tap_results/#{test}.t"
|
101
|
+
pipe = IO.popen(run_command)
|
102
|
+
puts "Running #{test} #{pipe.pid}"
|
103
|
+
@pids << pipe.pid
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def keep_running_full(all_to_run)
|
108
|
+
full_count = simultaneous_jobs + 2
|
109
|
+
running_count = count_autobot_process
|
110
|
+
while running_count >= full_count
|
111
|
+
sleep 5
|
112
|
+
running_count = count_autobot_process
|
113
|
+
end
|
114
|
+
to_run_count = full_count - running_count
|
115
|
+
tests_to_run = all_to_run.slice!(0, to_run_count)
|
116
|
+
run_test_set(tests_to_run)
|
117
|
+
if all_to_run.size > 0
|
118
|
+
keep_running_full(all_to_run)
|
119
|
+
else
|
120
|
+
return
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def wait_all_done_saucelabs
|
125
|
+
size = all_tests.size
|
126
|
+
job_statuses = saucelabs_last_n_statuses(size)
|
127
|
+
while job_statuses.include?('in progress')
|
128
|
+
puts "There are tests still running, waiting..."
|
129
|
+
sleep 20
|
130
|
+
job_statuses = saucelabs_last_n_statuses(size)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# call saucelabs REST API to get last #{limit} jobs' statuses
|
135
|
+
# possible job status: complete, error, in progress
|
136
|
+
def saucelabs_last_n_statuses(limit)
|
137
|
+
connector = MiniAutobot.settings.connector # eg. saucelabs:phu:win7_ie11
|
138
|
+
overrides = connector.to_s.split(/:/)
|
139
|
+
file_name = overrides.shift
|
140
|
+
path = MiniAutobot.root.join('config/mini_autobot', 'connectors')
|
141
|
+
filepath = path.join("#{file_name}.yml")
|
142
|
+
raise ArgumentError, "Cannot load profile #{file_name.inspect} because #{filepath.inspect} does not exist" unless filepath.exist?
|
143
|
+
cfg = YAML.load(File.read(filepath))
|
144
|
+
cfg = Connector.resolve(cfg, overrides)
|
145
|
+
cfg.freeze
|
146
|
+
username = cfg["hub"]["user"]
|
147
|
+
access_key = cfg["hub"]["pass"]
|
148
|
+
|
149
|
+
require 'json'
|
150
|
+
|
151
|
+
# call api to get most recent #{limit} jobs' ids
|
152
|
+
http_auth = "https://#{username}:#{access_key}@saucelabs.com/rest/v1/#{username}/jobs?limit=#{limit}"
|
153
|
+
response = get_response_with_retry(http_auth) # response was originally an array of hashs, but RestClient converts it to a string
|
154
|
+
# convert response back to array
|
155
|
+
response[0] = ''
|
156
|
+
response[response.length-1] = ''
|
157
|
+
array_of_hash = response.split(',')
|
158
|
+
id_array = Array.new
|
159
|
+
array_of_hash.each do |hash|
|
160
|
+
hash = hash.gsub(':', '=>')
|
161
|
+
hash = eval(hash)
|
162
|
+
id_array << hash['id'] # each hash contains key 'id' and value of id
|
163
|
+
end
|
164
|
+
|
165
|
+
# call api to get job statuses
|
166
|
+
statuses = Array.new
|
167
|
+
id_array.each do |id|
|
168
|
+
http_auth = "https://#{username}:#{access_key}@saucelabs.com/rest/v1/#{username}/jobs/#{id}"
|
169
|
+
response = get_response_with_retry(http_auth)
|
170
|
+
begin
|
171
|
+
# convert response back to hash
|
172
|
+
str = response.gsub(':', '=>')
|
173
|
+
# this is a good example why using eval is dangerous, the string has to contain only proper Ruby syntax, here it has 'null' instead of 'nil'
|
174
|
+
formatted_str = str.gsub('null', 'nil')
|
175
|
+
hash = eval(formatted_str)
|
176
|
+
statuses << hash['status']
|
177
|
+
rescue SyntaxError
|
178
|
+
puts "SyntaxError, response from saucelabs has syntax error"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
return statuses
|
182
|
+
end
|
183
|
+
|
184
|
+
def get_response_with_retry(url)
|
185
|
+
retries = 5 # number of retries
|
186
|
+
begin
|
187
|
+
response = RestClient.get(url) # returns a String
|
188
|
+
rescue
|
189
|
+
puts "Failed at getting response from #{url} via RestClient \n Retrying..."
|
190
|
+
retries -= 1
|
191
|
+
retry if retries > 0
|
192
|
+
response = RestClient.get(url) # retry the last time, fail if it still throws exception
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|