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
Changes from 1 commit
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
Next Next commit
Add support for optional dependencies
  • Loading branch information
segiddins committed Jan 5, 2017

Unverified

This user has not yet uploaded their public signing key.
commit 7695df1c84f9297c6b552fb83e01253713e84eb3
7 changes: 7 additions & 0 deletions lib/molinillo/delegates/specification_provider.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions lib/molinillo/modules/specification_provider.rb
Original file line number Diff line number Diff line change
@@ -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
@@ -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),
@@ -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
@@ -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?)
2 changes: 1 addition & 1 deletion spec/dependency_graph_spec.rb
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion spec/fuzz_spec.rb
Original file line number Diff line number Diff line change
@@ -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) }

70 changes: 63 additions & 7 deletions spec/resolver_spec.rb
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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'),
@@ -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'),
@@ -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'),
@@ -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|
[
@@ -288,6 +289,61 @@ 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 eq [
'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 eq [
'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) }
expect(&resolve).to raise_error(Molinillo::VersionConflict, <<-EOS.strip)
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)`
EOS
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
@@ -311,7 +367,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'),
15 changes: 12 additions & 3 deletions spec/spec_helper/index.rb
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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)
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