├── .rspec ├── spec ├── coveralls │ ├── fixtures │ │ ├── app │ │ │ ├── vendor │ │ │ │ └── vendored_gem.rb │ │ │ ├── models │ │ │ │ ├── dog.rb │ │ │ │ ├── house.rb │ │ │ │ ├── robot.rb │ │ │ │ ├── airplane.rb │ │ │ │ └── user.rb │ │ │ └── controllers │ │ │ │ └── sample.rb │ │ └── sample.rb │ ├── output_spec.rb │ ├── coveralls_spec.rb │ ├── simple_cov │ │ └── formatter_spec.rb │ └── configuration_spec.rb └── spec_helper.rb ├── lib ├── coveralls │ ├── version.rb │ ├── rake │ │ └── task.rb │ ├── command.rb │ ├── output.rb │ ├── simplecov.rb │ ├── api.rb │ └── configuration.rb └── coveralls.rb ├── .gitignore ├── bin └── coveralls ├── .github ├── dependabot.yml └── workflows │ ├── rubocop.yml │ └── ruby.yml ├── Rakefile ├── Gemfile ├── LICENSE ├── .rubocop.yml ├── coveralls-ruby.gemspec ├── CHANGELOG.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --warn 3 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/app/vendor/vendored_gem.rb: -------------------------------------------------------------------------------- 1 | # this file should not be covered 2 | -------------------------------------------------------------------------------- /lib/coveralls/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coveralls 4 | VERSION = '0.29.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/app/models/dog.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = 'baz' 5 | end 6 | 7 | def bar 8 | @foo 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/app/models/house.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = 'baz' 5 | end 6 | 7 | def bar 8 | @foo 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/app/models/robot.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = 'baz' 5 | end 6 | 7 | def bar 8 | @foo 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/sample.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = 'baz' 5 | end 6 | 7 | # :nocov: 8 | def bar 9 | @foo 10 | end 11 | # :nocov: 12 | end 13 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/app/models/airplane.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Foo class 4 | class Foo 5 | def initialize 6 | @foo = 'baz' 7 | end 8 | 9 | def bar 10 | @foo 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/app/controllers/sample.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Foo class 4 | class Foo 5 | def initialize 6 | @foo = 'baz' 7 | end 8 | 9 | # :nocov: 10 | def bar 11 | @foo 12 | end 13 | # :nocov: 14 | end 15 | -------------------------------------------------------------------------------- /spec/coveralls/fixtures/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # Foo class 2 | class Foo 3 | def initialize 4 | @foo = 'baz' 5 | end 6 | 7 | def bar 8 | @foo 9 | end 10 | 11 | def foo 12 | if @foo 13 | 'bar' 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | 19 | .DS_Store 20 | /vendor/ 21 | 22 | .byebug_history 23 | -------------------------------------------------------------------------------- /bin/coveralls: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path('../lib', __dir__) 5 | $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib) 6 | 7 | require 'coveralls' 8 | require 'coveralls/command' 9 | 10 | Coveralls::CommandLine.start 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | - package-ecosystem: bundler 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | time: "04:00" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | require 'rubygems' 6 | require 'rake' 7 | require 'rspec/core/rake_task' 8 | 9 | require 'rubocop/rake_task' 10 | 11 | RuboCop::RakeTask.new 12 | 13 | desc 'Run RSpec' 14 | RSpec::Core::RakeTask.new do |t| 15 | t.verbose = false 16 | end 17 | 18 | task default: %i[rubocop spec] 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in coveralls-ruby.gemspec 6 | gemspec 7 | 8 | platforms :jruby do 9 | gem 'jruby-openssl' 10 | end 11 | 12 | gem 'bundler' 13 | gem 'rake' 14 | gem 'rspec' 15 | gem 'rubocop' 16 | gem 'rubocop-packaging' 17 | gem 'rubocop-performance' 18 | gem 'rubocop-rake' 19 | gem 'rubocop-rspec' 20 | gem 'simplecov-lcov' 21 | gem 'vcr' 22 | gem 'webmock' 23 | -------------------------------------------------------------------------------- /lib/coveralls/rake/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake' 4 | require 'rake/tasklib' 5 | 6 | module Coveralls 7 | class RakeTask < ::Rake::TaskLib 8 | include ::Rake::DSL if defined?(::Rake::DSL) 9 | 10 | def initialize(*_args) # rubocop:disable Lint/MissingSuper 11 | namespace :coveralls do 12 | desc 'Push latest coverage results to Coveralls.io' 13 | task :push do 14 | require 'coveralls' 15 | 16 | Coveralls.push! 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: RuboCop 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | rubocop: 14 | name: RuboCop 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | BUNDLE_JOBS: 4 18 | BUNDLE_RETRY: 3 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest] 22 | ruby-version: ['3.4'] 23 | 24 | steps: 25 | - uses: actions/checkout@v6 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby-version }} 30 | bundler-cache: true 31 | - name: Ruby linter 32 | run: bundle exec rubocop --format github 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wil Gieseler 4 | 5 | Copyright (c) 2023 Geremia Taglialatela 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-packaging 3 | - rubocop-performance 4 | - rubocop-rake 5 | - rubocop-rspec 6 | 7 | AllCops: 8 | NewCops: enable 9 | DisplayStyleGuide: true 10 | ExtraDetails: true 11 | TargetRubyVersion: 2.6 12 | Exclude: 13 | - .git/**/* 14 | - spec/coveralls/fixtures/**/* 15 | - tmp/**/* 16 | - vendor/**/* 17 | 18 | Layout/HashAlignment: 19 | EnforcedColonStyle: table 20 | EnforcedHashRocketStyle: table 21 | 22 | Layout/LineLength: 23 | Enabled: false 24 | 25 | Metrics/AbcSize: 26 | Max: 26.31 27 | Exclude: 28 | - 'lib/coveralls/configuration.rb' 29 | 30 | Metrics/ClassLength: 31 | Exclude: 32 | - 'lib/coveralls/configuration.rb' 33 | 34 | Metrics/CyclomaticComplexity: 35 | Exclude: 36 | - 'lib/coveralls/configuration.rb' 37 | 38 | Metrics/PerceivedComplexity: 39 | Exclude: 40 | - 'lib/coveralls/configuration.rb' 41 | 42 | Metrics/BlockLength: 43 | Exclude: 44 | - 'spec/**/*' 45 | 46 | Metrics/MethodLength: 47 | Enabled: false 48 | 49 | Metrics/ModuleLength: 50 | Exclude: 51 | - 'lib/coveralls/configuration.rb' 52 | - 'spec/**/*' 53 | 54 | RSpec/ExampleLength: 55 | Max: 15 56 | 57 | RSpec/MultipleExpectations: 58 | Enabled: false 59 | 60 | RSpec/MultipleMemoizedHelpers: 61 | Enabled: false 62 | 63 | RSpec/NestedGroups: 64 | Max: 4 65 | 66 | Style/Documentation: 67 | Enabled: false 68 | 69 | Style/FetchEnvVar: 70 | Enabled: false 71 | 72 | Style/IfUnlessModifier: 73 | Enabled: false 74 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby specs 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: Tests 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', 'jruby-9.3', 'jruby-9.4', 'jruby-10.0'] 19 | channel: ['stable'] 20 | 21 | include: 22 | - ruby-version: 'head' 23 | channel: 'experimental' 24 | - ruby-version: 'jruby-head' 25 | channel: 'experimental' 26 | 27 | continue-on-error: ${{ matrix.channel != 'stable' }} 28 | 29 | steps: 30 | - uses: actions/checkout@v6 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: ${{ matrix.ruby-version }} 35 | bundler-cache: true 36 | - name: Run specs 37 | run: JRUBY_OPTS="--dev --debug" bundle exec rake spec 38 | - name: Coveralls Parallel 39 | uses: coverallsapp/github-action@v2 40 | with: 41 | github-token: ${{ secrets.github_token }} 42 | flag-name: run-${{ matrix.ruby-version }} 43 | parallel: true 44 | 45 | coverage: 46 | name: Coverage 47 | needs: test 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Coveralls Finished 51 | uses: coverallsapp/github-action@v2 52 | with: 53 | github-token: ${{ secrets.github_token }} 54 | parallel-finished: true 55 | -------------------------------------------------------------------------------- /coveralls-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'coveralls/version' 6 | 7 | Gem::Specification.new do |gem| 8 | gem.authors = ['Nick Merwin', 'Wil Gieseler', 'Geremia Taglialatela'] 9 | gem.email = ['nick@lemurheavy.com', 'supapuerco@gmail.com', 'tagliala.dev@gmail.com'] 10 | gem.description = 'A Ruby implementation of the Coveralls API.' 11 | gem.summary = 'A Ruby implementation of the Coveralls API.' 12 | gem.homepage = 'https://coveralls.io' 13 | gem.license = 'MIT' 14 | 15 | gem.files = Dir.glob('{CHANGELOG.md,LICENSE,README.md,lib/**/*.rb,bin/coveralls}', File::FNM_DOTMATCH) 16 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } 17 | gem.name = 'coveralls_reborn' 18 | gem.require_paths = ['lib'] 19 | gem.version = Coveralls::VERSION 20 | 21 | gem.metadata['rubygems_mfa_required'] = 'true' 22 | 23 | gem.metadata['bug_tracker_uri'] = 'https://github.com/tagliala/coveralls-ruby-reborn/issues' 24 | gem.metadata['changelog_uri'] = 'https://github.com/tagliala/coveralls-ruby-reborn/blob/main/CHANGELOG.md' 25 | gem.metadata['source_code_uri'] = 'https://github.com/tagliala/coveralls-ruby-reborn' 26 | 27 | gem.required_ruby_version = '>= 2.6' 28 | 29 | gem.add_dependency 'simplecov', '~> 0.22.0' 30 | gem.add_dependency 'term-ansicolor', '~> 1.7' 31 | gem.add_dependency 'thor', '~> 1.2' 32 | gem.add_dependency 'tins', '~> 1.32' 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | require 'webmock' 5 | require 'vcr' 6 | 7 | class InceptionFormatter 8 | def format(result) 9 | Coveralls::SimpleCov::Formatter.new.format(result) 10 | end 11 | end 12 | 13 | def setup_formatter 14 | if ENV['GITHUB_ACTIONS'] 15 | require 'simplecov-lcov' 16 | 17 | SimpleCov::Formatter::LcovFormatter.config do |c| 18 | c.report_with_single_file = true 19 | c.single_report_path = 'coverage/lcov.info' 20 | end 21 | end 22 | 23 | SimpleCov.formatter = 24 | if ENV['CI'] || ENV['COVERALLS_REPO_TOKEN'] 25 | if ENV['GITHUB_ACTIONS'] 26 | SimpleCov::Formatter::MultiFormatter.new([InceptionFormatter, SimpleCov::Formatter::LcovFormatter]) 27 | else 28 | InceptionFormatter 29 | end 30 | else 31 | SimpleCov::Formatter::HTMLFormatter 32 | end 33 | end 34 | 35 | setup_formatter 36 | 37 | SimpleCov.start do 38 | add_filter do |source_file| 39 | source_file.filename.include?('spec') && !source_file.filename.include?('fixture') 40 | end 41 | add_filter %r{/.bundle/} 42 | end 43 | 44 | # Leave this require after SimpleCov.start 45 | require 'coveralls' 46 | 47 | VCR.configure do |c| 48 | c.cassette_library_dir = 'fixtures/vcr_cassettes' 49 | c.hook_into :webmock 50 | end 51 | 52 | RSpec.configure do |config| 53 | config.run_all_when_everything_filtered = true 54 | config.filter_run :focus 55 | config.include WebMock::API 56 | 57 | config.after(:suite) do 58 | setup_formatter 59 | WebMock.disable! 60 | end 61 | end 62 | 63 | def stub_api_post 64 | body = '{"message":"","url":""}' 65 | stub_request(:post, "#{Coveralls::API::API_BASE}/jobs") 66 | .to_return(status: 200, body: body, headers: {}) 67 | end 68 | 69 | def silence(&block) 70 | return yield if ENV['silence'] == 'false' 71 | 72 | silence_stream($stdout, &block) 73 | end 74 | 75 | def silence_stream(stream) 76 | old_stream = stream.dup 77 | stream.reopen(IO::NULL) 78 | stream.sync = true 79 | yield 80 | ensure 81 | stream.reopen(old_stream) 82 | old_stream.close 83 | end 84 | -------------------------------------------------------------------------------- /lib/coveralls/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thor' 4 | 5 | module Coveralls 6 | class CommandLine < Thor 7 | desc 'push', 'Runs your test suite and pushes the coverage results to Coveralls.' 8 | def push 9 | return unless can_run_locally? 10 | 11 | ENV['COVERALLS_RUN_LOCALLY'] = 'true' 12 | cmds = ['bundle exec rake'] 13 | 14 | if File.exist?('.travis.yml') 15 | cmds = begin 16 | YAML.load_file('.travis.yml')['script'] || cmds 17 | rescue StandardError 18 | cmds 19 | end 20 | end 21 | 22 | cmds.each { |cmd| system cmd } 23 | 24 | ENV['COVERALLS_RUN_LOCALLY'] = nil 25 | end 26 | 27 | desc 'report', 'Runs your test suite locally and displays coverage statistics.' 28 | def report 29 | ENV['COVERALLS_NOISY'] = 'true' 30 | 31 | exec 'bundle exec rake' 32 | 33 | ENV['COVERALLS_NOISY'] = nil 34 | end 35 | 36 | desc 'open', 'View this repository on Coveralls.' 37 | def open 38 | open_token_based_url 'https://coveralls.io/repos/%@' 39 | end 40 | 41 | desc 'service', "View this repository on your CI service's website." 42 | def service 43 | open_token_based_url 'https://coveralls.io/repos/%@/service' 44 | end 45 | 46 | desc 'last', 'View the last build for this repository on Coveralls.' 47 | def last 48 | open_token_based_url 'https://coveralls.io/repos/%@/last_build' 49 | end 50 | 51 | desc 'version', 'See version' 52 | def version 53 | Coveralls::Output.puts Coveralls::VERSION 54 | end 55 | 56 | private 57 | 58 | def config 59 | Coveralls::Configuration.configuration 60 | end 61 | 62 | def open_token_based_url(url) 63 | if config[:repo_token] 64 | url = url.gsub('%@', config[:repo_token]) 65 | `open #{url}` 66 | else 67 | Coveralls::Output.puts 'No repo_token configured.' 68 | end 69 | end 70 | 71 | def can_run_locally? 72 | if config[:repo_token].nil? 73 | Coveralls::Output.puts 'Coveralls cannot run locally because no repo_secret_token is set in .coveralls.yml', color: 'red' 74 | Coveralls::Output.puts 'Please try again when you get your act together.', color: 'red' 75 | 76 | return false 77 | end 78 | 79 | true 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/coveralls/output_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Coveralls::Output do 6 | it 'defaults the IO to $stdout' do 7 | expect { described_class.puts 'this is a test' }.to output("this is a test\n").to_stdout 8 | end 9 | 10 | it 'accepts an IO injection' do 11 | out = StringIO.new 12 | allow(described_class).to receive(:output).and_return(out) 13 | described_class.puts 'this is a test' 14 | 15 | expect(out.string).to eq "this is a test\n" 16 | end 17 | 18 | describe '.puts' do 19 | it 'accepts an IO injection' do 20 | out = StringIO.new 21 | described_class.puts 'this is a test', output: out 22 | 23 | expect(out.string).to eq "this is a test\n" 24 | end 25 | end 26 | 27 | describe '.print' do 28 | it 'accepts an IO injection' do 29 | out = StringIO.new 30 | described_class.print 'this is a test', output: out 31 | 32 | expect(out.string).to eq 'this is a test' 33 | end 34 | end 35 | 36 | describe 'when silenced' do 37 | before { described_class.silent = true } 38 | 39 | it 'does not put' do 40 | expect { described_class.puts 'foo' }.not_to output("foo\n").to_stdout 41 | end 42 | 43 | it 'does not print' do 44 | expect { described_class.print 'foo' }.not_to output('foo').to_stdout 45 | end 46 | end 47 | 48 | describe '.format' do 49 | it 'accepts a color argument' do 50 | require 'term/ansicolor' 51 | string = 'Hello' 52 | ansi_color_string = Term::ANSIColor.red(string) 53 | expect(described_class.format(string, color: 'red')).to eq(ansi_color_string) 54 | end 55 | 56 | it 'accepts no color arguments' do 57 | unformatted_string = 'Hi Doggie!' 58 | expect(described_class.format(unformatted_string)).to eq(unformatted_string) 59 | end 60 | 61 | it 'rejects formats unrecognized by Term::ANSIColor' do 62 | string = 'Hi dog!' 63 | expect(described_class.format(string, color: 'not_a_real_color')).to eq(string) 64 | end 65 | 66 | it 'accepts more than 1 color argument' do 67 | string = 'Hi dog!' 68 | multi_formatted_string = Term::ANSIColor.red { Term::ANSIColor.underline(string) } 69 | expect(described_class.format(string, color: 'red underline')).to eq(multi_formatted_string) 70 | end 71 | 72 | context 'without color' do 73 | before { described_class.no_color = true } 74 | 75 | it 'does not add color to string' do 76 | unformatted_string = 'Hi Doggie!' 77 | expect(described_class.format(unformatted_string, color: 'red')).to eq(unformatted_string) 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/coveralls.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'coveralls/version' 4 | require_relative 'coveralls/configuration' 5 | require_relative 'coveralls/api' 6 | require_relative 'coveralls/output' 7 | require_relative 'coveralls/simplecov' 8 | 9 | module Coveralls 10 | class << self 11 | attr_accessor :adapter, :testing, :noisy, :run_locally 12 | end 13 | 14 | class NilFormatter 15 | def format(result); end 16 | end 17 | 18 | module_function 19 | 20 | def wear!(simplecov_setting = nil, &block) 21 | setup! 22 | start! simplecov_setting, &block 23 | end 24 | 25 | def wear_merged!(simplecov_setting = nil, &block) 26 | require 'simplecov' 27 | @adapter = :simplecov 28 | ::SimpleCov.formatter = NilFormatter 29 | start! simplecov_setting, &block 30 | end 31 | 32 | def push! 33 | require 'simplecov' 34 | result = ::SimpleCov::ResultMerger.merged_result 35 | Coveralls::SimpleCov::Formatter.new.format result 36 | end 37 | 38 | def setup! 39 | # Try to load up SimpleCov. 40 | @adapter = nil 41 | if defined?(::SimpleCov) 42 | @adapter = :simplecov 43 | else 44 | begin 45 | require 'simplecov' 46 | @adapter = :simplecov if defined?(::SimpleCov) 47 | rescue StandardError => e 48 | # TODO: Add error action 49 | puts e.message 50 | end 51 | end 52 | 53 | # Load the appropriate adapter. 54 | if @adapter == :simplecov 55 | ::SimpleCov.formatter = Coveralls::SimpleCov::Formatter 56 | Coveralls::Output.puts('[Coveralls] Set up the SimpleCov formatter.', color: 'green') 57 | else 58 | Coveralls::Output.puts("[Coveralls] Couldn't find an appropriate adapter.", color: 'red') 59 | end 60 | end 61 | 62 | def start!(simplecov_setting = 'test_frameworks', &block) 63 | return unless @adapter == :simplecov 64 | 65 | ::SimpleCov.add_filter 'vendor' 66 | 67 | if simplecov_setting 68 | Coveralls::Output.puts("[Coveralls] Using SimpleCov's '#{simplecov_setting}' settings.", color: 'green') 69 | if block 70 | ::SimpleCov.start(simplecov_setting) { instance_eval(&block) } 71 | else 72 | ::SimpleCov.start(simplecov_setting) 73 | end 74 | elsif block 75 | Coveralls::Output.puts('[Coveralls] Using SimpleCov settings defined in block.', color: 'green') 76 | ::SimpleCov.start { instance_eval(&block) } 77 | else 78 | Coveralls::Output.puts("[Coveralls] Using SimpleCov's default settings.", color: 'green') 79 | ::SimpleCov.start 80 | end 81 | end 82 | 83 | def should_run? 84 | # Fail early if we're not on a CI 85 | unless will_run? 86 | Coveralls::Output.puts('[Coveralls] Outside the CI environment, not sending data.', color: 'yellow') 87 | 88 | return false 89 | end 90 | 91 | if ENV['COVERALLS_RUN_LOCALLY'] || (defined?(@run_locally) && @run_locally) 92 | Coveralls::Output.puts('[Coveralls] Creating a new job on Coveralls from local coverage results.', color: 'cyan') 93 | end 94 | 95 | true 96 | end 97 | 98 | def will_run? 99 | ENV['CI'] || ENV['JENKINS_URL'] || ENV['TDDIUM'] || 100 | ENV['COVERALLS_RUN_LOCALLY'] || (defined?(@testing) && @testing) 101 | end 102 | 103 | def noisy? 104 | ENV['COVERALLS_NOISY'] || (defined?(@noisy) && @noisy) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/coveralls/output.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Coveralls 4 | # 5 | # Public: Methods for formatting strings with Term::ANSIColor. 6 | # Does not utilize monkey-patching and should play nicely when 7 | # included with other libraries. 8 | # 9 | # All methods are module methods and should be called on 10 | # the Coveralls::Output module. 11 | # 12 | # Examples 13 | # 14 | # Coveralls::Output.format("Hello World", :color => "cyan") 15 | # # => "\e[36mHello World\e[0m" 16 | # 17 | # Coveralls::Output.print("Hello World") 18 | # # Hello World => nil 19 | # 20 | # Coveralls::Output.puts("Hello World", :color => "underline") 21 | # # Hello World 22 | # # => nil 23 | # 24 | # To silence output completely: 25 | # 26 | # Coveralls::Output.silent = true 27 | # 28 | # or set this environment variable: 29 | # 30 | # COVERALLS_SILENT 31 | # 32 | # To disable color completely: 33 | # 34 | # Coveralls::Output.no_color = true 35 | 36 | module Output 37 | class << self 38 | attr_accessor :silent, :no_color 39 | attr_writer :output 40 | end 41 | 42 | module_function 43 | 44 | def output 45 | (defined?(@output) && @output) || $stdout 46 | end 47 | 48 | def no_color? 49 | defined?(@no_color) && @no_color 50 | end 51 | 52 | # Public: Formats the given string with the specified color 53 | # through Term::ANSIColor 54 | # 55 | # string - the text to be formatted 56 | # options - The hash of options used for formatting the text: 57 | # :color - The color to be passed as a method to 58 | # Term::ANSIColor 59 | # 60 | # Examples 61 | # 62 | # Coveralls::Output.format("Hello World!", :color => "cyan") 63 | # # => "\e[36mHello World\e[0m" 64 | # 65 | # Returns the formatted string. 66 | def format(string, options = {}) 67 | unless no_color? 68 | require 'term/ansicolor' 69 | options[:color]&.split(/\s/)&.reverse_each do |color| 70 | next unless Term::ANSIColor.respond_to?(color.to_sym) 71 | 72 | string = Term::ANSIColor.send(color.to_sym, string) 73 | end 74 | end 75 | string 76 | end 77 | 78 | # Public: Passes .format to Kernel#puts 79 | # 80 | # string - the text to be formatted 81 | # options - The hash of options used for formatting the text: 82 | # :color - The color to be passed as a method to 83 | # Term::ANSIColor 84 | # 85 | # 86 | # Example 87 | # 88 | # Coveralls::Output.puts("Hello World", :color => "cyan") 89 | # 90 | # Returns nil. 91 | def puts(string, options = {}) 92 | return if silent? 93 | 94 | (options[:output] || output).puts format(string, options) 95 | end 96 | 97 | # Public: Passes .format to Kernel#print 98 | # 99 | # string - the text to be formatted 100 | # options - The hash of options used for formatting the text: 101 | # :color - The color to be passed as a method to 102 | # Term::ANSIColor 103 | # 104 | # Example 105 | # 106 | # Coveralls::Output.print("Hello World!", :color => "underline") 107 | # 108 | # Returns nil. 109 | def print(string, options = {}) 110 | return if silent? 111 | 112 | (options[:output] || output).print format(string, options) 113 | end 114 | 115 | def silent? 116 | ENV['COVERALLS_SILENT'] || (defined?(@silent) && @silent) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/coveralls/coveralls_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Coveralls do 6 | before do 7 | allow(SimpleCov).to receive(:start) 8 | stub_api_post 9 | described_class.testing = true 10 | end 11 | 12 | describe '#will_run?' do 13 | it 'checks CI environment variables' do 14 | expect(described_class).to be_will_run 15 | end 16 | 17 | context 'with CI disabled' do 18 | before do 19 | allow(ENV).to receive(:[]) 20 | allow(ENV).to receive(:[]).with('COVERALLS_RUN_LOCALLY').and_return(nil) 21 | allow(ENV).to receive(:[]).with('CI').and_return(nil) 22 | described_class.testing = false 23 | end 24 | 25 | it 'indicates no run' do 26 | expect(described_class).not_to be_will_run 27 | end 28 | end 29 | end 30 | 31 | describe '#should_run?' do 32 | it 'outputs to stdout when running locally' do 33 | described_class.testing = false 34 | described_class.run_locally = true 35 | 36 | expect do 37 | silence { described_class.should_run? } 38 | end.not_to raise_error 39 | end 40 | end 41 | 42 | describe '#wear!' do 43 | it 'receives block' do 44 | silence do 45 | described_class.wear! do 46 | add_filter 's' 47 | end 48 | end 49 | 50 | expect(SimpleCov).to have_received(:start) 51 | end 52 | 53 | it 'uses string' do 54 | silence do 55 | described_class.wear! 'test_frameworks' 56 | end 57 | 58 | expect(SimpleCov).to have_received(:start).with 'test_frameworks' 59 | end 60 | 61 | it 'uses default' do 62 | silence do 63 | described_class.wear! 64 | end 65 | 66 | expect(SimpleCov).to have_received(:start).with no_args 67 | expect(SimpleCov.filters.map(&:filter_argument)).to include 'vendor' 68 | end 69 | end 70 | 71 | describe '#wear_merged!' do 72 | it 'sets formatter to NilFormatter' do 73 | silence do 74 | described_class.wear_merged! 'rails' do 75 | add_filter '/spec/' 76 | end 77 | end 78 | 79 | expect(SimpleCov.formatter).to be described_class::NilFormatter 80 | end 81 | end 82 | 83 | describe '#push!' do 84 | let(:coverage_hash) do 85 | { 'file.rb'=>{ 'lines'=>[nil] } } 86 | end 87 | 88 | before do 89 | allow(SimpleCov::ResultMerger).to receive(:merge_valid_results).and_return([['RSpec'], coverage_hash]) 90 | end 91 | 92 | it 'sends existing test results' do 93 | result = false 94 | silence do 95 | result = described_class.push! 96 | end 97 | expect(result).to be_truthy 98 | end 99 | end 100 | 101 | describe '#setup!' do 102 | it 'sets SimpleCov adapter' do 103 | previous_adapter = described_class.adapter 104 | described_class.adapter = nil 105 | 106 | expect do 107 | silence { described_class.setup! } 108 | end.to change(described_class, :adapter).from(nil).to(:simplecov) 109 | ensure 110 | described_class.adapter = previous_adapter 111 | end 112 | 113 | context 'when SimpleCov is not defined' do 114 | # rubocop:disable RSpec/LeakyConstantDeclaration 115 | it 'tries to load it' do 116 | SimpleCovTmp = SimpleCov 117 | Object.send :remove_const, :SimpleCov # rubocop:disable RSpec/RemoveConst 118 | expect do 119 | silence { described_class.setup! } 120 | end.not_to raise_error 121 | ensure 122 | SimpleCov = SimpleCovTmp 123 | end 124 | # rubocop:enable RSpec/LeakyConstantDeclaration 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/coveralls/simplecov.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | 5 | module Coveralls 6 | module SimpleCov 7 | class Formatter 8 | def display_result(result) # rubocop:disable Naming/PredicateMethod 9 | # Log which files would be submitted. 10 | if result.files.empty? 11 | Coveralls::Output.puts '[Coveralls] There are no covered files.', color: 'yellow' 12 | else 13 | Coveralls::Output.puts '[Coveralls] Some handy coverage stats:' 14 | end 15 | 16 | result.files.each do |f| 17 | Coveralls::Output.print ' * ' 18 | Coveralls::Output.print short_filename(f.filename).to_s, color: 'cyan' 19 | Coveralls::Output.print ' => ', color: 'white' 20 | cov = "#{f.covered_percent.round}%" 21 | if f.covered_percent > 90 22 | Coveralls::Output.print cov, color: 'green' 23 | elsif f.covered_percent > 80 24 | Coveralls::Output.print cov, color: 'yellow' 25 | else 26 | Coveralls::Output.print cov, color: 'red' 27 | end 28 | Coveralls::Output.puts '' 29 | end 30 | 31 | true 32 | end 33 | 34 | def get_source_files(result) 35 | # Gather the source files. 36 | source_files = [] 37 | result.files.each do |file| 38 | properties = {} 39 | 40 | # Get Source 41 | properties[:source] = File.open(file.filename, 'rb:utf-8').read 42 | 43 | # Get the root-relative filename 44 | properties[:name] = short_filename(file.filename) 45 | 46 | # Get the coverage 47 | properties[:coverage] = file.coverage_data['lines'] 48 | properties[:branches] = branches(file.coverage_data['branches']) if file.coverage_data['branches'] 49 | 50 | # Skip nocov lines 51 | file.lines.each_with_index do |line, i| 52 | properties[:coverage][i] = nil if line.skipped? 53 | end 54 | 55 | source_files << properties 56 | end 57 | 58 | source_files 59 | end 60 | 61 | def branches(simplecov_branches) 62 | branches_properties = [] 63 | simplecov_branches.each do |branch_data, data| 64 | branch_number = 0 65 | line_number = branch_data.split(', ')[2].to_i 66 | data.each_value do |hits| 67 | branch_number += 1 68 | branches_properties.push(line_number, 0, branch_number, hits) 69 | end 70 | end 71 | branches_properties 72 | end 73 | 74 | def format(result) 75 | unless Coveralls.should_run? 76 | display_result result if Coveralls.noisy? 77 | 78 | return 79 | end 80 | 81 | # Post to Coveralls. 82 | API.post_json 'jobs', 83 | source_files: get_source_files(result), 84 | test_framework: result.command_name.downcase, 85 | run_at: result.created_at 86 | 87 | Coveralls::Output.puts output_message result 88 | 89 | true 90 | rescue StandardError => e 91 | display_error e 92 | end 93 | 94 | def display_error(error) # rubocop:disable Naming/PredicateMethod 95 | Coveralls::Output.puts 'Coveralls encountered an exception:', color: 'red' 96 | Coveralls::Output.puts error.class.to_s, color: 'red' 97 | Coveralls::Output.puts error.message, color: 'red' 98 | 99 | error.backtrace&.each do |line| 100 | Coveralls::Output.puts line, color: 'red' 101 | end 102 | 103 | if error.respond_to?(:response) && error.response 104 | Coveralls::Output.puts error.response.to_s, color: 'red' 105 | end 106 | 107 | false 108 | end 109 | 110 | def output_message(result) 111 | "Coverage is at #{begin 112 | result.covered_percent.round(2) 113 | rescue StandardError 114 | result.covered_percent.round 115 | end}%.\nCoverage report sent to Coveralls." 116 | end 117 | 118 | def short_filename(filename) 119 | return filename unless ::SimpleCov.root 120 | 121 | filename = Pathname.new(filename) 122 | root = Pathname.new(::SimpleCov.root) 123 | filename.relative_path_from(root).to_s 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.29.0 / 2025-06-15 4 | 5 | * [ENHANCEMENT] Prefer `require_relative` for internal requires [#51](https://github.com/tagliala/coveralls-ruby-reborn/pull/51) 6 | * [ENHANCEMENT] Remove `truthy` dependency as obsolete and unsupported [#50](https://github.com/tagliala/coveralls-ruby-reborn/pull/50) 7 | * [ENHANCEMENT] Test against Ruby 3.4 8 | 9 | ## 0.28.0 / 2023-07-22 10 | 11 | * [ENHANCEMENT] Reduce gem size 12 | 13 | ## 0.27.0 / 2023-02-14 14 | 15 | * [FEATURE] Add Buildkite CI support [#43](https://github.com/tagliala/coveralls-ruby-reborn/pull/43) 16 | * [ENHANCEMENT] Update development dependencies 17 | 18 | ## 0.26.0 / 2022-12-25 19 | 20 | * [FEATURE] Drop Ruby 2.5 support 21 | * [FEATURE] Add SimpleCov 0.22.0 compatibility 22 | * [ENHANCEMENT] Test against Ruby 3.2 and JRuby 3.4 23 | 24 | ## 0.25.0 / 2022-08-05 25 | 26 | * [ENHANCEMENT] Bump `jruby-openssl` requirement to `0.14.0` [#39](https://github.com/tagliala/coveralls-ruby-reborn/pull/39) 27 | * [ENHANCEMENT] Improve Semaphore CI support [#37](https://github.com/tagliala/coveralls-ruby-reborn/pull/37) [#38](https://github.com/tagliala/coveralls-ruby-reborn/pull/38) 28 | * [ENHANCEMENT] Test against JRuby 9.3 [#36](https://github.com/tagliala/coveralls-ruby-reborn/pull/36) 29 | 30 | ## 0.24.0 / 2022-03-11 31 | 32 | * [ENHANCEMENT] Test against Ruby 3.1 33 | * [BUGFIX] Fix Circle CI configuration [#30](https://github.com/tagliala/coveralls-ruby-reborn/issues/30) 34 | * [BUGFIX] Fix Semaphore CI configuration [#34](https://github.com/tagliala/coveralls-ruby-reborn/pull/34) 35 | 36 | ## 0.23.1 / 2021-11-15 37 | 38 | * [ENHANCEMENT] Require MFA to publish gems 39 | * [ENHANCEMENT] Update development dependencies 40 | 41 | ## 0.23.0 / 2021-09-12 42 | 43 | * [FEATURE] Send branches coverage [#27](https://github.com/tagliala/coveralls-ruby-reborn/pull/27) 44 | 45 | ## 0.22.0 / 2021-04-30 46 | 47 | * [FEATURE] Drop Ruby 2.4 support 48 | * [ENHANCEMENT] Test against latest Ruby versions 49 | 50 | ## 0.21.0 / 2021-03-18 51 | 52 | * [FEATURE] Allow GitLab parallel builds [#20](https://github.com/tagliala/coveralls-ruby-reborn/pull/20) 53 | * [ENHANCEMENT] Test against JRuby 9.2.16.0 54 | 55 | ## 0.20.0 / 2021-01-09 56 | 57 | * [FEATURE] Add Ruby 3 compatibility 58 | * [FEATURE] Add SimpleCov 0.21.0 compatibility 59 | * [ENHANCEMENT] Update dependencies 60 | 61 | ## 0.19.0 / 2020-12-02 62 | 63 | * [FEATURE] Add SimpleCov 0.20.0 compatibility 64 | 65 | ## 0.18.0 / 2020-09-07 66 | 67 | * [ENHANCEMENT] Refactor HTTP client [#10](https://github.com/tagliala/coveralls-ruby-reborn/pull/10) 68 | * [ENHANCEMENT] Update dependencies 69 | 70 | ## 0.17.0 / 2020-08-26 71 | 72 | * [FEATURE] Add SimpleCov 0.19.0 compatibility 73 | * [ENHANCEMENT] Update dependencies 74 | 75 | ## 0.16.0 / 2020-04-30 76 | 77 | * [FEATURE] Remove dependency on json gem [#4](https://github.com/tagliala/coveralls-ruby-reborn/pull/4) 78 | * [ENHANCEMENT] Update dependencies 79 | 80 | ## 0.15.1 / 2020-04-17 81 | 82 | * [ENHANCEMENT] Test against latest Ruby versions 83 | * [ENHANCEMENT] Update dependencies 84 | 85 | ## 0.15.0 / 2020-02-01 86 | 87 | * [FEATURE] Add SimpleCov 0.18.1 compatibility 88 | * [FEATURE] Drop Ruby 2.3 Support 89 | * [ENHANCEMENT] Update dependencies 90 | 91 | ## 0.14.0 / 2019-12-15 92 | 93 | * [ENHANCEMENT] Allow Thor 1.0 94 | 95 | ## 0.13.4 / 2019-12-05 96 | 97 | * [ENHANCEMENT] Test against latest JRuby version 98 | * [ENHANCEMENT] Update dependencies 99 | 100 | ## 0.13.3 / 2019-10-13 101 | 102 | * [FIX] Do not rescue from LoadError with required gems 103 | * [FIX] Fix multipart content-type delimiter [lemurheavy/coveralls-ruby#154](https://github.com/lemurheavy/coveralls-ruby/pull/154) 104 | * [ENHANCEMENT] Test against latest JRuby version 105 | * [ENHANCEMENT] Update dependencies 106 | 107 | ## 0.13.2 / 2019-07-19 108 | 109 | * [FIX] Do not rescue from LoadError with required gems 110 | 111 | ## 0.13.1 / 2019-07-17 112 | 113 | * [FIX] Rescue from LoadError when VCR or webmock are not available 114 | 115 | ## 0.13.0 / 2019-07-16 116 | 117 | * [FEATURE] Add SimpleCov 0.17.0 compatibility 118 | * [FEATURE] Drop Ruby 2.2 Support 119 | * [ENHANCEMENT] Test against latest JRuby version 120 | * [ENHANCEMENT] Update dependencies 121 | 122 | ## 0.12.0 / 2018-07-27 123 | 124 | * [FEATURE] Drop Ruby 2.1 compatibility 125 | * [ENHANCEMENT] Remove gemnasium badge 126 | * [ENHANCEMENT] Test against latest JRuby version 127 | * [ENHANCEMENT] Update dependencies 128 | -------------------------------------------------------------------------------- /lib/coveralls/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'net/https' 5 | require 'tempfile' 6 | 7 | module Coveralls 8 | class API 9 | if ENV['COVERALLS_ENDPOINT'] 10 | API_HOST = ENV['COVERALLS_ENDPOINT'] 11 | API_DOMAIN = ENV['COVERALLS_ENDPOINT'] 12 | else 13 | API_HOST = ENV['COVERALLS_DEVELOPMENT'] ? 'localhost:3000' : 'coveralls.io' 14 | API_PROTOCOL = ENV['COVERALLS_DEVELOPMENT'] ? 'http' : 'https' 15 | API_DOMAIN = "#{API_PROTOCOL}://#{API_HOST}" 16 | end 17 | 18 | API_BASE = "#{API_DOMAIN}/api/v1" 19 | 20 | class << self 21 | def post_json(endpoint, hash) 22 | disable_net_blockers! 23 | 24 | uri = endpoint_to_uri(endpoint) 25 | 26 | Coveralls::Output.puts(JSON.pretty_generate(hash).to_s, color: 'green') if ENV['COVERALLS_DEBUG'] 27 | Coveralls::Output.puts("[Coveralls] Submitting to #{API_BASE}", color: 'cyan') 28 | 29 | client = build_client(uri) 30 | request = build_request(uri.path, hash) 31 | response = client.request(request) 32 | response_hash = JSON.parse(response.body.to_str) 33 | 34 | if response_hash['message'] 35 | Coveralls::Output.puts("[Coveralls] #{response_hash['message']}", color: 'cyan') 36 | end 37 | 38 | if response_hash['url'] 39 | Coveralls::Output.puts("[Coveralls] #{Coveralls::Output.format(response_hash['url'], color: 'underline')}", color: 'cyan') 40 | end 41 | 42 | case response 43 | when Net::HTTPServiceUnavailable 44 | Coveralls::Output.puts('[Coveralls] API timeout occurred, but data should still be processed', color: 'red') 45 | when Net::HTTPInternalServerError 46 | Coveralls::Output.puts("[Coveralls] API internal error occurred, we're on it!", color: 'red') 47 | end 48 | end 49 | 50 | private 51 | 52 | def disable_net_blockers! 53 | begin 54 | require 'webmock' 55 | 56 | allow = Array(WebMock::Config.instance.allow) 57 | WebMock::Config.instance.allow = allow.push API_HOST 58 | rescue LoadError 59 | rescue StandardError => e 60 | # TODO: Add error action 61 | puts e.message 62 | end 63 | 64 | begin 65 | require 'vcr' 66 | 67 | VCR.send(VCR.version.major < 2 ? :config : :configure) do |c| 68 | c.ignore_hosts API_HOST 69 | end 70 | rescue LoadError 71 | rescue StandardError => e 72 | # TODO: Add error action 73 | puts e.message 74 | end 75 | end 76 | 77 | def endpoint_to_uri(endpoint) 78 | URI.parse("#{API_BASE}/#{endpoint}") 79 | end 80 | 81 | def build_client(uri) 82 | client = Net::HTTP.new(uri.host, uri.port) 83 | client.use_ssl = uri.port == 443 84 | client 85 | end 86 | 87 | def build_request(path, hash) 88 | request = Net::HTTP::Post.new(path) 89 | boundary = rand(1_000_000).to_s 90 | 91 | request.body = build_request_body(hash, boundary) 92 | request.content_type = "multipart/form-data; boundary=#{boundary}" 93 | 94 | request 95 | end 96 | 97 | def build_request_body(hash, boundary) 98 | hash = apified_hash(hash) 99 | file = hash_to_file(hash) 100 | 101 | "--#{boundary}\r\n" \ 102 | "Content-Disposition: form-data; name=\"json_file\"; filename=\"#{File.basename(file.path)}\"\r\n" \ 103 | "Content-Type: text/plain\r\n\r\n" + 104 | File.read(file.path) + 105 | "\r\n--#{boundary}--\r\n" 106 | end 107 | 108 | def hash_to_file(hash) 109 | file = nil 110 | 111 | Tempfile.open(%w[coveralls-upload json]) do |f| 112 | f.write(JSON.dump(hash)) 113 | file = f 114 | end 115 | 116 | File.new(file.path, 'rb') 117 | end 118 | 119 | def apified_hash(hash) 120 | config = Coveralls::Configuration.configuration 121 | 122 | if ENV['COVERALLS_DEBUG'] || Coveralls.testing 123 | Coveralls::Output.puts '[Coveralls] Submitting with config:', color: 'yellow' 124 | output = JSON.pretty_generate(config).gsub(/"repo_token": ?"(.*?)"/, '"repo_token": "[secure]"') 125 | Coveralls::Output.puts output, color: 'yellow' 126 | end 127 | 128 | hash.merge(config) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/coveralls/simple_cov/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Coveralls::SimpleCov::Formatter do 6 | before do 7 | stub_api_post 8 | end 9 | 10 | def source_fixture(filename) 11 | File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures', filename)) 12 | end 13 | 14 | let(:result) do 15 | options = { 16 | source_fixture('app/controllers/sample.rb') => { lines: [nil, 1, 1, 1, nil, 0, 1, 1, nil, nil] }, 17 | source_fixture('app/models/airplane.rb') => { lines: [0, 0, 0, 0, 0] }, 18 | source_fixture('app/models/dog.rb') => { lines: [1, 1, 1, 1, 1] }, 19 | source_fixture('app/models/house.rb') => { lines: [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil] }, 20 | source_fixture('app/models/robot.rb') => { lines: [1, 1, 1, 1, nil, nil, 1, 0, nil, nil] }, 21 | source_fixture('app/models/user.rb') => { 22 | lines: [nil, 1, 1, 0, nil, nil, 1, 0, nil, nil, 1, 0, 0, nil, nil, nil], 23 | 'branches' => { 24 | '[:if, 0, 12, 4, 14, 7]' => { 25 | '[:then, 1, 13, 6, 13, 11]' => 1, 26 | '[:else, 2, 12, 4, 14, 7]' => 0 27 | } 28 | } 29 | }, 30 | source_fixture('sample.rb') => { lines: [nil, 1, 1, 1, nil, 0, 1, 1, nil, nil] } 31 | } 32 | 33 | SimpleCov::Result.new(options) 34 | end 35 | 36 | describe '#format' do 37 | context 'when should run' do 38 | before do 39 | Coveralls.testing = true 40 | Coveralls.noisy = false 41 | end 42 | 43 | it 'posts json' do 44 | expect(result.files).not_to be_empty 45 | silence do 46 | expect(described_class.new.format(result)).to be_truthy 47 | end 48 | end 49 | end 50 | 51 | context 'when should not run, noisy' do 52 | it 'only displays result' do 53 | silence do 54 | expect(described_class.new.display_result(result)).to be_truthy 55 | end 56 | end 57 | end 58 | 59 | context 'without files' do 60 | let(:result) { SimpleCov::Result.new({}) } 61 | 62 | it 'shows note that no files have been covered' do 63 | Coveralls.noisy = true 64 | Coveralls.testing = false 65 | 66 | silence do 67 | expect do 68 | described_class.new.format(result) 69 | end.not_to raise_error 70 | end 71 | end 72 | end 73 | 74 | context 'with api error' do 75 | it 'rescues' do 76 | e = SocketError.new 77 | 78 | silence do 79 | expect(described_class.new.display_error(e)).to be_falsy 80 | end 81 | end 82 | end 83 | 84 | describe '#get_source_files' do 85 | let(:source_files) { instance.get_source_files(result) } 86 | let(:instance) do 87 | described_class.new.tap do |ins| 88 | allow(ins).to receive(:branches) 89 | end 90 | end 91 | 92 | it 'nils the skipped lines' do 93 | source_file = source_files.first 94 | expect(source_file[:coverage]).to eq [nil, 1, 1, 1, nil, 0, 1, 1, nil, nil, nil, nil, nil] 95 | end 96 | 97 | it 'calls #branches when branch coverage is present' do 98 | source_files 99 | expect(instance).to have_received(:branches).once 100 | end 101 | end 102 | 103 | describe '#branches' do 104 | let(:branch_coverage_parsed) { described_class.new.branches(simplecov_branches_results) } 105 | let(:simplecov_branches_results) do 106 | { 107 | '[:if, 0, 12, 4, 14, 7]' => { 108 | '[:then, 1, 13, 6, 13, 11]' => 1, 109 | '[:else, 2, 12, 4, 14, 7]' => 0 110 | } 111 | } 112 | end 113 | 114 | it 'return coveralls required structure' do 115 | expect(branch_coverage_parsed).to eq [12, 0, 1, 1, 12, 0, 2, 0] 116 | end 117 | end 118 | 119 | describe '#short_filename' do 120 | subject { described_class.new.short_filename(filename) } 121 | 122 | let(:filename) { '/app/app/controllers/application_controller.rb' } 123 | 124 | before do 125 | allow(SimpleCov).to receive(:root).and_return(root_path) 126 | end 127 | 128 | context 'with nil root path' do 129 | let(:root_path) { nil } 130 | 131 | it { is_expected.to eql filename } 132 | end 133 | 134 | context 'with multiple matches of root path' do 135 | let(:root_path) { '/app' } 136 | 137 | it { is_expected.to eql 'app/controllers/application_controller.rb' } 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Coveralls Reborn](https://coveralls.io) for Ruby 2 | 3 | [![Gem Version](https://badge.fury.io/rb/coveralls_reborn.svg)](https://badge.fury.io/rb/coveralls_reborn) 4 | [![Build Status](https://github.com/tagliala/coveralls-ruby-reborn/actions/workflows/ruby.yml/badge.svg)](https://github.com/tagliala/coveralls-ruby-reborn/actions/workflows/ruby.yml) 5 | [![Rubocop](https://github.com/tagliala/coveralls-ruby-reborn/actions/workflows/rubocop.yml/badge.svg)](https://github.com/tagliala/coveralls-ruby-reborn/actions/workflows/rubocop.yml) 6 | [![Coverage Status](https://coveralls.io/repos/github/tagliala/coveralls-ruby-reborn/badge.svg?branch=main)](https://coveralls.io/github/tagliala/coveralls-ruby-reborn?branch=main) 7 | 8 | [Coveralls.io](https://coveralls.io) was designed with Ruby projects in mind, so we've made it as 9 | easy as possible to get started using [Coveralls](https://coveralls.io) with Ruby and Rails project. 10 | 11 | An up-to-date fork of [lemurheavy/coveralls-ruby](https://github.com/lemurheavy/coveralls-ruby) 12 | 13 | ### PREREQUISITES 14 | 15 | - Using a supported repo host ([GitHub](https://github.com/) | [Gitlab](https://gitlab.com/) | 16 | [Bitbucket](https://bitbucket.org/)) 17 | - Building on a supported CI service (see 18 | [supported CI services](https://docs.coveralls.io/ci-services) here) 19 | - Any Ruby project or test framework supported by 20 | [SimpleCov](https://github.com/colszowka/simplecov) is supported by the 21 | [coveralls-ruby-reborn](https://github.com/tagliala/coveralls-ruby-reborn) gem. This includes 22 | all your favorites, like [RSpec](https://rspec.info/), Cucumber, and Test::Unit. 23 | 24 | ### INSTALLING THE GEM 25 | 26 | You shouldn't need more than a quick change to get your project on Coveralls. Just include 27 | [coveralls-ruby-reborn](https://github.com/tagliala/coveralls-ruby-reborn) in your project's 28 | Gemfile like so: 29 | 30 | ```ruby 31 | # ./Gemfile 32 | 33 | gem 'coveralls_reborn', require: false 34 | ``` 35 | 36 | ### CONFIGURATION 37 | 38 | [coveralls-ruby-reborn](https://github.com/tagliala/coveralls-ruby-reborn) uses an optional 39 | `.coveralls.yml` file at the root level of your repository to configure options. 40 | 41 | The option `repo_token` (found on your repository's page on Coveralls) is used to specify which 42 | project on Coveralls your project maps to. 43 | 44 | Another important configuration option is `service_name`, which indicates your CI service and allows 45 | you to specify where Coveralls should look to find additional information about your builds. This 46 | can be any string, but using the appropriate string for your service may allow Coveralls to perform 47 | service-specific actions like fetching branch data and commenting on pull requests. 48 | 49 | **Example: A .coveralls.yml file configured for Travis Pro:** 50 | 51 | ```yml 52 | service_name: travis-pro 53 | ``` 54 | 55 | **Example: Passing `repo_token` from the command line:** 56 | 57 | ```console 58 | COVERALLS_REPO_TOKEN=asdfasdf bundle exec rspec spec 59 | ``` 60 | 61 | ### TEST SUITE SETUP 62 | 63 | After configuration, the next step is to add 64 | [coveralls-ruby-reborn](https://github.com/tagliala/coveralls-ruby-reborn) to your test suite. 65 | 66 | For a Ruby app: 67 | 68 | ```ruby 69 | # ./spec/spec_helper.rb 70 | # ./test/test_helper.rb 71 | # ..etc.. 72 | 73 | require 'coveralls' 74 | Coveralls.wear! 75 | ``` 76 | 77 | For a Rails app: 78 | 79 | ```ruby 80 | require 'coveralls' 81 | Coveralls.wear!('rails') 82 | ``` 83 | 84 | **Note:** The `Coveralls.wear!` must occur before any of your application code is required, so it 85 | should be at the **very top** of your `spec_helper.rb`, `test_helper.rb`, or `env.rb`, etc. 86 | 87 | And holy moly, you're done! 88 | 89 | Next time your project is built on CI, [SimpleCov](https://github.com/colszowka/simplecov) will dial 90 | up [Coveralls.io](https://coveralls.io) and send the hot details on your code coverage. 91 | 92 | ### SIMPLECOV CUSTOMIZATION 93 | 94 | *"But wait!"* you're saying, *"I already use SimpleCov, and I have some custom settings! Are you 95 | really just overriding everything I've already set up?"* 96 | 97 | Good news, just use this gem's [SimpleCov](https://github.com/colszowka/simplecov) formatter 98 | directly: 99 | 100 | ```ruby 101 | require 'simplecov' 102 | require 'coveralls' 103 | 104 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 105 | SimpleCov.start do 106 | add_filter 'app/secrets' 107 | end 108 | ``` 109 | 110 | Or alongside another formatter, like so: 111 | 112 | ```ruby 113 | require 'simplecov' 114 | require 'coveralls' 115 | 116 | SimpleCov.formatters = [ 117 | SimpleCov::Formatter::HTMLFormatter, 118 | Coveralls::SimpleCov::Formatter 119 | ] 120 | SimpleCov.start 121 | ``` 122 | 123 | ### MERGING MULTIPLE TEST SUITES 124 | 125 | If you're using more than one test suite and want the coverage results to be merged, use 126 | `Coveralls.wear_merged!` instead of `Coveralls.wear!`. 127 | 128 | Or, if you're using Coveralls alongside another [SimpleCov](https://github.com/colszowka/simplecov) 129 | formatter, simply omit the Coveralls formatter, then add the rake task `coveralls:push` to your 130 | `Rakefile` as a dependency to your testing task, like so: 131 | 132 | ```ruby 133 | require 'coveralls/rake/task' 134 | Coveralls::RakeTask.new 135 | task :test_with_coveralls => [:spec, :features, 'coveralls:push'] 136 | ``` 137 | 138 | This will prevent Coveralls from sending coverage data after each individual suite, instead waiting 139 | until [SimpleCov](https://github.com/colszowka/simplecov) has merged the results, which are then 140 | posted to [Coveralls.io](https://coveralls.io). 141 | 142 | Unless you've added `coveralls:push` to your default rake task, your build command will need to be 143 | updated on your CI to reflect this, for example: 144 | 145 | ```console 146 | bundle exec rake :test_with_coveralls 147 | ``` 148 | 149 | *Read more about [SimpleCov's result merging](https://github.com/colszowka/simplecov#merging-results).* 150 | 151 | ### MANUAL BUILDS VIA CLI 152 | 153 | [coveralls-ruby-reborn](https://github.com/tagliala/coveralls-ruby-reborn) also allows you to 154 | upload coverage data manually by running your test suite locally. 155 | 156 | To do this with [RSpec](https://rspec.info/), just type `bundle exec coveralls push` in your project 157 | directory. 158 | 159 | This will run [RSpec](https://rspec.info/) and upload the coverage data to 160 | [Coveralls.io](https://coveralls.io) as a one-off build, passing along any configuration options 161 | specified in `.coveralls.yml`. 162 | 163 | 164 | ### GitHub Actions 165 | 166 | Psst... you don't need this gem on GitHub Actions. 167 | 168 | For a Rails application, just add 169 | 170 | ```rb 171 | gem 'simplecov-lcov', '~> 0.9.0' 172 | ``` 173 | 174 | to your `Gemfile` and 175 | 176 | ```rb 177 | require 'simplecov' 178 | 179 | SimpleCov.start 'rails' do 180 | if ENV['CI'] 181 | require 'simplecov-lcov' 182 | 183 | SimpleCov::Formatter::LcovFormatter.config do |c| 184 | c.report_with_single_file = true 185 | c.single_report_path = 'coverage/lcov.info' 186 | end 187 | 188 | formatter SimpleCov::Formatter::LcovFormatter 189 | end 190 | 191 | add_filter %w[version.rb initializer.rb] 192 | end 193 | ``` 194 | 195 | at the top of `spec_helper.rb` / `rails_helper.rb` / `test_helper.rb`. 196 | 197 | Then follow instructions at [Coveralls GitHub Action](https://github.com/marketplace/actions/coveralls-github-action) 198 | -------------------------------------------------------------------------------- /lib/coveralls/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'securerandom' 5 | 6 | module Coveralls 7 | module Configuration 8 | class << self 9 | def configuration 10 | config = { 11 | environment: relevant_env, 12 | git: git 13 | } 14 | 15 | yml = yaml_config 16 | 17 | if yml 18 | config[:configuration] = yml 19 | config[:repo_token] = yml['repo_token'] || yml['repo_secret_token'] 20 | end 21 | 22 | if ENV['COVERALLS_REPO_TOKEN'] 23 | config[:repo_token] = ENV['COVERALLS_REPO_TOKEN'] 24 | end 25 | 26 | if ENV['COVERALLS_PARALLEL'] && ENV['COVERALLS_PARALLEL'] != 'false' 27 | config[:parallel] = true 28 | end 29 | 30 | if ENV['COVERALLS_FLAG_NAME'] 31 | config[:flag_name] = ENV['COVERALLS_FLAG_NAME'] 32 | end 33 | 34 | if ENV['TRAVIS'] 35 | define_service_params_for_travis(config, yml ? yml['service_name'] : nil) 36 | elsif ENV['CIRCLECI'] 37 | define_service_params_for_circleci(config) 38 | elsif ENV['SEMAPHORE'] 39 | define_service_params_for_semaphore(config) 40 | elsif ENV['JENKINS_URL'] || ENV['JENKINS_HOME'] 41 | define_service_params_for_jenkins(config) 42 | elsif ENV['APPVEYOR'] 43 | define_service_params_for_appveyor(config) 44 | elsif ENV['TDDIUM'] 45 | define_service_params_for_tddium(config) 46 | elsif ENV['GITLAB_CI'] 47 | define_service_params_for_gitlab(config) 48 | elsif ENV['BUILDKITE'] 49 | define_service_params_for_buildkite(config) 50 | elsif ENV['COVERALLS_RUN_LOCALLY'] || Coveralls.testing 51 | define_service_params_for_coveralls_local(config) 52 | end 53 | 54 | # standardized env vars 55 | define_standard_service_params_for_generic_ci(config) 56 | 57 | if ENV['COVERALLS_SERVICE_NAME'] 58 | config[:service_name] = ENV['COVERALLS_SERVICE_NAME'] 59 | end 60 | 61 | config 62 | end 63 | 64 | def define_service_params_for_travis(config, service_name) 65 | config[:service_job_id] = ENV['TRAVIS_JOB_ID'] 66 | config[:service_pull_request] = ENV['TRAVIS_PULL_REQUEST'] unless ENV['TRAVIS_PULL_REQUEST'] == 'false' 67 | config[:service_name] = service_name || 'travis-ci' 68 | config[:service_branch] = ENV['TRAVIS_BRANCH'] 69 | end 70 | 71 | def define_service_params_for_circleci(config) 72 | config[:service_name] = 'circleci' 73 | config[:service_number] = ENV['CIRCLE_WORKFLOW_ID'] 74 | config[:service_pull_request] = ENV['CI_PULL_REQUEST'].split('/pull/')[1] unless ENV['CI_PULL_REQUEST'].nil? 75 | config[:service_job_number] = ENV['CIRCLE_BUILD_NUM'] 76 | config[:git_commit] = ENV['CIRCLE_SHA1'] 77 | config[:git_branch] = ENV['CIRCLE_BRANCH'] 78 | end 79 | 80 | def define_service_params_for_semaphore(config) 81 | config[:service_name] = 'semaphore' 82 | config[:service_number] = ENV['SEMAPHORE_WORKFLOW_ID'] 83 | config[:service_job_id] = ENV['SEMAPHORE_JOB_ID'] 84 | config[:service_build_url] = "#{ENV['SEMAPHORE_ORGANIZATION_URL']}/jobs/#{ENV['SEMAPHORE_JOB_ID']}" 85 | config[:service_branch] = ENV['SEMAPHORE_GIT_WORKING_BRANCH'] 86 | config[:service_pull_request] = ENV['SEMAPHORE_GIT_PR_NUMBER'] 87 | end 88 | 89 | def define_service_params_for_jenkins(config) 90 | config[:service_name] = 'jenkins' 91 | config[:service_number] = ENV['BUILD_NUMBER'] 92 | config[:service_branch] = ENV['BRANCH_NAME'] 93 | config[:service_pull_request] = ENV['ghprbPullId'] 94 | end 95 | 96 | def define_service_params_for_appveyor(config) 97 | config[:service_name] = 'appveyor' 98 | config[:service_number] = ENV['APPVEYOR_BUILD_VERSION'] 99 | config[:service_branch] = ENV['APPVEYOR_REPO_BRANCH'] 100 | config[:commit_sha] = ENV['APPVEYOR_REPO_COMMIT'] 101 | repo_name = ENV['APPVEYOR_REPO_NAME'] 102 | config[:service_build_url] = format('https://ci.appveyor.com/project/%s/build/%s', repo_name: repo_name, service_number: config[:service_number]) 103 | end 104 | 105 | def define_service_params_for_tddium(config) 106 | config[:service_name] = 'tddium' 107 | config[:service_number] = ENV['TDDIUM_SESSION_ID'] 108 | config[:service_job_number] = ENV['TDDIUM_TID'] 109 | config[:service_pull_request] = ENV['TDDIUM_PR_ID'] 110 | config[:service_branch] = ENV['TDDIUM_CURRENT_BRANCH'] 111 | config[:service_build_url] = "https://ci.solanolabs.com/reports/#{ENV['TDDIUM_SESSION_ID']}" 112 | end 113 | 114 | def define_service_params_for_gitlab(config) 115 | config[:service_name] = 'gitlab-ci' 116 | config[:service_number] = ENV['CI_PIPELINE_ID'] 117 | config[:service_job_number] = ENV['CI_BUILD_NAME'] 118 | config[:service_job_id] = ENV['CI_BUILD_ID'] 119 | config[:service_branch] = ENV['CI_BUILD_REF_NAME'] 120 | config[:commit_sha] = ENV['CI_BUILD_REF'] 121 | end 122 | 123 | def define_service_params_for_buildkite(config) 124 | config[:service_name] = 'buildkite' 125 | config[:service_number] = ENV['BUILDKITE_BUILD_NUMBER'] 126 | config[:service_job_id] = ENV['BUILDKITE_BUILD_ID'] 127 | config[:service_branch] = ENV['BUILDKITE_BRANCH'] 128 | config[:service_build_url] = ENV['BUILDKITE_BUILD_URL'] 129 | config[:service_pull_request] = ENV['BUILDKITE_PULL_REQUEST'] 130 | config[:commit_sha] = ENV['BUILDKITE_COMMIT'] 131 | end 132 | 133 | def define_service_params_for_coveralls_local(config) 134 | config[:service_job_id] = nil 135 | config[:service_name] = 'coveralls-ruby' 136 | config[:service_event_type] = 'manual' 137 | end 138 | 139 | def define_standard_service_params_for_generic_ci(config) 140 | config[:service_name] ||= ENV['CI_NAME'] 141 | config[:service_number] ||= ENV['CI_BUILD_NUMBER'] 142 | config[:service_job_id] ||= ENV['CI_JOB_ID'] 143 | config[:service_build_url] ||= ENV['CI_BUILD_URL'] 144 | config[:service_branch] ||= ENV['CI_BRANCH'] 145 | config[:service_pull_request] ||= (ENV['CI_PULL_REQUEST'] || '')[/(\d+)$/, 1] 146 | end 147 | 148 | def yaml_config 149 | return unless configuration_path && File.exist?(configuration_path) 150 | 151 | YAML.load_file(configuration_path) 152 | end 153 | 154 | def configuration_path 155 | return unless root 156 | 157 | File.expand_path(File.join(root, '.coveralls.yml')) 158 | end 159 | 160 | def root 161 | pwd 162 | end 163 | 164 | def pwd 165 | Dir.pwd 166 | end 167 | 168 | def simplecov_root 169 | return unless defined?(::SimpleCov) 170 | 171 | ::SimpleCov.root 172 | end 173 | 174 | def rails_root 175 | Rails.root.to_s 176 | rescue StandardError 177 | nil 178 | end 179 | 180 | def git 181 | hash = {} 182 | 183 | Dir.chdir(root) do 184 | hash[:head] = { 185 | id: ENV.fetch('GIT_ID', `git log -1 --pretty=format:'%H'`), 186 | author_name: ENV.fetch('GIT_AUTHOR_NAME', `git log -1 --pretty=format:'%aN'`), 187 | author_email: ENV.fetch('GIT_AUTHOR_EMAIL', `git log -1 --pretty=format:'%ae'`), 188 | committer_name: ENV.fetch('GIT_COMMITTER_NAME', `git log -1 --pretty=format:'%cN'`), 189 | committer_email: ENV.fetch('GIT_COMMITTER_EMAIL', `git log -1 --pretty=format:'%ce'`), 190 | message: ENV.fetch('GIT_MESSAGE', `git log -1 --pretty=format:'%s'`) 191 | } 192 | 193 | # Branch 194 | hash[:branch] = ENV.fetch('GIT_BRANCH', `git rev-parse --abbrev-ref HEAD`) 195 | 196 | # Remotes 197 | remotes = nil 198 | begin 199 | remotes = `git remote -v`.split("\n").map do |remote| 200 | splits = remote.split.compact 201 | { name: splits[0], url: splits[1] } 202 | end.uniq 203 | rescue StandardError => e 204 | # TODO: Add error action 205 | puts e.message 206 | end 207 | 208 | hash[:remotes] = remotes 209 | end 210 | 211 | hash 212 | rescue StandardError => e 213 | Coveralls::Output.puts 'Coveralls git error:', color: 'red' 214 | Coveralls::Output.puts e.to_s, color: 'red' 215 | nil 216 | end 217 | 218 | def relevant_env 219 | base_env = { 220 | pwd: pwd, 221 | rails_root: rails_root, 222 | simplecov_root: simplecov_root, 223 | gem_version: VERSION 224 | } 225 | 226 | service_env = 227 | if ENV['TRAVIS'] 228 | travis_env_hash 229 | elsif ENV['CIRCLECI'] 230 | circleci_env_hash 231 | elsif ENV['JENKINS_URL'] 232 | jenkins_env_hash 233 | elsif ENV['SEMAPHORE'] 234 | semaphore_env_hash 235 | else 236 | {} 237 | end 238 | 239 | base_env.merge! service_env 240 | end 241 | 242 | private 243 | 244 | def circleci_env_hash 245 | { 246 | circleci_build_num: ENV['CIRCLE_BUILD_NUM'], 247 | branch: ENV['CIRCLE_BRANCH'], 248 | commit_sha: ENV['CIRCLE_SHA1'] 249 | } 250 | end 251 | 252 | def jenkins_env_hash 253 | { 254 | jenkins_build_num: ENV['BUILD_NUMBER'], 255 | jenkins_build_url: ENV['BUILD_URL'], 256 | branch: ENV['GIT_BRANCH'], 257 | commit_sha: ENV['GIT_COMMIT'] 258 | } 259 | end 260 | 261 | def semaphore_env_hash 262 | { 263 | branch: ENV['BRANCH_NAME'], 264 | commit_sha: ENV['REVISION'] 265 | } 266 | end 267 | 268 | def travis_env_hash 269 | { 270 | travis_job_id: ENV['TRAVIS_JOB_ID'], 271 | travis_pull_request: ENV['TRAVIS_PULL_REQUEST'], 272 | branch: ENV['TRAVIS_BRANCH'] 273 | } 274 | end 275 | end 276 | end 277 | end 278 | -------------------------------------------------------------------------------- /spec/coveralls/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Coveralls::Configuration do 6 | before do 7 | allow(ENV).to receive(:[]).and_return(nil) 8 | end 9 | 10 | describe '.configuration' do 11 | it 'returns a hash with the default keys' do 12 | config = described_class.configuration 13 | expect(config).to be_a(Hash) 14 | expect(config.keys).to include(:environment) 15 | expect(config.keys).to include(:git) 16 | end 17 | 18 | context 'with yaml_config' do 19 | let(:repo_token) { SecureRandom.hex(4) } 20 | let(:repo_secret_token) { SecureRandom.hex(4) } 21 | let(:yaml_config) do 22 | { 23 | 'repo_token' => repo_token, 24 | 'repo_secret_token' => repo_secret_token 25 | } 26 | end 27 | 28 | before do 29 | allow(File).to receive(:exist?).with(described_class.configuration_path).and_return(true) 30 | allow(YAML).to receive(:load_file).with(described_class.configuration_path).and_return(yaml_config) 31 | end 32 | 33 | it 'sets the Yaml config and associated variables if present' do 34 | config = described_class.configuration 35 | expect(config[:configuration]).to eq(yaml_config) 36 | expect(config[:repo_token]).to eq(repo_token) 37 | end 38 | 39 | it 'uses the repo_secret_token if the repo_token is not set' do 40 | yaml_config.delete('repo_token') 41 | config = described_class.configuration 42 | expect(config[:configuration]).to eq(yaml_config) 43 | expect(config[:repo_token]).to eq(repo_secret_token) 44 | end 45 | end 46 | 47 | context 'when repo_token is in environment' do 48 | let(:repo_token) { SecureRandom.hex(4) } 49 | 50 | before do 51 | allow(ENV).to receive(:[]).with('COVERALLS_REPO_TOKEN').and_return(repo_token) 52 | end 53 | 54 | it 'pulls the repo token from the environment if set' do 55 | config = described_class.configuration 56 | expect(config[:repo_token]).to eq(repo_token) 57 | end 58 | end 59 | 60 | context 'when parallel is in environment' do 61 | before do 62 | allow(ENV).to receive(:[]).with('COVERALLS_PARALLEL').and_return(true) 63 | end 64 | 65 | it 'sets parallel to true if present' do 66 | config = described_class.configuration 67 | expect(config[:parallel]).to be true 68 | end 69 | end 70 | 71 | context 'when flag_name is in environment' do 72 | before do 73 | allow(ENV).to receive(:[]).with('COVERALLS_FLAG_NAME').and_return(true) 74 | end 75 | 76 | it 'sets flag_name to true if present' do 77 | config = described_class.configuration 78 | expect(config[:flag_name]).to be true 79 | end 80 | end 81 | 82 | context 'with services' do 83 | def services 84 | { 85 | appveyor: 'APPVEYOR', 86 | circleci: 'CIRCLECI', 87 | gitlab: 'GITLAB_CI', 88 | buildkite: 'BUILDKITE', 89 | jenkins: 'JENKINS_URL', 90 | semaphore: 'SEMAPHORE', 91 | tddium: 'TDDIUM', 92 | travis: 'TRAVIS', 93 | coveralls_local: 'COVERALLS_RUN_LOCALLY', 94 | generic: 'CI_NAME' 95 | } 96 | end 97 | 98 | shared_examples 'a service' do |service_name| 99 | let(:service_variable) { options[:service_variable] } 100 | 101 | before do 102 | allow(ENV).to receive(:[]).with(services[service_name]).and_return('1') 103 | described_class.configuration 104 | end 105 | 106 | it 'sets service parameters for this service and no other' do 107 | services.each_key.reject { |service| service == service_name }.each do |service| 108 | expect(described_class).not_to have_received(:"define_service_params_for_#{service}") 109 | end 110 | 111 | expect(described_class).to have_received(:"define_service_params_for_#{service_name}") unless service_name == :generic 112 | expect(described_class).to have_received(:define_standard_service_params_for_generic_ci) 113 | end 114 | end 115 | 116 | before do 117 | services.each_key do |service| 118 | allow(described_class).to receive(:"define_service_params_for_#{service}") 119 | end 120 | 121 | allow(described_class).to receive(:define_standard_service_params_for_generic_ci) 122 | end 123 | 124 | context 'with env based service name' do 125 | let(:service_name) { 'travis-enterprise' } 126 | 127 | before do 128 | allow(ENV).to receive(:[]).with('TRAVIS').and_return('1') 129 | allow(ENV).to receive(:[]).with('COVERALLS_SERVICE_NAME').and_return(service_name) 130 | end 131 | 132 | it 'pulls the service name from the environment if set' do 133 | config = described_class.configuration 134 | expect(config[:service_name]).to eq(service_name) 135 | end 136 | end 137 | 138 | context 'when using AppVeyor' do 139 | it_behaves_like 'a service', :appveyor 140 | end 141 | 142 | context 'when using CircleCI' do 143 | it_behaves_like 'a service', :circleci 144 | end 145 | 146 | context 'when using GitLab CI' do 147 | it_behaves_like 'a service', :gitlab 148 | end 149 | 150 | context 'when using Buildkite' do 151 | it_behaves_like 'a service', :buildkite 152 | end 153 | 154 | context 'when using Jenkins' do 155 | it_behaves_like 'a service', :jenkins 156 | end 157 | 158 | context 'when using Semaphore' do 159 | it_behaves_like 'a service', :semaphore 160 | end 161 | 162 | context 'when using Tddium' do 163 | it_behaves_like 'a service', :tddium 164 | end 165 | 166 | context 'when using Travis' do 167 | it_behaves_like 'a service', :travis 168 | end 169 | 170 | context 'when running Coveralls locally' do 171 | it_behaves_like 'a service', :coveralls_local 172 | end 173 | 174 | context 'when using a generic CI' do 175 | it_behaves_like 'a service', :generic 176 | end 177 | end 178 | end 179 | 180 | describe '.define_service_params_for_travis' do 181 | let(:travis_job_id) { SecureRandom.hex(4) } 182 | 183 | before do 184 | allow(ENV).to receive(:[]).with('TRAVIS_JOB_ID').and_return(travis_job_id) 185 | end 186 | 187 | it 'sets the service_job_id' do 188 | config = {} 189 | described_class.define_service_params_for_travis(config, nil) 190 | expect(config[:service_job_id]).to eq(travis_job_id) 191 | end 192 | 193 | it 'sets the service_name to travis-ci by default' do 194 | config = {} 195 | described_class.define_service_params_for_travis(config, nil) 196 | expect(config[:service_name]).to eq('travis-ci') 197 | end 198 | 199 | it 'sets the service_name to a value if one is passed in' do 200 | config = {} 201 | random_name = SecureRandom.hex(4) 202 | described_class.define_service_params_for_travis(config, random_name) 203 | expect(config[:service_name]).to eq(random_name) 204 | end 205 | end 206 | 207 | describe '.define_service_params_for_circleci' do 208 | let(:circle_workflow_id) { 1234 } 209 | let(:ci_pull_request) { 'repo/pull/12' } 210 | let(:circle_build_num) { SecureRandom.hex(4) } 211 | let(:circle_sha1) { SecureRandom.hex(32) } 212 | let(:circle_branch) { SecureRandom.hex(4) } 213 | 214 | before do 215 | allow(ENV).to receive(:[]).with('CIRCLE_WORKFLOW_ID').and_return(circle_workflow_id) 216 | allow(ENV).to receive(:[]).with('CI_PULL_REQUEST').and_return(ci_pull_request) 217 | allow(ENV).to receive(:[]).with('CIRCLE_BUILD_NUM').and_return(circle_build_num) 218 | allow(ENV).to receive(:[]).with('CIRCLE_SHA1').and_return(circle_sha1) 219 | allow(ENV).to receive(:[]).with('CIRCLE_BRANCH').and_return(circle_branch) 220 | end 221 | 222 | it 'sets the expected parameters' do 223 | config = {} 224 | described_class.define_service_params_for_circleci(config) 225 | expect(config).to include( 226 | service_name: 'circleci', 227 | service_number: circle_workflow_id, 228 | service_pull_request: '12', 229 | service_job_number: circle_build_num, 230 | git_commit: circle_sha1, 231 | git_branch: circle_branch 232 | ) 233 | end 234 | end 235 | 236 | describe '.define_service_params_for_gitlab' do 237 | let(:commit_sha) { SecureRandom.hex(32) } 238 | let(:service_job_number) { 'spec:one' } 239 | let(:service_job_id) { 1234 } 240 | let(:service_branch) { 'feature' } 241 | let(:service_number) { 5678 } 242 | 243 | before do 244 | allow(ENV).to receive(:[]).with('CI_BUILD_NAME').and_return(service_job_number) 245 | allow(ENV).to receive(:[]).with('CI_PIPELINE_ID').and_return(service_number) 246 | allow(ENV).to receive(:[]).with('CI_BUILD_ID').and_return(service_job_id) 247 | allow(ENV).to receive(:[]).with('CI_BUILD_REF_NAME').and_return(service_branch) 248 | allow(ENV).to receive(:[]).with('CI_BUILD_REF').and_return(commit_sha) 249 | end 250 | 251 | it 'sets the expected parameters' do 252 | config = {} 253 | described_class.define_service_params_for_gitlab(config) 254 | expect(config).to include( 255 | service_name: 'gitlab-ci', 256 | service_number: service_number, 257 | service_job_number: service_job_number, 258 | service_job_id: service_job_id, 259 | service_branch: service_branch, 260 | commit_sha: commit_sha 261 | ) 262 | end 263 | end 264 | 265 | describe '.define_service_params_for_buildkite' do 266 | let(:service_number) { 5678 } 267 | let(:service_job_id) { 1234 } 268 | let(:service_branch) { 'feature' } 269 | let(:service_build_url) { SecureRandom.hex(4) } 270 | let(:service_pull_request) { SecureRandom.hex(4) } 271 | let(:commit_sha) { SecureRandom.hex(32) } 272 | 273 | before do 274 | allow(ENV).to receive(:[]).with('BUILDKITE_BUILD_NUMBER').and_return(service_number) 275 | allow(ENV).to receive(:[]).with('BUILDKITE_BUILD_ID').and_return(service_job_id) 276 | allow(ENV).to receive(:[]).with('BUILDKITE_BRANCH').and_return(service_branch) 277 | allow(ENV).to receive(:[]).with('BUILDKITE_BUILD_URL').and_return(service_build_url) 278 | allow(ENV).to receive(:[]).with('BUILDKITE_PULL_REQUEST').and_return(service_pull_request) 279 | allow(ENV).to receive(:[]).with('BUILDKITE_COMMIT').and_return(commit_sha) 280 | end 281 | 282 | it 'sets the expected parameters' do 283 | config = {} 284 | described_class.define_service_params_for_buildkite(config) 285 | expect(config).to include( 286 | service_name: 'buildkite', 287 | service_number: service_number, 288 | service_job_id: service_job_id, 289 | service_branch: service_branch, 290 | service_build_url: service_build_url, 291 | service_pull_request: service_pull_request, 292 | commit_sha: commit_sha 293 | ) 294 | end 295 | end 296 | 297 | describe '.define_service_params_for_semaphore' do 298 | let(:semaphore_workflow_id) { 1234 } 299 | let(:semaphore_git_pr_number) { 10 } 300 | let(:semaphore_git_working_branch) { 'pr-branch' } 301 | let(:semaphore_job_id) { 5678 } 302 | let(:semaphore_organization_url) { 'an-organization' } 303 | 304 | before do 305 | allow(ENV).to receive(:[]).with('SEMAPHORE_WORKFLOW_ID').and_return(semaphore_workflow_id) 306 | allow(ENV).to receive(:[]).with('SEMAPHORE_GIT_PR_NUMBER').and_return(semaphore_git_pr_number) 307 | allow(ENV).to receive(:[]).with('SEMAPHORE_GIT_WORKING_BRANCH').and_return(semaphore_git_working_branch) 308 | allow(ENV).to receive(:[]).with('SEMAPHORE_JOB_ID').and_return(semaphore_job_id) 309 | allow(ENV).to receive(:[]).with('SEMAPHORE_ORGANIZATION_URL').and_return(semaphore_organization_url) 310 | end 311 | 312 | it 'sets the expected parameters' do 313 | config = {} 314 | described_class.define_service_params_for_semaphore(config) 315 | expect(config).to include( 316 | service_name: 'semaphore', 317 | service_number: semaphore_workflow_id, 318 | service_job_id: semaphore_job_id, 319 | service_build_url: "#{semaphore_organization_url}/jobs/#{semaphore_job_id}", 320 | service_branch: semaphore_git_working_branch, 321 | service_pull_request: semaphore_git_pr_number 322 | ) 323 | end 324 | end 325 | 326 | describe '.define_service_params_for_jenkins' do 327 | let(:service_pull_request) { '1234' } 328 | let(:build_num) { SecureRandom.hex(4) } 329 | 330 | before do 331 | allow(ENV).to receive(:[]).with('CI_PULL_REQUEST').and_return(service_pull_request) 332 | allow(ENV).to receive(:[]).with('BUILD_NUMBER').and_return(build_num) 333 | end 334 | 335 | it 'sets the expected parameters' do 336 | config = {} 337 | described_class.define_service_params_for_jenkins config 338 | described_class.define_standard_service_params_for_generic_ci config 339 | expect(config).to include( 340 | service_name: 'jenkins', 341 | service_number: build_num, 342 | service_pull_request: service_pull_request 343 | ) 344 | end 345 | end 346 | 347 | describe '.define_service_params_for_coveralls_local' do 348 | it 'sets the expected parameters' do 349 | config = {} 350 | described_class.define_service_params_for_coveralls_local(config) 351 | expect(config).to include( 352 | service_name: 'coveralls-ruby', 353 | service_job_id: nil, 354 | service_event_type: 'manual' 355 | ) 356 | end 357 | end 358 | 359 | describe '.define_service_params_for_generic_ci' do 360 | let(:service_name) { SecureRandom.hex(4) } 361 | let(:service_number) { SecureRandom.hex(4) } 362 | let(:service_build_url) { SecureRandom.hex(4) } 363 | let(:service_branch) { SecureRandom.hex(4) } 364 | let(:service_pull_request) { '1234' } 365 | 366 | before do 367 | allow(ENV).to receive(:[]).with('CI_NAME').and_return(service_name) 368 | allow(ENV).to receive(:[]).with('CI_BUILD_NUMBER').and_return(service_number) 369 | allow(ENV).to receive(:[]).with('CI_BUILD_URL').and_return(service_build_url) 370 | allow(ENV).to receive(:[]).with('CI_BRANCH').and_return(service_branch) 371 | allow(ENV).to receive(:[]).with('CI_PULL_REQUEST').and_return(service_pull_request) 372 | end 373 | 374 | it 'sets the expected parameters' do 375 | config = {} 376 | described_class.define_standard_service_params_for_generic_ci(config) 377 | expect(config).to include( 378 | service_name: service_name, 379 | service_number: service_number, 380 | service_build_url: service_build_url, 381 | service_branch: service_branch, 382 | service_pull_request: service_pull_request 383 | ) 384 | end 385 | end 386 | 387 | describe '.define_service_params_for_appveyor' do 388 | let(:service_number) { SecureRandom.hex(4) } 389 | let(:service_branch) { SecureRandom.hex(4) } 390 | let(:commit_sha) { SecureRandom.hex(4) } 391 | let(:repo_name) { SecureRandom.hex(4) } 392 | 393 | before do 394 | allow(ENV).to receive(:[]).with('APPVEYOR_BUILD_VERSION').and_return(service_number) 395 | allow(ENV).to receive(:[]).with('APPVEYOR_REPO_BRANCH').and_return(service_branch) 396 | allow(ENV).to receive(:[]).with('APPVEYOR_REPO_COMMIT').and_return(commit_sha) 397 | allow(ENV).to receive(:[]).with('APPVEYOR_REPO_NAME').and_return(repo_name) 398 | end 399 | 400 | it 'sets the expected parameters' do 401 | config = {} 402 | described_class.define_service_params_for_appveyor(config) 403 | expect(config).to include( 404 | service_name: 'appveyor', 405 | service_number: service_number, 406 | service_branch: service_branch, 407 | commit_sha: commit_sha, 408 | service_build_url: format('https://ci.appveyor.com/project/%s/build/%s', repo_name: repo_name, service_number: service_number) 409 | ) 410 | end 411 | end 412 | 413 | describe '.define_service_params_for_tddium' do 414 | let(:service_number) { SecureRandom.hex(4) } 415 | let(:service_job_number) { SecureRandom.hex(4) } 416 | let(:service_pull_request) { SecureRandom.hex(4) } 417 | let(:service_branch) { SecureRandom.hex(4) } 418 | 419 | before do 420 | allow(ENV).to receive(:[]).with('TDDIUM_SESSION_ID').and_return(service_number) 421 | allow(ENV).to receive(:[]).with('TDDIUM_TID').and_return(service_job_number) 422 | allow(ENV).to receive(:[]).with('TDDIUM_PR_ID').and_return(service_pull_request) 423 | allow(ENV).to receive(:[]).with('TDDIUM_CURRENT_BRANCH').and_return(service_branch) 424 | end 425 | 426 | it 'sets the expected parameters' do 427 | config = {} 428 | described_class.define_service_params_for_tddium(config) 429 | expect(config).to include( 430 | service_name: 'tddium', 431 | service_number: service_number, 432 | service_job_number: service_job_number, 433 | service_pull_request: service_pull_request, 434 | service_branch: service_branch, 435 | service_build_url: format('https://ci.solanolabs.com/reports/%s', service_number: service_number) 436 | ) 437 | end 438 | end 439 | 440 | describe '.git' do 441 | let(:git_id) { SecureRandom.hex(2) } 442 | let(:author_name) { SecureRandom.hex(4) } 443 | let(:author_email) { SecureRandom.hex(4) } 444 | let(:committer_name) { SecureRandom.hex(4) } 445 | let(:committer_email) { SecureRandom.hex(4) } 446 | let(:message) { SecureRandom.hex(4) } 447 | let(:branch) { SecureRandom.hex(4) } 448 | 449 | before do 450 | allow(ENV).to receive(:fetch).with('GIT_ID', anything).and_return(git_id) 451 | allow(ENV).to receive(:fetch).with('GIT_AUTHOR_NAME', anything).and_return(author_name) 452 | allow(ENV).to receive(:fetch).with('GIT_AUTHOR_EMAIL', anything).and_return(author_email) 453 | allow(ENV).to receive(:fetch).with('GIT_COMMITTER_NAME', anything).and_return(committer_name) 454 | allow(ENV).to receive(:fetch).with('GIT_COMMITTER_EMAIL', anything).and_return(committer_email) 455 | allow(ENV).to receive(:fetch).with('GIT_MESSAGE', anything).and_return(message) 456 | allow(ENV).to receive(:fetch).with('GIT_BRANCH', anything).and_return(branch) 457 | end 458 | 459 | it 'uses ENV vars' do 460 | config = described_class.git 461 | expect(config[:branch]).to eq(branch) 462 | expect(config[:head]).to include( 463 | id: git_id, 464 | author_name: author_name, 465 | author_email: author_email, 466 | committer_name: committer_name, 467 | committer_email: committer_email, 468 | message: message 469 | ) 470 | end 471 | end 472 | end 473 | --------------------------------------------------------------------------------