Skip to content

Commit

Permalink
[Fix rubocop#155] Provide assert_offense and assert_no_offenses f…
Browse files Browse the repository at this point in the history
…or custom cop development

Resolves rubocop#155.

This PR provides `assert_offense`, `assert_correction`, and `assert_no_offenses`
for custom Minitest cop development.

Developers can use these APIs by requesting the following in custom Minitest cop test code:

```ruby
require 'rubocop/minitest/support'
```
  • Loading branch information
koic committed Nov 18, 2021
1 parent 291feb3 commit 443ef48
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 118 deletions.
1 change: 1 addition & 0 deletions changelog/new_provide_assertion_apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#155](https://github.com/rubocop/rubocop-minitest/issues/155): Provide `assert_offense`, `assert_correction`, and `assert_no_offenses` for custom Minitest cop development. ([@koic][])
183 changes: 183 additions & 0 deletions lib/rubocop/minitest/assert_offense.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# frozen_string_literal: true

# Laziness copied from rubocop source code
require 'rubocop/rspec/expect_offense'
require 'rubocop/cop/legacy/corrector'

module RuboCop
module Minitest
# Mixin for `assert_offense` and `assert_no_offenses`
#
# This mixin makes it easier to specify strict offense assertions
# in a declarative and visual fashion. Just type out the code that
# should generate a offense, annotate code by writing '^'s
# underneath each character that should be highlighted, and follow
# the carets with a string (separated by a space) that is the
# message of the offense. You can include multiple offenses in
# one code snippet.
#
# @example Usage
#
# assert_offense(<<~RUBY)
# class FooTest < Minitest::Test
# def test_do_something
# assert_equal(nil, somestuff)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using `assert_nil(somestuff)` over `assert_equal(nil, somestuff)`.
# end
# end
# RUBY
#
# Auto-correction can be tested using `assert_correction` after
# `assert_offense`.
#
# @example `assert_offense` and `assert_correction`
#
# assert_offense(<<~RUBY)
# class FooTest < Minitest::Test
# def test_do_something
# assert_equal(nil, somestuff)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using `assert_nil(somestuff)` over `assert_equal(nil, somestuff)`.
# end
# end
# RUBY
#
# assert_correction(<<~RUBY)
# class FooTest < Minitest::Test
# def test_do_something
# assert_nil(somestuff)
# end
# end
# RUBY
#
# If you do not want to specify an offense then use the
# companion method `assert_no_offenses`. This method is a much
# simpler assertion since it just inspects the source and checks
# that there were no offenses. The `assert_offense` method has
# to do more work by parsing out lines that contain carets.
#
# If the code produces an offense that could not be auto-corrected, you can
# use `assert_no_corrections` after `assert_offense`.
#
# @example `assert_offense` and `assert_no_corrections`
#
# assert_offense(<<~RUBY)
# class FooTest < Minitest::Test
# def test_do_something
# assert_equal(nil, somestuff)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using `assert_nil(somestuff)` over `assert_equal(nil, somestuff)`.
# end
# end
# RUBY
#
# assert_no_corrections
module AssertOffense
private

def setup
cop_name = self.class.to_s.delete_suffix('Test')

@cop = RuboCop::Cop::Minitest.const_get(cop_name).new
end

def assert_no_offenses(source, file = nil)
setup_assertion

offenses = inspect_source(source, @cop, file)

expected_annotations = RuboCop::RSpec::ExpectOffense::AnnotatedSource.parse(source)
actual_annotations = expected_annotations.with_offense_annotations(offenses)

assert_equal(source, actual_annotations.to_s)
end

def assert_offense(source, file = nil)
setup_assertion

@cop.instance_variable_get(:@options)[:auto_correct] = true

expected_annotations = RuboCop::RSpec::ExpectOffense::AnnotatedSource.parse(source)
if expected_annotations.plain_source == source
raise 'Use `assert_no_offenses` to assert that no offenses are found'
end

@processed_source = parse_source!(expected_annotations.plain_source, file)

offenses = _investigate(@cop, @processed_source)

actual_annotations = expected_annotations.with_offense_annotations(offenses)

assert_equal(expected_annotations.to_s, actual_annotations.to_s)
end

def _investigate(cop, processed_source)
team = RuboCop::Cop::Team.new([cop], nil, raise_error: true)
report = team.investigate(processed_source)
@last_corrector = report.correctors.first || RuboCop::Cop::Corrector.new(processed_source)
report.offenses
end

def assert_correction(correction, loop: true)
raise '`assert_correction` must follow `assert_offense`' unless @processed_source

iteration = 0
new_source = loop do
iteration += 1

corrected_source = @last_corrector.rewrite

break corrected_source unless loop
break corrected_source if @last_corrector.empty? || corrected_source == @processed_source.buffer.source

if iteration > RuboCop::Runner::MAX_ITERATIONS
raise RuboCop::Runner::InfiniteCorrectionLoop.new(@processed_source.path, [])
end

# Prepare for next loop
@processed_source = parse_source!(corrected_source, @processed_source.path)

_investigate(@cop, @processed_source)
end

assert_equal(correction, new_source)
end

def setup_assertion
RuboCop::Formatter::DisabledConfigFormatter.config_to_allow_offenses = {}
RuboCop::Formatter::DisabledConfigFormatter.detected_styles = {}
end

def inspect_source(source, cop, file = nil)
processed_source = parse_source!(source, file)
raise 'Error parsing example code' unless processed_source.valid_syntax?

_investigate(cop, processed_source)
end

def investigate(cop, processed_source)
needed = Hash.new { |h, k| h[k] = [] }
Array(cop.class.joining_forces).each { |force| needed[force] << cop }
forces = needed.map do |force_class, joining_cops|
force_class.new(joining_cops)
end

commissioner = RuboCop::Cop::Commissioner.new([cop], forces, raise_error: true)
commissioner.investigate(processed_source)
commissioner
end

def parse_source!(source, file = nil)
if file.respond_to?(:write)
file.write(source)
file.rewind
file = file.path
end

RuboCop::ProcessedSource.new(source, ruby_version, file)
end

def ruby_version
2.5
end
end
end
end
10 changes: 10 additions & 0 deletions lib/rubocop/minitest/support.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

# Require this file to load code that supports testing using Minitest.

require 'rubocop'
require 'minitest/autorun'
require 'minitest/pride'
require_relative 'assert_offense'

Minitest::Test.include RuboCop::Minitest::AssertOffense
113 changes: 0 additions & 113 deletions test/assertion_helper.rb

This file was deleted.

7 changes: 2 additions & 5 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
# frozen_string_literal: true

require 'rubocop'
require 'rubocop-minitest'
require 'minitest/autorun'
require 'minitest/pride'
require 'assertion_helper'

Minitest::Test.include AssertionHelper
# Require supporting files exposed for testing.
require 'rubocop/minitest/support'

0 comments on commit 443ef48

Please sign in to comment.