Class: Lcoveralls::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/lcoveralls/runner.rb

Overview

Runs the Lcoveralls application.

Instance Method Summary collapse

Constructor Details

#initializeRunner

Initializes a new Locveralls::Runner instance.



26
27
28
29
30
31
32
33
34
35
36
# 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

Instance Method Details

#find_root(info_files) ⇒ String?

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.

Parameters:

  • info_files (Array)

    A list of LCOV tracefiles (aka *.info files).

Returns:

  • (String, nil)

    A git repository root, if found, otherwise nil.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 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) ⇒ Hash

Get git repository information in the Coveralls API structure.

Parameters:

  • root_dir

    Git repository root directory.

Returns:

  • (Hash)

    Git repository information.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# 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) ⇒ String

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.

Parameters:

  • lines_hit (Integer, nil)

    Number of lines hit by unit tests.

  • lines_found (Integer, nil)

    Number of executable lines.

Returns:

  • (String)

    Percentage of lines overered.



93
94
95
96
97
98
99
100
# 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) ⇒ Hash

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.

Parameters:

  • info_files (Array)

    LCOV tracefiles containing source files to load.

  • root_dir (String)

    Repository root directory.

Returns:

  • (Hash)

    Source files in Coveralls API structure.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# 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

#runObject

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.



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# 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?Boolean

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.

Returns:

  • (Boolean)

    true if the caller should retry the API request, or false if no more retries should be attempted.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
# 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