Skip to content

Commit

Permalink
[Fix #7686] Add new JUnitFormatter
Browse files Browse the repository at this point in the history
Fixes #7686.

This PR Adds new `JUnitFormatter` formatter based on rubocop-junit-formatter gem.
https://github.com/mikian/rubocop-junit-formatter

And this PR includes the following patch to implementation of `JUnitFormatter`.
mikian/rubocop-junit-formatter#11.

REXML gem has been bundled gem since Ruby 2.8.0-dev (Ruby 3.0), it is
added to depend on gemspec.

- https://bugs.ruby-lang.org/issues/16485
- ruby/ruby@c3ccf23
  • Loading branch information
koic authored and bbatsov committed Feb 10, 2020
1 parent 492ebf8 commit ebe2121
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Expand Up @@ -95,6 +95,10 @@ Metrics/ModuleLength:
Exclude:
- 'spec/**/*.rb'

RSpec/FilePath:
Exclude:
- spec/rubocop/formatter/junit_formatter_spec.rb

RSpec/PredicateMatcher:
EnforcedStyle: explicit

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
5 changes: 0 additions & 5 deletions Gemfile
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop/formatter/formatter_set.rb
Expand Up @@ -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,
Expand Down
63 changes: 63 additions & 0 deletions 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
31 changes: 31 additions & 0 deletions manual/formatters.md
Expand Up @@ -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
<?xml version='1.0'?>
<testsuites>
<testsuite name='rubocop'>
<testcase classname='example' name='Style/FrozenStringLiteralComment'>
<failure type='Style/FrozenStringLiteralComment' message='Style/FrozenStringLiteralComment: Missing frozen string literal comment.'>
/tmp/src/example.rb:1:1
</failure>
</testcase>
<testcase classname='example' name='Naming/MethodName'>
<failure type='Naming/MethodName' message='Naming/MethodName: Use snake_case for method names.'>
/tmp/src/example.rb:1:5
</failure>
</testcase>
<testcase classname='example' name='Lint/DeprecatedClassMethods'>
<failure type='Lint/DeprecatedClassMethods' message='Lint/DeprecatedClassMethods: `File.exists?` is deprecated in favor of `File.exist?`.'>
/tmp/src/example.rb:2:8
</failure>
</testcase>
</testsuite>
</testsuites>
```
### Offense Count Formatter
Sometimes when first applying RuboCop to a codebase, it's nice to be able to
Expand Down
1 change: 1 addition & 0 deletions rubocop.gemspec
Expand Up @@ -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')

Expand Down
55 changes: 55 additions & 0 deletions 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)
<?xml version='1.0'?>
<testsuites>
<testsuite name='rubocop'>
<testcase classname='test_1' name='Cop/Cop'>
<failure type='Cop/Cop' message='message 1'>
test:1:1
</failure>
<failure type='Cop/Cop' message='message 2'>
test:3:2
</failure>
</testcase>
<testcase classname='test_2' name='Cop/Cop'>
<failure type='Cop/Cop' message='message 1'>
test:1:1
</failure>
<failure type='Cop/Cop' message='message 2'>
test:3:2
</failure>
</testcase>
</testsuite>
</testsuites>
XML
end
end
end
1 change: 1 addition & 0 deletions spec/rubocop/options_spec.rb
Expand Up @@ -77,6 +77,7 @@ def abs(path)
[fu]ubar
[h]tml
[j]son
[ju]nit
[o]ffenses
[pa]cman
[p]rogress
Expand Down

0 comments on commit ebe2121

Please sign in to comment.