Skip to content

Commit

Permalink
Include Ruby versions in health score calculation (#5)
Browse files Browse the repository at this point in the history
* Add ruby versions module

* Fetch ruby versions if ruby is a dependency

* Add ruby to dependencies if available

* Set default ruby priority to 10

* Ensure only semver-compliant versions are selected

* Refactor

* Refactor with an early return

* Fix a regression
  • Loading branch information
lovro-bikic authored Oct 25, 2024
1 parent 3083ab8 commit 9d9bd95
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 44 deletions.
2 changes: 1 addition & 1 deletion lib/polariscope/scanner/calculation_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Polariscope
module Scanner
class CalculationContext
DEPENDENCY_PRIORITIES = { rails: 10.0 }.freeze
DEPENDENCY_PRIORITIES = { ruby: 10.0, rails: 10.0 }.freeze
GROUP_PRIORITIES = { default: 2.0, production: 2.0 }.freeze
DEFAULT_DEPENDENCY_PRIORITY = 1.0

Expand Down
16 changes: 15 additions & 1 deletion lib/polariscope/scanner/dependency_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def no_dependencies?
end

def dependencies
bundle_definition.dependencies
@dependencies ||= dependencies_with_ruby
end

def dependency_versions(dependency)
Expand Down Expand Up @@ -67,9 +67,23 @@ def bundle_definition
end

def current_dependency_version(dependency)
return ruby_scanner.version if dependency.name == GemVersions::RUBY_NAME

specs.find { |spec| dependency.name == spec.name }.version
end

def dependencies_with_ruby
return installed_dependencies unless ruby_scanner.version

installed_dependencies + [Bundler::Dependency.new(GemVersions::RUBY_NAME, false)]
end

def installed_dependencies
spec_names = specs.to_set(&:name)

bundle_definition.dependencies.select { |dependency| spec_names.include?(dependency.name) }
end

def specs
bundle_definition.locked_gems.specs
end
Expand Down
9 changes: 9 additions & 0 deletions lib/polariscope/scanner/gem_versions.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# frozen_string_literal: true

require_relative 'ruby_versions'

require 'set'

module Polariscope
module Scanner
class GemVersions
RUBY_NAME = 'ruby'

def initialize(dependency_names, spec_type:)
@dependency_names = dependency_names.to_set
@spec_type = spec_type
@gem_versions = Hash.new { |h, k| h[k] = Set.new }

fetch_gems
fetch_ruby_versions if dependency_names.include?(RUBY_NAME)
end

def versions_for(gem_name)
Expand All @@ -23,6 +28,10 @@ def versions_for(gem_name)
attr_reader :spec_type
attr_reader :gem_versions

def fetch_ruby_versions
gem_versions[RUBY_NAME] = RubyVersions.available_versions
end

def fetch_gems
gem_tuples.each { |(name_tuple, _)| gem_versions[name_tuple.name] << name_tuple.version }
end
Expand Down
29 changes: 29 additions & 0 deletions lib/polariscope/scanner/ruby_versions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

require 'open-uri'

module Polariscope
module Scanner
module RubyVersions
VERSIONS_INDEX_FILE_URL = 'https://cache.ruby-lang.org/pub/ruby/index.txt'
MINIMUM_RUBY_VERSION = Gem::Version.new('1.0.0')
OPEN_TIMEOUT = 5
READ_TIMEOUT = 5

module_function

def available_versions # rubocop:disable Metrics/AbcSize
URI
.parse(VERSIONS_INDEX_FILE_URL)
.open(open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT, &:readlines)
.drop(1) # header row
.map { |line| line.split("\t").first.sub('ruby-', 'ruby ') } # ruby-2.3.4 -> ruby 2.3.4
.filter_map { |ruby_version| Bundler::RubyVersion.from_string(ruby_version)&.gem_version }
.select { |gem_version| gem_version >= MINIMUM_RUBY_VERSION && gem_version.segments.size == 3 }
.to_set
rescue Timeout::Error
Set.new
end
end
end
end
1 change: 1 addition & 0 deletions spec/files/gemfile.lock_with_dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ DEPENDENCIES
rspec-rails (~> 5)
shrine
sidekiq (~> 6)
tzinfo-data

BUNDLED WITH
2.5.17
2 changes: 1 addition & 1 deletion spec/files/gemfile.lock_with_ruby_version
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ DEPENDENCIES
sidekiq (~> 6)

RUBY VERSION
ruby 3.0.0p100
ruby 2.5.0p100

BUNDLED WITH
2.5.17
3 changes: 3 additions & 0 deletions spec/files/gemfile_with_dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ gem 'sidekiq', '~> 6'
group :development, :test do
gem 'rspec-rails', '~> 5'
end

# dependency which is installed only on some platforms
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
2 changes: 1 addition & 1 deletion spec/files/gemfile_with_ruby_version
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.0.0'
ruby '2.5.0'

gem 'rails', '~> 7.0.0.0'
gem 'shrine'
Expand Down
118 changes: 81 additions & 37 deletions spec/lib/polariscope/scanner/dependency_context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,55 +54,99 @@
end

describe '#dependencies' do
let(:opts) do
{
gemfile_content: File.read('spec/files/gemfile_with_dependencies'),
gemfile_lock_content: File.read('spec/files/gemfile.lock_with_dependencies')
}
context "when gemfile lock doesn't have a ruby version" do
let(:opts) do
{
gemfile_content: File.read('spec/files/gemfile_with_dependencies'),
gemfile_lock_content: File.read('spec/files/gemfile.lock_with_dependencies')
}
end

it 'returns dependencies without ruby' do
expect(dependency_context.dependencies.map(&:class).uniq).to contain_exactly(Bundler::Dependency)
expect(dependency_context.dependencies.map(&:name)).to contain_exactly('rails', 'shrine', 'sidekiq',
'rspec-rails')
end
end

it 'returns dependencies' do
expect(dependency_context.dependencies.map(&:class).uniq).to contain_exactly(Bundler::Dependency)
expect(dependency_context.dependencies.map(&:name)).to contain_exactly('rails', 'shrine', 'sidekiq',
'rspec-rails')
context 'when gemfile lock has a ruby version' do
let(:opts) do
{
gemfile_content: File.read('spec/files/gemfile_with_ruby_version'),
gemfile_lock_content: File.read('spec/files/gemfile.lock_with_ruby_version')
}
end

it 'returns dependencies with ruby' do
expect(dependency_context.dependencies.map(&:class).uniq).to contain_exactly(Bundler::Dependency)
expect(dependency_context.dependencies.map(&:name)).to contain_exactly('rails', 'shrine', 'sidekiq',
'rspec-rails', 'ruby')
end
end
end

describe '#dependency_versions' do
let(:opts) do
{
gemfile_content: File.read('spec/files/gemfile_with_dependencies'),
gemfile_lock_content: File.read('spec/files/gemfile.lock_with_dependencies')
}
end
context "when gemfile lock doesn't have a ruby version" do
let(:opts) do
{
gemfile_content: File.read('spec/files/gemfile_with_dependencies'),
gemfile_lock_content: File.read('spec/files/gemfile.lock_with_dependencies')
}
end

let(:dependency) { Bundler::Dependency.new('rails', false) }

before do
gem_tuples = [
[
Gem::NameTuple.new('rails', Gem::Version.new('5.0.0')),
anything
],
[
Gem::NameTuple.new('rails', Gem::Version.new('6.0.0')),
anything
],
[
Gem::NameTuple.new('rails', Gem::Version.new('7.0.0')),
anything
let(:dependency) { Bundler::Dependency.new('rails', false) }

before do
gem_tuples = [
[
Gem::NameTuple.new('rails', Gem::Version.new('5.0.0')),
anything
],
[
Gem::NameTuple.new('rails', Gem::Version.new('6.0.0')),
anything
],
[
Gem::NameTuple.new('rails', Gem::Version.new('7.0.0')),
anything
]
]
]

allow(Gem::SpecFetcher.fetcher).to receive(:detect).with(:released).and_return(gem_tuples)
allow(Gem::SpecFetcher.fetcher).to receive(:detect).with(:released).and_return(gem_tuples)
end

it 'returns current version and all dependency versions' do
current_version, all_versions = dependency_context.dependency_versions(dependency)

expect(current_version).to eq(Gem::Version.new('7.0.0'))
expect(all_versions).to contain_exactly(Gem::Version.new('5.0.0'), Gem::Version.new('6.0.0'),
Gem::Version.new('7.0.0'))
end
end

it 'returns current version and all dependency versions' do
current_version, all_versions = dependency_context.dependency_versions(dependency)
context 'when gemfile lock has a ruby version' do
let(:opts) do
{
gemfile_content: File.read('spec/files/gemfile_with_ruby_version'),
gemfile_lock_content: File.read('spec/files/gemfile.lock_with_ruby_version')
}
end

let(:dependency) { Bundler::Dependency.new('ruby', false) }

before do
available_versions = Set[Gem::Version.new('2.5.0'), Gem::Version.new('2.6.0')]

expect(current_version).to eq(Gem::Version.new('7.0.0'))
expect(all_versions).to contain_exactly(Gem::Version.new('5.0.0'), Gem::Version.new('6.0.0'),
Gem::Version.new('7.0.0'))
allow(Gem::SpecFetcher.fetcher).to receive(:detect).with(:released).and_return([])
allow(Polariscope::Scanner::RubyVersions).to receive(:available_versions).and_return(available_versions)
end

it 'returns current version and all dependency versions' do
current_version, all_versions = dependency_context.dependency_versions(dependency)

expect(current_version).to eq(Gem::Version.new('2.5.0'))
expect(all_versions).to contain_exactly(Gem::Version.new('2.5.0'), Gem::Version.new('2.6.0'))
end
end
end

Expand Down
28 changes: 25 additions & 3 deletions spec/lib/polariscope/scanner/gem_versions_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe Polariscope::Scanner::GemVersions do
subject(:scanner) { described_class.new(['devise', 'rails'], spec_type: :released) }
subject(:scanner) { described_class.new(dependencies, spec_type: :released) }

before do
gem_tuples = [
Expand All @@ -27,8 +27,30 @@
end

describe '#versions_for' do
it 'returns only distinct versions for given gem name' do
expect(scanner.versions_for('devise').map(&:to_s)).to contain_exactly('4.6.2', '4.5.0')
before { allow(Polariscope::Scanner::RubyVersions).to receive(:available_versions) }

context 'when ruby is not in dependencies' do
let(:dependencies) { ['devise', 'rails'] }

it 'returns distinct versions for given gem name' do
expect(scanner.versions_for('devise').map(&:to_s)).to contain_exactly('4.6.2', '4.5.0')
end

it "doesn't fetch ruby versions" do
scanner

expect(Polariscope::Scanner::RubyVersions).not_to have_received(:available_versions)
end
end

context 'when ruby is in dependencies' do
let(:dependencies) { ['devise', 'ruby', 'rails'] }

it 'fetches ruby versions' do
scanner

expect(Polariscope::Scanner::RubyVersions).to have_received(:available_versions)
end
end
end
end
32 changes: 32 additions & 0 deletions spec/lib/polariscope/scanner/ruby_versions_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

RSpec.describe Polariscope::Scanner::RubyVersions do
subject(:ruby_versions) { described_class }

describe '.available_versions' do
it 'returns published ruby versions' do
result = ruby_versions.available_versions

expect(result).to be_a(Set)
expect(result.map(&:class).uniq).to contain_exactly(Gem::Version)
expect(result.none?(&:prerelease?)).to be(true)
expect(result.min).to eq(Gem::Version.new('1.2.1'))
end

context 'when an open timeout error is raised' do
before { allow(URI).to receive(:parse).and_raise(Net::OpenTimeout) }

it 'returns an empty set' do
expect(ruby_versions.available_versions).to eq(Set.new)
end
end

context 'when a read timeout error is raised' do
before { allow(URI).to receive(:parse).and_raise(Net::ReadTimeout) }

it 'returns an empty set' do
expect(ruby_versions.available_versions).to eq(Set.new)
end
end
end
end

0 comments on commit 9d9bd95

Please sign in to comment.