class Browsery::Parallel

Attributes

all_tests[R]
simultaneous_jobs[R]

Public Class Methods

new(simultaneous_jobs, all_tests) click to toggle source
# File lib/browsery/parallel.rb, line 6
def initialize(simultaneous_jobs, all_tests)
  @start_time = Time.now

  @result_dir = 'logs/tap_results'

  connector = Browsery.settings.connector
  @on_sauce = true if connector.include? 'saucelabs'
  @platform = connector.split(':')[2] || ''

  @simultaneous_jobs = simultaneous_jobs
  @simultaneous_jobs = 10 if run_on_mac? # saucelabs account limit for parallel is 10 for mac
  @all_tests = all_tests

  @pids = []
  @static_run_command = "browsery -c #{Browsery.settings.connector} -e #{Browsery.settings.env}"
  if Browsery.settings.rerun_failure
    @static_run_command += " -R #{Browsery.settings.rerun_failure}"
  end
  tap_reporter_path = Browsery.gem_root.join('lib/tapout/custom_reporters/fancy_tap_reporter.rb')
  @pipe_tap = "--tapy | tapout --no-color -r #{tap_reporter_path.to_s} fancytap"
end

Public Instance Methods

aggregate_tap_results() click to toggle source

Aggregate all individual test_*.t files replace them with one file - test_aggregated_result.tap so they will be considered as one test plan by tap result parser

# File lib/browsery/parallel.rb, line 72
def aggregate_tap_results
  results_count = Dir.glob("#{@result_dir}/*.t").size
  File.open("#{@result_dir}/test_aggregated_result.tap", 'a+') do |result_file|
    result_stats = {
        'pass' => 0,
        'fail' => 0,
        'errs' => 0,
        'todo' => 0,
        'omit' => 0
    }
    result_stats_line_start = '  # 1 tests:'
    result_file.puts "1..#{results_count}"
    file_count = 0
    Dir.glob("#{@result_dir}/*.t") do |filename|
      file_count += 1
      File.open(filename, 'r') do |file|
        breakpoint_line = 0
        file.each_with_index do |line, index|
          next if index == 0 || (breakpoint_line > 0 && index > breakpoint_line)
          if line.start_with?(result_stats_line_start)
            pass, fail, errs, todo, omit = line.match(/(\d+) pass, (\d+) fail, (\d+) errs, (\d+) todo, (\d+) omit/).captures
            one_test_result = {
                'pass' => pass.to_i,
                'fail' => fail.to_i,
                'errs' => errs.to_i,
                'todo' => todo.to_i,
                'omit' => omit.to_i
            }
            result_stats = result_stats.merge(one_test_result) { |k, total, one| total + one }
            breakpoint_line = index
          elsif line.strip == '#'
            next
          else
            if line.start_with?('ok 1') || line.start_with?('not ok 1')
              line_begin, line_end = line.split('1 -')
              result_file.puts [line_begin, line_end].join("#{file_count} -")
            else
              result_file.puts line
            end
          end
        end
      end
      File.delete(filename)
    end
    result_file.puts '  #'
    result_file.puts "  # #{results_count} tests: #{result_stats['pass']} pass, #{result_stats['fail']} fail, #{result_stats['errs']} errs, #{result_stats['todo']} todo, #{result_stats['omit']} omit"
    result_file.puts "  # [00:00:00.00 0.00t/s 00.0000s/t] Finished at: #{Time.now}"
  end
end
clean_result!() click to toggle source

remove all results files under @result_dir if there's any

# File lib/browsery/parallel.rb, line 35
def clean_result!
  raise Exception, '@result_dir is not set' if @result_dir.nil?
  unless Dir.glob("#{@result_dir}/*").empty?
    FileUtils.rm_rf(Dir.glob("#{@result_dir}/*"))
  end
  puts "Cleaning result files.\n"
end
count_browsery_process() click to toggle source
# File lib/browsery/parallel.rb, line 122
def count_browsery_process
  counting_process_output = IO.popen "ps -ef | grep 'bin/#{@static_run_command}' -c"
  counting_process_output.readlines[0].to_i - 1 # minus grep process
end
keep_running_full(all_to_run) click to toggle source

recursively keep running #{simultaneous_jobs} number of tests in parallel exit when no test left to run

# File lib/browsery/parallel.rb, line 159
def keep_running_full(all_to_run)
  running_subprocess_count = count_browsery_process - 1 # minus parent process
  puts "WARNING: running_subprocess_count = #{running_subprocess_count}
        is more than what it is supposed to run(#{simultaneous_jobs}),
        notify browsery maintainers" if running_subprocess_count > simultaneous_jobs + 1
  while running_subprocess_count >= simultaneous_jobs
    sleep 5
    running_subprocess_count = count_browsery_process - 1
  end
  to_run_count = simultaneous_jobs - running_subprocess_count
  tests_to_run = all_to_run.slice!(0, to_run_count)

  run_test_set(tests_to_run)

  keep_running_full(all_to_run) if all_to_run.size > 0
end
remove_redundant_tap() click to toggle source
# File lib/browsery/parallel.rb, line 43
def remove_redundant_tap
  ever_failed_tests_file = "#{@result_dir}/ever_failed_tests.json"
  if File.file? ever_failed_tests_file
    data_hash = JSON.parse(File.read(ever_failed_tests_file))
    data_hash.keys.each do |test|
      if test.start_with? 'test_'
        tap_result_file = "#{@result_dir}/#{test}.t"
        result_lines = IO.readlines(tap_result_file)
        last_tap_start_index = 0
        last_tap_end_index = result_lines.size - 1
        result_lines.each_with_index do |l, index|
          last_tap_start_index = index if l.delete!("\n") == '1..1'
        end
        File.open(tap_result_file, 'w') do |f|
          f.puts result_lines[last_tap_start_index..last_tap_end_index]
        end
        puts "Processed #{tap_result_file}"
      else
        next
      end
    end
  else
    puts "==> File #{ever_failed_tests_file} doesn't exist - all tests passed!"
  end
end
run_in_parallel!() click to toggle source

run multiple commands with logging to start multiple tests in parallel @param [Integer, Array] n = number of tests will be running in parallel

# File lib/browsery/parallel.rb, line 130
def run_in_parallel!
  size = all_tests.size
  if size <= simultaneous_jobs
    run_test_set(all_tests)
    puts "CAUTION! All #{size} tests are starting at the same time!"
    puts "will not really run it since computer will die" if size > 30
    sleep 20
  else
    first_test_set = all_tests[0, simultaneous_jobs]
    all_to_run = all_tests[simultaneous_jobs..(all_tests.size - 1)]
    run_test_set(first_test_set)
    keep_running_full(all_to_run)
  end

  Process.waitall
  puts "\nAll Complete! Started at #{@start_time} and finished at #{Time.now}\n"
end
run_on_mac?() click to toggle source

return true only if specified to run on mac in connector @return [boolean]

# File lib/browsery/parallel.rb, line 30
def run_on_mac?
  @platform.include?('osx')
end
run_test_set(test_set) click to toggle source

runs each test from a test set in a separate child process

# File lib/browsery/parallel.rb, line 149
def run_test_set(test_set)
  test_set.each do |test|
    run_command = "#{@static_run_command} -n #{test} #{@pipe_tap} > #{@result_dir}/#{test}.t"
    pipe = IO.popen(run_command)
    puts "Running #{test}  #{pipe.pid}"
  end
end
wait_all_done_saucelabs() click to toggle source

@deprecated Too time consuming and fragile, should use more native wait/check of Process

# File lib/browsery/parallel.rb, line 192
def wait_all_done_saucelabs
  size = all_tests.size
  job_statuses = saucelabs_last_n_statuses(size)
  while job_statuses.include?('in progress')
    puts "There are tests still running, waiting..."
    sleep 20
    job_statuses = saucelabs_last_n_statuses(size)
  end
end
wait_for_pids(pids) click to toggle source

@deprecated Use more native wait/check of Process

# File lib/browsery/parallel.rb, line 177
def wait_for_pids(pids)
  running_pids = pids # assume all pids are running at this moment
  while running_pids.size > 1
    sleep 5
    puts "running_pids = #{running_pids}"
    running_pids.each do |pid|
      unless process_running?(pid)
        puts "#{pid} is not running, removing it from pool"
        running_pids.delete(pid)
      end
    end
  end
end

Private Instance Methods

get_response_with_retry(url) click to toggle source
# File lib/browsery/parallel.rb, line 243
def get_response_with_retry(url)
  retries = 5 # number of retries
  begin
    response = RestClient.get(url) # returns a String
  rescue
    puts "Failed at getting response from #{url} via RestClient \n Retrying..."
    retries -= 1
    retry if retries > 0
    response = RestClient.get(url) # retry the last time, fail if it still throws exception
  end
end
process_running?(pid) click to toggle source
# File lib/browsery/parallel.rb, line 255
def process_running?(pid)
  begin
    Process.getpgid(pid)
    true
  rescue Errno::ESRCH
    false
  end
end
saucelabs_last_n_statuses(limit) click to toggle source

call saucelabs REST API to get last #{limit} jobs' statuses possible job status: complete, error, in progress

# File lib/browsery/parallel.rb, line 206
def saucelabs_last_n_statuses(limit)
  username = Browsery.settings.sauce_username
  access_key = Browsery.settings.sauce_access_key

  # call api to get most recent #{limit} jobs' ids
  http_auth = "https://#{username}:#{access_key}@saucelabs.com/rest/v1/#{username}/jobs?limit=#{limit}"
  response = get_response_with_retry(http_auth) # response was originally an array of hashs, but RestClient converts it to a string
  # convert response back to array
  response[0] = ''
  response[response.length-1] = ''
  array_of_hash = response.split(',')
  id_array = Array.new
  array_of_hash.each do |hash|
    hash = hash.gsub(':', '=>')
    hash = eval(hash)
    id_array << hash['id'] # each hash contains key 'id' and value of id
  end

  # call api to get job statuses
  statuses = Array.new
  id_array.each do |id|
    http_auth = "https://#{username}:#{access_key}@saucelabs.com/rest/v1/#{username}/jobs/#{id}"
    response = get_response_with_retry(http_auth)
    begin
      # convert response back to hash
      str = response.gsub(':', '=>')
      # 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'
      formatted_str = str.gsub('null', 'nil')
      hash = eval(formatted_str)
      statuses << hash['status']
    rescue SyntaxError
      puts "SyntaxError, response from saucelabs has syntax error"
    end
  end
  return statuses
end