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:

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

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