mini_autobot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +191 -0
  7. data/LICENSE +22 -0
  8. data/README.md +632 -0
  9. data/bin/mini_autobot +5 -0
  10. data/lib/mini_autobot.rb +44 -0
  11. data/lib/mini_autobot/connector.rb +288 -0
  12. data/lib/mini_autobot/console.rb +15 -0
  13. data/lib/mini_autobot/emails.rb +5 -0
  14. data/lib/mini_autobot/emails/mailbox.rb +15 -0
  15. data/lib/mini_autobot/endeca/base.rb +6 -0
  16. data/lib/mini_autobot/init.rb +63 -0
  17. data/lib/mini_autobot/logger.rb +12 -0
  18. data/lib/mini_autobot/page_objects.rb +22 -0
  19. data/lib/mini_autobot/page_objects/base.rb +264 -0
  20. data/lib/mini_autobot/page_objects/overlay/base.rb +76 -0
  21. data/lib/mini_autobot/page_objects/widgets/base.rb +47 -0
  22. data/lib/mini_autobot/parallel.rb +197 -0
  23. data/lib/mini_autobot/runner.rb +91 -0
  24. data/lib/mini_autobot/settings.rb +78 -0
  25. data/lib/mini_autobot/test_case.rb +233 -0
  26. data/lib/mini_autobot/test_cases.rb +7 -0
  27. data/lib/mini_autobot/utils.rb +10 -0
  28. data/lib/mini_autobot/utils/assertion_helper.rb +35 -0
  29. data/lib/mini_autobot/utils/castable.rb +103 -0
  30. data/lib/mini_autobot/utils/data_generator_helper.rb +145 -0
  31. data/lib/mini_autobot/utils/endeca_helper.rb +46 -0
  32. data/lib/mini_autobot/utils/loggable.rb +16 -0
  33. data/lib/mini_autobot/utils/overlay_and_widget_helper.rb +78 -0
  34. data/lib/mini_autobot/utils/page_object_helper.rb +209 -0
  35. data/lib/mini_autobot/version.rb +3 -0
  36. data/lib/minitap/minitest5_rent.rb +22 -0
  37. data/lib/minitest/autobot_settings_plugin.rb +77 -0
  38. data/lib/tapout/custom_reporters/fancy_tap_reporter.rb +94 -0
  39. data/lib/yard/tagged_test_case_handler.rb +61 -0
  40. data/mini_autobot.gemspec +38 -0
  41. 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