lib/xcjobs/coverage.rb
require 'rake/tasklib'
require 'json'
require 'pathname'
require 'tempfile'
require 'open3'
require 'digest/md5'
module XCJobs
module Coverage
class Coveralls < Rake::TaskLib
include Rake::DSL if defined?(Rake::DSL)
attr_accessor :repo_token
attr_accessor :service_name
attr_accessor :service_job_id
attr_accessor :service_number
attr_accessor :service_pull_request
attr_accessor :parallel
attr_accessor :service_job_number
attr_accessor :service_event_type
def initialize()
if ENV['TRAVIS']
@service_name = 'travis-ci'
@service_job_id = ENV['TRAVIS_JOB_ID']
elsif ENV['CIRCLECI']
@service_name = 'circleci'
@service_number = ENV['CIRCLE_BUILD_NUM']
@service_pull_request = (ENV['CI_PULL_REQUEST'] || "")[/(\d+)$/, 1]
@parallel = ENV['CIRCLE_NODE_TOTAL'].to_i > 1
@service_job_number = ENV['CIRCLE_NODE_INDEX']
end
@extensions = []
@excludes = []
@exclude_patterns = []
yield self if block_given?
define
end
def add_extension(extension)
@extensions << extension
end
def add_exclude(exclude)
@excludes << exclude
end
def add_exclude_pattern(exclude_pattern)
if !exclude_pattern.kind_of?(Regexp)
exclude_pattern = Regexp.new(exclude_pattern)
end
@exclude_patterns << exclude_pattern
end
private
def define
namespace :coverage do
desc 'send coverage report to Coveralls'
task :coveralls do
root = %x[git rev-parse --show-toplevel].strip
report = collect(root)
file = write_report(report)
upload(file)
end
end
end
def collect(base_dir)
report = {}
report['repo_token'] = repo_token if repo_token
report['service_name'] = service_name if service_name
report['service_job_id'] = service_job_id if service_job_id
report['service_number'] = service_number if service_number
report['service_pull_request'] = service_pull_request if service_pull_request
report['parallel'] = parallel if parallel
report['service_job_number'] = service_job_number if service_job_number
report['service_event_type'] = service_event_type if service_event_type
report['source_files'] = []
Dir.glob("#{base_dir}/**/*.gcov").each do |file|
File.open(file, "r") do |handle|
source_file = {}
name = ''
source_digest = nil
coverage = []
handle.each_line do |line|
match = /^[ ]*([0-9]+|-|#####):[ ]*([0-9]+):(.*)/.match(line)
next unless match.to_a.count == 4
count, number, text = match.to_a[1..3]
if number.to_i == 0
key, val = /([^:]+):(.*)$/.match(text).to_a[1..2]
if key == 'Source'
name = Pathname(val).relative_path_from(Pathname(base_dir)).to_s
if File.exist?(val)
source_digest = Digest::MD5.file(val).to_s
end
end
else
coverage[number.to_i - 1] = case count.strip
when '-'
nil
when '#####'
if text.strip == '}'
nil
else
0
end
else count.to_i
end
end
end
if !is_excluded_path(name) && !source_digest.nil?
source_file['name'] = name
source_file['source_digest'] = source_digest
source_file['coverage'] = coverage
report['source_files'] << source_file
end
end
end
remotes = %x[git remote -v].rstrip.split(/\r?\n/).map {|line| line.chomp }.select { |line| line.include? 'fetch'}.first.split(' ')
report['git'] = {
'head' => {
'id' => %x[git --no-pager log -1 --pretty=format:%H],
'author_name' => %x[git --no-pager log -1 --pretty=format:%aN],
'author_email' => %x[git --no-pager log -1 --pretty=format:%ae],
'committer_name' => %x[git --no-pager log -1 --pretty=format:%cN],
'committer_email' => %x[git --no-pager log -1 --pretty=format:%ce],
'message' => %x[git --no-pager log -1 --pretty=format:%s] },
'branch' => %x[git rev-parse --abbrev-ref HEAD].strip,
'remotes' => {'name' => remotes[0], 'remote' => remotes[1]} }
report
end
def is_excluded_path(filepath)
if filepath.start_with?('..')
return true
end
@excludes.each do |exclude|
if filepath.start_with?(exclude)
return true
end
end
@exclude_patterns.each do |pattern|
if filepath.match(pattern)
return true
end
end
if !@extensions.empty?
@extensions.each do |extension|
if File.extname(filepath) == extension
return false
end
end
return true
else
return false
end
end
def write_report(report)
tempdir = Pathname.new(Dir.tmpdir).join(SecureRandom.hex)
FileUtils.mkdir_p(tempdir)
tempfile = File::open(tempdir.join('coveralls.json'), "w")
tempfile.puts(report.to_json)
tempfile.flush
tempfile.path
end
def upload(json_file)
curl_options = ['curl', '-sSf', '-F', "json_file=@#{json_file}", 'https://coveralls.io/api/v1/jobs']
puts curl_options.join(' ')
Open3.popen2e({ "NSUnbufferedIO" => "YES" }, *curl_options) do |stdin, stdout_err, wait_thr|
output = ''
while line = stdout_err.gets
puts line
output << line
end
status = wait_thr.value
if !status.success?
fail "upload failed (exited with status: #{status.exitstatus})"
end
end
end
end
end
end