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

Add support for optional dependencies #42

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions lib/molinillo/delegates/specification_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ def allow_missing?(dependency)
end
end

# (see Molinillo::SpecificationProvider#optional_dependency?)
def optional_dependency?(dependency)
with_no_such_dependency_error_handling do
specification_provider.optional_dependency?(dependency)
end
end

private

# Ensures any raised {NoSuchDependencyError} has its
Expand Down
13 changes: 13 additions & 0 deletions lib/molinillo/modules/specification_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,18 @@ def sort_dependencies(dependencies, activated, conflicts)
def allow_missing?(dependency)
false
end

# Returns whether this dependency is considered optional.
#
# If the only dependencies for a specification name are optional, that
# specification will not be activated. If there is a strong dependency as
# well, than the requirements imposed by optional dependencies will be taken
# into account.
#
# @param [Object] dependency
# @return [Boolean] whether this dependency is optional.
def optional_dependency?(dependency)
false
end
end
end
38 changes: 30 additions & 8 deletions lib/molinillo/resolution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def initial_state
end

requirements = sort_dependencies(original_requested, graph, {})
requirements.reject! { |r| optional_dependency?(r) }
initial_requirement = requirements.shift
DependencyState.new(
initial_requirement && name_for(initial_requirement),
Expand Down Expand Up @@ -396,13 +397,26 @@ def attempt_to_activate_new_spec
# @return [Boolean] whether the current spec is satisfied as a new
# possibility.
def new_spec_satisfied?
unless requirement_satisfied_by?(requirement, activated, possibility)
debug(depth) { 'Requirement unsatisfied by requested spec' }
return false
end

vertex = activated.vertex_named(name)
vertex.requirements.each do |d|
next unless optional_dependency?(d) # TODO: investigate getting rid of this
next if requirement_satisfied_by?(d, activated, possibility)
debug(depth) { "Vertex requirement #{d} unsatisfied by the possibility" }
return false
end

locked_requirement = locked_requirement_named(name)
requested_spec_satisfied = requirement_satisfied_by?(requirement, activated, possibility)
locked_spec_satisfied = !locked_requirement ||
requirement_satisfied_by?(locked_requirement, activated, possibility)
debug(depth) { 'Unsatisfied by requested spec' } unless requested_spec_satisfied
debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied
requested_spec_satisfied && locked_spec_satisfied
if locked_requirement && !requirement_satisfied_by?(locked_requirement, activated, possibility)
debug(depth) { "Possibility unsatisfied by locked spec #{locked_requirement}" }
return false
end

true
end

# @param [String] requirement_name the spec name to search for
Expand Down Expand Up @@ -430,10 +444,18 @@ def activate_spec
def require_nested_dependencies_for(activated_spec)
nested_dependencies = dependencies_for(activated_spec)
debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" }
nested_dependencies.each do |d|
activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d)

nested_dependencies = nested_dependencies.reject do |d|
name = name_for(d)
activated.add_child_vertex(name, nil, [name_for(activated_spec)], d)

parent_index = states.size - 1
@parent_of[d] ||= parent_index

next false unless optional_dependency?(d)
next true unless existing = activated.vertex_named(name).payload
next true if requirement_satisfied_by?(d, activated, existing)
false
end

push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?)
Expand Down
2 changes: 1 addition & 1 deletion spec/dependency_graph_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module Molinillo
@graph.detach_vertex_named(root.name)
expect(@graph.vertex_named(root.name)).to be_nil
expect(@graph.vertex_named(child.name)).to eq(child)
expect(child.predecessors).to eq([root2])
expect(child.predecessors).to contain_exactly(root2)
expect(@graph.vertices.count).to eq(2)
end

Expand Down
2 changes: 1 addition & 1 deletion spec/fuzz_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
end
end
let(:index) { Molinillo::TestIndex.new('fuzz') }
let(:index) { Molinillo::TestIndex.from_fixture('fuzz') }
let(:ui) { Molinillo::TestUI.new }
let(:resolver) { Molinillo::Resolver.new(index, ui) }

Expand Down
74 changes: 67 additions & 7 deletions spec/resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def initialize(fixture_path)
File.open(fixture_path) do |fixture|
JSON.load(fixture).tap do |test_case|
self.name = test_case['name']
self.index = TestIndex.new(test_case['index'] || 'awesome')
self.index = TestIndex.from_fixture(test_case['index'] || 'awesome')
self.requested = test_case['requested'].map do |(name, reqs)|
VersionKit::Dependency.new name, reqs.split(',').map(&:chomp)
end
Expand Down Expand Up @@ -81,8 +81,9 @@ def initialize(fixture_path)
end

describe 'in general' do
let(:index) { TestIndex.from_fixture('awesome') }
before do
@resolver = described_class.new(TestIndex.new('awesome'), TestUI.new)
@resolver = described_class.new(index, TestUI.new)
end

it 'can resolve a list of 0 requirements' do
Expand Down Expand Up @@ -114,7 +115,7 @@ def initialize(fixture_path)
end

it 'can resolve when two specs have the same dependencies' do
index = BundlerIndex.new('rubygems-2016-09-11')
index = BundlerIndex.from_fixture('rubygems-2016-09-11')
@resolver = described_class.new(index, TestUI.new)
demands = [
VersionKit::Dependency.new('chef', '~> 12.1.2'),
Expand Down Expand Up @@ -174,7 +175,7 @@ def initialize(fixture_path)
end

it 'can resolve when two specs have the same dependencies and swapping happens' do
index = BundlerIndex.new('rubygems-2016-10-06')
index = BundlerIndex.from_fixture('rubygems-2016-10-06')
@resolver = described_class.new(index, TestUI.new)
demands = [
VersionKit::Dependency.new('avro_turf', '0.6.2'),
Expand Down Expand Up @@ -235,7 +236,7 @@ def initialize(fixture_path)
end

it 'can unwind when the conflict has a common parent' do
index = BundlerIndex.new('rubygems-2016-11-05')
index = BundlerIndex.from_fixture('rubygems-2016-11-05')
@resolver = described_class.new(index, TestUI.new)
demands = [
VersionKit::Dependency.new('github-pages', '>= 0'),
Expand All @@ -248,7 +249,7 @@ def initialize(fixture_path)
end

it 'can resolve when swapping changes transitive dependencies' do
index = TestIndex.new('restkit')
index = TestIndex.from_fixture('restkit')
def index.sort_dependencies(dependencies, activated, conflicts)
dependencies.sort_by do |d|
[
Expand Down Expand Up @@ -288,6 +289,65 @@ def index.requirement_satisfied_by?(requirement, activated, spec)
expect(resolved.map(&:payload).map(&:to_s)).to match_array(expected)
end

describe 'optional dependencies' do
let(:index) do
TestIndex.new(
'no_deps' => [
TestSpecification.new('name' => 'no_deps', 'version' => '1.0'),
TestSpecification.new('name' => 'no_deps', 'version' => '2.0'),
TestSpecification.new('name' => 'no_deps', 'version' => '3.0'),
],
'strong_dep' => [
TestSpecification.new('name' => 'strong_dep', 'version' => '1.0',
'dependencies' => { 'no_deps' => '< 3' }),
],
'weak_dep' => [
TestSpecification.new('name' => 'weak_dep', 'version' => '1.0',
'dependencies' => { 'no_deps' => '< 2 optional' }),
]
)
end

let(:no_deps) { VersionKit::Dependency.new('no_deps', '> 1') }
let(:weak_dep) { VersionKit::Dependency.new('weak_dep', '>= 0') }
let(:strong_dep) { VersionKit::Dependency.new('strong_dep', '>= 0') }

it 'ignores optional explicit dependencies' do
no_deps.optional = true
expect(@resolver.resolve([no_deps], DependencyGraph.new).map(&:payload).compact).to be_empty
end

it 'ignores nested optional dependencies' do
expect(@resolver.resolve([weak_dep], DependencyGraph.new).map(&:payload).compact.map(&:to_s)).
to contain_exactly(
'weak_dep (1.0.0)',
'no_deps (1.0.0)'
)
end

it 'uses nested optional dependencies' do
expect(@resolver.resolve([weak_dep, strong_dep], DependencyGraph.new).map(&:payload).compact.map(&:to_s)).
to contain_exactly(
'weak_dep (1.0.0)',
'strong_dep (1.0.0)',
'no_deps (1.0.0)'
)
end

it 'raises when an optional dependency conflicts' do
resolve = proc { @resolver.resolve([weak_dep, no_deps], DependencyGraph.new) }
error = proc do |e|
expect(e.message.split("\n")).to contain_exactly(
'Unable to satisfy the following requirements:',
'',
'- `no_deps (> 1.0.0)` required by `user-specified dependency`',
'- `no_deps (< 2.0.0)` required by `weak_dep (1.0.0)`'
)
end
expect(&resolve).to raise_error(Molinillo::VersionConflict, &error)
end
end

# Regression test. See: https://github.com/CocoaPods/Molinillo/pull/38
it 'can resolve when swapping children with successors' do
swap_child_with_successors_index = Class.new(TestIndex) do
Expand All @@ -311,7 +371,7 @@ def versions_of(dependency_name)
end
end

index = swap_child_with_successors_index.new('swap_child_with_successors')
index = swap_child_with_successors_index.from_fixture('swap_child_with_successors')
@resolver = described_class.new(index, TestUI.new)
demands = [
VersionKit::Dependency.new('build-essential', '>= 0.0.0'),
Expand Down
15 changes: 12 additions & 3 deletions spec/spec_helper/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ class TestIndex
attr_accessor :specs
include SpecificationProvider

def initialize(fixture_name)
def self.from_fixture(fixture_name)
File.open(FIXTURE_INDEX_DIR + (fixture_name + '.json'), 'r') do |fixture|
self.specs = JSON.load(fixture).reduce(Hash.new([])) do |specs_by_name, (name, versions)|
sorted_specs = JSON.load(fixture).reduce(Hash.new([])) do |specs_by_name, (name, versions)|
specs_by_name.tap do |specs|
specs[name] = versions.map { |s| TestSpecification.new s }.sort_by(&:version)
end
end
return new(sorted_specs)
end
end

def initialize(specs_by_name)
self.specs = specs_by_name
end

def requirement_satisfied_by?(requirement, _activated, spec)
case requirement
when TestSpecification
Expand All @@ -30,7 +35,7 @@ def search_for(dependency)
@search_for ||= {}
@search_for[dependency] ||= begin
pre_release = dependency_pre_release?(dependency)
specs[dependency.name].select do |spec|
Array(specs[dependency.name]).select do |spec|
(pre_release ? true : !spec.version.pre_release?) &&
dependency.satisfied_by?(spec.version)
end
Expand All @@ -57,6 +62,10 @@ def sort_dependencies(dependencies, activated, conflicts)
end
end

def optional_dependency?(dependency)
dependency.optional?
end

private

def dependency_pre_release?(dependency)
Expand Down
13 changes: 11 additions & 2 deletions spec/spec_helper/specification.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
# frozen_string_literal: true
module VersionKit
class Dependency
attr_accessor :optional
alias optional? optional
end
end

module Molinillo
class TestSpecification
attr_accessor :name, :version, :dependencies
def initialize(hash)
self.name = hash['name']
self.version = VersionKit::Version.new(hash['version'])
self.dependencies = hash['dependencies'].map do |(name, requirement)|
VersionKit::Dependency.new(name, requirement.split(',').map(&:chomp))
self.dependencies = hash.fetch('dependencies') { Hash.new }.map do |(name, requirement)|
requirements = requirement.split(',').map(&:chomp)
optional = requirements.delete('optional')
VersionKit::Dependency.new(name, requirements).tap { |d| d.optional = optional }
end
end

Expand Down