diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 53fbee6..9adfb80 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,7 +21,7 @@ jobs:
rubocop_version: [""]
include:
- ruby: "2.6"
- rubocop_version: "'1.0.0'"
+ rubocop_version: "'1.45.0'"
- ruby: "3.2"
rubocop_version: "github: 'rubocop/rubocop'"
steps:
diff --git a/lib/rubocop/markdown/preprocess.rb b/lib/rubocop/markdown/preprocess.rb
index 6165a9b..1210103 100644
--- a/lib/rubocop/markdown/preprocess.rb
+++ b/lib/rubocop/markdown/preprocess.rb
@@ -39,11 +39,15 @@ class << self
# using preproccessed source buffer.
#
# We have to restore it.
- def restore!(file)
+ def restore_and_save!(file)
contents = File.read(file)
- contents.gsub!(/^##{MARKER}/m, "")
+ restore!(contents)
File.write(file, contents)
end
+
+ def restore!(src)
+ src.gsub!(/^##{MARKER}/m, "")
+ end
end
attr_reader :config
diff --git a/lib/rubocop/markdown/rubocop_ext.rb b/lib/rubocop/markdown/rubocop_ext.rb
index ed918e1..1283b97 100644
--- a/lib/rubocop/markdown/rubocop_ext.rb
+++ b/lib/rubocop/markdown/rubocop_ext.rb
@@ -62,30 +62,32 @@ def file_offense_cache(file)
super
end
- def inspect_file(*args)
- super.tap do |(offenses, *)|
- # Skip offenses reported for ignored MD source (trailing whitespaces, etc.)
- marker_comment = "##{RuboCop::Markdown::Preprocess::MARKER}"
- offenses.reject! do |offense|
- next if RuboCop::Markdown::MARKDOWN_OFFENSE_COPS.include?(offense.cop_name)
-
- offense.location.source_line.start_with?(marker_comment)
- end
- end
- end
-
def file_finished(file, offenses)
return super unless RuboCop::Markdown.markdown_file?(file)
# Run Preprocess.restore if file has been autocorrected
if @options[:auto_correct] || @options[:autocorrect]
- RuboCop::Markdown::Preprocess.restore!(file)
+ RuboCop::Markdown::Preprocess.restore_and_save!(file)
end
super(file, offenses)
end
end)
+RuboCop::Cop::Commissioner::InvestigationReport.prepend(Module.new do
+ # Skip offenses reported for ignored MD source (trailing whitespaces, etc.)
+ def offenses
+ @offenses ||= begin
+ marker_comment = "##{RuboCop::Markdown::Preprocess::MARKER}"
+ offenses_per_cop.flatten(1).reject do |offense|
+ next if RuboCop::Markdown::MARKDOWN_OFFENSE_COPS.include?(offense.cop_name)
+
+ offense.location.source_line.start_with?(marker_comment)
+ end
+ end
+ end
+end)
+
# Allow Rubocop to analyze markdown files
RuboCop::TargetFinder.prepend(Module.new do
def ruby_file?(file)
diff --git a/rubocop-md.gemspec b/rubocop-md.gemspec
index 47629a3..b2b2376 100644
--- a/rubocop-md.gemspec
+++ b/rubocop-md.gemspec
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
- spec.add_runtime_dependency "rubocop", ">= 1.0"
+ spec.add_runtime_dependency "rubocop", ">= 1.45"
spec.add_development_dependency "bundler", ">= 1.15"
spec.add_development_dependency "rake", ">= 13.0"
diff --git a/test/fixtures/NON_CODE_OFFENSES.md b/test/fixtures/NON_CODE_OFFENSES.md
deleted file mode 100644
index 3cd5232..0000000
--- a/test/fixtures/NON_CODE_OFFENSES.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# No Code
-
-Just a line with a trailining whitespace
diff --git a/test/fixtures/backticks.md b/test/fixtures/backticks.md
deleted file mode 100644
index 2819a83..0000000
--- a/test/fixtures/backticks.md
+++ /dev/null
@@ -1,7 +0,0 @@
-```ruby
-`method_call
-```
-
-```ruby
-further_code("", '')
-```
diff --git a/test/fixtures/configs/no_autodetect.yml b/test/fixtures/configs/config.yml
similarity index 55%
rename from test/fixtures/configs/no_autodetect.yml
rename to test/fixtures/configs/config.yml
index a6e1f35..f54004a 100644
--- a/test/fixtures/configs/no_autodetect.yml
+++ b/test/fixtures/configs/config.yml
@@ -1,4 +1 @@
inherit_from: "../../../.rubocop.yml"
-
-Markdown:
- Autodetect: false
diff --git a/test/fixtures/configs/no_autodetect_with_require.yml b/test/fixtures/configs/config_with_require.yml
similarity index 67%
rename from test/fixtures/configs/no_autodetect_with_require.yml
rename to test/fixtures/configs/config_with_require.yml
index 8f0587f..81096ea 100644
--- a/test/fixtures/configs/no_autodetect_with_require.yml
+++ b/test/fixtures/configs/config_with_require.yml
@@ -2,6 +2,3 @@ inherit_from: "../../../.rubocop.yml"
require:
- "rubocop-md"
-
-Markdown:
- Autodetect: false
diff --git a/test/fixtures/configs/no_warn_invalid.yml b/test/fixtures/configs/no_warn_invalid.yml
deleted file mode 100644
index b736833..0000000
--- a/test/fixtures/configs/no_warn_invalid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-inherit_from: "../../../.rubocop.yml"
-
-Markdown:
- WarnInvalid: false
-
diff --git a/test/fixtures/configs/no_warn_invalid_with_require.yml b/test/fixtures/configs/no_warn_invalid_with_require.yml
deleted file mode 100644
index f569fba..0000000
--- a/test/fixtures/configs/no_warn_invalid_with_require.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-inherit_from: "../../../.rubocop.yml"
-
-require:
- - "rubocop-md"
-
-Markdown:
- WarnInvalid: false
-
diff --git a/test/fixtures/in_flight_parse.rb b/test/fixtures/in_flight_parse.rb
deleted file mode 100644
index 7820ffd..0000000
--- a/test/fixtures/in_flight_parse.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-{ complex_symbol: 0 }
diff --git a/test/fixtures/multiple_invalid_snippets.md b/test/fixtures/multiple_invalid_snippets.md
deleted file mode 100644
index e2514cb..0000000
--- a/test/fixtures/multiple_invalid_snippets.md
+++ /dev/null
@@ -1,25 +0,0 @@
-TestProf provides a built-in shared context for RSpec to profile examples individually:
-
-```ruby
-it "is doing heavy stuff", :rprof do
- ...
-end
-```
-
-### Configuration
-
-The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers).
-
-You can specify a printer through environment variable `TEST_RUBY_PROF`:
-
-```sh
-TEST_RUBY_PROF=call_stack bundle exec rake test
-```
-
-Or in your code:
-
-```ruby
-TestProf::RubyProf.configure do |config|
- config.printer = :call_stack
-end
-```
\ No newline at end of file
diff --git a/test/fixtures/multiple_invalid_snippets_unknown.md b/test/fixtures/multiple_invalid_snippets_unknown.md
deleted file mode 100644
index e67421d..0000000
--- a/test/fixtures/multiple_invalid_snippets_unknown.md
+++ /dev/null
@@ -1,20 +0,0 @@
-TestProf provides a built-in shared context for RSpec to profile examples individually:
-
-```
-it "is doing heavy stuff", :rprof do
- ...
-end
-```
-
-### Configuration
-
-The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers).
-
-
-Or in your code:
-
-```
-TestProf:: RubyProf. configure { |config|
- config.printer=:call_stack
-}
-```
\ No newline at end of file
diff --git a/test/fixtures/multiple_snippets.markdown b/test/fixtures/multiple_snippets.markdown
deleted file mode 100644
index f181622..0000000
--- a/test/fixtures/multiple_snippets.markdown
+++ /dev/null
@@ -1,52 +0,0 @@
-# Custom RuboCop Cops
-
-TestProf comes with the [RuboCop](https://github.com/bbatsov/rubocop) cops that help you write more performant tests.
-
-To enable them:
-
-- Require `test_prof/rubocop` in your RuboCop configuration:
-
-```yml
-# .rubocop.yml
-require:
- - 'test_prof/rubocop'
-```
-
-- Enable cops:
-
-```yml
-RSpec/AggregateFailures:
- Enabled: true
- Include:
- - 'spec/**/*.rb'
-```
-
-## RSpec/AggregateFailures
-
-This cop encourages you to use one of the greatest features of the recent RSpec – aggregating failures within an example.
-
-Instead of writing one example per assertion, you can group _independent_ assertions together, thus running all setup hooks only once. That can dramatically increase your performance (by reducing the total number of examples).
-
-Consider an example:
-
-```ruby
-# bad
-it { is_expected.to be_success }
-it { is_expected.to have_header("X-TOTAL-PAGES",10) }
-it {is_expected.to have_header("X-NEXT-PAGE", 2)}
-```
-
-That's the better way:
-
-```
-# good
-it "returns the second page",:aggregate_failures do
- is_expected.to be_success
- is_expected.to have_header("X-TOTAL-PAGES", 10)
- is_expected.to have_header("X-NEXT-PAGE", 2)
-end
-```
-
-This cop supports auto-correct feature, so you can automatically refactor you legacy tests!
-
-**NOTE**: auto-correction may break your tests (especially the ones using block-matchers, such as `change`).
diff --git a/test/integration_test.rb b/test/integration_test.rb
index a635090..41b9c7d 100644
--- a/test/integration_test.rb
+++ b/test/integration_test.rb
@@ -33,14 +33,6 @@ def run_rubocop(path, options: "", config: nil)
class RuboCop::Markdown::AnalyzeTest < Minitest::Test
include RuboCopRunner
- def test_single_snippet_file
- res = run_rubocop("single_snippet.md")
-
- assert_match %r{Inspecting 1 file}, res
- assert_match %r{1 offense detected}, res
- assert_match %r{Style/StringLiterals}, res
- end
-
def test_file_with_format_options
res = run_rubocop("single_snippet.md", options: "--format progress")
@@ -49,50 +41,25 @@ def test_file_with_format_options
assert_match %r{Style/StringLiterals}, res
end
- def test_multiple_snippets_file
- res = run_rubocop("multiple_snippets.markdown")
-
- assert_match %r{Inspecting 1 file}, res
- assert_match %r{4 offenses detected}, res
- assert_match %r{Layout/SpaceAfterComma}, res
- assert_match %r{Layout/SpaceInsideBlockBraces}, res
- end
-
- def test_multiple_invalid_snippets_file
- res = run_rubocop("multiple_invalid_snippets.md")
-
- assert_match %r{Inspecting 1 file}, res
- assert_match %r{Lint/Syntax: unexpected token}, res
- end
-
- def test_multiple_invalid_snippets_file_no_warn
- res = run_rubocop(
- "multiple_invalid_snippets.md",
- config: "configs/no_warn_invalid.yml"
- )
-
- assert_match %r{Inspecting 1 file}, res
- assert_match %r{no offenses detected}, res
- end
-
- def test_multiple_invalid_snippets_file_no_autodetect
+ def test_rubocop_with_passed_config
res = run_rubocop(
- "multiple_invalid_snippets_unknown.md",
- config: "configs/no_autodetect.yml"
+ "single_snippet.md",
+ config: "configs/config.yml"
)
assert_match %r{Inspecting 1 file}, res
- assert_match %r{no offenses detected}, res
+ assert_match %r{1 offense detected}, res
+ assert_match %r{Style/StringLiterals}, res
end
def test_with_cache
- res = run_rubocop("multiple_snippets.markdown", options: "--cache true")
+ res = run_rubocop("single_snippet.md", options: "--cache true")
assert_match %r{Inspecting 1 file}, res
- assert_match %r{4 offenses detected}, res
- assert_includes res, 'have_header("X-TOTAL-PAGES",10)'
+ assert_match %r{1 offense detected}, res
+ assert_includes res, "create(:beatle, name: 'John')"
- res_cached = run_rubocop("multiple_snippets.markdown", options: "--cache true")
- assert_includes res_cached, 'have_header("X-TOTAL-PAGES",10)'
+ res_cached = run_rubocop("single_snippet.md", options: "--cache true")
+ assert_includes res_cached, "create(:beatle, name: 'John')"
end
def test_file_extensions
@@ -110,257 +77,4 @@ def test_file_extensions
assert_includes res, "file_extensions/11.livemd:"
assert_includes res, "file_extensions/12.scd:"
end
-
- def test_in_flight_parsing
- res = run_rubocop("in_flight_parse.rb")
-
- assert_match %r{Inspecting 1 file}, res
- assert_match %r{no offenses detected}, res
- end
-
- def test_non_code_offenses
- res = run_rubocop("NON_CODE_OFFENSES.md")
-
- assert_match %r{Inspecting 1 file}, res
- assert_match %r{no offenses detected}, res
- end
-
- def test_backticks_in_code
- res = run_rubocop("backticks.md")
-
- assert_match %r{Inspecting 1 file}, res
- assert_match %r{1 offense detected}, res
- end
-end
-
-class RuboCop::Markdown::AutocorrectTest < Minitest::Test
- include RuboCopRunner
-
- def fixture_name
- @fixture_name ||= "autocorrect_test.md"
- end
-
- def fixture_file
- @fixture_file ||= File.join(__dir__, "fixtures", fixture_name)
- end
-
- def prepare_test(contents)
- File.write(fixture_file, contents)
- end
-
- def teardown
- FileUtils.rm(fixture_file)
- end
-
- def test_autocorrect_single_snippet
- prepare_test(
- <<~CODE
- # Before All
-
- Rails has a great feature – `transactional_tests`.
-
- We can do something like this:
-
- ```ruby
- describe BeatleWeightedSearchQuery do
- before(:each) do
- @paul = create(:beatle, name: "Paul")
- @john = create(:beatle, name: 'John')
- end
-
- # and about 15 examples here
- end
- ```
- CODE
- )
-
- expected = <<~CODE
- # Before All
-
- Rails has a great feature – `transactional_tests`.
-
- We can do something like this:
-
- ```ruby
- describe BeatleWeightedSearchQuery do
- before(:each) do
- @paul = create(:beatle, name: "Paul")
- @john = create(:beatle, name: "John")
- end
-
- # and about 15 examples here
- end
- ```
- CODE
-
- res = run_rubocop(fixture_name, options: "-a")
- assert_match %r{1 offense detected}, res
- assert_match %r{1 offense corrected}, res
-
- assert_equal expected, File.read(fixture_file)
- end
-
- def test_autocorrect_multiple_snippets
- prepare_test(
- <<~CODE
- ```ruby
- # bad
- it { is_expected.to be_success }
- it { is_expected.to have_header("X-TOTAL-PAGES",10) }
- it {is_expected.to have_header("X-NEXT-PAGE", 2)}
- ```
-
- That's the better way:
-
- ```
- # good
- it "returns the second page",:aggregate_failures do
- is_expected.to be_success
- is_expected.to have_header("X-TOTAL-PAGES", 10)
- is_expected.to have_header("X-NEXT-PAGE", 2)
- end
- ```
-
- To enable them:
-
- - Require `test_prof/rubocop` in your RuboCop configuration:
-
- ```yml
- # .rubocop.yml
- require:
- - 'test_prof/rubocop'
- ```
- CODE
- )
-
- expected = <<~CODE
- ```ruby
- # bad
- it { is_expected.to be_success }
- it { is_expected.to have_header("X-TOTAL-PAGES", 10) }
- it { is_expected.to have_header("X-NEXT-PAGE", 2) }
- ```
-
- That's the better way:
-
- ```
- # good
- it "returns the second page", :aggregate_failures do
- is_expected.to be_success
- is_expected.to have_header("X-TOTAL-PAGES", 10)
- is_expected.to have_header("X-NEXT-PAGE", 2)
- end
- ```
-
- To enable them:
-
- - Require `test_prof/rubocop` in your RuboCop configuration:
-
- ```yml
- # .rubocop.yml
- require:
- - 'test_prof/rubocop'
- ```
- CODE
-
- res = run_rubocop(fixture_name, options: "-a")
- assert_match %r{4 offenses detected}, res
- assert_match %r{4 offenses corrected}, res
-
- assert_equal expected, File.read(fixture_file)
- end
-
- def test_autocorrect_with_compound_snippets
- prepare_test(
- <<~CODE
- Passing an array of symbols is also acceptable.
-
- ```ruby
- class Book
- include ActiveModel::Validations
-
- validates :title, presence:true, on:[:update, :ensure_title]
- end
- ```
-
- Assuming we have a model that's been saved in an instance variable named
- `@article`, it looks like this:
-
- ```html+erb
- <% if @article.errors.any? %>
-
-
<%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:
-
-
- <% @article.errors.each do |error| %>
- - <%= error.full_message %>
- <% end %>
-
-
- <% end %>
- ```
-
- When triggered by an explicit context, validations are run for that context,
- as well as any validations _without_ a context.
-
- ```ruby
- class Person < ApplicationRecord
- validates :email, uniqueness: true, on: :account_setup
- validates :age, numericality: true, on: :account_setup
- validates :name, presence: true
- end
- ```
-
- That's it.
- CODE
- )
-
- expected = <<~CODE
- Passing an array of symbols is also acceptable.
-
- ```ruby
- class Book
- include ActiveModel::Validations
-
- validates :title, presence: true, on: %i[update ensure_title]
- end
- ```
-
- Assuming we have a model that's been saved in an instance variable named
- `@article`, it looks like this:
-
- ```html+erb
- <% if @article.errors.any? %>
-
-
<%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:
-
-
- <% @article.errors.each do |error| %>
- - <%= error.full_message %>
- <% end %>
-
-
- <% end %>
- ```
-
- When triggered by an explicit context, validations are run for that context,
- as well as any validations _without_ a context.
-
- ```ruby
- class Person < ApplicationRecord
- validates :email, uniqueness: true, on: :account_setup
- validates :age, numericality: true, on: :account_setup
- validates :name, presence: true
- end
- ```
-
- That's it.
- CODE
-
- res = run_rubocop(fixture_name, options: "-A")
- assert_match %r{7 offenses detected}, res
- assert_match %r{7 offenses corrected}, res
-
- assert_equal expected, File.read(fixture_file)
- end
end
diff --git a/test/markdown_assertions.rb b/test/markdown_assertions.rb
new file mode 100644
index 0000000..65dadc6
--- /dev/null
+++ b/test/markdown_assertions.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require "fileutils"
+
+module RuboCop
+ module Markdown
+ # Necessary overwrites over rubocop minitest assertions to run all cops and handle markdown autocorrection
+ class Test < Minitest::Test
+ include AssertOffense
+
+ class DummyCop
+ def initialize
+ @options = {}
+ end
+ end
+
+ # Lint/Syntax has a multiline offense which is impossible to match against
+ class RuboCop::Cop::Lint::Syntax
+ def add_offense_from_diagnostic(diagnostic, _ruby_version)
+ add_offense(diagnostic.location, message: diagnostic.message, severity: diagnostic.level)
+ end
+ end
+
+ def assert_offense(source, file = "test.md", **replacements)
+ @cop = DummyCop.new
+ super
+ end
+
+ def assert_no_offenses(source, file = "test.md")
+ super
+ end
+
+ # rubocop:disable Metrics/AbcSize
+ 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
+ if @last_corrector.empty? || corrected_source == @processed_source.buffer.source
+ break corrected_source
+ end
+
+ if iteration > RuboCop::Runner::MAX_ITERATIONS
+ raise RuboCop::Runner::InfiniteCorrectionLoop.new(@processed_source.path, [])
+ end
+
+ # Prepare for next loop
+ RuboCop::Markdown::Preprocess.restore!(corrected_source)
+ @processed_source = parse_source!(corrected_source)
+
+ _investigate(@cop, @processed_source)
+ end
+
+ RuboCop::Markdown::Preprocess.restore!(new_source)
+
+ assert_equal(correction, new_source)
+ ensure
+ FileUtils.rm_f(@processed_source.path)
+ end
+ # rubocop:enable Metrics/AbcSize
+
+ def investigate(_cop, processed_source)
+ commissioner = RuboCop::Cop::Commissioner.new(registry.cops, registry.class.forces_for(registry.cops), raise_error: true)
+ commissioner.investigate(processed_source)
+ commissioner
+ end
+
+ def _investigate(_cop, processed_source)
+ team = RuboCop::Cop::Team.new(registry.cops, configuration, raise_error: true, autocorrect: true)
+ report = team.investigate(processed_source)
+ @last_corrector = report.correctors.compact.first || RuboCop::Cop::Corrector.new(processed_source)
+ report.offenses
+ end
+
+ def inspect_source(source, cop, file = "test.md")
+ super
+ end
+
+ def parse_source!(source, file = "test.md")
+ super
+ end
+
+ def config
+ project_config_path = RuboCop::Markdown::PROJECT_ROOT.join(".rubocop.yml").to_s
+ project_config = RuboCop::ConfigLoader.load_file(project_config_path)
+ test_config = RuboCop::Config.new(project_config.merge(@config || {}))
+ RuboCop::ConfigLoader.merge_with_default(test_config, project_config_path)
+ end
+ end
+ end
+end
diff --git a/test/offense_test.rb b/test/offense_test.rb
new file mode 100644
index 0000000..23c1e8c
--- /dev/null
+++ b/test/offense_test.rb
@@ -0,0 +1,369 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class RuboCop::Markdown::OffenseTest < RuboCop::Markdown::Test
+ def overwrite_config
+ @old_store = RuboCop::Markdown.config_store
+ store = RuboCop::ConfigStore.new
+ store.instance_variable_set(:@options_config, config)
+ RuboCop::Markdown.config_store = store
+ end
+
+ def teardown
+ RuboCop::Markdown.config_store = @old_store if @old_store
+ end
+
+ def test_single_snippet
+ assert_offense(<<~MARKDOWN)
+ # Before All
+
+ Rails has a great feature – `transactional_tests`, i.e. running each example within a transaction which is roll-backed in the end.
+
+ Of course, we can do something like this:
+
+ ```ruby
+ describe BeatleWeightedSearchQuery do
+ before(:each) do
+ @paul = create(:beatle, name: "Paul")
+ @ringo = create(:beatle, name: "Ringo")
+ @george = create(:beatle, name: "George")
+ @john = create(:beatle, name: 'John')
+ ^^^^^^ Style/StringLiterals: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
+ end
+
+ # and about 15 examples here
+ end
+ ```
+ MARKDOWN
+
+ assert_correction(<<~MARKDOWN)
+ # Before All
+
+ Rails has a great feature – `transactional_tests`, i.e. running each example within a transaction which is roll-backed in the end.
+
+ Of course, we can do something like this:
+
+ ```ruby
+ describe BeatleWeightedSearchQuery do
+ before(:each) do
+ @paul = create(:beatle, name: "Paul")
+ @ringo = create(:beatle, name: "Ringo")
+ @george = create(:beatle, name: "George")
+ @john = create(:beatle, name: "John")
+ end
+
+ # and about 15 examples here
+ end
+ ```
+ MARKDOWN
+ end
+
+ def test_multiple_snippets
+ assert_offense(<<~MARKDOWN)
+ # Custom RuboCop Cops
+
+ TestProf comes with the [RuboCop](https://github.com/bbatsov/rubocop) cops that help you write more performant tests.
+
+ To enable them:
+
+ - Require `test_prof/rubocop` in your RuboCop configuration:
+
+ ```yml
+ # .rubocop.yml
+ require:
+ - 'test_prof/rubocop'
+ ```
+
+ - Enable cops:
+
+ ```yml
+ RSpec/AggregateFailures:
+ Enabled: true
+ Include:
+ - 'spec/**/*.rb'
+ ```
+
+ ## RSpec/AggregateFailures
+
+ This cop encourages you to use one of the greatest features of the recent RSpec – aggregating failures within an example.
+
+ Instead of writing one example per assertion, you can group _independent_ assertions together, thus running all setup hooks only once. That can dramatically increase your performance (by reducing the total number of examples).
+
+ Consider an example:
+
+ ```ruby
+ # bad
+ it { is_expected.to be_success }
+ it { is_expected.to have_header("X-TOTAL-PAGES",10) }
+ ^ Layout/SpaceAfterComma: Space missing after comma.
+ it {is_expected.to have_header("X-NEXT-PAGE", 2)}
+ ^ Layout/SpaceInsideBlockBraces: Space missing inside }.
+ ^ Layout/SpaceInsideBlockBraces: Space missing inside {.
+ ```
+
+ That's the better way:
+
+ ```
+ # good
+ it "returns the second page",:aggregate_failures do
+ ^ Layout/SpaceAfterComma: Space missing after comma.
+ is_expected.to be_success
+ is_expected.to have_header("X-TOTAL-PAGES", 10)
+ is_expected.to have_header("X-NEXT-PAGE", 2)
+ end
+ ```
+
+ This cop supports auto-correct feature, so you can automatically refactor you legacy tests!
+
+ **NOTE**: auto-correction may break your tests (especially the ones using block-matchers, such as `change`).
+ MARKDOWN
+
+ assert_correction(<<~MARKDOWN)
+ # Custom RuboCop Cops
+
+ TestProf comes with the [RuboCop](https://github.com/bbatsov/rubocop) cops that help you write more performant tests.
+
+ To enable them:
+
+ - Require `test_prof/rubocop` in your RuboCop configuration:
+
+ ```yml
+ # .rubocop.yml
+ require:
+ - 'test_prof/rubocop'
+ ```
+
+ - Enable cops:
+
+ ```yml
+ RSpec/AggregateFailures:
+ Enabled: true
+ Include:
+ - 'spec/**/*.rb'
+ ```
+
+ ## RSpec/AggregateFailures
+
+ This cop encourages you to use one of the greatest features of the recent RSpec – aggregating failures within an example.
+
+ Instead of writing one example per assertion, you can group _independent_ assertions together, thus running all setup hooks only once. That can dramatically increase your performance (by reducing the total number of examples).
+
+ Consider an example:
+
+ ```ruby
+ # bad
+ it { is_expected.to be_success }
+ it { is_expected.to have_header("X-TOTAL-PAGES", 10) }
+ it { is_expected.to have_header("X-NEXT-PAGE", 2) }
+ ```
+
+ That's the better way:
+
+ ```
+ # good
+ it "returns the second page", :aggregate_failures do
+ is_expected.to be_success
+ is_expected.to have_header("X-TOTAL-PAGES", 10)
+ is_expected.to have_header("X-NEXT-PAGE", 2)
+ end
+ ```
+
+ This cop supports auto-correct feature, so you can automatically refactor you legacy tests!
+
+ **NOTE**: auto-correction may break your tests (especially the ones using block-matchers, such as `change`).
+ MARKDOWN
+ end
+
+ def test_multiple_invalid_snippets
+ skip("JRuby doesn't produce the second Lint/Syntax error") if RUBY_ENGINE == "jruby"
+ assert_offense(<<~MARKDOWN)
+ TestProf provides a built-in shared context for RSpec to profile examples individually:
+
+ ```ruby
+ it "is doing heavy stuff", :rprof do
+ { unclosed: hash
+ end
+ ^^^ Lint/Syntax: unexpected token kEND
+ ```
+
+ ### Configuration
+
+ The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers).
+
+ You can specify a printer through environment variable `TEST_RUBY_PROF`:
+
+ ```sh
+ TEST_RUBY_PROF=call_stack bundle exec rake test
+ ```
+
+ Or in your code:
+
+ ```ruby
+ TestProf::RubyProf.configure do |config|
+ config.printer = :call_stack
+ end
+ ```
+ ^{} Lint/Syntax: unexpected token $end
+ MARKDOWN
+ end
+
+ def test_multiple_invalid_snippets_file_no_warn
+ @config = { "Markdown" => { "WarnInvalid" => false } }
+ overwrite_config
+
+ assert_no_offenses(<<~MARKDOWN)
+ TestProf provides a built-in shared context for RSpec to profile examples individually:
+
+ ```ruby
+ it "is doing heavy stuff", :rprof do
+ ...
+ end
+ ```
+
+ ### Configuration
+
+ The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers).
+
+ You can specify a printer through environment variable `TEST_RUBY_PROF`:
+
+ ```sh
+ TEST_RUBY_PROF=call_stack bundle exec rake test
+ ```
+
+ Or in your code:
+
+ ```ruby
+ TestProf::RubyProf.configure do |config|
+ config.printer = :call_stack
+ end
+ ```
+ MARKDOWN
+ end
+
+ def test_in_flight_parsing
+ assert_no_offenses(<<~RUBY, "test.rb")
+ # frozen_string_literal: true
+
+ { complex_symbol: 0 }
+ RUBY
+ end
+
+ # rubocop:disable Layout/TrailingWhitespace
+ def test_non_code_offenses
+ assert_no_offenses(<<~MARKDOWN)
+ # No Code
+
+ Just a line with a trailining whitespace
+
+ MARKDOWN
+ end
+ # rubocop:enable Layout/TrailingWhitespace
+
+ def test_backticks_in_code
+ assert_offense(<<~MARKDOWN, marker: "##{RuboCop::Markdown::Preprocess::MARKER}")
+ ```ruby
+ `method_call
+ ```
+ _{marker} ^ Lint/Syntax: unexpected token tXSTRING_BEG
+
+
+ ```ruby
+ further_code("", '')
+ ```
+ MARKDOWN
+ end
+
+ def test_compound_snippets
+ assert_offense(<<~MARKDOWN)
+ Passing an array of symbols is also acceptable.
+
+ ```ruby
+ class Book
+ include ActiveModel::Validations
+
+ validates :title, presence:true, on:[:update, :ensure_title]
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Style/SymbolArray: Use `%i` or `%I` for an array of symbols.
+ ^ Layout/SpaceAfterColon: Space missing after colon.
+ ^ Layout/SpaceAfterColon: Space missing after colon.
+ end
+ ```
+
+ Assuming we have a model that's been saved in an instance variable named
+ `@article`, it looks like this:
+
+ ```html+erb
+ <% if @article.errors.any? %>
+
+
<%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:
+
+
+ <% @article.errors.each do |error| %>
+ - <%= error.full_message %>
+ <% end %>
+
+
+ <% end %>
+ ```
+
+ When triggered by an explicit context, validations are run for that context,
+ as well as any validations _without_ a context.
+
+ ```ruby
+ class Person < ApplicationRecord
+ validates :email, uniqueness: true, on: :account_setup
+ validates :age, numericality: true, on: :account_setup
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Layout/IndentationConsistency: Inconsistent indentation detected.
+ ^^^^^^ Layout/IndentationWidth: Use 2 (not 6) spaces for indentation.
+ validates :name, presence: true
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Layout/IndentationConsistency: Inconsistent indentation detected.
+ ^^^^ Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
+ end
+ ```
+
+ That's it.
+ MARKDOWN
+
+ assert_correction(<<~MARKDOWN)
+ Passing an array of symbols is also acceptable.
+
+ ```ruby
+ class Book
+ include ActiveModel::Validations
+
+ validates :title, presence: true, on: %i[update ensure_title]
+ end
+ ```
+
+ Assuming we have a model that's been saved in an instance variable named
+ `@article`, it looks like this:
+
+ ```html+erb
+ <% if @article.errors.any? %>
+
+
<%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:
+
+
+ <% @article.errors.each do |error| %>
+ - <%= error.full_message %>
+ <% end %>
+
+
+ <% end %>
+ ```
+
+ When triggered by an explicit context, validations are run for that context,
+ as well as any validations _without_ a context.
+
+ ```ruby
+ class Person < ApplicationRecord
+ validates :email, uniqueness: true, on: :account_setup
+ validates :age, numericality: true, on: :account_setup
+ validates :name, presence: true
+ end
+ ```
+
+ That's it.
+ MARKDOWN
+ end
+end
diff --git a/test/rubocop_assertions.rb b/test/rubocop_assertions.rb
new file mode 100644
index 0000000..ebf841f
--- /dev/null
+++ b/test/rubocop_assertions.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+# Extracted from https://github.com/rubocop/rubocop-minitest/blob/b446022ea09b3f5558df9c0106c9e714c6fc1ec5/lib/rubocop/minitest/assert_offense.rb
+# rubocop:disable all
+
+require 'rubocop/rspec/expect_offense'
+require 'rubocop/cop/legacy/corrector'
+
+module RuboCop
+ module Markdown
+ module AssertOffense
+ private
+
+ def format_offense(source, **replacements)
+ replacements.each do |keyword, value|
+ value = value.to_s
+ source = source.gsub("%{#{keyword}}", value)
+ .gsub("^{#{keyword}}", '^' * value.size)
+ .gsub("_{#{keyword}}", ' ' * value.size)
+ end
+ source
+ 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, **replacements)
+ setup_assertion
+
+ @cop.instance_variable_get(:@options)[:autocorrect] = true
+
+ source = format_offense(source, **replacements)
+ 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], configuration, 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
+
+ processed_source = RuboCop::ProcessedSource.new(source, ruby_version, file)
+
+ # Follow up https://github.com/rubocop/rubocop/pull/10987.
+ # When support for RuboCop 1.37.1 ends, this condition can be removed.
+ if processed_source.respond_to?(:config) && processed_source.respond_to?(:registry)
+ processed_source.config = configuration
+ processed_source.registry = registry
+ end
+
+ processed_source
+ end
+
+ def configuration
+ @configuration ||= if defined?(config)
+ config
+ else
+ RuboCop::Config.new({}, "#{Dir.pwd}/.rubocop.yml")
+ end
+ end
+
+ def registry
+ @registry ||= begin
+ cops = configuration.keys.map { |cop| RuboCop::Cop::Registry.global.find_by_cop_name(cop) }
+ cops << cop_class if defined?(cop_class) && !cops.include?(cop_class)
+ cops.compact!
+ RuboCop::Cop::Registry.new(cops)
+ end
+ end
+
+ def ruby_version
+ RuboCop::TargetRuby::DEFAULT_VERSION
+ end
+ end
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 3edc02e..fa89018 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -8,9 +8,11 @@
$stdout.puts "⚙️ Run rubocop with 'require: - rubocop-md' in .rubocop.yml"
end
+require "minitest/autorun"
+
require "rubocop"
+require "rubocop_assertions"
+require "markdown_assertions"
require "rubocop-md"
RuboCop::Markdown.config_store = RuboCop::ConfigStore.new
-
-require "minitest/autorun"