├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── Rakefile ├── Readme.md ├── bin └── codeclimate-batch ├── codeclimate_batch.gemspec ├── lib ├── codeclimate_batch.rb └── codeclimate_batch │ └── version.rb └── spec ├── codeclimate_batch_spec.rb ├── files ├── report_a.json └── report_b.json └── spec_helper.rb /.travis.yml: -------------------------------------------------------------------------------- 1 | bundler_args: "" 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.5 6 | branches: 7 | only: master 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | gem "bump" 5 | gem "rake" 6 | gem "rspec" 7 | gem "codeclimate-test-reporter" # only needed for .start 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | codeclimate_batch (0.5.0) 5 | json 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | bump (0.5.2) 11 | codeclimate-test-reporter (0.4.8) 12 | simplecov (>= 0.7.1, < 1.0.0) 13 | diff-lcs (1.2.5) 14 | docile (1.1.5) 15 | json (1.8.2) 16 | rake (10.4.2) 17 | rspec (3.2.0) 18 | rspec-core (~> 3.2.0) 19 | rspec-expectations (~> 3.2.0) 20 | rspec-mocks (~> 3.2.0) 21 | rspec-core (3.2.3) 22 | rspec-support (~> 3.2.0) 23 | rspec-expectations (3.2.1) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.2.0) 26 | rspec-mocks (3.2.1) 27 | diff-lcs (>= 1.2.0, < 2.0) 28 | rspec-support (~> 3.2.0) 29 | rspec-support (3.2.2) 30 | simplecov (0.10.0) 31 | docile (~> 1.1.0) 32 | json (~> 1.8) 33 | simplecov-html (~> 0.10.0) 34 | simplecov-html (0.10.0) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | bump 41 | codeclimate-test-reporter 42 | codeclimate_batch! 43 | rake 44 | rspec 45 | 46 | BUNDLED WITH 47 | 1.12.3 48 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Michael Grosser 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "bundler/gem_tasks" 3 | require "bump/tasks" 4 | 5 | task :default do 6 | sh "rspec spec/" 7 | end 8 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [Deprecated, use builtin](https://docs.codeclimate.com/docs/setting-up-test-coverage#section-parallel-tests-and-multiple-test-suites) 2 | 3 | Report a batch of codeclimate results by merging and from multiple servers.
4 | Uses [cc-amend](https://github.com/grosser/cc-amend) to do the merging since workers will not share a common disk. 5 | 6 | Install 7 | ======= 8 | 9 | ```Bash 10 | gem install codeclimate_batch 11 | ``` 12 | 13 | Usage 14 | ===== 15 | 16 | ```Ruby 17 | # test_helper.rb 18 | if ENV['CI'] 19 | require 'codeclimate_batch' 20 | CodeclimateBatch.start 21 | end 22 | ``` 23 | 24 | - Will run when `ENV['CODECLIMATE_REPO_TOKEN']` is set and running on `master` branch 25 | - If your default branch is not `master`, set `ENV['DEFAULT_BRANCH']` 26 | - Will also run on Pull Requests on Travis 27 | 28 | After tests have finished: 29 | 30 | ```Bash 31 | # send coverage reports to cc-amend, unifying once 4 reports arrive 32 | codeclimate-batch --groups 4 33 | 34 | # custom key (when not using travis), must be the same on all hosts 35 | codeclimate-batch --groups 4 --key my-app/$BUILD_NUMBER 36 | ``` 37 | 38 | Author 39 | ====== 40 | [Michael Grosser](http://grosser.it)
41 | michael@grosser.it
42 | License: MIT
43 | [![Build Status](https://travis-ci.org/grosser/codeclimate_batch.png)](https://travis-ci.org/grosser/codeclimate_batch) 44 | -------------------------------------------------------------------------------- /bin/codeclimate-batch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # report coverage to code-climate by combining reports from multiple servers 3 | 4 | # enable local usage from cloned repo 5 | root = File.expand_path("../..", __FILE__) 6 | $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile") 7 | 8 | require 'benchmark' 9 | require 'optparse' 10 | require 'codeclimate_batch' 11 | require 'tmpdir' 12 | 13 | options = {} 14 | OptionParser.new do |opts| 15 | opts.banner = <= 0.4.8' # get CODECLIMATE_TO_FILE support and avoid deprecations 12 | require 'codeclimate-test-reporter' 13 | CodeClimate::TestReporter.start 14 | end 15 | 16 | def unify(coverage_files) 17 | initial, *rest = coverage_files 18 | report = load(initial) 19 | rest.each do |file| 20 | merge_source_files(report.fetch("source_files"), load(file).fetch("source_files")) 21 | end 22 | recalculate_counters(report) 23 | report 24 | end 25 | 26 | private 27 | 28 | # Return the default branch. Most of the time it's master, but can be overridden 29 | # by setting DEFAULT_BRANCH in the environment. 30 | def default_branch 31 | ENV['DEFAULT_BRANCH'] || 'master' 32 | end 33 | 34 | # Check if we are running on Travis CI. 35 | def travis? 36 | ENV['TRAVIS'] 37 | end 38 | 39 | # Check if our Travis build is running on the default branch. 40 | def outside_default_branch? 41 | default_branch != ENV['TRAVIS_BRANCH'] 42 | end 43 | 44 | # Check if running a pull request. 45 | def pull_request? 46 | ENV['TRAVIS_PULL_REQUEST'].to_i != 0 47 | end 48 | 49 | def load(file) 50 | JSON.load(File.read(file)) 51 | end 52 | 53 | def recalculate_counters(report) 54 | source_files = report.fetch("source_files").map { |s| s["line_counts"] } 55 | report["line_counts"].keys.each do |k| 56 | report["line_counts"][k] = source_files.map { |s| s[k] }.inject(:+) 57 | end 58 | end 59 | 60 | def merge_source_files(all, source_files) 61 | source_files.each do |new_file| 62 | old_file = all.detect { |source_file| source_file["name"] == new_file["name"] } 63 | 64 | if old_file 65 | # merge source files 66 | coverage = merge_coverage( 67 | JSON.load(new_file.fetch("coverage")), 68 | JSON.load(old_file.fetch("coverage")) 69 | ) 70 | old_file["coverage"] = JSON.dump(coverage) 71 | 72 | total = coverage.size 73 | missed, covered = coverage.compact.partition { |l| l == 0 }.map(&:size) 74 | old_file["covered_percent"] = (covered == 0 ? 0.0 : covered * 100.0 / (covered + missed)) 75 | old_file["line_counts"] = {"total" => total, "covered" => covered, "missed" => missed} 76 | else 77 | # just use the new value 78 | all << new_file 79 | end 80 | end 81 | end 82 | 83 | # [nil,1,0] + [nil,nil,2] -> [nil,1,2] 84 | def merge_coverage(a,b) 85 | b.map! do |b_count| 86 | a_count = a.shift 87 | (!b_count && !a_count) ? nil : b_count.to_i + a_count.to_i 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/codeclimate_batch/version.rb: -------------------------------------------------------------------------------- 1 | module CodeclimateBatch 2 | VERSION = "0.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/codeclimate_batch_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "codeclimate-test-reporter" 3 | 4 | describe CodeclimateBatch do 5 | def with_env(env) 6 | env.each { |k,v| ENV[k] = v } 7 | yield 8 | ensure 9 | env.each { |k,_v| ENV[k] = nil } 10 | end 11 | 12 | it "has a VERSION" do 13 | CodeclimateBatch::VERSION.should =~ /^[\.\da-z]+$/ 14 | end 15 | 16 | describe "CLI" do 17 | def sh(command, options={}) 18 | result = `#{command} #{"2>&1" unless options[:keep_output]}` 19 | raise "#{options[:fail] ? "SUCCESS" : "FAIL"} #{command}\n#{result}" if $?.success? == !!options[:fail] 20 | result 21 | end 22 | 23 | def batch(command, options={}) 24 | sh("#{Bundler.root}/bin/codeclimate-batch #{command}", options) 25 | end 26 | 27 | it "shows --version" do 28 | batch("--version").should include(CodeclimateBatch::VERSION) 29 | end 30 | 31 | it "shows --help" do 32 | batch("--help").should include("codeclimate") 33 | end 34 | 35 | it "does nothing when there are no files" do 36 | batch("--groups 4").should == "Code climate: No files found to report\n" 37 | end 38 | 39 | it "merges and reports" do 40 | with_env("TRAVIS_REPO_SLUG" => "xxx/yyy", "TRAVIS_BUILD_NUMBER" => rand(900000).to_s) do 41 | base = "#{Dir.tmpdir}/codeclimate-test-coverage-" 42 | 43 | # pretend we just ran code climate reporter 44 | ["report_a.json", "report_b.json"].each do |r| 45 | sh "cp #{Bundler.root}/spec/files/#{r} #{base}#{r}" 46 | end 47 | Dir["#{base}*"].size.should == 2 48 | 49 | # send reports 50 | result = batch("--groups 4") 51 | result.should include "waiting for 3/4 reports on xxx-yyy-" 52 | result.sub(/\d+\.\d+s/,'TIME').should include "Code climate: TIME to send 2 reports" 53 | 54 | # all cleaned up ? 55 | Dir["#{base}*"].size.should == 0 56 | end 57 | end 58 | end 59 | 60 | describe ".start" do 61 | let(:default) {{"TRAVIS" => "1", "TRAVIS_BRANCH" => "master", "CODECLIMATE_TO_FILE" => nil, "TRAVIS_PULL_REQUEST" => nil}} 62 | 63 | it "calls start when on travis master" do 64 | with_env(default) do 65 | CodeClimate::TestReporter.should_receive(:start) 66 | CodeclimateBatch.start 67 | ENV["CODECLIMATE_TO_FILE"].should == "1" 68 | end 69 | end 70 | 71 | it "starts without travis since we don't know how to handle other cis" do 72 | default.delete("TRAVIS") 73 | with_env(default) do 74 | CodeClimate::TestReporter.should_receive(:start) 75 | CodeclimateBatch.start 76 | end 77 | end 78 | 79 | it "does not start on different branch" do 80 | default["TRAVIS_BRANCH"] = "mooo" 81 | with_env(default) do 82 | CodeClimate::TestReporter.should_not_receive(:start) 83 | CodeclimateBatch.start 84 | end 85 | end 86 | 87 | it "starts on different branch if set as default branch" do 88 | default.merge! "TRAVIS_BRANCH" => "moooo", "DEFAULT_BRANCH" => "moooo" 89 | with_env(default) do 90 | CodeClimate::TestReporter.should_receive(:start) 91 | CodeclimateBatch.start 92 | end 93 | end 94 | 95 | it "does not starts on different branch if it doesn't match default branch" do 96 | default.merge! "TRAVIS_BRANCH" => "moooo", "DEFAULT_BRANCH" => "monster" 97 | with_env(default) do 98 | CodeClimate::TestReporter.should_not_receive(:start) 99 | CodeclimateBatch.start 100 | end 101 | end 102 | 103 | it "starts on PR" do 104 | default["TRAVIS_PULL_REQUEST"] = "123" 105 | with_env(default) do 106 | CodeClimate::TestReporter.should_receive(:start) 107 | CodeclimateBatch.start 108 | end 109 | end 110 | end 111 | 112 | describe ".unify" do 113 | it "merges reports 1 report" do 114 | report = CodeclimateBatch.unify(["spec/files/report_a.json"]) 115 | report["line_counts"].should == {"total" => 18, "covered" => 7, "missed" => 3} 116 | end 117 | 118 | it "merges multiple reports" do 119 | report = CodeclimateBatch.unify(["spec/files/report_a.json", "spec/files/report_b.json"]) 120 | report["line_counts"].should == {"total" => 18, "covered" => 9, "missed" => 1} 121 | end 122 | end 123 | 124 | describe ".merge_source_files" do 125 | it "merges" do 126 | all = [{"name" => "a.rb", "coverage" => '[null,1,null]'}, {"name" => "b.rb", "coverage" => '[1,1,null]'}] 127 | CodeclimateBatch.send(:merge_source_files, 128 | all, 129 | [{"name" => "b.rb", "coverage" => '[null,2,1]'}] 130 | ) 131 | all.should == [ 132 | {"name"=>"a.rb", "coverage"=>"[null,1,null]"}, 133 | {"name"=>"b.rb", "coverage"=>"[1,3,1]", "covered_percent"=>100.0, "line_counts"=>{"total"=>3, "covered"=>3, "missed"=>0}} 134 | ] 135 | end 136 | 137 | it "merges uncovered" do 138 | all = [{"name" => "a.rb", "coverage" => '[]'}] 139 | CodeclimateBatch.send(:merge_source_files, 140 | all, 141 | all 142 | ) 143 | all.should == [{"name"=>"a.rb", "coverage"=>"[]", "covered_percent"=>0.0, "line_counts"=>{"total"=>0, "covered"=>0, "missed"=>0}}] 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /spec/files/report_a.json: -------------------------------------------------------------------------------- 1 | {"repo_token":"63c0e1b7dad4058225454f297914889b2ea19974983df707e3272ffa821ca7f5","source_files":[{"name":"lib/cover_me.rb","blob_id":"765eb20832f79ab2dd508594eda2431cacd42480","coverage":"[1,1,1,null,null,1,0,null,null,1,0,null,null]","covered_percent":71.43,"covered_strength":0.7,"line_counts":{"total":13,"covered":5,"missed":2}},{"name":"lib/cover_me2.rb","blob_id":"93cbc4b8a1a5348d325f294889634b0f8d8b381a","coverage":"[1,1,0,null,null]","covered_percent":66.67,"covered_strength":0.7,"line_counts":{"total":5,"covered":2,"missed":1}}],"run_at":1428707600,"covered_percent":70.0,"covered_strength":0.7,"line_counts":{"total":18,"covered":7,"missed":3},"partial":false,"git":{"head":"ca88697f408f880b1d9418af651f21f9e62135d8","committed_at":1428657631,"branch":"master"},"environment":{"test_framework":"fooo","pwd":"/Users/mgrosser/Code/tools/cc-amend","rails_root":null,"simplecov_root":"/Users/mgrosser/Code/tools/cc-amend","gem_version":"0.4.7"},"ci_service":{}} -------------------------------------------------------------------------------- /spec/files/report_b.json: -------------------------------------------------------------------------------- 1 | {"repo_token":"63c0e1b7dad4058225454f297914889b2ea19974983df707e3272ffa821ca7f5","source_files":[{"name":"lib/cover_me.rb","blob_id":"765eb20832f79ab2dd508594eda2431cacd42480","coverage":"[1,1,0,null,null,1,1,null,null,1,0,null,null]","covered_percent":71.43,"covered_strength":0.7,"line_counts":{"total":13,"covered":5,"missed":2}},{"name":"lib/cover_me2.rb","blob_id":"93cbc4b8a1a5348d325f294889634b0f8d8b381a","coverage":"[1,1,1,null,null]","covered_percent":100.0,"covered_strength":1.0,"line_counts":{"total":5,"covered":3,"missed":0}}],"run_at":1428707601,"covered_percent":80.0,"covered_strength":0.79,"line_counts":{"total":18,"covered":8,"missed":2},"partial":false,"git":{"head":"ca88697f408f880b1d9418af651f21f9e62135d8","committed_at":1428657631,"branch":"master"},"environment":{"test_framework":"fooo","pwd":"/Users/mgrosser/Code/tools/cc-amend","rails_root":null,"simplecov_root":"/Users/mgrosser/Code/tools/cc-amend","gem_version":"0.4.7"},"ci_service":{}} -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "codeclimate_batch/version" 3 | require "codeclimate_batch" 4 | 5 | RSpec.configure do |config| 6 | config.expect_with(:rspec) { |c| c.syntax = :should } 7 | config.mock_with(:rspec) { |c| c.syntax = :should } 8 | end 9 | --------------------------------------------------------------------------------