diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock new file mode 100644 index 0000000..cdb844b --- /dev/null +++ b/.rubocop_gradual.lock @@ -0,0 +1,23 @@ +{ + "spec/gem_bench/team_spec.rb:1266648289": [ + [39, 7, 21, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 4057686576], + [39, 7, 463, "RSpec/ExampleLength: Example has too many lines. [12/5]", 717044346], + [238, 9, 22, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 837383149], + [247, 9, 39, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3959041976], + [258, 11, 20, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 608832949], + [258, 11, 969, "RSpec/ExampleLength: Example has too many lines. [15/5]", 3322102119], + [276, 19, 17, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1188500192], + [281, 13, 20, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 608832949], + [281, 13, 1001, "RSpec/ExampleLength: Example has too many lines. [15/5]", 252278759], + [302, 15, 20, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 608832949], + [302, 15, 1174, "RSpec/ExampleLength: Example has too many lines. [16/5]", 1786252903], + [328, 9, 18, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 2487416927], + [342, 9, 22, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 837383149], + [351, 9, 39, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3959041976], + [362, 11, 20, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 608832949], + [362, 11, 956, "RSpec/ExampleLength: Example has too many lines. [15/5]", 1208468539], + [383, 13, 20, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 608832949], + [383, 13, 1083, "RSpec/ExampleLength: Example has too many lines. [16/5]", 3055949499], + [408, 9, 18, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 2487416927] + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 374a54b..84b4391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,26 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2. ### Fixed ### Removed +## [2.0.5] SEP.21.2024 +- COVERAGE: 99.80% -- 495/496 lines in 9 files +- BRANCH COVERAGE: 94.35% -- 167/177 branches in 9 files +- 58.87% documented +### Added +- More specs +- More documentation +- Even closer to 100% test coverage +### Fixed +- Documentation errors +- Minor improvements to logic and performance (a bit more idiomatic Ruby) + ## [2.0.4] SEP.20.2024 - COVERAGE: 98.19% -- 488/497 lines in 9 files - BRANCH COVERAGE: 88.95% -- 161/181 branches in 9 files - 58.06% documented ### Added -- More Documentation +- More documentation - Almost 100% test coverage -- Thread Safety (removed `GemBench.roster`, which was effectively never used internally) +- Thread safety (removed `GemBench.roster`, which was effectively never used internally) - Performance improvements ### Fixed - Can now handle more variations of Ruby syntax in the Gemfile analyzer diff --git a/Gemfile b/Gemfile index 43e0ca4..1320318 100644 --- a/Gemfile +++ b/Gemfile @@ -10,5 +10,8 @@ gem <<~GEM_NAME.chomp pry-byebug GEM_NAME +# Need this to be loaded by bundler, to exercise the "excluded" logic, since faker is excluded. +gem "faker" + # Specify your gem's dependencies in gem_bench.gemspec gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 20c4de0..db5259a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,13 +10,18 @@ GEM specs: ansi (1.5.0) ast (2.4.2) - awesome_print (1.9.2) + attr_extras (7.1.0) backports (3.25.0) byebug (11.1.3) coderay (1.1.3) + concurrent-ruby (1.3.4) diff-lcs (1.5.1) diffy (3.4.2) docile (1.4.1) + faker (3.4.2) + i18n (>= 1.8.11, < 2) + i18n (1.14.6) + concurrent-ruby (~> 1.0) json (2.7.2) kettle-soup-cover (1.0.4) simplecov (~> 0.22) @@ -30,11 +35,14 @@ GEM language_server-protocol (3.17.0.3) lint_roller (1.1.0) method_source (1.1.0) + optimist (3.1.0) ostruct (0.6.0) parallel (1.26.3) parser (3.3.5.0) ast (~> 2.4.1) racc + patience_diff (1.2.0) + optimist (~> 3.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -141,6 +149,10 @@ GEM standard-custom (>= 1.0.2, < 2) standard-performance (>= 1.3.1, < 2) version_gem (>= 1.1.4, < 3) + super_diff (0.12.1) + attr_extras (>= 6.2.4) + diff-lcs + patience_diff terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) unicode-display_width (2.6.0) @@ -156,9 +168,9 @@ PLATFORMS ruby DEPENDENCIES - awesome_print (~> 1.9) bundler byebug (>= 2.0.3) + faker gem_bench! kettle-soup-cover (~> 1.0, >= 1.0.2) method_source (>= 1.1.0) @@ -170,6 +182,7 @@ DEPENDENCIES rubocop-packaging (~> 0.5, >= 0.5.2) rubocop-rspec (~> 3.0) standard (~> 1.40) + super_diff (~> 0.12, >= 0.12.1) yard (~> 0.9, >= 0.9.34) yard-junk (~> 0.0.10) diff --git a/gem_bench.gemspec b/gem_bench.gemspec index c81ed43..9543dac 100644 --- a/gem_bench.gemspec +++ b/gem_bench.gemspec @@ -62,11 +62,12 @@ Gem::Specification.new do |spec| spec.add_development_dependency("kettle-soup-cover", "~> 1.0", ">= 1.0.2") # Unit tests - spec.add_development_dependency("awesome_print", "~> 1.9") + spec.add_development_dependency("faker", "~> 3.4", "3.4.2") spec.add_development_dependency("method_source", ">= 1.1.0") spec.add_development_dependency("rake", ">= 10") spec.add_development_dependency("rspec", "~> 3.13") spec.add_development_dependency("rspec-block_is_expected", "~> 1.0", ">= 1.0.6") + spec.add_development_dependency("super_diff", "~> 0.12", ">= 0.12.1") # Linting spec.add_development_dependency("rubocop-lts", "~> 10.1") # Lint & Style Support for Ruby 2.3+ diff --git a/gemfiles/ancient.gemfile b/gemfiles/ancient.gemfile index c8aa4d4..857e3b2 100644 --- a/gemfiles/ancient.gemfile +++ b/gemfiles/ancient.gemfile @@ -12,10 +12,12 @@ source "https://rubygems.org" gem "bundler", ">= 1.14" gem "version_gem", "~> 1.1", ">= 1.1.4" +gem "faker", "~> 3.4", ">= 3.4.2" gem "method_source", ">= 1.1.0" gem "rake" gem "rspec" gem "rspec-block_is_expected" +gem "super_diff" # For debugging, casecmp is only available in Ruby 2.4+ if RUBY_VERSION > "2.4" && ENV.fetch("DEBUG", "false").casecmp?("true") diff --git a/lib/gem_bench/team.rb b/lib/gem_bench/team.rb index b3d09fd..7e1f12f 100644 --- a/lib/gem_bench/team.rb +++ b/lib/gem_bench/team.rb @@ -6,6 +6,13 @@ module GemBench # - if you are in a rails console, and want to evaluate the Gemfile of the Rails app, that's great! # - if you are in a context with no Gemfile loaded, or a different Gemfile loaded than the one you want to evaluate, # this class may not give sensible results. This is because it checks loaded gems via RubyGems and Bundler. + # + # Terminology: + # + # starter: a gem that needs to be loaded when bundler normally loads gems + # + # bencher: a gem that can, or should, have require: false to delay loading until after bootstrap + # class Team EXCLUDE = %w[ bundler @@ -53,8 +60,8 @@ def initialize(**options) ) @exclude_file_pattern_regex_proc = options[:exclude_file_pattern_regex_proc].respond_to?(:call) ? options[:exclude_file_pattern_regex_proc] : GemBench::EXCLUDE_FILE_PATTERN_REGEX_PROC # Among the loaded gems there may be some that did not need to be. - @excluded, @all = @scout.loaded_gems.partition { |x| EXCLUDE.include?(x[0]) } - exclusions = " + #{excluded.length} loaded gems which GemBench is configured to ignore.\n" if @excluded.length > 0 + exclude! + exclusions = " + #{excluded.length} loaded gems which GemBench is configured to ignore.\n" if excluded.any? @starters = [] @benchers = [] @current_gemfile_suggestions = [] @@ -70,11 +77,11 @@ def initialize(**options) false end puts "[GemBench] Will search for gems in #{gem_paths.inspect}\n#{if benching? - @scout.check_gemfile? ? "[GemBench] Will check Gemfile at #{gemfile_path}.\n" : "[GemBench] No Gemfile found.\n" + check_gemfile? ? "[GemBench] Will check Gemfile at #{gemfile_path}.\n" : "[GemBench] No Gemfile found.\n" else "" end}#{bad_ideas ? "[GemBench] Will show bad ideas. Be Careful.\n" : ""}[GemBench] Detected #{all.length} loaded gems#{exclusions}" - compare_gemfile if benching? && @scout.check_gemfile? + compare_gemfile if benching? && check_gemfile? self.print if verbose end @@ -82,6 +89,7 @@ def list_starters(format: :name) starters.map { |starter| starter.to_s(format) } end + # @return void def print string = "" if all.empty? @@ -92,7 +100,7 @@ def print else "[GemBench] Found no gems containing #{look_for_regex} in Ruby code.\n" end - elsif starters.length > 0 + else string << "\n#{GemBench::USAGE}" unless check_gemfile? string << if benching? "[GemBench] We found a Rails::Railtie or Rails::Engine in the following files. However, it is possible that there are false positives, so you may want to verify that this is the case.\n\n" @@ -116,18 +124,15 @@ def print starters.each_with_index do |starter, index| string << "#{starter.info(index + 1)}\n" end - if extra_verbose? && !benching? && benchers.length > 0 + if extra_verbose? && !benching? && benchers.any? string << "[GemBench] #{benchers.length} out of #{all.length} evaluated gems did not contain #{look_for_regex}. They are:\n" benchers.each_with_index do |bencher, index| string << "#{bencher.info(index + 1)}\n" end end - else - string << "[GemBench] Congrats! All gems appear clean.\n" - string << "\n#{GemBench::USAGE}" unless check_gemfile? end if check_gemfile? && benching? - if current_gemfile_suggestions.length > 0 + if current_gemfile_suggestions.any? string << "[GemBench] Evaluated #{all.length} gems and Gemfile at #{gemfile_path}.\n[GemBench] Here are #{current_gemfile_suggestions.length} suggestions for improvement:\n" current_gemfile_suggestions.each_with_index do |player, index| string << "#{player.suggest(index + 1)}\n" @@ -159,7 +164,7 @@ def nothing def prepare_bad_ideas string = "" - if benchers.length > 0 + if benchers.any? gemfile_instruction = check_gemfile? ? "" : "To safely evaluate a Gemfile:\n\t1. Make sure you are in the root of a project with a Gemfile\n\t2. Make sure the gem is actually a dependency in the Gemfile\n" string << "[GemBench] Evaluated #{all.length} loaded gems and found #{benchers.length} which may be able to skip boot loading (require: false).\n*** => WARNING <= ***: Be careful adding non-primary dependencies to your Gemfile as it is generally a bad idea.\n#{gemfile_instruction}" benchers.each_with_index do |player, index| @@ -223,6 +228,10 @@ def add_to_roster(player) private + def exclude! + self.excluded, self.all = loaded_gems.partition { |x| EXCLUDE.include?(x[0]) } + end + def extra_verbose? verbose == "extra" end diff --git a/spec/gem_bench/scout_spec.rb b/spec/gem_bench/scout_spec.rb index 458c039..1cf9015 100644 --- a/spec/gem_bench/scout_spec.rb +++ b/spec/gem_bench/scout_spec.rb @@ -165,6 +165,7 @@ [ "# For complexity!\n", "# (this syntax is not supported by gem_bench, but also shouldn't make it blow up)\n", + %(# Need this to be loaded by bundler, to exercise the "excluded" logic, since faker is excluded.\n), "# Specify your gem's dependencies in gem_bench.gemspec\n", ], ) @@ -204,6 +205,7 @@ [ "# For complexity!\n", "# (this syntax is not supported by gem_bench, but also shouldn't make it blow up)\n", + %(# Need this to be loaded by bundler, to exercise the "excluded" logic, since faker is excluded.\n), "# Specify your gem's dependencies in gem_bench.gemspec\n", ], ) @@ -226,6 +228,7 @@ [ "# For complexity!\n", "# (this syntax is not supported by gem_bench, but also shouldn't make it blow up)\n", + %(# Need this to be loaded by bundler, to exercise the "excluded" logic, since faker is excluded.\n), "# Specify your gem's dependencies in gem_bench.gemspec\n", ], ) diff --git a/spec/gem_bench/team_spec.rb b/spec/gem_bench/team_spec.rb index ac21bf2..9f31d5e 100644 --- a/spec/gem_bench/team_spec.rb +++ b/spec/gem_bench/team_spec.rb @@ -28,6 +28,29 @@ block_is_expected.to not_raise_error end end + + context "when excluded" do + it "excludes the excluded gems" do + expect(instance.excluded.map(&:first).sort).to eq(%w(bundler faker gem_bench)) + end + end + + context "when not excluded" do + it "excludes nothing" do + allow(GemBench::Scout).to receive(:new).and_return( + instance_double( + GemBench::Scout, + "instance.scout", + loaded_gems: [["rspec", "4.0.0"]], + gem_paths: [], + gemfile_path: "", + "check_gemfile?": false, + ), + ) + expect(instance.excluded.map(&:first).sort).to be_empty + expect(GemBench::Scout).to have_received(:new) + end + end end describe "#print" do @@ -205,6 +228,190 @@ end end end + + context "with no starters" do + let(:scout) { GemBench::Scout.new(check_gemfile: check_gemfile, **scout_options) } + let(:scout_options) { {gemfile_path: File.join(File.dirname(__FILE__), "..", "support", "no_starters_benchable.gemfile")} } + let(:check_gemfile) { nil } # when nil it can default to true + + context "when :all empty" do + it "evaluates nothing" do + allow(scout).to receive(:loaded_gems).and_return([]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output(include("[GemBench] No gems were evaluated by GemBench.\n")).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + end + + context "when :starters empty" do + it "finds no gems to load at boot time" do + allow(scout).to receive(:loaded_gems).and_return([["test-unit", "3.6.2"]]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output(include("[GemBench] Found no gems that need to load at boot time.\n")).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + + context "when bad ideas" do + let(:options) { {bad_ideas: true} } + let(:check_gemfile) { false } + + it "shows bad ideas" do + allow(scout).to receive(:loaded_gems).and_return([["test-unit", "3.6.2"]]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output( + include( + <<~OUT.chomp, + [GemBench] Evaluated 1 loaded gems and found 1 which may be able to skip boot loading (require: false). + *** => WARNING <= ***: Be careful adding non-primary dependencies to your Gemfile as it is generally a bad idea. + To safely evaluate a Gemfile: + \t1. Make sure you are in the root of a project with a Gemfile + \t2. Make sure the gem is actually a dependency in the Gemfile + \t[BE CAREFUL] 1) gem 'test-unit', '~> 3.6', require: false + OUT + ), + ).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + + context "already benched" do + let(:scout_options) { {gemfile_path: File.join(File.dirname(__FILE__), "..", "support", "no_starters_benched.gemfile")} } + let(:options) { {bad_ideas: true} } + let(:check_gemfile) { false } + + it "shows bad ideas" do + allow(scout).to receive(:loaded_gems).and_return([["test-unit", "3.6.2"]]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output( + include( + <<~OUT.chomp, + [GemBench] Evaluated 1 loaded gems and found 1 which may be able to skip boot loading (require: false). + *** => WARNING <= ***: Be careful adding non-primary dependencies to your Gemfile as it is generally a bad idea. + To safely evaluate a Gemfile: + \t1. Make sure you are in the root of a project with a Gemfile + \t2. Make sure the gem is actually a dependency in the Gemfile + \t[BE CAREFUL] 1) gem 'test-unit', '~> 3.6', require: false + OUT + ), + ).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + + context "when check gemfile" do + let(:check_gemfile) { true } + + it "shows bad ideas" do + allow(scout).to receive(:loaded_gems).and_return([["test-unit", "3.6.2"]]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output( + include( + <<~OUT.chomp, + [GemBench] Will show bad ideas. Be Careful. + [GemBench] Detected 1 loaded gems + [GemBench] Found no gems that need to load at boot time. + [GemBench] Evaluated 1 gems against your Gemfile but found no primary dependencies which can safely skip require on boot (require: false). + [GemBench] Evaluated 1 loaded gems and found 1 which may be able to skip boot loading (require: false). + *** => WARNING <= ***: Be careful adding non-primary dependencies to your Gemfile as it is generally a bad idea. + \t[BE CAREFUL] 1) gem 'test-unit', '~> 3.6', require: false + OUT + ), + ).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + end + end + end + end + + context "when :look_for_regex" do + let(:options) { {look_for_regex: /luke-i-am-your-mother/} } + + it "tries to find" do + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output(include("[GemBench] Found no gems containing (?-mix:luke-i-am-your-mother) in Ruby code.\n")).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + end + end + + context "with no benchers" do + let(:scout) { GemBench::Scout.new(check_gemfile: check_gemfile, **scout_options) } + let(:scout_options) { {gemfile_path: File.join(File.dirname(__FILE__), "..", "support", "no_benchers.gemfile")} } + let(:check_gemfile) { nil } # when nil it can default to true + + context "when :all empty" do + it "evaluates nothing" do + allow(scout).to receive(:loaded_gems).and_return([]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output(include("[GemBench] No gems were evaluated by GemBench.\n")).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + end + + context "when :starters empty" do + it "finds no gems to load at boot time" do + allow(scout).to receive(:loaded_gems).and_return([["rspec", "3.13.0"]]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output(include("[GemBench] Found no gems that need to load at boot time.\n")).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + + context "when bad ideas" do + let(:options) { {bad_ideas: true} } + let(:check_gemfile) { false } + + it "shows bad ideas" do + allow(scout).to receive(:loaded_gems).and_return([["rspec", "3.13.0"]]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output( + include( + <<~OUT.chomp, + [GemBench] Evaluated 1 loaded gems and found 1 which may be able to skip boot loading (require: false). + *** => WARNING <= ***: Be careful adding non-primary dependencies to your Gemfile as it is generally a bad idea. + To safely evaluate a Gemfile: + \t1. Make sure you are in the root of a project with a Gemfile + \t2. Make sure the gem is actually a dependency in the Gemfile + \t[BE CAREFUL] 1) rspec had no files to evaluate. + OUT + ), + ).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + + context "when check gemfile" do + let(:check_gemfile) { true } + + it "shows bad ideas" do + allow(scout).to receive(:loaded_gems).and_return([["rspec", "3.13.0"]]) + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output( + include( + <<~OUT.chomp, + [GemBench] Will show bad ideas. Be Careful. + [GemBench] Detected 1 loaded gems + [GemBench] Found no gems that need to load at boot time. + [GemBench] Evaluated 1 gems and Gemfile at /Users/pboling/src/my/gem_bench/spec/gem_bench/../support/no_benchers.gemfile. + [GemBench] Here are 1 suggestions for improvement: + \t[SUGGESTION] 1) rspec had no files to evaluate. + [GemBench] Evaluated 1 gems against your Gemfile but found no primary dependencies which can safely skip require on boot (require: false). + OUT + ), + ).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + end + end + end + + context "when :look_for_regex" do + let(:options) { {look_for_regex: /luke-i-am-your-mother/} } + + it "tries to find" do + allow(GemBench::Scout).to receive(:new).and_return(scout) + block_is_expected.to not_raise_error.and output(include("[GemBench] Found no gems containing (?-mix:luke-i-am-your-mother) in Ruby code.\n")).to_stdout + expect(GemBench::Scout).to have_received(:new) + end + end + end end describe "#list_starters" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 60d4d22..c86f9c3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,6 @@ # External library dependencies require "version_gem/ruby" +require "super_diff/rspec" # RSpec Configs require "config/byebug" diff --git a/spec/support/no_benchers.gemfile b/spec/support/no_benchers.gemfile new file mode 100644 index 0000000..2118eb2 --- /dev/null +++ b/spec/support/no_benchers.gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +# starter: a gem that needs to be loaded when bundler normally loads gems +# bencher: a gem that can, or should, have require: false to delay loading until after bootstrap +gem "rspec", "~> 3.13" diff --git a/spec/support/no_starters_benchable.gemfile b/spec/support/no_starters_benchable.gemfile new file mode 100644 index 0000000..ec93d86 --- /dev/null +++ b/spec/support/no_starters_benchable.gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +# starter: a gem that needs to be loaded when bundler normally loads gems +# bencher: a gem that can, or should, have require: false to delay loading until after bootstrap +gem "test-unit", "~> 3.6" diff --git a/spec/support/no_starters_benched.gemfile b/spec/support/no_starters_benched.gemfile new file mode 100644 index 0000000..ac55a20 --- /dev/null +++ b/spec/support/no_starters_benched.gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +# starter: a gem that needs to be loaded when bundler normally loads gems +# bencher: a gem that can, or should, have require: false to delay loading until after bootstrap +gem "test-unit", "~> 3.6", require: false