class Lcoveralls::Runner

Runs the Lcoveralls application.

Public Class Methods

new() click to toggle source

Initializes a new Locveralls::Runner instance.

# File lib/lcoveralls/runner.rb, line 26
def initialize
  # Parse the command line options.
  parser = Lcoveralls::OptionParser.new
  @options = parser.parse! ARGV

  # Setup a logger instance.
  @log = Logger.new(STDERR)
  @log.formatter = Lcoveralls::ColorFormatter.new @options[:color]
  @log.sev_threshold = @options[:severity]
  @log.debug { "Options: #{@options}" }
end

Public Instance Methods

find_root(info_files) click to toggle source

Attempts to auto-detect the git repository root.

This method looks throuhgh all source files covered by the supplied tracefiles, and for each, check if they are part of a git repository. The method then returns git repository's root directory.

If more than one git repository are found to be covered by the tracefiles then a warning will be logged, and the root of the repository with the largest number of source files covered will be returned.

If no git repository roots could be found, then nil is returned.

@param info_files [Array] A list of LCOV tracefiles (aka *.info files).

@return [String, nil] A git repository root, if found, otherwise nil.

# File lib/lcoveralls/runner.rb, line 53
def find_root(info_files)
  # Try source file(s) covered by the lcov tracefile(s).
  root_dirs = Hash.new(0)
  info_files.each do |file|
    File.open(file).each do |line|
      line.match(/^SF:(.*)$/) do |match|
        Dir.chdir(File.dirname(match[1])) do
          root_dir = `"#{@options[:git]}" rev-parse --show-toplevel`.rstrip
         root_dirs[root_dir] = root_dirs[root_dir] + 1 unless root_dir.empty?
        end if Dir.exist?(File.dirname(match[1]))
      end
    end
  end

  if root_dirs.empty?
    nil
  elsif root_dirs.size == 1
    root_dirs.shift[0]
  else
    root_dir = root_dirs.max_by { |key, value| value }[0]
    @log.warn "Found multiple possible repo roots; settled on: #{root_dir}"
    root_dir
  end
end
get_git_info(root_dir) click to toggle source

Get git repository information in the Coveralls API structure.

@param root_dir Git repository root directory.

@return [Hash] Git repository information.

# File lib/lcoveralls/runner.rb, line 183
def get_git_info(root_dir)
  Dir.chdir(root_dir) do
    info = {
      :head => {
        :id             => `"#{@options[:git]}" show --format='%H' --no-patch`.rstrip,
        :author_name    => `"#{@options[:git]}" show --format='%an' --no-patch`.rstrip,
        :author_email   => `"#{@options[:git]}" show --format='%ae' --no-patch`.rstrip,
        :commiter_name  => `"#{@options[:git]}" show --format='%cn' --no-patch`.rstrip,
        :commiter_email => `"#{@options[:git]}" show --format='%ce' --no-patch`.rstrip,
        :message        => `"#{@options[:git]}" show --format='%B' --no-patch`.rstrip,
      },
      :branch  => `"#{@options[:git]}" rev-parse --abbrev-ref HEAD`.rstrip,
      :remotes => []
    }

    `"#{@options[:git]}" remote --verbose`.each_line do |line|
      line.match(/^(?<name>\S+)\s+(?<url>\S+)(\s+\((fetch|push)\))?/) do |match|
        info[:remotes] << Hash[match.names.zip(match.captures)]
      end
    end
    info[:remotes].uniq!
    info.delete(:remotes) if info[:remotes].empty?

    info
  end if Dir.exist?(root_dir)
end
get_percentage(lines_hit, lines_found, bold=false) click to toggle source

Format a percentage string.

This method formats the number of lines hit, as a percentage of the total number of lines, including prepended spaces, and color codes where appropriate.

If the percentage cannot be calculated (for example, either parameter is nil, NaN, +/- ininity, etc), then this function will return a 'blank' string - one with enough spaces to match the width of other valid percentage strings returned by this function.

@param lines_hit [Integer, nil] Number of lines hit by unit tests. @param lines_found [Integer, nil] Number of executable lines.

@return [String] Percentage of lines overered.

# File lib/lcoveralls/runner.rb, line 93
def get_percentage(lines_hit, lines_found, bold=false)
  perc = lines_hit.to_f / lines_found.to_f * 100.0
  color = case when perc >= 90; 32 when perc >= 75; 33 else 31 end
  if bold then color = "1;#{color}" end
  perc = perc.finite? ? format('%5.1f%%', perc) : ' ' * 6
  perc = "\x1b[#{color}m#{perc}\x1b[0m" if @options[:color] and color
  perc
end
get_source_files(info_files, root_dir) click to toggle source

Builds a hash of source files matching the Coveralls API.

This method will build a Hash containing all source files covered by the supplied LCOV tracefiles, that reside within the specified repository root directory.

@param info_files [Array] LCOV tracefiles containing source files to load. @param root_dir [String] Repository root directory.

@return [Hash] Source files in Coveralls API structure.

# File lib/lcoveralls/runner.rb, line 112
def get_source_files(info_files, root_dir)
  sources = {}
  total_lines_found = 0
  total_lines_hit = 0
  info_files.each do |file|
    @log.debug "Processing tracefile: #{file}"
    source_pathname = nil
    in_record = false
    lines_found = nil
    lines_hit = nil
    File.open(file).each do |line|
      @log.debug "#{file}: #{line.rstrip}"

      # SF:<absolute path to the source file>
      line.match('^SF:' + Regexp.quote(root_dir) + '/(.*)$') do |match|
        @log.warn 'Found source filename without preceding end_of_record' if in_record
        @log.debug "Found source filename: #{match[1]}"
        source_pathname = match[1]
        if !sources.has_key?(source_pathname) then
          source = File.read(match[1])
          sources[source_pathname] = {
            :name => source_pathname,
            :source => source,
            :coverage => Array.new(source.lines.count)
          }
        end
        in_record = true
      end

      # DA:<line number>,<execution count>[,<checksum>]
      line.match(/^DA:(?<line>\d+),(?<count>\d+)(,(?<checksum>.*))?$/) do |match|
        line_index = match[:line].to_i - 1
        if !sources[source_pathname][:coverage][line_index] then
          sources[source_pathname][:coverage][line_index] = 0
        end
        sources[source_pathname][:coverage][line_index] = 
          sources[source_pathname][:coverage][line_index] + match[:count].to_i;
      end if in_record

      # LF:<lines found> or LH:<lines hit>
      line.match(/^LF:(?<count>\d+)$/) { |match| lines_found = match[:count] }
      line.match(/^LH:(?<count>\d+)$/) { |match| lines_hit   = match[:count] }

      # end_of_record
      if line == "end_of_record\n" and in_record then
        @log.info begin
          perc = get_percentage(lines_hit, lines_found)
          "[#{perc}] #{source_pathname} (#{lines_hit}/#{lines_found})"
        end
        total_lines_found = total_lines_found + lines_found.to_i
        total_lines_hit = total_lines_hit + lines_hit.to_i
        in_record = false
        lines_found = nil
        lines_hit = nil
      end
    end
  end

  @log.info begin
    perc = get_percentage(total_lines_hit, total_lines_found, true)
    "[#{perc}] Total (#{total_lines_hit}/#{total_lines_found})"
  end

  sources.values
end
run() click to toggle source

Runs the Lcoveralls application.

This method does the real work of building up the Coveralls API request according to the parsed options, and submitting the request to Coveralls.

# File lib/lcoveralls/runner.rb, line 241
def run
  # Find *.info tracefiles if none specified on the command line.
  Find.find('.') do |path|
    @log.trace { "Looking for tracefiles: #{path}" }
    if path =~ /.*\.info$/ then
      @log.info { "Found tracefile: #{path}" }
      ARGV << path
    end
  end unless ARGV.any?

  @options[:root] = find_root(ARGV) unless @options.include?(:root)
  if !@options[:root] then
    @log.error 'Root not specified, nor detected; consider using --root'
    exit!
  end

  # Build the coveralls.io job request.
  job = {}
  job[:repo_token] = @options[:token] if @options.has_key? :token
  job[:service_name] = @options[:service] if @options.has_key? :service
  job[:service_job_id] = @options[:job_id] if @options.has_key? :job_id
  if !job.has_key?(:token) and !job.has_key?(:service_job_id) then
    @log.warn 'No service job id detected; consider using --token'
  end
  job[:source_files] = get_source_files(ARGV, @options[:root])
  job[:git] = get_git_info(@options[:root]) unless !@options[:git]
  job[:run_at] = Time.new
  request = Lcoveralls::CoverallsRequest.new(job)
  @log.trace { request.body }

  # If asked to, export the Coveralls API job request JSON document.
  if @options.has_key? :export then
    @options[:export].write(JSON::pretty_generate job);
  end

  # Send (if not in dryrun mode) the Coveralls API request.
  uri = URI('https://coveralls.io/api/v1/jobs')
  http = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = @options[:timeout] if @options.has_key? :timeout
  http.read_timeout = @options[:timeout] if @options.has_key? :timeout
  http.ssl_timeout = @options[:timeout] if @options.has_key? :timeout
  http.use_ssl = true
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER

  if !@options[:dryrun] then
    begin
      @log.debug { "Sending #{request.body.size} bytes to coveralls.io" }
      response = http.request(request)
      @log.debug { "HTTP response status: #{response.code} #{response.message}" }
      raise response.code unless response.is_a? Net::HTTPSuccess
      puts response.body
    rescue RuntimeError
      raise unless response
      @log.error { "Received non-OK response: #{response.code} #{response.message}" }
      puts response.body
      retry if should_retry? unless response.is_a? Net::HTTPClientError
      exit!
    rescue SocketError => error
      @log.error { error }
      retry if should_retry?
      exit!
    end
  end
end
should_retry?() click to toggle source

Should we retry a failed Coveralls API request?

This method is called by {#run} on internal and server errors to check if the API request should be retried. Specifically, this function checks the :retry_count option, and if greater than zero decrements it before returning true.

Additionally, if retrying is appropriate, and the :retry_interval option is greater than zero, this function will also sleep for that interval.

@return [Boolean] true if the caller should retry the API request, or

+false+ if no more retries should be attempted.
# File lib/lcoveralls/runner.rb, line 222
def should_retry?
  return false unless @options[:retry_count] > 0
  @options[:retry_count] = @options[:retry_count] - 1;

  if @options[:retry_interval] > 0 then
    @log.info { "Sleeping for #{@options[:retry_interval]} seconds before retrying" }
    begin
      sleep @options[:retry_interval]
    rescue Interrupt
      return false
    end
  end
  true
end