mini_autobot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|