From 061e33165654c03a746d48c39f1e336abfaf7972 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:48:42 +0100 Subject: [PATCH 1/3] Hook into rejecting offenses earlier The runner is pretty high-level, if tests are running without involving the cli offenses aren't being rejected --- lib/rubocop/markdown/rubocop_ext.rb | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/rubocop/markdown/rubocop_ext.rb b/lib/rubocop/markdown/rubocop_ext.rb index ed918e1..17a950c 100644 --- a/lib/rubocop/markdown/rubocop_ext.rb +++ b/lib/rubocop/markdown/rubocop_ext.rb @@ -62,18 +62,6 @@ 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) @@ -86,6 +74,20 @@ def file_finished(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) From 8a32f40dfcf4f99de0aacb7ffa970f2842659845 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:15:49 +0100 Subject: [PATCH 2/3] Import rubocop test harness, use it This moves most tests to run inline instead of spawning an extra process. This isn't only faster but also much more explicit in what is actually being tested against. A few tests are left intact for issues that actually manifested during live rubocop runs. --- lib/rubocop/markdown/preprocess.rb | 8 +- lib/rubocop/markdown/rubocop_ext.rb | 2 +- test/fixtures/NON_CODE_OFFENSES.md | 3 - test/fixtures/backticks.md | 7 - .../configs/{no_autodetect.yml => config.yml} | 3 - ...th_require.yml => config_with_require.yml} | 3 - test/fixtures/configs/no_warn_invalid.yml | 5 - .../configs/no_warn_invalid_with_require.yml | 8 - test/fixtures/in_flight_parse.rb | 3 - test/fixtures/multiple_invalid_snippets.md | 25 -- .../multiple_invalid_snippets_unknown.md | 20 - test/fixtures/multiple_snippets.markdown | 52 --- test/integration_test.rb | 306 +-------------- test/markdown_assertions.rb | 96 +++++ test/offense_test.rb | 369 ++++++++++++++++++ test/rubocop_assertions.rb | 152 ++++++++ test/test_helper.rb | 6 +- 17 files changed, 638 insertions(+), 430 deletions(-) delete mode 100644 test/fixtures/NON_CODE_OFFENSES.md delete mode 100644 test/fixtures/backticks.md rename test/fixtures/configs/{no_autodetect.yml => config.yml} (55%) rename test/fixtures/configs/{no_autodetect_with_require.yml => config_with_require.yml} (67%) delete mode 100644 test/fixtures/configs/no_warn_invalid.yml delete mode 100644 test/fixtures/configs/no_warn_invalid_with_require.yml delete mode 100644 test/fixtures/in_flight_parse.rb delete mode 100644 test/fixtures/multiple_invalid_snippets.md delete mode 100644 test/fixtures/multiple_invalid_snippets_unknown.md delete mode 100644 test/fixtures/multiple_snippets.markdown create mode 100644 test/markdown_assertions.rb create mode 100644 test/offense_test.rb create mode 100644 test/rubocop_assertions.rb 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 17a950c..1283b97 100644 --- a/lib/rubocop/markdown/rubocop_ext.rb +++ b/lib/rubocop/markdown/rubocop_ext.rb @@ -67,7 +67,7 @@ def file_finished(file, offenses) # 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) 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 77a0511..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:

- - -
- <% 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:

- - -
- <% 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: "--autocorrect-all") - 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:

+ + +
+ <% 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:

+ + +
+ <% 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" From ef62838f49d1f567147e2eff6bfbd957413eca51 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:25:53 +0100 Subject: [PATCH 3/3] Bump the minimum supported rubocop version to 1.45 The new test harness has issues with older versions and it's planned to bump to 1.45 anyways for its introduction of 3rd party templating support --- .github/workflows/test.yml | 2 +- rubocop-md.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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"