Skip to content

Commit

Permalink
SpecChangeSet understands how a new spec differs from an old or locke…
Browse files Browse the repository at this point in the history
…d resolution.
  • Loading branch information
Jay Feldblum committed May 27, 2011
1 parent 2a3ccd0 commit 3f2cb98
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 0 deletions.
5 changes: 5 additions & 0 deletions lib/librarian.rb
Expand Up @@ -9,6 +9,7 @@
require 'librarian/particularity'
require 'librarian/resolver'
require 'librarian/source'
require 'librarian/spec_change_set'
require 'librarian/specfile'
require 'librarian/lockfile'
require 'librarian/ui'
Expand Down Expand Up @@ -71,6 +72,10 @@ def project_relative_path_to(path)
Pathname.new(path).relative_path_from(project_path)
end

def spec_change_set(spec, lock)
SpecChangeSet.new(self, spec, lock)
end

def ensure!
unless project_path
raise Error, "Cannot find #{specfile_name}!"
Expand Down
169 changes: 169 additions & 0 deletions lib/librarian/spec_change_set.rb
@@ -0,0 +1,169 @@
require 'librarian/helpers'
require 'librarian/helpers/debug'

require 'librarian/manifest_set'
require 'librarian/resolution'
require 'librarian/spec'

module Librarian
class SpecChangeSet

include Helpers::Debug

attr_reader :root_module
attr_reader :spec, :lock

def initialize(root_module, spec, lock)
@root_module = root_module
raise TypeError, "can't convert #{spec.class} into Spec" unless Spec === spec
raise TypeError, "can't convert #{lock.class} into Resolution" unless Resolution === lock
@spec, @lock = spec, lock
end

def same?
@same ||= spec.dependencies.sort_by{|d| d.name} == lock.dependencies.sort_by{|d| d.name}
end

def changed?
!same?
end

def spec_dependencies
@spec_dependencies ||= spec.dependencies
end
def spec_dependency_names
@spec_dependency_names ||= Set.new(spec_dependencies.map{|d| d.name})
end
def spec_dependency_index
@spec_dependency_index ||= Hash[spec_dependencies.map{|d| [d.name, d]}]
end

def lock_dependencies
@lock_dependencies ||= lock.dependencies
end
def lock_dependency_names
@lock_dependency_names ||= Set.new(lock_dependencies.map{|d| d.name})
end
def lock_dependency_index
@lock_dependency_index ||= Hash[lock_dependencies.map{|d| [d.name, d]}]
end

def lock_manifests
@lock_manifests ||= lock.manifests
end
def lock_manifests_index
@lock_manifests_index ||= ManifestSet.new(lock_manifests).to_hash
end

def removed_dependency_names
@removed_dependency_names ||= lock_dependency_names - spec_dependency_names
end

# A dependency which is deleted from the specfile will, in the general case,
# be removed conservatively. This means it might not actually be removed.
# But if the dependency originally declared a source which is now non-
# default, it must be removed, even if another dependency has a transitive
# dependency on the one that was removed (which is the scenario in which
# a conservative removal would not remove it). In this case, we must also
# remove it explicitly so that it can be re-resolved from the default
# source.
def explicit_removed_dependency_names
@explicit_removed_dependency_names ||= removed_dependency_names.reject do |name|
lock_manifest = lock_manifests_index[name]
lock_manifest.source == spec.source
end.to_set
end

def added_dependency_names
@added_dependency_names ||= spec_dependency_names - lock_dependency_names
end

def nonmatching_added_dependency_names
@nonmatching_added_dependency_names ||= added_dependency_names.reject do |name|
spec_dependency = spec_dependency_index[name]
lock_manifest = lock_manifests_index[name]
if lock_manifest
matching = true
matching &&= spec_dependency.satisfied_by?(lock_manifest)
matching &&= spec_dependency.source == lock_manifest.source
matching
else
false
end
end.to_set
end

def common_dependency_names
@common_dependency_names ||= lock_dependency_names & spec_dependency_names
end

def changed_dependency_names
@changed_dependency_names ||= common_dependency_names.reject do |name|
spec_dependency = spec_dependency_index[name]
lock_dependency = lock_dependency_index[name]
lock_manifest = lock_manifests_index[name]
same = true
same &&= spec_dependency.satisfied_by?(lock_manifest)
same &&= spec_dependency.source == lock_dependency.source
same
end.to_set
end

def deep_keep_manifest_names
@deep_keep_manifest_names ||= begin
lock_dependency_names - (
removed_dependency_names +
changed_dependency_names +
nonmatching_added_dependency_names
)
end
end

def shallow_strip_manifest_names
@shallow_strip_manifest_names ||= begin
explicit_removed_dependency_names + changed_dependency_names
end
end

def inspect
Helpers.strip_heredoc(<<-INSPECT)
<##{self.class.name}:
Removed: #{removed_dependency_names.to_a.join(", ")}
ExplicitRemoved: #{explicit_removed_dependency_names.to_a.join(", ")}
Added: #{added_dependency_names.to_a.join(", ")}
NonMatchingAdded: #{nonmatching_added_dependency_names.to_a.join(", ")}
Changed: #{changed_dependency_names.to_a.join(", ")}
DeepKeep: #{deep_keep_manifest_names.to_a.join(", ")}
ShallowStrip: #{shallow_strip_manifest_names.to_a.join(", ")}
>
INSPECT
end

# Returns an array of those manifests from the previous spec which should be kept,
# based on inspecting the new spec against the locked resolution from the previous spec.
def analyze
@analyze ||= begin
debug { "Analyzing spec and lock:" }

if same?
debug { " Same!" }
return lock.manifests
end

debug { " Removed:" } ; removed_dependency_names.each { |name| debug { " #{name}" } }
debug { " ExplicitRemoved:" } ; explicit_removed_dependency_names.each { |name| debug { " #{name}" } }
debug { " Added:" } ; added_dependency_names.each { |name| debug { " #{name}" } }
debug { " NonMatchingAdded:" } ; nonmatching_added_dependency_names.each { |name| debug { " #{name}" } }
debug { " Changed:" } ; changed_dependency_names.each { |name| debug { " #{name}" } }
debug { " DeepKeep:" } ; deep_keep_manifest_names.each { |name| debug { " #{name}" } }
debug { " ShallowStrip:" } ; shallow_strip_manifest_names.each { |name| debug { " #{name}" } }

manifests = ManifestSet.new(lock_manifests)
manifests.deep_keep!(deep_keep_manifest_names)
manifests.shallow_strip!(shallow_strip_manifest_names)
manifests.to_a
end
end

end
end
137 changes: 137 additions & 0 deletions spec/spec_change_set_spec.rb
@@ -0,0 +1,137 @@
require 'librarian'
require 'librarian/mock'

module Librarian
describe SpecChangeSet do

context "a simple root removal" do

it "should work" do
Mock.registry :clear => true do
source 'source-1' do
spec 'butter', '1.0'
spec 'jam', '1.0'
end
end
spec = Mock.dsl do
src 'source-1'
dep 'butter'
dep 'jam'
end
lock = Mock.resolver.resolve(spec)
lock.should be_correct

spec = Mock.dsl do
src 'source-1'
dep 'jam'
end
changes = Mock.spec_change_set(spec, lock)
changes.should_not be_same

manifests = ManifestSet.new(changes.analyze).to_hash
manifests.should have_key('jam')
manifests.should_not have_key('butter')
end

end

context "a simple root add" do

it "should work" do
Mock.registry :clear => true do
source 'source-1' do
spec 'butter', '1.0'
spec 'jam', '1.0'
end
end
spec = Mock.dsl do
src 'source-1'
dep 'jam'
end
lock = Mock.resolver.resolve(spec)
lock.should be_correct

spec = Mock.dsl do
src 'source-1'
dep 'butter'
dep 'jam'
end
changes = Mock.spec_change_set(spec, lock)
changes.should_not be_same
manifests = ManifestSet.new(changes.analyze).to_hash
manifests.should have_key('jam')
manifests.should_not have_key('butter')
end

end

context "a simple root change" do

context "when the change is consistent" do

it "should work" do
Mock.registry :clear => true do
source 'source-1' do
spec 'butter', '1.0'
spec 'jam', '1.0'
spec 'jam', '1.1'
end
end
spec = Mock.dsl do
src 'source-1'
dep 'butter'
dep 'jam', '= 1.1'
end
lock = Mock.resolver.resolve(spec)
lock.should be_correct

spec = Mock.dsl do
src 'source-1'
dep 'butter'
dep 'jam', '>= 1.0'
end
changes = Mock.spec_change_set(spec, lock)
changes.should_not be_same
manifests = ManifestSet.new(changes.analyze).to_hash
manifests.should have_key('butter')
manifests.should have_key('jam')
end

end

context "when the change is inconsistent" do

it "should work" do
Mock.registry :clear => true do
source 'source-1' do
spec 'butter', '1.0'
spec 'jam', '1.0'
spec 'jam', '1.1'
end
end
spec = Mock.dsl do
src 'source-1'
dep 'butter'
dep 'jam', '= 1.0'
end
lock = Mock.resolver.resolve(spec)
lock.should be_correct

spec = Mock.dsl do
src 'source-1'
dep 'butter'
dep 'jam', '>= 1.1'
end
changes = Mock.spec_change_set(spec, lock)
changes.should_not be_same
manifests = ManifestSet.new(changes.analyze).to_hash
manifests.should have_key('butter')
manifests.should_not have_key('jam')
end

end

end

end
end

0 comments on commit 3f2cb98

Please sign in to comment.