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