├── .codeclimate.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.ruby-19 ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── cc-tddium-post-worker ├── ci └── codeclimate-test-reporter ├── circle.yml ├── codeclimate-test-reporter.gemspec ├── config └── cacert.pem ├── lib ├── code_climate │ ├── test_reporter.rb │ └── test_reporter │ │ ├── calculate_blob.rb │ │ ├── ci.rb │ │ ├── client.rb │ │ ├── configuration.rb │ │ ├── exception_message.rb │ │ ├── formatter.rb │ │ ├── git.rb │ │ ├── payload_validator.rb │ │ ├── post_results.rb │ │ ├── shorten_filename.rb │ │ └── version.rb └── codeclimate-test-reporter.rb └── spec ├── code_climate ├── test_reporter │ ├── calculate_blob_spec.rb │ ├── ci_spec.rb │ ├── client_spec.rb │ ├── configuration_spec.rb │ ├── formatter_spec.rb │ ├── git_spec.rb │ ├── payload_validator_spec.rb │ └── shorten_filename_spec.rb └── test_reporter_spec.rb ├── fixtures ├── encoding_test.rb ├── encoding_test_iso.rb ├── fake_project.tar.gz ├── issue_7.tar.gz ├── issue_7_resultset.json └── test_file.rb ├── spec_helper.rb └── support ├── fixture_helper.rb ├── io_helper.rb └── requests_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | fixme: 9 | enabled: true 10 | rubocop: 11 | enabled: true 12 | ratings: 13 | paths: 14 | - "**.rb" 15 | exclude_paths: 16 | - config/ 17 | - spec/ 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | *.DS_Store 7 | Gemfile.lock 8 | Gemfile.ruby-19.lock 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --order rand 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Metrics 3 | ################################################################################ 4 | 5 | Metrics/LineLength: 6 | Enabled: false 7 | 8 | Metrics/AbcSize: 9 | Enabled: false 10 | 11 | ################################################################################ 12 | # Style 13 | ################################################################################ 14 | 15 | # and/or in conditionals has no meaningful difference (only gotchas), so we 16 | # disallow them there. When used for control flow, the difference in precedence 17 | # can make for a less noisy expression, as in: 18 | # 19 | # x = find_x or raise XNotFound 20 | # 21 | Style/AndOr: 22 | EnforcedStyle: conditionals 23 | 24 | # Executables are conventionally named bin/foo-bar 25 | Style/FileName: 26 | Exclude: 27 | - bin/**/* 28 | 29 | # We don't (currently) document our code 30 | Style/Documentation: 31 | Enabled: false 32 | 33 | # Always use double-quotes to keep things simple 34 | Style/StringLiterals: 35 | EnforcedStyle: double_quotes 36 | 37 | Style/StringLiteralsInInterpolation: 38 | EnforcedStyle: double_quotes 39 | 40 | # Use a trailing comma to keep diffs clean when elements are inserted or removed 41 | Style/TrailingCommaInArguments: 42 | EnforcedStyleForMultiline: comma 43 | 44 | Style/TrailingCommaInLiteral: 45 | EnforcedStyleForMultiline: comma 46 | 47 | # We avoid GuardClause because it can result in "suprise return" 48 | Style/GuardClause: 49 | Enabled: false 50 | 51 | # We avoid IfUnlessModifier because it can result in "suprise if" 52 | Style/IfUnlessModifier: 53 | Enabled: false 54 | 55 | # We don't care about the fail/raise distinction 56 | Style/SignalException: 57 | EnforcedStyle: only_raise 58 | 59 | Style/DotPosition: 60 | EnforcedStyle: trailing 61 | 62 | # Common globals we allow 63 | Style/GlobalVars: 64 | AllowedVariables: 65 | - "$statsd" 66 | - "$mongo" 67 | - "$rollout" 68 | 69 | # Using english names requires loading an extra module, which is annoying, so 70 | # we prefer the perl names for consistency. 71 | Style/SpecialGlobalVars: 72 | EnforcedStyle: use_perl_names 73 | 74 | # We have common cases where has_ and have_ make sense 75 | Style/PredicateName: 76 | Enabled: true 77 | NamePrefixBlacklist: 78 | - is_ 79 | 80 | # We use %w[ ], not %w( ) because the former looks like an array 81 | Style/PercentLiteralDelimiters: 82 | PreferredDelimiters: 83 | "%w": [] 84 | "%W": [] 85 | 86 | # Allow "trivial" accessors when defined as a predicate? method 87 | Style/TrivialAccessors: 88 | AllowPredicates: true 89 | 90 | Style/Next: 91 | Enabled: false 92 | 93 | # We think it's OK to use the "extend self" module pattern 94 | Style/ModuleFunction: 95 | Enabled: false 96 | 97 | # Disallow extra spacing for token alignment 98 | Style/ExtraSpacing: 99 | AllowForAlignment: false 100 | 101 | ################################################################################ 102 | # Performance 103 | ################################################################################ 104 | 105 | Performance/RedundantMerge: 106 | Enabled: false 107 | 108 | ################################################################################ 109 | # Rails - disable things because we're primarily non-rails 110 | ################################################################################ 111 | 112 | Rails/Delegate: 113 | Enabled: false 114 | 115 | Rails/TimeZone: 116 | Enabled: false 117 | 118 | ################################################################################ 119 | # Specs - be more lenient on length checks and block styles 120 | ################################################################################ 121 | 122 | Metrics/ModuleLength: 123 | Exclude: 124 | - spec/**/* 125 | 126 | Metrics/MethodLength: 127 | Exclude: 128 | - spec/**/* 129 | 130 | Style/ClassAndModuleChildren: 131 | Exclude: 132 | - spec/**/* 133 | 134 | Style/BlockDelimiters: 135 | Exclude: 136 | - spec/**/* 137 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## master (unreleased) 4 | 5 | ### New features 6 | 7 | ### Bug fixes 8 | 9 | ### Changes 10 | 11 | ### v1.0.9 (2018-10-08) 12 | 13 | * Add deprecation notice to post-install gem message. 14 | 15 | ### v1.0.8 (2017-03-20) 16 | 17 | ### Bug fixes 18 | 19 | * Lock simplecov to `<= 0.13`, so we can safely use an internal-to-simplecov 20 | method. 21 | ([@bliof](https://github.com/codeclimate/ruby-test-reporter/pull/181)) 22 | 23 | Note: you may need to run `bundle update codeclimate-test-reporter simplecov` 24 | to resolve your bundle. 25 | 26 | ### v1.0.7 (2017-03-08) 27 | 28 | ### Bug fixes 29 | 30 | * Continue improving our support for sending payloads in contexts where git is 31 | not available. 32 | ([@sullerandras](https://github.com/codeclimate/ruby-test-reporter/pull/177)) 33 | 34 | ### v1.0.6 (2017-02-22) 35 | 36 | ### Bug fixes 37 | 38 | * Allow Codeship users to send a test coverage report without mounting their 39 | `.git` directory within the docker container where they run their tests 40 | _without_ manually exposing an environment variable. Instead, use an 41 | environment variable already exposed by the CI environment. 42 | ([@c-knowles](https://github.com/codeclimate/ruby-test-reporter/pull/172)) 43 | 44 | ### v1.0.5 (2017-01-19) 45 | 46 | ### Bug fixes 47 | 48 | * Allow Codeship users to send a test coverage report without mounting their 49 | `.git` directory within the docker container where they run their tests. 50 | ([@antoniobg](https://github.com/codeclimate/ruby-test-reporter/pull/168)) 51 | 52 | ### v1.0.4 (2016-12-29) 53 | 54 | ### New features 55 | 56 | * Accept path to coverage results as optional first argument ([@jreinert](https://github.com/codeclimate/ruby-test-reporter/pull/158)) 57 | 58 | ### Bug fixes 59 | 60 | * Handle multi-command resultsets ([@pbrisbin](https://github.com/codeclimate/ruby-test-reporter/pull/163)) 61 | 62 | ## v1.0.3 (2016-11-09) 63 | 64 | ### Bug fixes 65 | 66 | * Improve strategy for Ruby 1.9.3 compatibility testing 67 | 68 | ## v1.0.2 (2016-11-08) 69 | 70 | ### Bug fixes 71 | 72 | * Fixed crashing error when the path to a file in the coverage report 73 | contains a parenthesis. 74 | 75 | ## v1.0.1 (2016-11-06) 76 | 77 | ### Bug fixes 78 | 79 | * Made sure the gem can be built while running Ruby 1.9.3 80 | 81 | ## v1.0.0 (2016-11-03) 82 | 83 | ### Changes 84 | 85 | * Previously, this gem extended `Simplecov` with a custom formatter which posted 86 | results to Code Climate. Now, you are responsible for executing `Simplecov` 87 | yourself. 88 | 89 | * If you already have the following in your test/test_helper.rb 90 | (or spec_helper.rb, cucumber env.rb, etc) 91 | 92 | ```ruby 93 | require 'codeclimate-test-reporter' 94 | CodeClimate::TestReporter.start 95 | ``` 96 | 97 | then you should replace it with 98 | 99 | ```ruby 100 | require 'simplecov' 101 | SimpleCov.start 102 | ``` 103 | 104 | * Previously, the `codeclimate-test-reporter` automatically uploaded results at 105 | the end of your test suite. Now, you are responsible for running 106 | `codeclimate-test-reporter` as a separate step in your build. 107 | * Previously, this gem added some exclusion rules tuned according to feedback 108 | from its users, and now these no longer happen automatically. *If you are 109 | experiencing a discrepancy in test coverage % after switching to the new gem 110 | version, it may be due to missing exclusions. Filtering `vendor`, `spec`, or 111 | `test` directories may fix this issue.* 112 | * Previously, during the execution of multiple test suites, this gem would send 113 | results from the first suite completed. You are now expected to run an 114 | executable packaged with this gem as a separate build step, which means that 115 | whatever results are there (likely the results from the last suite) will be 116 | posted to Code Climate. 117 | 118 | ## v0.6.0 (2016-06-27) 119 | 120 | ### New features 121 | 122 | * Support `ENV["SSL_CERT_PATH"]` for custom SSL certificates 123 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.ruby-19: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "addressable", "< 2.5" 4 | gem "json", "~> 1.8", "< 2" 5 | gem "rake", "< 12.3.0" 6 | gem "webmock", "< 2.3.1" 7 | 8 | gemspec 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Code Climate LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | This gem includes code by Wil Gieseler, distributed under the MIT license: 22 | 23 | Permission is hereby granted, free of charge, to any person obtaining 24 | a copy of this software and associated documentation files (the 25 | "Software"), to deal in the Software without restriction, including 26 | without limitation the rights to use, copy, modify, merge, publish, 27 | distribute, sublicense, and/or sell copies of the Software, and to 28 | permit persons to whom the Software is furnished to do so, subject to 29 | the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be 32 | included in all copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 35 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 36 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 37 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 38 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 39 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codeclimate-test-reporter - [DEPRECATED] 2 | 3 | These configuration instructions refer to a language-specific test reporter who is now deprecated in favor of our new unified test reporter client. The [new test reporter](https://docs.codeclimate.com/v1.0/docs/configuring-test-coverage) is faster, distributed as a static binary, has support for parallelized CI builds, and will receive ongoing support by the team here. The existing test reporters for Ruby, Python, PHP, and Javascript are now deprecated. 4 | 5 | [![Code Climate](https://codeclimate.com/github/codeclimate/ruby-test-reporter/badges/gpa.svg)](https://codeclimate.com/github/codeclimate/ruby-test-reporter) 6 | 7 | Posts SimpleCov test coverage data from your Ruby test suite to Code Climate's 8 | hosted, automated code review service. 9 | 10 | Code Climate - [https://codeclimate.com](https://codeclimate.com) 11 | 12 | ## Installation 13 | 14 | This gem requires a user, but not necessarily a paid account, on Code Climate, 15 | so if you don't have one the first step is to signup at: 16 | [https://codeclimate.com](https://codeclimate.com). Then follow the 17 | instructions on our [documentation site](https://docs.codeclimate.com/docs/test-coverage-ruby). 18 | 19 | Please contact hello@codeclimate.com if you need any assistance setting this up. 20 | 21 | ## Usage 22 | 23 | ```console 24 | bundle exec rspec && CODECLIMATE_REPO_TOKEN=my_token bundle exec codeclimate-test-reporter 25 | ``` 26 | 27 | **Optional**: configure `CODECLIMATE_API_HOST` to point to a self-hosted version of Code Climate. 28 | 29 | ## Troubleshooting / FYIs 30 | 31 | Across the many different testing frameworks, setups, and environments, there 32 | are lots of variables at play. If you're having any trouble with your test 33 | coverage reporting or the results are confusing, please see our full 34 | documentation here: https://docs.codeclimate.com/docs/setting-up-test-coverage 35 | 36 | ## Upgrading from pre-1.0 Versions 37 | 38 | Version `1.0` of this gem introduced new, breaking changes to the way the 39 | test reporter is meant to be executed. The following list summarizes the major 40 | differences: 41 | 42 | See [the changelog entry for v1.0.0](CHANGELOG.md#v100-2016-11-03) for details. 43 | 44 | ## Contributions 45 | 46 | Patches, bug fixes, feature requests, and pull requests are welcome on the 47 | GitHub page for this project: 48 | [https://github.com/codeclimate/ruby-test-reporter](https://github.com/codeclimate/ruby-test-reporter) 49 | 50 | When making a pull request, please update the [changelog](CHANGELOG.md). 51 | 52 | This gem is maintained by Code Climate (hello@codeclimate.com). 53 | 54 | ### Release Process 55 | 56 | * Update the changelog to mark the unreleased changes as part of the new release. 57 | * Update the version.rb with the new version number 58 | * Make a pull request with those changes 59 | * Merge those changes to master 60 | * Check out and pull down the latest master locally 61 | * `rake release` which will 62 | * tag the latest commit based on version.rb 63 | * push to github 64 | * push to rubygems 65 | 66 | ## Copyright 67 | 68 | See LICENSE.txt 69 | 70 | Portions of the implementation were inspired by the coveralls-ruby gem. 71 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/cc-tddium-post-worker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "codeclimate-test-reporter" 4 | require "tmpdir" 5 | 6 | if ENV["CODECLIMATE_REPO_TOKEN"] 7 | tmpdir = Dir.tmpdir 8 | puts "Searching #{tmpdir} for files to POST." 9 | coverage_report_files = Dir.glob("#{tmpdir}/codeclimate-test-coverage-*") 10 | if coverage_report_files.any? 11 | puts "Found: " 12 | puts coverage_report_files.join("\n") 13 | client = CodeClimate::TestReporter::Client.new 14 | print "Sending reports to #{client.host}..." 15 | client.batch_post_results(coverage_report_files) 16 | puts "done." 17 | else 18 | puts "No files found to POST." 19 | end 20 | else 21 | $stderr.puts "Cannot batch post - environment variable CODECLIMATE_REPO_TOKEN must be set." 22 | exit(1) 23 | end 24 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/bin/bash --login 2 | 3 | set -e 4 | 5 | rvm use 1.9.3 6 | ruby -v 7 | bundle install --gemfile Gemfile.ruby-19 8 | bundle exec rake 9 | 10 | rvm use 2.2.2 11 | ruby -v 12 | bundle install 13 | bundle exec rake 14 | 15 | CODECLIMATE_REPO_TOKEN=c4881e09870b0fac1291c93339b36ffe36210a2645c1ad25e52d8fda3943fb4d bundle exec codeclimate-test-reporter 16 | -------------------------------------------------------------------------------- /bin/codeclimate-test-reporter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "codeclimate-test-reporter" 4 | 5 | repo_token = ENV["CODECLIMATE_REPO_TOKEN"] 6 | if repo_token.nil? || repo_token.empty? 7 | STDERR.puts "Cannot post results: environment variable CODECLIMATE_REPO_TOKEN must be set." 8 | exit 9 | end 10 | 11 | COVERAGE_FILE = ARGV.first || "coverage/.resultset.json" 12 | 13 | abort "Coverage results not found" unless File.exist?(COVERAGE_FILE) 14 | 15 | begin 16 | results = JSON.parse(File.read(COVERAGE_FILE)) 17 | rescue JSON::ParserError => e 18 | abort "Error encountered while parsing #{COVERAGE_FILE}: #{e}" 19 | end 20 | 21 | CodeClimate::TestReporter.run(results) 22 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | pre: 3 | - git config --global user.email "ci@codeclimate.com" 4 | - git config --global user.name "Code Climate CI" 5 | override: 6 | - echo "skip" 7 | 8 | test: 9 | override: 10 | - bin/ci 11 | -------------------------------------------------------------------------------- /codeclimate-test-reporter.gemspec: -------------------------------------------------------------------------------- 1 | require "./lib/code_climate/test_reporter/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "codeclimate-test-reporter" 5 | spec.version = CodeClimate::TestReporter::VERSION 6 | spec.authors = ["Bryan Helmkamp", "Code Climate"] 7 | spec.email = ["bryan@brynary.com", "hello@codeclimate.com"] 8 | spec.description = "Collects test coverage data from your Ruby test suite and sends it to Code Climate's hosted, automated code review service. Based on SimpleCov." 9 | spec.summary = "Uploads Ruby test coverage data to Code Climate." 10 | spec.homepage = "https://github.com/codeclimate/ruby-test-reporter" 11 | spec.license = "MIT" 12 | 13 | spec.files = `git ls-files bin lib config LICENSE.txt README.md`.split($/) 14 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }.reject { |f| f == "ci" } 15 | 16 | spec.required_ruby_version = ">= 1.9" 17 | spec.add_runtime_dependency "simplecov", "<= 0.13" 18 | 19 | spec.add_development_dependency "bundler" 20 | spec.add_development_dependency "pry" 21 | spec.add_development_dependency "rake" 22 | spec.add_development_dependency "rspec" 23 | spec.add_development_dependency "webmock" 24 | 25 | spec.post_install_message = %q( 26 | Code Climate's codeclimate-test-reporter gem has been deprecated in favor of 27 | our language-agnostic unified test reporter. The new test reporter is faster, 28 | distributed as a static binary so dependency conflicts never occur, and 29 | supports parallelized CI builds & multi-language CI configurations. 30 | 31 | Please visit https://docs.codeclimate.com/v1.0/docs/configuring-test-coverage 32 | for help setting up your CI process with our new test reporter. 33 | ) 34 | end 35 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | WARNING_MESSAGE = <<-EOS.freeze 4 | This usage of the Code Climate Test Reporter is now deprecated. Since version 5 | 1.0, we now require you to run `SimpleCov` in your test/spec helper, and then 6 | run the provided `codeclimate-test-reporter` binary separately to report your 7 | results to Code Climate. 8 | 9 | More information here: https://github.com/codeclimate/ruby-test-reporter/blob/master/README.md 10 | EOS 11 | 12 | def self.start 13 | logger.warn(WARNING_MESSAGE) 14 | exit(1) 15 | end 16 | 17 | def self.run(results) 18 | return unless CodeClimate::TestReporter.run? 19 | formatted_results = CodeClimate::TestReporter::Formatter.new.format(results) 20 | CodeClimate::TestReporter::PostResults.new(formatted_results).post 21 | end 22 | 23 | def self.run? 24 | environment_variable_set? && run_on_current_branch? 25 | end 26 | 27 | def self.environment_variable_set? 28 | return @environment_variable_set if defined?(@environment_variable_set) 29 | 30 | @environment_variable_set = !!ENV["CODECLIMATE_REPO_TOKEN"] 31 | if @environment_variable_set 32 | logger.info("Reporting coverage data to Code Climate.") 33 | end 34 | 35 | @environment_variable_set 36 | end 37 | 38 | def self.run_on_current_branch? 39 | return @run_on_current_branch if defined?(@run_on_current_branch) 40 | 41 | @run_on_current_branch = true if configured_branch.nil? 42 | @run_on_current_branch ||= !!(current_branch =~ /#{configured_branch}/i) 43 | 44 | unless @run_on_current_branch 45 | logger.info("Not reporting to Code Climate because #{configured_branch} is set as the reporting branch.") 46 | end 47 | 48 | @run_on_current_branch 49 | end 50 | 51 | def self.configured_branch 52 | configuration.branch 53 | end 54 | 55 | def self.current_branch 56 | Git.branch_from_git_or_ci 57 | end 58 | 59 | def self.logger 60 | CodeClimate::TestReporter.configuration.logger 61 | end 62 | 63 | def self.tddium? 64 | ci_service_data && ci_service_data[:name] == "tddium" 65 | end 66 | 67 | def self.ci_service_data 68 | Ci.service_data 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/calculate_blob.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | class CalculateBlob 4 | def initialize(file_path) 5 | @file_path = file_path 6 | end 7 | 8 | def blob_id 9 | calculate_with_file or calculate_with_git 10 | end 11 | 12 | private 13 | 14 | def calculate_with_file 15 | File.open(@file_path, "rb") do |file| 16 | header = "blob #{file.size}\0" 17 | content = file.read 18 | store = header + content 19 | 20 | return Digest::SHA1.hexdigest(store) 21 | end 22 | rescue EncodingError 23 | puts "WARNING: Unable to read #{@file_path}\nUsing git for blob calculation" 24 | nil 25 | end 26 | 27 | def calculate_with_git 28 | output = `git hash-object -t blob #{@file_path}`.chomp 29 | raise "ERROR: Failed to calculate blob with git" unless $?.success? 30 | 31 | output 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/ci.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | class Ci 4 | def self.service_data(env = ENV) 5 | if env["TRAVIS"] 6 | { 7 | name: "travis-ci", 8 | branch: env["TRAVIS_BRANCH"], 9 | build_identifier: env["TRAVIS_JOB_ID"], 10 | pull_request: env["TRAVIS_PULL_REQUEST"], 11 | } 12 | elsif env["CIRCLECI"] 13 | { 14 | name: "circleci", 15 | build_identifier: env["CIRCLE_BUILD_NUM"], 16 | branch: env["CIRCLE_BRANCH"], 17 | commit_sha: env["CIRCLE_SHA1"], 18 | } 19 | elsif env["SEMAPHORE"] 20 | { 21 | name: "semaphore", 22 | branch: env["BRANCH_NAME"], 23 | build_identifier: env["SEMAPHORE_BUILD_NUMBER"], 24 | } 25 | elsif env["JENKINS_URL"] 26 | { 27 | name: "jenkins", 28 | build_identifier: env["BUILD_NUMBER"], 29 | build_url: env["BUILD_URL"], 30 | branch: env["GIT_BRANCH"], 31 | commit_sha: env["GIT_COMMIT"], 32 | } 33 | elsif env["TDDIUM"] 34 | { 35 | name: "tddium", 36 | build_identifier: env["TDDIUM_SESSION_ID"], 37 | worker_id: env["TDDIUM_TID"], 38 | } 39 | elsif env["WERCKER"] 40 | { 41 | name: "wercker", 42 | build_identifier: env["WERCKER_BUILD_ID"], 43 | build_url: env["WERCKER_BUILD_URL"], 44 | branch: env["WERCKER_GIT_BRANCH"], 45 | commit_sha: env["WERCKER_GIT_COMMIT"], 46 | } 47 | elsif env["APPVEYOR"] 48 | { 49 | name: "appveyor", 50 | build_identifier: env["APPVEYOR_BUILD_ID"], 51 | build_url: env["APPVEYOR_API_URL"], 52 | branch: env["APPVEYOR_REPO_BRANCH"], 53 | commit_sha: env["APPVEYOR_REPO_COMMIT"], 54 | pull_request: env["APPVEYOR_PULL_REQUEST_NUMBER"], 55 | } 56 | elsif env["CI_NAME"] =~ /DRONE/i 57 | { 58 | name: "drone", 59 | build_identifier: env["CI_BUILD_NUMBER"], 60 | build_url: env["CI_BUILD_URL"], 61 | branch: env["CI_BRANCH"], 62 | commit_sha: env["CI_COMMIT"], 63 | pull_request: env["CI_PULL_REQUEST"], 64 | } 65 | elsif env["CI_NAME"] =~ /codeship/i 66 | { 67 | name: "codeship", 68 | build_identifier: env["CI_BUILD_ID"], 69 | # build URL cannot be reconstructed for Codeship since env does not contain project ID 70 | build_url: env["CI_BUILD_URL"], 71 | branch: env["CI_BRANCH"], 72 | commit_sha: env["CI_COMMIT_ID"], 73 | # CI timestamp is not quite equivalent to commited at but there's no equivalent in Codeship 74 | committed_at: env["CI_TIMESTAMP"], 75 | } 76 | elsif env["CI_NAME"] =~ /VEXOR/i 77 | { 78 | name: "vexor", 79 | build_identifier: env["CI_BUILD_NUMBER"], 80 | build_url: env["CI_BUILD_URL"], 81 | branch: env["CI_BRANCH"], 82 | commit_sha: env["CI_BUILD_SHA"], 83 | pull_request: env["CI_PULL_REQUEST_ID"], 84 | } 85 | elsif env["BUILDKITE"] 86 | { 87 | name: "buildkite", 88 | build_identifier: env["BUILDKITE_JOB_ID"], 89 | build_url: env["BUILDKITE_BUILD_URL"], 90 | branch: env["BUILDKITE_BRANCH"], 91 | commit_sha: env["BUILDKITE_COMMIT"], 92 | } 93 | elsif env["GITLAB_CI"] 94 | { 95 | name: "gitlab-ci", 96 | build_identifier: env["CI_BUILD_ID"], 97 | branch: env["CI_BUILD_REF_NAME"], 98 | commit_sha: env["CI_BUILD_REF"], 99 | } 100 | else 101 | {} 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/client.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "uri" 3 | require "net/https" 4 | 5 | module CodeClimate 6 | module TestReporter 7 | class Client 8 | DEFAULT_TIMEOUT = 5 # in seconds 9 | USER_AGENT = "Code Climate (Ruby Test Reporter v#{CodeClimate::TestReporter::VERSION})".freeze 10 | 11 | def host 12 | ENV["CODECLIMATE_API_HOST"] || 13 | "https://codeclimate.com" 14 | end 15 | 16 | # N.B. Not a generalized solution for posting multiple results 17 | # N.B. Only works with in tandem with additional communication from 18 | # Solano. 19 | def batch_post_results(files) 20 | uri = URI.parse("#{host}/test_reports/batch") 21 | http = http_client(uri) 22 | 23 | boundary = SecureRandom.uuid 24 | post_body = [] 25 | post_body << "--#{boundary}\r\n" 26 | post_body << "Content-Disposition: form-data; name=\"repo_token\"\r\n" 27 | post_body << "\r\n" 28 | post_body << ENV["CODECLIMATE_REPO_TOKEN"] 29 | files.each_with_index do |file, index| 30 | post_body << "\r\n--#{boundary}\r\n" 31 | post_body << "Content-Disposition: form-data; name=\"coverage_reports[#{index}]\"; filename=\"#{File.basename(file)}\"\r\n" 32 | post_body << "Content-Type: application/json\r\n" 33 | post_body << "\r\n" 34 | post_body << File.read(file) 35 | end 36 | post_body << "\r\n--#{boundary}--\r\n" 37 | request = Net::HTTP::Post.new(uri.request_uri) 38 | request["User-Agent"] = USER_AGENT 39 | request.body = post_body.join 40 | request["Content-Type"] = "multipart/form-data, boundary=#{boundary}" 41 | response = http.request(request) 42 | 43 | if response.code.to_i >= 200 && response.code.to_i < 300 44 | response 45 | else 46 | raise "HTTP Error: #{response.code}" 47 | end 48 | end 49 | 50 | def post_results(result) 51 | uri = URI.parse("#{host}/test_reports") 52 | http = http_client(uri) 53 | 54 | request = Net::HTTP::Post.new(uri.path) 55 | request["User-Agent"] = USER_AGENT 56 | request["Content-Type"] = "application/json" 57 | 58 | if CodeClimate::TestReporter.configuration.gzip_request 59 | request["Content-Encoding"] = "gzip" 60 | request.body = compress(result.to_json) 61 | else 62 | request.body = result.to_json 63 | end 64 | 65 | response = http.request(request) 66 | 67 | if response.code.to_i >= 200 && response.code.to_i < 300 68 | response 69 | else 70 | raise "HTTP Error: #{response.code}" 71 | end 72 | end 73 | 74 | private 75 | 76 | def http_client(uri) 77 | Net::HTTP.new(uri.host, uri.port).tap do |http| 78 | if uri.scheme == "https" 79 | http.use_ssl = true 80 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 81 | http.ca_file = ca_file 82 | http.verify_depth = 5 83 | end 84 | http.open_timeout = CodeClimate::TestReporter.configuration.timeout 85 | http.read_timeout = CodeClimate::TestReporter.configuration.timeout 86 | end 87 | end 88 | 89 | def compress(str) 90 | sio = StringIO.new("w") 91 | gz = Zlib::GzipWriter.new(sio) 92 | gz.write(str) 93 | gz.close 94 | sio.string 95 | end 96 | 97 | def ca_file 98 | ENV["SSL_CERT_FILE"] || 99 | File.expand_path("../../../../config/cacert.pem", __FILE__) 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/configuration.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | 3 | module CodeClimate 4 | module TestReporter 5 | @@configuration = nil 6 | 7 | def self.configure 8 | @@configuration = Configuration.new 9 | 10 | if block_given? 11 | yield configuration 12 | end 13 | 14 | configuration 15 | end 16 | 17 | def self.configuration 18 | @@configuration || configure 19 | end 20 | 21 | class Configuration 22 | attr_accessor :branch, :path_prefix, :gzip_request, :git_dir 23 | 24 | attr_writer :logger, :profile, :timeout 25 | 26 | def initialize 27 | @gzip_request = true 28 | end 29 | 30 | def logger 31 | @logger ||= default_logger 32 | end 33 | 34 | def profile 35 | @profile ||= "test_frameworks" 36 | end 37 | 38 | def skip_token 39 | @skip_token ||= "nocov" 40 | end 41 | 42 | def timeout 43 | @timeout ||= Client::DEFAULT_TIMEOUT 44 | end 45 | 46 | private 47 | 48 | def default_logger 49 | log = Logger.new($stderr) 50 | log.level = Logger::INFO 51 | 52 | log 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/exception_message.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | class WebMockMessage 4 | def library_name 5 | "WebMock" 6 | end 7 | 8 | def instructions 9 | <<-STR 10 | WebMock.disable_net_connect!(:allow => "codeclimate.com") 11 | STR 12 | end 13 | end 14 | 15 | class VCRMessage 16 | def library_name 17 | "VCR" 18 | end 19 | 20 | def instructions 21 | <<-STR 22 | VCR.configure do |config| 23 | # your existing configuration 24 | config.ignore_hosts 'codeclimate.com' 25 | end 26 | STR 27 | end 28 | end 29 | 30 | class ExceptionMessage 31 | HTTP_STUBBING_MESSAGES = { 32 | "VCR::Errors::UnhandledHTTPRequestError".freeze => VCRMessage, 33 | "WebMock::NetConnectNotAllowedError".freeze => WebMockMessage, 34 | }.freeze 35 | 36 | def initialize(exception) 37 | @exception = exception 38 | end 39 | 40 | def message 41 | parts = [] 42 | parts << "Code Climate encountered an exception: #{exception_class}" 43 | if http_stubbing_exception 44 | message = http_stubbing_exception.new 45 | parts << "======" 46 | parts << "Hey! Looks like you are using #{message.library_name}, which will prevent the codeclimate-test-reporter from reporting results to codeclimate.com. 47 | Add the following to your spec or test helper to ensure codeclimate-test-reporter can post coverage results:" 48 | parts << "\n" + message.instructions + "\n" 49 | parts << "======" 50 | parts << "If this doesn't work, please consult https://codeclimate.com/docs#test-coverage-troubleshooting" 51 | parts << "======" 52 | else 53 | parts << @exception.message 54 | @exception.backtrace.each do |line| 55 | parts << line 56 | end 57 | end 58 | parts.join("\n") 59 | end 60 | 61 | private 62 | 63 | def exception_class 64 | @exception.class.to_s 65 | end 66 | 67 | def http_stubbing_exception 68 | HTTP_STUBBING_MESSAGES[exception_class] 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/formatter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "tmpdir" 4 | require "securerandom" 5 | require "json" 6 | require "digest/sha1" 7 | require "simplecov" 8 | 9 | require "code_climate/test_reporter/exception_message" 10 | require "code_climate/test_reporter/payload_validator" 11 | 12 | module CodeClimate 13 | module TestReporter 14 | class Formatter 15 | def format(results) 16 | simplecov_results = results.map do |command_name, data| 17 | SimpleCov::Result.from_hash(command_name => data) 18 | end 19 | 20 | simplecov_result = 21 | if simplecov_results.size == 1 22 | simplecov_results.first 23 | else 24 | merge_results(simplecov_results) 25 | end 26 | 27 | payload = to_payload(simplecov_result) 28 | PayloadValidator.validate(payload) 29 | 30 | payload 31 | end 32 | 33 | private 34 | 35 | def partial? 36 | CodeClimate::TestReporter.tddium? 37 | end 38 | 39 | def to_payload(result) 40 | totals = Hash.new(0) 41 | source_files = result.files.map do |file| 42 | totals[:total] += file.lines.count 43 | totals[:covered] += file.covered_lines.count 44 | totals[:missed] += file.missed_lines.count 45 | 46 | # Set coverage for all skipped lines to nil 47 | file.skipped_lines.each do |skipped_line| 48 | file.coverage[skipped_line.line_number - 1] = nil 49 | end 50 | 51 | { 52 | name: ShortenFilename.new(file.filename).short_filename, 53 | blob_id: CalculateBlob.new(file.filename).blob_id, 54 | coverage: file.coverage.to_json, 55 | covered_percent: round(file.covered_percent, 2), 56 | covered_strength: round(file.covered_strength, 2), 57 | line_counts: { 58 | total: file.lines.count, 59 | covered: file.covered_lines.count, 60 | missed: file.missed_lines.count, 61 | }, 62 | } 63 | end 64 | 65 | { 66 | repo_token: ENV["CODECLIMATE_REPO_TOKEN"], 67 | source_files: source_files, 68 | run_at: result.created_at.to_i, 69 | covered_percent: result.source_files.covered_percent.round(2), 70 | covered_strength: result.source_files.covered_strength.round(2), 71 | line_counts: totals, 72 | partial: partial?, 73 | git: Git.info, 74 | environment: { 75 | pwd: Dir.pwd, 76 | rails_root: (Rails.root.to_s rescue nil), 77 | simplecov_root: ::SimpleCov.root, 78 | gem_version: VERSION, 79 | }, 80 | ci_service: CodeClimate::TestReporter.ci_service_data, 81 | } 82 | end 83 | 84 | # Convert to Float before rounding. 85 | # Fixes [#7] possible segmentation fault when calling #round on a Rational 86 | def round(numeric, precision) 87 | Float(numeric).round(precision) 88 | end 89 | 90 | # Re-implementation of Simplecov::ResultMerger#merged_result, which is 91 | # needed because calling it directly gets you into caching land with files 92 | # on disk. 93 | def merge_results(results) 94 | merged = {} 95 | results.each do |result| 96 | merged = result.original_result.merge_resultset(merged) 97 | end 98 | result = SimpleCov::Result.new(merged) 99 | result.command_name = results.map(&:command_name).sort.join(", ") 100 | result 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/git.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | class Git 4 | class << self 5 | def info 6 | { 7 | head: head_from_git_or_ci, 8 | committed_at: committed_at_from_git_or_ci, 9 | branch: branch_from_git_or_ci, 10 | } 11 | end 12 | 13 | def head_from_git_or_ci 14 | head_from_git || head_from_ci 15 | end 16 | 17 | def branch_from_git_or_ci 18 | clean_service_branch || clean_git_branch || "master" 19 | end 20 | 21 | def committed_at_from_git_or_ci 22 | committed_at_from_git || committed_at_from_ci 23 | end 24 | 25 | def clean_service_branch 26 | ci_branch = String(Ci.service_data[:branch]) 27 | clean = ci_branch.strip.sub(%r{^origin/}, "") 28 | 29 | !clean.empty? ? clean : nil 30 | end 31 | 32 | def clean_git_branch 33 | git_branch = String(branch_from_git) 34 | clean = git_branch.sub(%r{^origin/}, "") unless git_branch.start_with?("(") 35 | 36 | !clean.empty? ? clean : nil 37 | end 38 | 39 | private 40 | 41 | def head_from_git 42 | commit_hash = git("log -1 --pretty=format:'%H'") 43 | !commit_hash.empty? ? commit_hash : nil 44 | end 45 | 46 | def head_from_ci 47 | Ci.service_data[:commit_sha] 48 | end 49 | 50 | def committed_at_from_ci 51 | if (value = Ci.service_data[:committed_at]) 52 | value.to_i 53 | end 54 | end 55 | 56 | def committed_at_from_git 57 | committed_at = git("log -1 --pretty=format:%ct") 58 | committed_at.to_i.zero? ? nil : committed_at.to_i 59 | end 60 | 61 | def branch_from_git 62 | git("rev-parse --abbrev-ref HEAD").chomp 63 | end 64 | 65 | def git(command) 66 | `git --git-dir="#{git_dir}/.git" #{command}` 67 | end 68 | 69 | def git_dir 70 | return configured_git_dir unless configured_git_dir.nil? 71 | rails_git_dir_present? ? Rails.root : "." 72 | end 73 | 74 | def configured_git_dir 75 | CodeClimate::TestReporter.configuration.git_dir 76 | end 77 | 78 | def rails_git_dir_present? 79 | const_defined?(:Rails) && Rails.respond_to?(:root) && !Rails.root.nil? && 80 | File.directory?(File.expand_path(".git", Rails.root)) 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/payload_validator.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | InvalidPayload = Class.new(StandardError) 4 | 5 | class PayloadValidator 6 | def initialize(payload) 7 | @payload = payload 8 | end 9 | 10 | def self.validate(payload) 11 | new(payload).validate 12 | end 13 | 14 | def validate 15 | raise InvalidPayload, "A git commit sha was not found in the test report payload" unless commit_sha 16 | raise InvalidPayload, "A git commit timestamp was not found in the test report payload" unless committed_at 17 | raise InvalidPayload, "A run at timestamp was not found in the test report payload" unless run_at 18 | raise InvalidPayload, "No source files were found in the test report payload" unless source_files? 19 | raise InvalidPayload, "Invalid source files were found in the test report payload" unless valid_source_files? 20 | true 21 | end 22 | 23 | private 24 | 25 | def commit_sha 26 | commit_sha_from_git || commit_sha_from_ci_service 27 | end 28 | 29 | def committed_at 30 | (@payload[:git] && @payload[:git][:committed_at]) || 31 | (@payload[:ci_service] && @payload[:ci_service][:committed_at]) 32 | end 33 | 34 | def run_at 35 | @payload[:run_at] 36 | end 37 | 38 | def source_files? 39 | @payload[:source_files] && @payload[:source_files].any? 40 | end 41 | 42 | def valid_source_files? 43 | @payload[:source_files].all? { |s| valid_source_file?(s) } 44 | end 45 | 46 | def valid_source_file?(file) 47 | file.is_a?(Hash) && file[:coverage] && file[:name] 48 | end 49 | 50 | def commit_sha_from_git 51 | @payload[:git] && @payload[:git][:head] 52 | end 53 | 54 | def commit_sha_from_ci_service 55 | @payload[:ci_service] && @payload[:ci_service][:commit_sha] 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/post_results.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | class PostResults 4 | def initialize(results) 5 | @results = results 6 | end 7 | 8 | def post 9 | if write_to_file? 10 | file_path = File.join(Dir.tmpdir, "codeclimate-test-coverage-#{SecureRandom.uuid}.json") 11 | print "Coverage results saved to #{file_path}... " 12 | File.open(file_path, "w") { |file| file.write(@results.to_json) } 13 | else 14 | client = Client.new 15 | print "Sending report to #{client.host} for branch #{Git.branch_from_git_or_ci}... " 16 | client.post_results(@results) 17 | end 18 | 19 | puts "done." 20 | end 21 | 22 | private 23 | 24 | def write_to_file? 25 | warn "TO_FILE is deprecated, use CODECLIMATE_TO_FILE" if ENV["TO_FILE"] 26 | CodeClimate::TestReporter.tddium? || ENV["CODECLIMATE_TO_FILE"] || ENV["TO_FILE"] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/shorten_filename.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | class ShortenFilename 4 | def initialize(filename) 5 | @filename = filename 6 | end 7 | 8 | def short_filename 9 | return @filename unless ::SimpleCov.root 10 | apply_prefix @filename.gsub(/^#{Regexp.escape(::SimpleCov.root)}/, ".").gsub(%r{^\./}, "") 11 | end 12 | 13 | private 14 | 15 | def apply_prefix(filename) 16 | if (prefix = CodeClimate::TestReporter.configuration.path_prefix) 17 | File.join(prefix, filename) 18 | else 19 | filename 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/code_climate/test_reporter/version.rb: -------------------------------------------------------------------------------- 1 | module CodeClimate 2 | module TestReporter 3 | VERSION = "1.0.9".freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/codeclimate-test-reporter.rb: -------------------------------------------------------------------------------- 1 | require "code_climate/test_reporter" 2 | require "code_climate/test_reporter/calculate_blob" 3 | require "code_climate/test_reporter/version" 4 | require "code_climate/test_reporter/client" 5 | require "code_climate/test_reporter/post_results" 6 | require "code_climate/test_reporter/shorten_filename" 7 | require "code_climate/test_reporter/formatter" 8 | require "code_climate/test_reporter/configuration" 9 | require "code_climate/test_reporter/git" 10 | require "code_climate/test_reporter/ci" 11 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/calculate_blob_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module CodeClimate::TestReporter 4 | 5 | describe CalculateBlob do 6 | 7 | subject { CalculateBlob.new(fixture) } 8 | let(:fixture) { File.expand_path("../../../fixtures/encoding_test.rb", __FILE__) } 9 | 10 | it 'hex digests content of file' do 11 | expect(subject.blob_id).to_not be_nil 12 | end 13 | 14 | context 'encoding error' do 15 | 16 | let(:fixture) { File.expand_path("../../../fixtures/encoding_test_iso.rb", __FILE__) } 17 | 18 | it 'falls back to git' do 19 | capture_io do 20 | expect(File).to receive(:open).and_raise(EncodingError) 21 | expect(subject.blob_id).to eq('eb82c22dadb9c47a7fed87211623f6856e112f46') 22 | end 23 | end 24 | 25 | end 26 | 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/ci_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module CodeClimate::TestReporter 4 | describe Ci do 5 | describe '.service_data' do 6 | before :each do 7 | @env = { 8 | 'SEMAPHORE' => 'yes?', 9 | 'BRANCH_NAME' => 'master', 10 | 'SEMAPHORE_BUILD_NUMBER' => '1234' 11 | } 12 | end 13 | 14 | it 'returns a hash of CI environment info' do 15 | expected_semaphore_hash = { 16 | name: 'semaphore', 17 | branch: 'master', 18 | build_identifier: '1234' 19 | } 20 | 21 | expect(Ci.service_data(@env)).to include expected_semaphore_hash 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module CodeClimate::TestReporter 4 | describe Client do 5 | it 'sets the http timeout per configuration' do 6 | new_timeout = 969 7 | CodeClimate::TestReporter.configure do |config| 8 | config.timeout = new_timeout 9 | end 10 | 11 | response = double(:response, code: 200) 12 | net_http = double(:net_http, request: response) 13 | allow(Net::HTTP).to receive(:new). 14 | and_return(net_http) 15 | 16 | expect(net_http).to receive(:open_timeout=). 17 | with(new_timeout) 18 | expect(net_http).to receive(:read_timeout=). 19 | with(new_timeout) 20 | 21 | Client.new.post_results("") 22 | end 23 | 24 | describe "#batch_post_results" do 25 | let(:uuid) { "my-uuid" } 26 | let(:token) { ENV["CODECLIMATE_REPO_TOKEN"] } 27 | 28 | before { expect(SecureRandom).to receive(:uuid).and_return uuid } 29 | around { |test| Dir.mktmpdir { |dir| Dir.chdir(dir, &test) } } 30 | 31 | it "posts a single file" do 32 | File.write("a", "Something") 33 | requests = capture_requests(stub_request(:post, "http://cc.dev/test_reports/batch")) 34 | Client.new.batch_post_results(["a"]) 35 | 36 | expect(requests.first.body).to eq "--#{uuid}\r\nContent-Disposition: form-data; name=\"repo_token\"\r\n\r\n#{token}\r\n--#{uuid}\r\nContent-Disposition: form-data; name=\"coverage_reports[0]\"; filename=\"a\"\r\nContent-Type: application/json\r\n\r\nSomething\r\n--#{uuid}--\r\n" 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'logger' 3 | 4 | module CodeClimate::TestReporter 5 | describe Configuration do 6 | describe 'none given' do 7 | before do 8 | CodeClimate::TestReporter.configure 9 | end 10 | 11 | it 'provides defaults' do 12 | expect(CodeClimate::TestReporter.configuration.branch).to be_nil 13 | expect(CodeClimate::TestReporter.configuration.logger).to be_instance_of Logger 14 | expect(CodeClimate::TestReporter.configuration.logger.level).to eq Logger::INFO 15 | expect(CodeClimate::TestReporter.configuration.profile).to eq('test_frameworks') 16 | expect(CodeClimate::TestReporter.configuration.path_prefix).to be_nil 17 | expect(CodeClimate::TestReporter.configuration.skip_token).to eq('nocov') 18 | expect(CodeClimate::TestReporter.configuration.timeout).to eq(Client::DEFAULT_TIMEOUT) 19 | end 20 | end 21 | 22 | describe 'with config block' do 23 | after do 24 | CodeClimate::TestReporter.configure 25 | end 26 | 27 | it 'stores logger' do 28 | logger = Logger.new($stderr) 29 | 30 | CodeClimate::TestReporter.configure do |config| 31 | logger.level = Logger::DEBUG 32 | config.logger = logger 33 | end 34 | 35 | expect(CodeClimate::TestReporter.configuration.logger).to eq logger 36 | end 37 | 38 | it 'stores branch' do 39 | CodeClimate::TestReporter.configure do |config| 40 | config.branch = :master 41 | end 42 | 43 | expect(CodeClimate::TestReporter.configuration.branch).to eq :master 44 | end 45 | 46 | it 'stores profile' do 47 | CodeClimate::TestReporter.configure do |config| 48 | config.profile = 'custom' 49 | end 50 | 51 | expect(CodeClimate::TestReporter.configuration.profile).to eq('custom') 52 | end 53 | 54 | it 'stores path prefix' do 55 | CodeClimate::TestReporter.configure do |config| 56 | config.path_prefix = 'custom' 57 | end 58 | 59 | expect(CodeClimate::TestReporter.configuration.path_prefix).to eq('custom') 60 | 61 | CodeClimate::TestReporter.configure do |config| 62 | config.path_prefix = nil 63 | end 64 | end 65 | 66 | it 'stores timeout' do 67 | CodeClimate::TestReporter.configure do |config| 68 | config.timeout = 666 69 | end 70 | 71 | expect(CodeClimate::TestReporter.configuration.timeout).to eq(666) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | 4 | module CodeClimate::TestReporter 5 | describe Formatter do 6 | it "converts simplecov format to code climate http payload format" do 7 | expect(Git).to receive(:branch_from_git_or_ci).and_return("master") 8 | formatter = Formatter.new 9 | formatted_request = within_repository("fake_project") do 10 | formatter.format( 11 | "RSpec" => { 12 | "coverage" => { 13 | "#{SimpleCov.root}/spec/fixtures/fake_project/fake_project.rb" => [5,3,nil,0] 14 | }, 15 | "timestamp" => Time.now.to_i, 16 | } 17 | ) 18 | end 19 | 20 | expect(formatted_request).to eq( 21 | ci_service: CodeClimate::TestReporter.ci_service_data, 22 | covered_percent: 66.67, 23 | covered_strength: 2.7, 24 | environment: { 25 | gem_version: VERSION, 26 | pwd: "#{Dir.pwd}/spec/fixtures/fake_project", 27 | rails_root: nil, 28 | simplecov_root: SimpleCov.root, 29 | }, 30 | git: { 31 | branch: "master", 32 | committed_at: 1474318896, 33 | head: "7a36651c654c73e7e9a6dfc9f9fa78c5fe37241e", 34 | }, 35 | line_counts: { total: 4, covered: 2, missed: 1 }, 36 | partial: false, 37 | repo_token: "172754c1bf9a3c698f7770b9fb648f1ebb214425120022d0b2ffc65b97dff531", 38 | run_at: Time.now.to_i, 39 | source_files: [ 40 | { 41 | blob_id: "84275f9939456e87efd6932bdf7fe01d52a53116", 42 | coverage: "[5,3,null,0]", 43 | covered_percent: 66.67, 44 | covered_strength: 2.7, 45 | line_counts: { total: 4, covered: 2, missed: 1 }, 46 | name: "spec/fixtures/fake_project/fake_project.rb", 47 | } 48 | ], 49 | ) 50 | end 51 | 52 | it "addresses Issue #7" do 53 | simplecov_result = load_resultset("issue_7", %r{^.*/i18n-tasks/}) 54 | formatter = Formatter.new 55 | formatted_request = within_repository("issue_7") do 56 | formatter.format(simplecov_result) 57 | end 58 | 59 | expect(formatted_request[:covered_percent]).to be_within(1.0).of(94) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/git_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module CodeClimate::TestReporter 4 | describe Git do 5 | describe '.info' do 6 | it 'returns a hash with git information.' do 7 | expected_git_hash = { 8 | head: `git log -1 --pretty=format:'%H'`, 9 | committed_at: `git log -1 --pretty=format:%ct`.to_i, 10 | branch: Git.send(:branch_from_git) 11 | } 12 | 13 | expect(Git.info).to include expected_git_hash 14 | end 15 | end 16 | 17 | describe 'git' do 18 | it 'should quote the git repository directory' do 19 | path = '/path/to/foo bar' 20 | 21 | allow(CodeClimate::TestReporter.configuration).to receive(:git_dir).and_return path 22 | expect(Git).to receive(:`).once.with "git --git-dir=\"#{path}/.git\" help" 23 | 24 | Git.send :git, 'help' 25 | end 26 | 27 | context 'ensure logic that replies on Rails is robust in non-rails environments' do 28 | before :all do 29 | module ::Rails; end 30 | end 31 | 32 | after :all do 33 | Object.send(:remove_const, :Rails) 34 | end 35 | 36 | after :each do 37 | Git.send :git, 'help' 38 | end 39 | 40 | it 'will check if constant Rails is defined' do 41 | expect(Git).to receive(:configured_git_dir).once.and_return(nil) 42 | end 43 | 44 | it 'will not call method "root" (a 3rd time) if constant Rails is defined but does not respond to root' do 45 | expect(Git).to receive(:configured_git_dir).once.and_return(nil) 46 | expect(Rails).to receive(:root).twice.and_return('/path') 47 | end 48 | 49 | it 'will call rails root if constant Rails is defined and root method is defined' do 50 | module ::Rails 51 | def self.root 52 | '/path' 53 | end 54 | end 55 | expect(Git).to receive(:configured_git_dir).once.and_return(nil) 56 | expect(Rails).to receive(:root).twice.and_return('/path') 57 | end 58 | end 59 | end 60 | 61 | describe 'branch_from_git_or_ci' do 62 | it 'returns the branch from ci' do 63 | allow(Ci).to receive(:service_data).and_return({branch: 'ci-branch'}) 64 | 65 | expect(Git.branch_from_git_or_ci).to eq 'ci-branch' 66 | end 67 | 68 | it 'returns the branch from git if there is no ci branch' do 69 | allow(Ci).to receive(:service_data).and_return({}) 70 | 71 | expect(Git.branch_from_git_or_ci).to eq Git.clean_git_branch 72 | end 73 | 74 | it 'returns master otherwise' do 75 | allow(Ci).to receive(:service_data).and_return({}) 76 | allow(Git).to receive(:branch_from_git).and_return(nil) 77 | 78 | expect(Git.branch_from_git_or_ci).to eq 'master' 79 | end 80 | end 81 | 82 | describe 'head_from_git_or_ci' do 83 | it 'returns the head sha from git' do 84 | expect(Git).to receive(:git).with("log -1 --pretty=format:'%H'").and_return("1234") 85 | 86 | expect(Git.head_from_git_or_ci).to eq '1234' 87 | end 88 | 89 | it 'returns the head sha from ci if git is not available' do 90 | expect(Git).to receive(:git).with("log -1 --pretty=format:'%H'").and_return("") 91 | expect(Ci).to receive(:service_data).and_return({commit_sha: "4567"}) 92 | 93 | expect(Git.head_from_git_or_ci).to eq '4567' 94 | end 95 | end 96 | 97 | describe 'committed_at_from_git_or_ci' do 98 | it 'returns the committed_at from git' do 99 | expect(Git.committed_at_from_git_or_ci).to eq Git.send(:committed_at_from_git) 100 | end 101 | 102 | it 'returns the committed_at from ci if there is no git committed_at' do 103 | expect(Git).to receive(:committed_at_from_git).and_return(nil) 104 | allow(Ci).to receive(:service_data).and_return({committed_at: '1484768698'}) 105 | 106 | expect(Git.committed_at_from_git_or_ci).to eq 1484768698 107 | end 108 | 109 | it 'returns nil when there is neither' do 110 | expect(Git).to receive(:committed_at_from_git).and_return(nil) 111 | allow(Ci).to receive(:service_data).and_return({}) 112 | 113 | expect(Git.committed_at_from_git_or_ci).to be_nil 114 | end 115 | end 116 | 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/payload_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module CodeClimate::TestReporter 4 | describe PayloadValidator do 5 | let(:payload) { 6 | { 7 | git: { 8 | committed_at: 1389603672, 9 | head: "4b968f076d169c3d98089fba27988f0d52ba803d" 10 | }, 11 | run_at: 1379704336, 12 | source_files: [ 13 | { coverage: "[0,3,4]", name: "user.rb" } 14 | ] 15 | } 16 | } 17 | 18 | it "does not raise if there's a minimally valid test report payload" do 19 | expect { 20 | PayloadValidator.validate(payload) 21 | }.to_not raise_error 22 | end 23 | 24 | it "raises when there's no commit sha" do 25 | payload[:git][:head] = nil 26 | expect { 27 | PayloadValidator.validate(payload) 28 | }.to raise_error(InvalidPayload, /A git commit sha was not found/) 29 | end 30 | 31 | it "does not raise if there's a commit sha in ci_service data" do 32 | payload[:git][:head] = nil 33 | payload[:ci_service] = {} 34 | payload[:ci_service][:commit_sha] = "4b968f076d169c3d98089fba27988f0d52ba803d" 35 | expect { 36 | PayloadValidator.validate(payload) 37 | }.to_not raise_error 38 | end 39 | 40 | it "does not raise if there's a committed_at in ci_service data" do 41 | payload[:git][:committed_at] = nil 42 | payload[:ci_service] = {} 43 | payload[:ci_service][:committed_at] = Time.now.to_i.to_s 44 | expect { 45 | PayloadValidator.validate(payload) 46 | }.to_not raise_error 47 | end 48 | 49 | it "raises when there is no committed_at" do 50 | payload[:git][:committed_at] = nil 51 | expect { 52 | PayloadValidator.validate(payload) 53 | }.to raise_error(InvalidPayload, /A git commit timestamp was not found/) 54 | end 55 | 56 | it "raises when there's no run_at" do 57 | payload[:run_at] = nil 58 | expect { 59 | PayloadValidator.validate(payload) 60 | }.to raise_error(InvalidPayload, /A run at timestamp was not found/) 61 | end 62 | 63 | it "raises when no source_files parameter is passed" do 64 | payload[:source_files] = nil 65 | expect { 66 | PayloadValidator.validate(payload) 67 | }.to raise_error(InvalidPayload, /No source files were found/) 68 | end 69 | 70 | it "raises when there's no source files" do 71 | payload[:source_files] = [] 72 | expect { 73 | PayloadValidator.validate(payload) 74 | }.to raise_error(InvalidPayload, /No source files were found/) 75 | end 76 | 77 | it "raises if source files aren't hashes" do 78 | payload[:source_files] = [1,2,3] 79 | expect { 80 | PayloadValidator.validate(payload) 81 | }.to raise_error(InvalidPayload, /Invalid source files/) 82 | end 83 | 84 | it "raises if source files don't have names" do 85 | payload[:source_files] = [{ coverage: "[1,1]" }] 86 | expect { 87 | PayloadValidator.validate(payload) 88 | }.to raise_error(InvalidPayload, /Invalid source files/) 89 | end 90 | 91 | it "raises if source files don't have coverage" do 92 | payload[:source_files] = [{ name: "foo.rb" }] 93 | expect { 94 | PayloadValidator.validate(payload) 95 | }.to raise_error(InvalidPayload, /Invalid source files/) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter/shorten_filename_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | 4 | module CodeClimate::TestReporter 5 | describe ShortenFilename do 6 | let(:shorten_filename){ ShortenFilename.new('file1') } 7 | let(:shorten_filename_with_simplecov_root) { ShortenFilename.new("#{::SimpleCov.root}/file1") } 8 | let(:shorten_filename_with_double_simplecov_root) { ShortenFilename.new("#{::SimpleCov.root}/#{::SimpleCov.root}/file1") } 9 | let(:root) { "/Users/oink/my-great-project" } 10 | 11 | before do 12 | allow(::SimpleCov).to receive(:root).and_return(root) 13 | end 14 | 15 | describe '#short_filename' do 16 | it 'should return the filename of the file relative to the SimpleCov root' do 17 | expect(shorten_filename.short_filename).to eq('file1') 18 | expect(shorten_filename_with_simplecov_root.short_filename).to eq('file1') 19 | end 20 | 21 | context "when the root has parentheses in it" do 22 | let(:root) { "/Users/oink/my-great-project/hello world (ok)" } 23 | 24 | it 'should return the filename of the file relative to the SimpleCov root' do 25 | expect(shorten_filename.short_filename).to eq('file1') 26 | expect(shorten_filename_with_simplecov_root.short_filename).to eq('file1') 27 | end 28 | end 29 | 30 | context "with path prefix" do 31 | before do 32 | CodeClimate::TestReporter.configure do |config| 33 | config.path_prefix = 'custom' 34 | end 35 | end 36 | 37 | after do 38 | CodeClimate::TestReporter.configure do |config| 39 | config.path_prefix = nil 40 | end 41 | end 42 | 43 | it 'should include the path prefix if set' do 44 | expect(shorten_filename.short_filename).to eq('custom/file1') 45 | expect(shorten_filename_with_simplecov_root.short_filename).to eq('custom/file1') 46 | end 47 | end 48 | 49 | it "should not strip the subdirectory if it has the same name as the root" do 50 | expect(shorten_filename_with_double_simplecov_root.short_filename).to eq("#{::SimpleCov.root}/file1") 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/code_climate/test_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CodeClimate::TestReporter do 4 | let(:logger) { double.as_null_object } 5 | let(:reporter) { CodeClimate::TestReporter.dup } 6 | 7 | before do 8 | allow(CodeClimate::TestReporter.configuration).to receive(:logger).and_return(logger) 9 | end 10 | 11 | describe '.run_on_current_branch?' do 12 | it 'returns true if there is no branch configured' do 13 | allow(reporter).to receive(:configured_branch).and_return(nil) 14 | expect(reporter).to be_run_on_current_branch 15 | end 16 | 17 | it 'returns true if the current branch matches the configured branch' do 18 | allow(reporter).to receive(:current_branch).and_return("master\n") 19 | allow(reporter).to receive(:configured_branch).and_return(:master) 20 | 21 | expect(reporter).to be_run_on_current_branch 22 | end 23 | 24 | it 'returns false if the current branch and configured branch dont match' do 25 | allow(reporter).to receive(:current_branch).and_return("some-branch") 26 | allow(reporter).to receive(:configured_branch).and_return(:master) 27 | 28 | expect(reporter).to_not be_run_on_current_branch 29 | end 30 | 31 | it 'logs a message if false' do 32 | expect(logger).to receive(:info) 33 | 34 | allow(reporter).to receive(:current_branch).and_return("another-branch") 35 | allow(reporter).to receive(:configured_branch).and_return(:master) 36 | 37 | reporter.run_on_current_branch? 38 | end 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/fixtures/encoding_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class EncodingTest 3 | def foo 4 | "ä" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/encoding_test_iso.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeclimate/ruby-test-reporter/faa99c6fb8bbd2372c585c1ff55879ede29b8f67/spec/fixtures/encoding_test_iso.rb -------------------------------------------------------------------------------- /spec/fixtures/fake_project.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeclimate/ruby-test-reporter/faa99c6fb8bbd2372c585c1ff55879ede29b8f67/spec/fixtures/fake_project.tar.gz -------------------------------------------------------------------------------- /spec/fixtures/issue_7.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeclimate/ruby-test-reporter/faa99c6fb8bbd2372c585c1ff55879ede29b8f67/spec/fixtures/issue_7.tar.gz -------------------------------------------------------------------------------- /spec/fixtures/test_file.rb: -------------------------------------------------------------------------------- 1 | line1 2 | line2 3 | line3 4 | line4 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start 3 | 4 | require 'bundler/setup' 5 | require 'pry' 6 | require 'codeclimate-test-reporter' 7 | require 'webmock/rspec' 8 | 9 | ENV['CODECLIMATE_REPO_TOKEN'] = "172754c1bf9a3c698f7770b9fb648f1ebb214425120022d0b2ffc65b97dff531" 10 | ENV['CODECLIMATE_API_HOST'] = "http://cc.dev" 11 | 12 | Dir.glob("spec/support/**/*.rb").sort.each(&method(:load)) 13 | -------------------------------------------------------------------------------- /spec/support/fixture_helper.rb: -------------------------------------------------------------------------------- 1 | module FixtureHelper 2 | # Unpack the git project at spec/fixtures/{name}.tar.gz and run the block 3 | # within it, presumably formatting a simplecov result. 4 | def within_repository(name) 5 | old_pwd = Dir.pwd 6 | FileUtils.cd("spec/fixtures") 7 | system("tar -xzf #{name}.tar.gz >/dev/null") or 8 | raise ArgumentError, "could not extract #{name}.tar.gz" 9 | FileUtils.cd(name) 10 | yield 11 | ensure 12 | FileUtils.cd(old_pwd) 13 | FileUtils.rm_rf("spec/fixtures/#{name}") 14 | end 15 | 16 | # Load spec/fixtures/{name}_resultset.json and correct the file paths, 17 | # stripping the given prefix and pre-pending the fixture project's directory. 18 | def load_resultset(name, project_prefix) 19 | fixture = File.join("spec", "fixtures", "#{name}_resultset.json") 20 | fixture_result = JSON.parse(File.read(fixture)) 21 | updated_prefix = "#{SimpleCov.root}/spec/fixtures/#{name}/" 22 | update_source_paths(fixture_result, project_prefix, updated_prefix) 23 | end 24 | 25 | # :private: actual munging of the simplecov nest hash 26 | def update_source_paths(fixture_result, from, to) 27 | fixture_result.each_with_object({}) do |(name, values), out| 28 | out[name] = {} 29 | values.each do |k, v| 30 | if k == "coverage" 31 | out[name][k] = {} 32 | v.each do |p, lines| 33 | path = p.sub(from, to) 34 | out[name][k][path] = lines 35 | end 36 | else 37 | out[name][k] = v 38 | end 39 | end 40 | end 41 | end 42 | end 43 | 44 | RSpec.configure do |conf| 45 | conf.include(FixtureHelper) 46 | end 47 | -------------------------------------------------------------------------------- /spec/support/io_helper.rb: -------------------------------------------------------------------------------- 1 | module IOHelper 2 | def capture_io 3 | stdout = $stdout 4 | stderr = $stderr 5 | $stdout = StringIO.new 6 | $stderr = StringIO.new 7 | 8 | yield if block_given? 9 | 10 | [$stdout, $stderr] 11 | ensure 12 | $stdout = stdout 13 | $stderr = stderr 14 | end 15 | end 16 | 17 | RSpec.configure do |conf| 18 | conf.include(IOHelper) 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/requests_helper.rb: -------------------------------------------------------------------------------- 1 | module RequestsHelper 2 | def capture_requests(stub) 3 | requests = [] 4 | stub.to_return { |r| requests << r; {body: "hello"} } 5 | requests 6 | end 7 | end 8 | 9 | RSpec.configure do |conf| 10 | conf.include(RequestsHelper) 11 | end 12 | --------------------------------------------------------------------------------