diff --git a/.rubocop.yml b/.rubocop.yml index 15d9d64f4ac..564fe88d901 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -95,6 +95,10 @@ Metrics/ModuleLength: Exclude: - 'spec/**/*.rb' +RSpec/FilePath: + Exclude: + - spec/rubocop/formatter/junit_formatter_spec.rb + RSpec/PredicateMatcher: EnforcedStyle: explicit diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a016e5b57e..6bc3a17418b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#7659](https://github.com/rubocop-hq/rubocop/pull/7659): Layout/LineLength autocorrect now breaks up long lines with blocks. ([@maxh][]) * [#7677](https://github.com/rubocop-hq/rubocop/pull/7677): Add a cop for `Hash#each_key` and `Hash#each_value`. ([@jemmaissroff][]) * Add `BracesRequiredMethods` parameter to `Style/BlockDelimiters` to require braces for specific methods such as Sorbet's `sig`. ([@maxh][]) +* [#7686](https://github.com/rubocop-hq/rubocop/pull/7686): Add new `JUnitFormatter` formatter based on rubocop-junit-formatter gem. ([@koic][]) ### Bug fixes diff --git a/Gemfile b/Gemfile index b6a4041ba96..0551942c2b0 100644 --- a/Gemfile +++ b/Gemfile @@ -18,11 +18,6 @@ gem 'test-queue' gem 'yard', '~> 0.9' group :test do - # Workaround for crack 0.4.3 or lower. - # Depends on `rexml` until the release that includes - # the following changes: - # https://github.com/jnunemaker/crack/pull/62 - gem 'rexml' gem 'safe_yaml', require: false gem 'webmock', require: false end diff --git a/lib/rubocop.rb b/lib/rubocop.rb index 53502594be6..a88668a7d9c 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -588,6 +588,7 @@ require_relative 'rubocop/formatter/fuubar_style_formatter' require_relative 'rubocop/formatter/html_formatter' require_relative 'rubocop/formatter/json_formatter' +require_relative 'rubocop/formatter/junit_formatter' require_relative 'rubocop/formatter/offense_count_formatter' require_relative 'rubocop/formatter/progress_formatter' require_relative 'rubocop/formatter/quiet_formatter' diff --git a/lib/rubocop/formatter/formatter_set.rb b/lib/rubocop/formatter/formatter_set.rb index 1375beac201..08e83de3357 100644 --- a/lib/rubocop/formatter/formatter_set.rb +++ b/lib/rubocop/formatter/formatter_set.rb @@ -17,6 +17,7 @@ class FormatterSet < Array '[fu]ubar' => FuubarStyleFormatter, '[h]tml' => HTMLFormatter, '[j]son' => JSONFormatter, + '[ju]nit' => JUnitFormatter, '[o]ffenses' => OffenseCountFormatter, '[pa]cman' => PacmanFormatter, '[p]rogress' => ProgressFormatter, diff --git a/lib/rubocop/formatter/junit_formatter.rb b/lib/rubocop/formatter/junit_formatter.rb new file mode 100644 index 00000000000..3bfc5f6567a --- /dev/null +++ b/lib/rubocop/formatter/junit_formatter.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rexml/document' + +# +# This code is based on https://github.com/mikian/rubocop-junit-formatter. +# +# Copyright (c) 2015 Mikko Kokkonen +# +# MIT License +# +# https://github.com/mikian/rubocop-junit-formatter/blob/master/LICENSE.txt +# +module RuboCop + module Formatter + # This formatter formats the report data in JUnit format. + class JUnitFormatter < BaseFormatter + def initialize(output, options = {}) + super + + @document = REXML::Document.new.tap do |document| + document << REXML::XMLDecl.new + end + testsuites = REXML::Element.new('testsuites', @document) + testsuite = REXML::Element.new('testsuite', testsuites) + @testsuite = testsuite.tap do |element| + element.add_attributes('name' => 'rubocop') + end + end + + def file_finished(file, offenses) + offenses.group_by(&:cop_name).each do |cop_name, grouped_offenses| + REXML::Element.new('testcase', @testsuite).tap do |testcase| + testcase.attributes['classname'] = file.gsub( + /\.rb\Z/, '' + ).gsub("#{Dir.pwd}/", '').tr('/', '.') + testcase.attributes['name'] = cop_name + + add_failure_to(testcase, grouped_offenses, cop_name) + end + end + end + + def finished(_inspected_files) + @document.write(output, 2) + end + + private + + def add_failure_to(testcase, offenses, cop_name) + # One failure per offense. Zero failures is a passing test case, + # for most surefire/nUnit parsers. + offenses.each do |offense| + REXML::Element.new('failure', testcase).tap do |failure| + failure.attributes['type'] = cop_name + failure.attributes['message'] = offense.message + failure.add_text(offense.location.to_s) + end + end + end + end + end +end diff --git a/manual/formatters.md b/manual/formatters.md index 433ec96d646..15f77d77a1b 100644 --- a/manual/formatters.md +++ b/manual/formatters.md @@ -237,6 +237,37 @@ The JSON structure is like the following example: } ``` +### JUnit Style Formatter + +**Machine-parsable** + +The `junit` style formatter provides the JUnit formatting. +This formatter is based on [rubocop-junit-formatter gem](https://github.com/mikian/rubocop-junit-formatter). + +```sh +$ rubocop --format junit + + + + + + /tmp/src/example.rb:1:1 + + + + + /tmp/src/example.rb:1:5 + + + + + /tmp/src/example.rb:2:8 + + + + +``` + ### Offense Count Formatter Sometimes when first applying RuboCop to a codebase, it's nice to be able to diff --git a/rubocop.gemspec b/rubocop.gemspec index ccd7b4fb98a..ee6aefba430 100644 --- a/rubocop.gemspec +++ b/rubocop.gemspec @@ -37,6 +37,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency('parallel', '~> 1.10') s.add_runtime_dependency('parser', '>= 2.7.0.1') s.add_runtime_dependency('rainbow', '>= 2.2.2', '< 4.0') + s.add_runtime_dependency('rexml') s.add_runtime_dependency('ruby-progressbar', '~> 1.7') s.add_runtime_dependency('unicode-display_width', '>= 1.4.0', '< 1.7') diff --git a/spec/rubocop/formatter/junit_formatter_spec.rb b/spec/rubocop/formatter/junit_formatter_spec.rb new file mode 100644 index 00000000000..4057a5604cc --- /dev/null +++ b/spec/rubocop/formatter/junit_formatter_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Formatter::JUnitFormatter do + subject(:formatter) { described_class.new(output) } + + let(:output) { StringIO.new } + + describe '#file_finished' do + it 'displays parsable text' do + cop = RuboCop::Cop::Cop.new + source_buffer = Parser::Source::Buffer.new('test', 1) + source_buffer.source = %w[foo bar baz].join("\n") + + cop.add_offense( + nil, + location: Parser::Source::Range.new(source_buffer, 0, 1), + message: 'message 1' + ) + cop.add_offense( + nil, + location: Parser::Source::Range.new(source_buffer, 9, 10), + message: 'message 2' + ) + + formatter.file_finished('test_1', cop.offenses) + formatter.file_finished('test_2', cop.offenses) + + formatter.finished(nil) + + expect(output.string).to eq(<<~XML.chop) + + + + + + test:1:1 + + + test:3:2 + + + + + test:1:1 + + + test:3:2 + + + + + XML + end + end +end diff --git a/spec/rubocop/options_spec.rb b/spec/rubocop/options_spec.rb index dcdb544e233..505367cf633 100644 --- a/spec/rubocop/options_spec.rb +++ b/spec/rubocop/options_spec.rb @@ -77,6 +77,7 @@ def abs(path) [fu]ubar [h]tml [j]son + [ju]nit [o]ffenses [pa]cman [p]rogress