Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor OutdatedGem; install FactoryBot for improved test coverage #5

Merged
merged 7 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require:
- rubocop-factory_bot
- rubocop-minitest
- rubocop-packaging
- rubocop-performance
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
source "https://rubygems.org"
gemspec

gem "factory_bot", "~> 6.3.0"
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗒️ I had to pin at 6.3 because newer versions of FactoryBot do not support Ruby 2.7, which is still part of our CI.

gem "minitest", "~> 5.11"
gem "minitest-snapshots", "~> 1.1"
gem "mocha", "~> 2.4"
gem "observer"
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗒️ FactoryBot 6.3 has an undeclared dependency on observer, which won't be part of Ruby 3.4. I had to make it an explicit dependency so that CI would pass on Ruby head.

gem "rake", "~> 13.0"

if RUBY_VERSION >= "3.3"
gem "mighty_test", "~> 0.3"
gem "rubocop", "1.64.1"
gem "rubocop-factory_bot", "2.26.1"
gem "rubocop-minitest", "0.35.0"
gem "rubocop-packaging", "0.5.2"
gem "rubocop-performance", "1.21.1"
Expand Down
2 changes: 1 addition & 1 deletion lib/bundle_update_interactive/cli/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def render_gem(name)

def render
lines = [render_header]
rows.each_key { |name| lines << render_gem(name) }
rows.keys.sort.each { |name| lines << render_gem(name) }
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗒️ I moved sorting behavior into CLI::Table, out of the core Report class. This makes the report implementation simpler, and sorting is naturally more of a UI (CLI in our case) concern.

lines.join("\n")
end

Expand Down
47 changes: 18 additions & 29 deletions lib/bundle_update_interactive/outdated_gem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@

module BundleUpdateInteractive
class OutdatedGem
attr_reader :current_lockfile_entry, :updated_lockfile_entry, :gemfile_groups
attr_writer :vulnerable
attr_accessor :name,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗒️ This refactor removes dependencies on lock file entries (which are deep data structures that in turn depend on internal Bundler classes) in favor of a flat set of simple PORO attributes.

:gemfile_groups,
:git_source_uri,
:current_version,
:current_git_version,
:updated_version,
:updated_git_version

def initialize(current_lockfile_entry:, updated_lockfile_entry:, gemfile_groups:)
@current_lockfile_entry = current_lockfile_entry
@updated_lockfile_entry = updated_lockfile_entry
@gemfile_groups = gemfile_groups
@changelog_locator = ChangelogLocator.new
attr_writer :rubygems_source, :vulnerable

def initialize(**attrs)
@vulnerable = nil
end
@changelog_locator = ChangelogLocator.new

def name
current_lockfile_entry.name
attrs.each { |name, value| public_send(:"#{name}=", value) }
end

def semver_change
Expand All @@ -25,13 +27,17 @@ def vulnerable?
@vulnerable
end

def rubygems_source?
@rubygems_source
end

def changelog_uri
return @changelog_uri if defined?(@changelog_uri)

@changelog_uri =
if git_version_changed?
"https://github.com/#{github_repo}/compare/#{current_git_version}...#{updated_git_version}"
elsif updated_lockfile_entry.rubygems_source?
elsif rubygems_source?
changelog_locator.find_changelog_uri(name: name, version: updated_version.to_s)
else
begin
Expand All @@ -42,22 +48,6 @@ def changelog_uri
end
end

def current_version
current_lockfile_entry.version
end

def updated_version
updated_lockfile_entry.version
end

def current_git_version
current_lockfile_entry.git_version
end

def updated_git_version
updated_lockfile_entry.git_version
end

def git_version_changed?
current_git_version && updated_git_version && current_git_version != updated_git_version
end
Expand All @@ -69,8 +59,7 @@ def git_version_changed?
def github_repo
return nil unless updated_git_version

updated_lockfile_entry.git_source_uri.to_s[%r{^(?:git@github.com:|https://github.com/)([^/]+/[^/]+?)(:?\.git)?(?:$|/)}i,
1]
git_source_uri.to_s[%r{^(?:git@github.com:|https://github.com/)([^/]+/[^/]+?)(:?\.git)?(?:$|/)}i, 1]
end
end
end
35 changes: 23 additions & 12 deletions lib/bundle_update_interactive/report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,12 @@ def generate

def initialize(gemfile:, current_lockfile:, updated_lockfile:)
@current_lockfile = current_lockfile
outdated_names = current_lockfile.entries.each_with_object([]) do |current_entry, arr|
updated_entry = updated_lockfile[current_entry.name]
arr << current_entry.name if current_entry.older_than?(updated_entry)
end
@outdated_gems ||= outdated_names.sort.each_with_object({}) do |name, hash|
hash[name] = OutdatedGem.new(
current_lockfile_entry: current_lockfile[name],
updated_lockfile_entry: updated_lockfile[name],
gemfile_groups: gemfile[name]&.groups
)
@outdated_gems ||= current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
name = current_lockfile_entry.name
updated_lockfile_entry = updated_lockfile[name]
next unless current_lockfile_entry.older_than?(updated_lockfile_entry)

hash[name] = build_outdated_gem(current_lockfile_entry, updated_lockfile_entry, gemfile[name]&.groups)
end.freeze
end

Expand All @@ -39,12 +35,14 @@ def [](gem_name)
end

def updateable_gems
outdated_gems.reject { |_, gem| gem.current_lockfile_entry.exact_dependency? }
@updateable_gems ||= outdated_gems.reject do |name, _|
current_lockfile[name].exact_dependency?
end.freeze
end

def expand_gems_with_exact_dependencies(*gem_names)
gem_names.flatten!
gem_names.flat_map { [_1, *outdated_gems[_1].current_lockfile_entry.exact_dependencies] }.uniq
gem_names.flat_map { |name| [name, *current_lockfile[name].exact_dependencies] }.uniq
end

def scan_for_vulnerabilities!
Expand All @@ -68,5 +66,18 @@ def bundle_update!(*gem_names)
private

attr_reader :current_lockfile

def build_outdated_gem(current_lockfile_entry, updated_lockfile_entry, gemfile_groups)
OutdatedGem.new(
name: current_lockfile_entry.name,
gemfile_groups: gemfile_groups,
rubygems_source: updated_lockfile_entry.rubygems_source?,
git_source_uri: updated_lockfile_entry.git_source_uri&.to_s,
current_version: current_lockfile_entry.version.to_s,
current_git_version: current_lockfile_entry.git_version&.strip,
updated_version: updated_lockfile_entry.version.to_s,
updated_git_version: updated_lockfile_entry.git_version&.strip
)
end
end
end
65 changes: 65 additions & 0 deletions test/bundle_update_interactive/cli/row_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

require "test_helper"

class BundleUpdateInteractive::CLI
class RowTest < Minitest::Test
def test_formatted_gem_name_for_vulnerable_gem_is_red_on_white
outdated_gem = build(:outdated_gem, name: "rails", vulnerable: true)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗒️ These tests were super easy to write, because I can now use FactoryBot's build to construct the exact scenarios I needed.

row = Row.new(outdated_gem)

assert_equal "\e[37;41mrails\e[0m", row.formatted_gem_name
end

def test_formatted_updated_version_highlights_diff_in_cyan_regardless_of_semver_change
outdated_gem = build(
:outdated_gem,
current_version: "7.0.5",
updated_version: "7.1.2",
current_git_version: "a1a1207",
updated_git_version: "0e5bafe"
)
row = Row.new(outdated_gem)

assert_equal "7.\e[36m1.2\e[0m \e[36m0e5bafe\e[0m", row.formatted_updated_version
end

def test_name_and_version_red_if_major_semver_change
outdated_gem = build(:outdated_gem, name: "rails", current_version: "6.1.2", updated_version: "7.0.3")
row = Row.new(outdated_gem)

assert_equal "\e[31mrails\e[0m", row.formatted_gem_name
assert_equal "\e[31m7.0.3\e[0m", row.formatted_updated_version
end

def test_name_and_version_yellow_if_minor_semver_change
outdated_gem = build(:outdated_gem, name: "rails", current_version: "7.0.3", updated_version: "7.1.0")
row = Row.new(outdated_gem)

assert_equal "\e[33mrails\e[0m", row.formatted_gem_name
assert_equal "7.\e[33m1.0\e[0m", row.formatted_updated_version
end

def test_name_and_version_green_if_patch_semver_change
outdated_gem = build(:outdated_gem, name: "rails", current_version: "7.0.3", updated_version: "7.0.4")
row = Row.new(outdated_gem)

assert_equal "\e[32mrails\e[0m", row.formatted_gem_name
assert_equal "7.0.\e[32m4\e[0m", row.formatted_updated_version
end

def test_formatted_gemfile_groups_handles_nil_groups
outdated_gem = build(:outdated_gem, gemfile_groups: nil)
row = Row.new(outdated_gem)

assert_nil row.formatted_gemfile_groups
end

def test_formatted_gemfile_groups_returns_comma_separated_symbols
outdated_gem = build(:outdated_gem, gemfile_groups: %i[development test])
row = Row.new(outdated_gem)

assert_equal ":development, :test", row.formatted_gemfile_groups
end
end
end
14 changes: 14 additions & 0 deletions test/factories/outdated_gems.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

FactoryBot.define do
factory :outdated_gem, class: "BundleUpdateInteractive::OutdatedGem" do
current_git_version { nil }
current_version { "7.0.3" }
git_source_uri { nil }
name { "rails" }
rubygems_source { true }
updated_git_version { nil }
updated_version { "7.1.0" }
vulnerable { false }
end
end
9 changes: 9 additions & 0 deletions test/support/factory_bot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require "factory_bot"

FactoryBot.find_definitions

class Minitest::Test
include FactoryBot::Syntax::Methods
end
2 changes: 2 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
require "mocha/minitest"

BundleUpdateInteractive.pastel = Pastel.new(enabled: true)

Dir[File.expand_path("support/**/*.rb", __dir__)].sort.each { |rb| require(rb) }