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

Include Ruby versions in health score calculation #5

Merged
merged 8 commits into from
Oct 25, 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
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
Copy link
Member Author

@lovro-bikic lovro-bikic Oct 25, 2024

Choose a reason for hiding this comment

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

There is no ruby gem: https://rubygems.org/gems/ruby

image

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