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.
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