semantic_puppet-0.1.1/0000755000004100000410000000000012633261621014743 5ustar www-datawww-datasemantic_puppet-0.1.1/Rakefile0000644000004100000410000000271112633261621016411 0ustar www-datawww-data# RSpec tasks begin require 'rspec/core/rake_task' # Create the 'spec' task RSpec::Core::RakeTask.new(:spec) do |task| task.rspec_opts = '--color' end namespace :spec do desc "Run the test suite and generate coverage metrics" task :coverage => [ :simplecov, :spec ] # Add test coverage to the 'spec' task. task :simplecov do ENV['COVERAGE'] = '1' end end task :default => :spec rescue LoadError warn "[Warning]: Could not load `rspec`." end # YARD tasks begin require 'yard' require 'yard/rake/yardoc_task' YARD::Rake::YardocTask.new(:doc) do |yardoc| yardoc.files = [ 'lib/**/*.rb', '-', '**/*.md' ] end rescue LoadError warn "[Warning]: Could not load `yard`." end # Cane tasks begin require 'cane/rake_task' Cane::RakeTask.new(:cane) do |cane| cane.add_threshold 'coverage/.last_run.json', :>=, 100 cane.abc_max = 15 end Rake::Task['cane'].prerequisites << Rake::Task['spec:coverage'] Rake::Task[:default].clear_prerequisites task :default => :cane rescue LoadError warn "[Warning]: Could not load `cane`." end # Gem tasks begin require 'rubygems/tasks' task :gem => 'gem:build' task :validate => [ 'cane', 'doc', 'gem:validate' ] namespace :gem do Gem::Tasks.new( :tag => { :format => 'v%s' }, :sign => { :checksum => true, :pgp => true }, :build => { :tar => true } ) end rescue LoadError warn "[Warning]: Could not load `rubygems/tasks`." end semantic_puppet-0.1.1/Gemfile.lock0000644000004100000410000000130512633261621017164 0ustar www-datawww-dataGEM remote: https://rubygems.org/ specs: cane (2.6.1) parallel diff-lcs (1.2.5) docile (1.1.1) multi_json (1.8.2) parallel (0.9.1) rake (10.1.0) redcarpet (3.0.0) rspec (2.14.1) rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) rspec-core (2.14.7) rspec-expectations (2.14.4) diff-lcs (>= 1.1.3, < 2.0) rspec-mocks (2.14.4) rubygems-tasks (0.2.4) simplecov (0.8.2) docile (~> 1.1.0) multi_json simplecov-html (~> 0.8.0) simplecov-html (0.8.0) yard (0.8.7.3) PLATFORMS ruby DEPENDENCIES cane rake redcarpet rspec rubygems-tasks simplecov yard (~> 0.8) semantic_puppet-0.1.1/Gemfile0000644000004100000410000000065312633261621016242 0ustar www-datawww-datasource "https://rubygems.org" group :development do gem 'rake' gem 'rubygems-tasks' end group :test do gem 'rspec' end group :metrics do gem 'cane', :platform => [ :mri_19, :mri_20, :mri_21 ] gem 'simplecov', :platform => [ :mri_19, :mri_20, :mri_21 ] end group :doc do gem 'yard', '~> 0.8', :platform => [ :mri_19, :mri_20, :mri_21 ] gem 'redcarpet', :platform => [ :mri_19, :mri_20, :mri_21 ] end semantic_puppet-0.1.1/spec/0000755000004100000410000000000012633261621015675 5ustar www-datawww-datasemantic_puppet-0.1.1/spec/spec_helper.rb0000644000004100000410000000123412633261621020513 0ustar www-datawww-dataPROJECT_ROOT = File.join(File.dirname(__FILE__), '..') if ENV['COVERAGE'] require 'simplecov' SimpleCov.start do add_filter "/spec/" end end RSpec.configure do |config| config.treat_symbols_as_metadata_keys_with_true_values = true config.run_all_when_everything_filtered = true config.filter_run :focus # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = 'random' config.before do SemanticPuppet::Dependency.instance_variable_set(:@sources, nil) end end semantic_puppet-0.1.1/spec/unit/0000755000004100000410000000000012633261621016654 5ustar www-datawww-datasemantic_puppet-0.1.1/spec/unit/semantic_puppet/0000755000004100000410000000000012633261621022054 5ustar www-datawww-datasemantic_puppet-0.1.1/spec/unit/semantic_puppet/version_range_spec.rb0000644000004100000410000002332612633261621026262 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/version' describe SemanticPuppet::VersionRange do describe '.parse' do def self.test_range(range_list, str, includes, excludes) Array(range_list).each do |expr| example "#{expr.inspect} stringifies as #{str}" do range = SemanticPuppet::VersionRange.parse(expr) expect(range.to_s).to eql str end includes.each do |vstring| example "#{expr.inspect} includes #{vstring}" do range = SemanticPuppet::VersionRange.parse(expr) expect(range).to include(SemanticPuppet::Version.parse(vstring)) end example "parse(#{expr.inspect}).to_s includes #{vstring}" do range = SemanticPuppet::VersionRange.parse(expr) range = SemanticPuppet::VersionRange.parse(range.to_s) expect(range).to include(SemanticPuppet::Version.parse(vstring)) end end excludes.each do |vstring| example "#{expr.inspect} excludes #{vstring}" do range = SemanticPuppet::VersionRange.parse(expr) expect(range).to_not include(SemanticPuppet::Version.parse(vstring)) end example "parse(#{expr.inspect}).to_s excludes #{vstring}" do range = SemanticPuppet::VersionRange.parse(expr) range = SemanticPuppet::VersionRange.parse(range.to_s) expect(range).to_not include(SemanticPuppet::Version.parse(vstring)) end end end end context 'loose version expressions' do expressions = { [ '1.2.3-alpha' ] => { :to_str => '1.2.3-alpha', :includes => [ '1.2.3-alpha' ], :excludes => [ '1.2.3-999', '1.2.3-beta' ], }, [ '1.2.3' ] => { :to_str => '1.2.3', :includes => [ '1.2.3-alpha', '1.2.3' ], :excludes => [ '1.2.2', '1.2.4-alpha' ], }, [ '1.2', '1.2.x', '1.2.X' ] => { :to_str => '1.2.x', :includes => [ '1.2.0-alpha', '1.2.0', '1.2.999' ], :excludes => [ '1.1.999', '1.3.0-0' ], }, [ '1', '1.x', '1.X' ] => { :to_str => '1.x', :includes => [ '1.0.0-alpha', '1.999.0' ], :excludes => [ '0.999.999', '2.0.0-0' ], }, } expressions.each do |range, vs| test_range(range, vs[:to_str], vs[:includes], vs[:excludes]) end end context 'open-ended expressions' do expressions = { [ '>1.2.3', '> 1.2.3' ] => { :to_str => '>=1.2.4', :includes => [ '1.2.4-0', '999.0.0' ], :excludes => [ '1.2.3' ], }, [ '>1.2.3-alpha', '> 1.2.3-alpha' ] => { :to_str => '>1.2.3-alpha', :includes => [ '1.2.3-alpha.0', '1.2.3-alpha0', '999.0.0' ], :excludes => [ '1.2.3-alpha' ], }, [ '>=1.2.3', '>= 1.2.3' ] => { :to_str => '>=1.2.3', :includes => [ '1.2.3-0', '999.0.0' ], :excludes => [ '1.2.2' ], }, [ '>=1.2.3-alpha', '>= 1.2.3-alpha' ] => { :to_str => '>=1.2.3-alpha', :includes => [ '1.2.3-alpha', '1.2.3-alpha0', '999.0.0' ], :excludes => [ '1.2.3-alph' ], }, [ '<1.2.3', '< 1.2.3' ] => { :to_str => '<1.2.3', :includes => [ '0.0.0-0', '1.2.2' ], :excludes => [ '1.2.3-0', '2.0.0' ], }, [ '<1.2.3-alpha', '< 1.2.3-alpha' ] => { :to_str => '<1.2.3-alpha', :includes => [ '0.0.0-0', '1.2.3-alph' ], :excludes => [ '1.2.3-alpha', '2.0.0' ], }, [ '<=1.2.3', '<= 1.2.3' ] => { :to_str => '<1.2.4', :includes => [ '0.0.0-0', '1.2.3' ], :excludes => [ '1.2.4-0' ], }, [ '<=1.2.3-alpha', '<= 1.2.3-alpha' ] => { :to_str => '<=1.2.3-alpha', :includes => [ '0.0.0-0', '1.2.3-alpha' ], :excludes => [ '1.2.3-alpha0', '1.2.3-alpha.0', '1.2.3-alpha'.next ], }, } expressions.each do |range, vs| test_range(range, vs[:to_str], vs[:includes], vs[:excludes]) end end context '"reasonably close" expressions' do expressions = { [ '~ 1', '~1' ] => { :to_str => '1.x', :includes => [ '1.0.0-0', '1.999.999' ], :excludes => [ '0.999.999', '2.0.0-0' ], }, [ '~ 1.2', '~1.2' ] => { :to_str => '1.2.x', :includes => [ '1.2.0-0', '1.2.999' ], :excludes => [ '1.1.999', '1.3.0-0' ], }, [ '~ 1.2.3', '~1.2.3' ] => { :to_str => '>=1.2.3 <1.3.0', :includes => [ '1.2.3-0', '1.2.5' ], :excludes => [ '1.2.2', '1.3.0-0' ], }, [ '~ 1.2.3-alpha', '~1.2.3-alpha' ] => { :to_str => '>=1.2.3-alpha <1.2.4', :includes => [ '1.2.3-alpha', '1.2.3' ], :excludes => [ '1.2.3-alph', '1.2.4-0' ], }, } expressions.each do |range, vs| test_range(range, vs[:to_str], vs[:includes], vs[:excludes]) end end context 'inclusive range expressions' do expressions = { '1.2.3 - 1.3.4' => { :to_str => '>=1.2.3 <1.3.5', :includes => [ '1.2.3-0', '1.3.4' ], :excludes => [ '1.2.2', '1.3.5-0' ], }, '1.2.3 - 1.3.4-alpha' => { :to_str => '>=1.2.3 <=1.3.4-alpha', :includes => [ '1.2.3-0', '1.3.4-alpha' ], :excludes => [ '1.2.2', '1.3.4-alpha0', '1.3.5' ], }, '1.2.3-alpha - 1.3.4' => { :to_str => '>=1.2.3-alpha <1.3.5', :includes => [ '1.2.3-alpha', '1.3.4' ], :excludes => [ '1.2.3-alph', '1.3.5-0' ], }, '1.2.3-alpha - 1.3.4-alpha' => { :to_str => '>=1.2.3-alpha <=1.3.4-alpha', :includes => [ '1.2.3-alpha', '1.3.4-alpha' ], :excludes => [ '1.2.3-alph', '1.3.4-alpha0', '1.3.5' ], }, } expressions.each do |range, vs| test_range(range, vs[:to_str], vs[:includes], vs[:excludes]) end end context 'unioned expressions' do expressions = { [ '1.2 <1.2.5' ] => { :to_str => '>=1.2.0 <1.2.5', :includes => [ '1.2.0-0', '1.2.4' ], :excludes => [ '1.1.999', '1.2.5-0', '1.9.0' ], }, [ '1 <=1.2.5' ] => { :to_str => '>=1.0.0 <1.2.6', :includes => [ '1.0.0-0', '1.2.5' ], :excludes => [ '0.999.999', '1.2.6-0', '1.9.0' ], }, [ '>1.0.0 >2.0.0 >=3.0.0 <5.0.0' ] => { :to_str => '>=3.0.0 <5.0.0', :includes => [ '3.0.0-0', '4.999.999' ], :excludes => [ '2.999.999', '5.0.0-0' ], }, [ '<1.0.0 >2.0.0' ] => { :to_str => '<0.0.0', :includes => [ ], :excludes => [ '0.0.0-0' ], }, } expressions.each do |range, vs| test_range(range, vs[:to_str], vs[:includes], vs[:excludes]) end end context 'invalid expressions' do example 'raise an appropriate exception' do ex = [ ArgumentError, 'Unparsable version range: "invalid"' ] expect { SemanticPuppet::VersionRange.parse('invalid') }.to raise_error(*ex) end end end describe '#intersection' do def self.v(num) SemanticPuppet::Version.parse("#{num}.0.0") end def self.range(x, y, ex = false) SemanticPuppet::VersionRange.new(v(x), v(y), ex) end EMPTY_RANGE = SemanticPuppet::VersionRange::EMPTY_RANGE tests = { # This falls entirely before the target range range(1, 4) => [ EMPTY_RANGE ], # This falls entirely after the target range range(11, 15) => [ EMPTY_RANGE ], # This overlaps the beginning of the target range range(1, 6) => [ range(5, 6) ], # This overlaps the end of the target range range(9, 15) => [ range(9, 10), range(9, 10, true) ], # This shares the first value of the target range range(1, 5) => [ range(5, 5) ], # This shares the last value of the target range range(10, 15) => [ range(10, 10), EMPTY_RANGE ], # This shares both values with the target range range(5, 10) => [ range(5, 10), range(5, 10, true) ], # This is a superset of the target range range(4, 11) => [ range(5, 10), range(5, 10, true) ], # This is a subset of the target range range(6, 9) => [ range(6, 9) ], # This shares the first value of the target range, but excludes it range(1, 5, true) => [ EMPTY_RANGE ], # This overlaps the beginning of the target range, with an excluded end range(1, 7, true) => [ range(5, 7, true) ], # This shares both values with the target range, and excludes the end range(5, 10, true) => [ range(5, 10, true) ], } inclusive = range(5, 10) context "between #{inclusive} &" do tests.each do |subject, result| result = result.first example subject do expect(inclusive & subject).to eql(result) end end end exclusive = range(5, 10, true) context "between #{exclusive} &" do tests.each do |subject, result| result = result.last example subject do expect(exclusive & subject).to eql(result) end end end context 'is commutative' do tests.each do |subject, _| example "between #{inclusive} & #{subject}" do expect(inclusive & subject).to eql(subject & inclusive) end example "between #{exclusive} & #{subject}" do expect(exclusive & subject).to eql(subject & exclusive) end end end it 'cannot intersect with non-VersionRanges' do msg = "value must be a SemanticPuppet::VersionRange" expect { inclusive.intersection(1..2) }.to raise_error(msg) end end end semantic_puppet-0.1.1/spec/unit/semantic_puppet/version_spec.rb0000644000004100000410000005541212633261621025107 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/version' describe SemanticPuppet::Version do def subject(str) SemanticPuppet::Version.parse(str) end describe '.parse' do context 'Spec v2.0.0' do context 'Section 2' do # A normal version number MUST take the form X.Y.Z where X, Y, and Z are # non-negative integers, and MUST NOT contain leading zeroes. X is the # major version, Y is the minor version, and Z is the patch version. # Each element MUST increase numerically. # For instance: 1.9.0 -> 1.10.0 -> 1.11.0. let(:parse_failure) do /Unable to parse .* as a semantic version identifier/ end it 'rejects versions that contain too few parts' do expect { subject('1.2') }.to raise_error(parse_failure) end it 'rejects versions that contain too many parts' do expect { subject('1.2.3.4') }.to raise_error(parse_failure) end it 'rejects versions that contain non-integers' do expect { subject('x.2.3') }.to raise_error(parse_failure) expect { subject('1.y.3') }.to raise_error(parse_failure) expect { subject('1.2.z') }.to raise_error(parse_failure) end it 'rejects versions that contain negative integers' do expect { subject('-1.2.3') }.to raise_error(parse_failure) expect { subject('1.-2.3') }.to raise_error(parse_failure) expect { subject('1.2.-3') }.to raise_error(parse_failure) end it 'rejects version numbers containing leading zeroes' do expect { subject('01.2.3') }.to raise_error(parse_failure) expect { subject('1.02.3') }.to raise_error(parse_failure) expect { subject('1.2.03') }.to raise_error(parse_failure) end it 'permits zeroes in version number parts' do expect { subject('0.2.3') }.to_not raise_error expect { subject('1.0.3') }.to_not raise_error expect { subject('1.2.0') }.to_not raise_error end context 'examples' do example '1.9.0' do version = subject('1.9.0') expect(version.major).to eql 1 expect(version.minor).to eql 9 expect(version.patch).to eql 0 end example '1.10.0' do version = subject('1.10.0') expect(version.major).to eql 1 expect(version.minor).to eql 10 expect(version.patch).to eql 0 end example '1.11.0' do version = subject('1.11.0') expect(version.major).to eql 1 expect(version.minor).to eql 11 expect(version.patch).to eql 0 end end end context 'Section 9' do # A pre-release version MAY be denoted by appending a hyphen and a # series of dot separated identifiers immediately following the patch # version. Identifiers MUST comprise only ASCII alphanumerics and # hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric # identifiers MUST NOT include leading zeroes. Pre-release versions # have a lower precedence than the associated normal version. A # pre-release version indicates that the version is unstable and # might not satisfy the intended compatibility requirements as denoted # by its associated normal version. # Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92. let(:restricted_charset) do 'Prerelease identifiers MUST use only ASCII alphanumerics and hyphens' end let(:must_not_be_empty) do 'Prerelease identifiers MUST NOT be empty' end let(:no_leading_zeroes) do 'Prerelease identifiers MUST NOT contain leading zeroes' end it 'rejects prerelease identifiers with non-alphanumerics' do expect { subject('1.2.3-$100') }.to raise_error(restricted_charset) expect { subject('1.2.3-rc.1@me') }.to raise_error(restricted_charset) end it 'rejects empty prerelease versions' do expect { subject('1.2.3-') }.to raise_error(must_not_be_empty) end it 'rejects empty prerelease version identifiers' do expect { subject('1.2.3-.rc1') }.to raise_error(must_not_be_empty) expect { subject('1.2.3-rc1.') }.to raise_error(must_not_be_empty) expect { subject('1.2.3-rc..1') }.to raise_error(must_not_be_empty) end it 'rejects numeric prerelease identifiers with leading zeroes' do expect { subject('1.2.3-01') }.to raise_error(no_leading_zeroes) expect { subject('1.2.3-rc.01') }.to raise_error(no_leading_zeroes) end it 'permits numeric prerelease identifiers of zero' do expect { subject('1.2.3-0') }.to_not raise_error expect { subject('1.2.3-rc.0') }.to_not raise_error end it 'permits non-numeric prerelease identifiers with leading zeroes' do expect { subject('1.2.3-0xDEADBEEF') }.to_not raise_error expect { subject('1.2.3-rc.0x10c') }.to_not raise_error end context 'examples' do example '1.0.0-alpha' do version = subject('1.0.0-alpha') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'alpha' end example '1.0.0-alpha.1' do version = subject('1.0.0-alpha.1') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'alpha.1' end example '1.0.0-0.3.7' do version = subject('1.0.0-0.3.7') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql '0.3.7' end example '1.0.0-x.7.z.92' do version = subject('1.0.0-x.7.z.92') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'x.7.z.92' end end end context 'Section 10' do # Build metadata is not yet in scope so it 'rejects build identifiers' do ver = '1.2.3+anything' expect { subject(ver) }.to raise_error("'#{ver}' MUST NOT include build identifiers") end end context 'Section 10', :pending => "build metadata is not yet in scope" do # Build metadata MAY be denoted by appending a plus sign and a series # of dot separated identifiers immediately following the patch or # pre-release version. Identifiers MUST comprise only ASCII # alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. # Build metadata SHOULD be ignored when determining version precedence. # Thus two versions that differ only in the build metadata, have the # same precedence. # Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, # 1.0.0-beta+exp.sha.5114f85. let(:restricted_charset) do 'Build identifiers MUST use only ASCII alphanumerics and hyphens' end let(:must_not_be_empty) do 'Build identifiers MUST NOT be empty' end it 'rejects build identifiers with non-alphanumerics' do expect { subject('1.2.3+$100') }.to raise_error(restricted_charset) expect { subject('1.2.3+rc.1@me') }.to raise_error(restricted_charset) end it 'rejects empty build metadata' do expect { subject('1.2.3+') }.to raise_error(must_not_be_empty) end it 'rejects empty build identifiers' do expect { subject('1.2.3+.rc1') }.to raise_error(must_not_be_empty) expect { subject('1.2.3+rc1.') }.to raise_error(must_not_be_empty) expect { subject('1.2.3+rc..1') }.to raise_error(must_not_be_empty) end it 'permits numeric build identifiers with leading zeroes' do expect { subject('1.2.3+01') }.to_not raise_error expect { subject('1.2.3+rc.01') }.to_not raise_error end it 'permits numeric build identifiers of zero' do expect { subject('1.2.3+0') }.to_not raise_error expect { subject('1.2.3+rc.0') }.to_not raise_error end it 'permits non-numeric build identifiers with leading zeroes' do expect { subject('1.2.3+0xDEADBEEF') }.to_not raise_error expect { subject('1.2.3+rc.0x10c') }.to_not raise_error end context 'examples' do example '1.0.0-alpha+001' do version = subject('1.0.0-alpha+001') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'alpha' expect(version.build).to eql '001' end example '1.0.0+20130313144700' do version = subject('1.0.0+20130313144700') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql nil expect(version.build).to eql '20130313144700' end example '1.0.0-beta+exp.sha.5114f85' do version = subject('1.0.0-beta+exp.sha.5114f85') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'beta' expect(version.build).to eql 'exp.sha.5114f85' end end end end context 'Spec v1.0.0' do context 'Section 2' do # A normal version number MUST take the form X.Y.Z where X, Y, and Z # are integers. X is the major version, Y is the minor version, and Z # is the patch version. Each element MUST increase numerically by # increments of one. # For instance: 1.9.0 -> 1.10.0 -> 1.11.0 let(:parse_failure) do /Unable to parse .* as a semantic version identifier/ end it 'rejects versions that contain too few parts' do expect { subject('1.2') }.to raise_error(parse_failure) end it 'rejects versions that contain too many parts' do expect { subject('1.2.3.4') }.to raise_error(parse_failure) end it 'rejects versions that contain non-integers' do expect { subject('x.2.3') }.to raise_error(parse_failure) expect { subject('1.y.3') }.to raise_error(parse_failure) expect { subject('1.2.z') }.to raise_error(parse_failure) end it 'permits zeroes in version number parts' do expect { subject('0.2.3') }.to_not raise_error expect { subject('1.0.3') }.to_not raise_error expect { subject('1.2.0') }.to_not raise_error end context 'examples' do example '1.9.0' do version = subject('1.9.0') expect(version.major).to eql 1 expect(version.minor).to eql 9 expect(version.patch).to eql 0 end example '1.10.0' do version = subject('1.10.0') expect(version.major).to eql 1 expect(version.minor).to eql 10 expect(version.patch).to eql 0 end example '1.11.0' do version = subject('1.11.0') expect(version.major).to eql 1 expect(version.minor).to eql 11 expect(version.patch).to eql 0 end end end context 'Section 4' do # A pre-release version number MAY be denoted by appending an arbitrary # string immediately following the patch version and a dash. The string # MUST be comprised of only alphanumerics plus dash [0-9A-Za-z-]. # Pre-release versions satisfy but have a lower precedence than the # associated normal version. Precedence SHOULD be determined by # lexicographic ASCII sort order. # For instance: 1.0.0-alpha1 < 1.0.0-beta1 < 1.0.0-beta2 < 1.0.0-rc1 let(:restricted_charset) do 'Prerelease identifiers MUST use only ASCII alphanumerics and hyphens' end let(:must_not_be_empty) do 'Prerelease identifiers MUST NOT be empty' end let(:no_leading_zeroes) do 'Prerelease identifiers MUST NOT contain leading zeroes' end it 'rejects prerelease identifiers with non-alphanumerics' do expect { subject('1.2.3-$100') }.to raise_error(restricted_charset) expect { subject('1.2.3-rc.1@me') }.to raise_error(restricted_charset) end it 'rejects empty prerelease versions' do expect { subject('1.2.3-') }.to raise_error(must_not_be_empty) end it 'rejects numeric prerelease identifiers with leading zeroes' do expect { subject('1.2.3-01') }.to raise_error(no_leading_zeroes) end it 'permits numeric prerelease identifiers of zero' do expect { subject('1.2.3-0') }.to_not raise_error end it 'permits non-numeric prerelease identifiers with leading zeroes' do expect { subject('1.2.3-0xDEADBEEF') }.to_not raise_error end context 'examples' do example '1.0.0-alpha1' do version = subject('1.0.0-alpha1') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'alpha1' end example '1.0.0-beta1' do version = subject('1.0.0-beta1') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'beta1' end example '1.0.0-beta2' do version = subject('1.0.0-beta2') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'beta2' end example '1.0.0-rc1' do version = subject('1.0.0-rc1') expect(version.major).to eql 1 expect(version.minor).to eql 0 expect(version.patch).to eql 0 expect(version.prerelease).to eql 'rc1' end end end end end describe '.valid?' do # All the specific variations are tested in the .parse tests, so these are just basic # smoke tests. def subject(str) SemanticPuppet::Version.valid?(str) end it 'recognizes valid versions' do expect(subject('1.0.1')).to be true expect(subject('1.0.3-p324')).to be true end it 'does not recognize invalid versions' do expect(subject('1.0')).to be false # too few segments expect(subject('1.0.3.6')).to be false # too many segments expect(subject('1.03.14')).to be false # leading zero in segment end end describe '#<=>' do def parse(vstring) SemanticPuppet::Version.parse(vstring) end context 'Spec v2.0.0' do context 'Section 11' do # Precedence refers to how versions are compared to each other when # ordered. Precedence MUST be calculated by separating the version into # major, minor, patch and pre-release identifiers in that order (Build # metadata does not figure into precedence). Precedence is determined # by the first difference when comparing each of these identifiers from # left to right as follows: Major, minor, and patch versions are always # compared numerically. # Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. # When major, minor, and patch are equal, a pre-release version has # lower precedence than a normal version. # Example: 1.0.0-alpha < 1.0.0. # Precedence for two pre-release versions with the same major, minor, # and patch version MUST be determined by comparing each dot separated # identifier from left to right until a difference is found as follows: # identifiers consisting of only digits are compared numerically and # identifiers with letters or hyphens are compared lexically in ASCII # sort order. Numeric identifiers always have lower precedence than # non-numeric identifiers. A larger set of pre-release fields has a # higher precedence than a smaller set, if all of the preceding # identifiers are equal. # Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta # < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. context 'comparisons without prereleases' do subject do %w[ 1.0.0 2.0.0 2.1.0 2.1.1 ].map { |v| parse(v) }.shuffle end example 'sorted order' do sorted = subject.sort.map { |v| v.to_s } expect(sorted).to eql(%w[ 1.0.0 2.0.0 2.1.0 2.1.1 ]) end end context 'comparisons against prereleases' do let(:stable) { parse('1.0.0') } let(:prerelease) { parse('1.0.0-alpha') } example 'prereleases have lower precedence' do expect(stable).to be > prerelease expect(prerelease).to be < stable end end context 'comparisions between prereleases' do example 'identical prereleases are equal' do expect(parse('1.0.0-rc1')).to eql parse('1.0.0-rc1') end example 'non-numeric identifiers sort ASCIIbetically' do alpha, beta = parse('1.0.0-alpha'), parse('1.0.0-beta') expect(alpha).to be < beta expect(beta).to be > alpha end example 'numeric identifiers sort numerically' do two, eleven = parse('1.0.0-2'), parse('1.0.0-11') expect(two).to be < eleven expect(eleven).to be > two end example 'non-numeric identifiers have a higher precendence' do number, word = parse('1.0.0-1'), parse('1.0.0-one') expect(number).to be < word expect(word).to be > number end example 'identifiers are parsed left-to-right' do a = parse('1.0.0-these.parts.are.the-same.but.not.waffles.123') b = parse('1.0.0-these.parts.are.the-same.but.not.123.waffles') expect(b).to be < a expect(a).to be > b end example 'larger identifier sets have precendence' do a = parse('1.0.0-alpha') b = parse('1.0.0-alpha.1') expect(a).to be < b expect(b).to be > a end example 'build metadata does not figure into precendence' do pending 'build metadata is not yet in scope' a = parse('1.0.0-alpha+SHA1') b = parse('1.0.0-alpha+MD5') expect(a).to eql b expect(a.to_s).to_not eql b.to_s end example 'sorted order' do list = %w[ 1.0.0-alpha 1.0.0-alpha.1 1.0.0-alpha.beta 1.0.0-beta 1.0.0-beta.2 1.0.0-beta.11 1.0.0-rc.1 1.0.0 ].map { |v| parse(v) }.shuffle sorted = list.sort.map { |v| v.to_s } expect(sorted).to eql %w[ 1.0.0-alpha 1.0.0-alpha.1 1.0.0-alpha.beta 1.0.0-beta 1.0.0-beta.2 1.0.0-beta.11 1.0.0-rc.1 1.0.0 ] end end end end context 'Spec v1.0.0' do context 'Section 4' do # A pre-release version number MAY be denoted by appending an arbitrary # string immediately following the patch version and a dash. The string # MUST be comprised of only alphanumerics plus dash [0-9A-Za-z-]. # Pre-release versions satisfy but have a lower precedence than the # associated normal version. Precedence SHOULD be determined by # lexicographic ASCII sort order. # For instance: 1.0.0-alpha1 < 1.0.0-beta1 < 1.0.0-beta2 < 1.0.0-rc1 < # 1.0.0 example 'sorted order' do list = %w[ 1.0.0-alpha1 1.0.0-beta1 1.0.0-beta2 1.0.0-rc1 1.0.0 ].map { |v| parse(v) }.shuffle sorted = list.sort.map { |v| v.to_s } expect(sorted).to eql %w[ 1.0.0-alpha1 1.0.0-beta1 1.0.0-beta2 1.0.0-rc1 1.0.0 ] end end end end describe '#next' do context 'with :major' do it 'returns the next major version' do expect(subject('1.0.0').next(:major)).to eql(subject('2.0.0')) end it 'does not modify the original version' do v1 = subject('1.0.0') v2 = v1.next(:major) expect(v1).to_not eql(v2) end it 'resets the minor and patch versions to 0' do expect(subject('1.1.1').next(:major)).to eql(subject('2.0.0')) end it 'removes any prerelease information' do expect(subject('1.0.0-alpha').next(:major)).to eql(subject('2.0.0')) end it 'removes any build information' do pending 'build metadata is not yet in scope' expect(subject('1.0.0+abc').next(:major)).to eql(subject('2.0.0')) end end context 'with :minor' do it 'returns the next minor version' do expect(subject('1.0.0').next(:minor)).to eql(subject('1.1.0')) end it 'does not modify the original version' do v1 = subject('1.0.0') v2 = v1.next(:minor) expect(v1).to_not eql(v2) end it 'resets the patch version to 0' do expect(subject('1.1.1').next(:minor)).to eql(subject('1.2.0')) end it 'removes any prerelease information' do expect(subject('1.1.0-alpha').next(:minor)).to eql(subject('1.2.0')) end it 'removes any build information' do pending 'build metadata is not yet in scope' expect(subject('1.1.0+abc').next(:minor)).to eql(subject('1.2.0')) end end context 'with :patch' do it 'returns the next patch version' do expect(subject('1.1.1').next(:patch)).to eql(subject('1.1.2')) end it 'does not modify the original version' do v1 = subject('1.0.0') v2 = v1.next(:patch) expect(v1).to_not eql(v2) end it 'removes any prerelease information' do expect(subject('1.0.0-alpha').next(:patch)).to eql(subject('1.0.1')) end it 'removes any build information' do pending 'build metadata is not yet in scope' expect(subject('1.0.0+abc').next(:patch)).to eql(subject('1.0.1')) end end end end semantic_puppet-0.1.1/spec/unit/semantic_puppet/dependency/0000755000004100000410000000000012633261621024172 5ustar www-datawww-datasemantic_puppet-0.1.1/spec/unit/semantic_puppet/dependency/source_spec.rb0000644000004100000410000000016612633261621027034 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/dependency/source' describe SemanticPuppet::Dependency::Source do end semantic_puppet-0.1.1/spec/unit/semantic_puppet/dependency/graph_node_spec.rb0000644000004100000410000000767412633261621027655 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/dependency/graph_node' describe SemanticPuppet::Dependency::GraphNode do let(:klass) do Class.new do include SemanticPuppet::Dependency::GraphNode attr_accessor :name def initialize(name, *satisfying) @name = name @satisfying = satisfying @satisfying.each { |x| add_dependency(x.name) } end # @override def satisfies_dependency?(node) @satisfying.include?(node) end end end def instance(*args) name = args.first.name unless args.empty? klass.new(name || 'unnamed', *args) end context 'dependencies' do subject { instance() } example 'are added by #add_dependency' do subject.add_dependency('foo') subject.add_dependency('bar') subject.add_dependency('baz') expect(subject.dependency_names).to match_array %w[ foo bar baz ] end example 'are maintained in the #dependencies Hash' do expect(subject.dependencies).to be_empty subject.add_dependency('foo') expect(subject.dependencies).to have_key 'foo' expect(subject.dependencies).to respond_to :to_a end end describe '#<<' do let(:foo) { double('Node', :name => 'foo') } let(:bar1) { double('Node', :name => 'bar', :'<=>' => 0) } let(:bar2) { double('Node', :name => 'bar', :'<=>' => 0) } let(:bar3) { double('Node', :name => 'bar') } let(:baz) { double('Node', :name => 'baz') } subject { instance(foo, bar1, bar2) } it 'appends satisfying nodes to the dependencies' do subject << foo << bar1 << bar2 expect(Array(subject.dependencies['foo'])).to match_array [ foo ] expect(Array(subject.dependencies['bar'])).to match_array [ bar1, bar2 ] end it 'does not append nodes with unknown names' do subject << baz expect(Array(subject.dependencies['baz'])).to be_empty end it 'does not append unsatisfying nodes' do subject << bar3 expect(Array(subject.dependencies['bar'])).to be_empty end end describe '#satisfied' do let(:foo) { double('Node', :name => 'foo') } let(:bar) { double('Node', :name => 'bar') } subject { instance(foo, bar) } it 'is unsatisfied when no nodes have been appended' do expect(subject).to_not be_satisfied end it 'is unsatisfied when any dependencies are missing' do subject << foo expect(subject).to_not be_satisfied end it 'is satisfied when all dependencies are fulfilled' do subject << foo << bar expect(subject).to be_satisfied end end describe '#populate_children' do let(:foo) { double('Node', :name => 'foo') } let(:bar1) { double('Node', :name => 'bar', :'<=>' => 0) } let(:bar2) { double('Node', :name => 'bar', :'<=>' => 0) } let(:baz1) { double('Node', :name => 'baz', :'<=>' => 0) } let(:baz2) { double('Node', :name => 'baz', :'<=>' => 0) } let(:quxx) { double('Node', :name => 'quxx') } subject do graph = instance(foo, bar1, bar2, baz1, baz2) graph << foo << bar1 << bar2 << baz1 << baz2 end it 'saves all relevant nodes as its children' do nodes = [ foo, bar2, baz1, quxx ] nodes.each do |node| allow(node).to receive(:populate_children) end subject.populate_children(nodes) expected = { 'foo' => foo, 'bar' => bar2, 'baz' => baz1 } expect(subject.children).to eql expected end it 'accepts a graph solution and populates it across all nodes' do nodes = [ foo, bar2, baz1 ] nodes.each do |node| expect(node).to receive(:populate_children).with(nodes) end subject.populate_children(nodes) end end describe '#<=>' do it 'can be compared' do a = instance(double('Node', :name => 'a')) b = instance(double('Node', :name => 'b')) expect(a).to be < b expect(b).to be > a expect([b, a].sort).to eql [a, b] expect([a, b].sort).to eql [a, b] end end end semantic_puppet-0.1.1/spec/unit/semantic_puppet/dependency/graph_spec.rb0000644000004100000410000001176112633261621026640 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/dependency/graph' describe SemanticPuppet::Dependency::Graph do Graph = SemanticPuppet::Dependency::Graph GraphNode = SemanticPuppet::Dependency::GraphNode ModuleRelease = SemanticPuppet::Dependency::ModuleRelease Version = SemanticPuppet::Version VersionRange = SemanticPuppet::VersionRange describe '#initialize' do it 'can be called without arguments' do expect { Graph.new }.to_not raise_error end it 'implements the GraphNode protocol' do expect(Graph.new).to be_a GraphNode end it 'adds constraints for every key in the passed hash' do graph = Graph.new('foo' => 1, 'bar' => 2, 'baz' => 3) expect(graph.constraints.keys).to match_array %w[ foo bar baz ] end it 'adds the named dependencies for every key in the passed hash' do graph = Graph.new('foo' => 1, 'bar' => 2, 'baz' => 3) expect(graph.dependency_names).to match_array %w[ foo bar baz ] end end describe '#add_constraint' do let(:graph) { Graph.new } it 'can create a new constraint on a module' do expect(graph.constraints.keys).to be_empty graph.add_constraint('test', 'module-name', 'nil') { } expect(graph.constraints.keys).to match_array %w[ module-name ] end it 'permits multiple constraints against the same module name' do expect(graph.constraints.keys).to be_empty graph.add_constraint('test', 'module-name', 'nil') { } graph.add_constraint('test', 'module-name', 'nil') { } expect(graph.constraints.keys).to match_array %w[ module-name ] end end describe '#satisfies_dependency?' do it 'is not satisfied by modules it does not depend on' do graph = Graph.new('foo' => VersionRange.parse('1.x')) release = ModuleRelease.new(nil, 'bar', Version.parse('1.0.0')) expect(graph.satisfies_dependency?(release)).to_not be true end it 'is not satisfied by modules that do not fulfill the constraint' do graph = Graph.new('foo' => VersionRange.parse('1.x')) release = ModuleRelease.new(nil, 'foo', Version.parse('2.3.1')) expect(graph.satisfies_dependency?(release)).to_not be true end it 'is not satisfied by modules that do not fulfill all the constraints' do graph = Graph.new('foo' => VersionRange.parse('1.x')) graph.add_constraint('me', 'foo', '1.2.3') do |node| node.version.to_s == '1.2.3' end release = ModuleRelease.new(nil, 'foo', Version.parse('1.2.1')) expect(graph.satisfies_dependency?(release)).to_not be true end it 'is satisfied by modules that do fulfill all the constraints' do graph = Graph.new('foo' => VersionRange.parse('1.x')) graph.add_constraint('me', 'foo', '1.2.3') do |node| node.version.to_s == '1.2.3' end release = ModuleRelease.new(nil, 'foo', Version.parse('1.2.3')) expect(graph.satisfies_dependency?(release)).to be true end end describe '#add_graph_constraint' do let(:graph) { Graph.new } it 'can create a new constraint on a graph' do expect(graph.constraints.keys).to be_empty graph.add_graph_constraint('test') { } expect(graph.constraints.keys).to match_array [ :graph ] end it 'permits multiple graph constraints' do expect(graph.constraints.keys).to be_empty graph.add_graph_constraint('test') { } graph.add_graph_constraint('test') { } expect(graph.constraints.keys).to match_array [ :graph ] end end describe '#satisfies_graph?' do it 'returns false if the solution violates a graph constraint' do graph = Graph.new graph.add_graph_constraint('me') do |nodes| nodes.none? { |node| node.name =~ /z/ } end releases = [ double('Node', :name => 'foo'), double('Node', :name => 'bar'), double('Node', :name => 'baz'), ] expect(graph.satisfies_graph?(releases)).to_not be true end it 'returns false if the solution violates any graph constraint' do graph = Graph.new graph.add_graph_constraint('me') do |nodes| nodes.all? { |node| node.name.length < 5 } end graph.add_graph_constraint('me') do |nodes| nodes.none? { |node| node.name =~ /z/ } end releases = [ double('Node', :name => 'foo'), double('Node', :name => 'bar'), double('Node', :name => 'bangerang'), ] expect(graph.satisfies_graph?(releases)).to_not be true end it 'returns true if the solution violates no graph constraints' do graph = Graph.new graph.add_graph_constraint('me') do |nodes| nodes.all? { |node| node.name.length < 5 } end graph.add_graph_constraint('me') do |nodes| nodes.none? { |node| node.name =~ /z/ } end releases = [ double('Node', :name => 'foo'), double('Node', :name => 'bar'), double('Node', :name => 'boom'), ] expect(graph.satisfies_graph?(releases)).to be true end end end semantic_puppet-0.1.1/spec/unit/semantic_puppet/dependency/unsatisfiable_graph_spec.rb0000644000004100000410000000210612633261621031542 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/dependency/unsatisfiable_graph' describe SemanticPuppet::Dependency::UnsatisfiableGraph do let(:modules) { %w[ foo bar baz ] } let(:graph) { double('Graph', :modules => modules) } let(:instance) { described_class.new(graph) } subject { instance } describe '#message' do subject { instance.message } it { should match /#{instance.send(:sentence_from_list, modules)}/ } end describe '#sentence_from_list' do subject { instance.send(:sentence_from_list, modules) } context 'with a list of one item' do let(:modules) { %w[ foo ] } it { should eql 'foo' } end context 'with a list of two items' do let(:modules) { %w[ foo bar ] } it { should eql 'foo and bar' } end context 'with a list of three items' do let(:modules) { %w[ foo bar baz ] } it { should eql 'foo, bar, and baz' } end context 'with a list of more than three items' do let(:modules) { %w[ foo bar baz quux ] } it { should eql 'foo, bar, baz, and quux' } end end end semantic_puppet-0.1.1/spec/unit/semantic_puppet/dependency/module_release_spec.rb0000644000004100000410000001017312633261621030520 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/dependency/module_release' describe SemanticPuppet::Dependency::ModuleRelease do def source @source ||= SemanticPuppet::Dependency::Source.new end def make_release(name, version, deps = {}) source.create_release(name, version, deps) end let(:no_dependencies) do make_release('module', '1.2.3') end let(:one_dependency) do make_release('module', '1.2.3', 'foo' => '1.0.0') end let(:three_dependencies) do dependencies = { 'foo' => '1.0.0', 'bar' => '2.0.0', 'baz' => '3.0.0' } make_release('module', '1.2.3', dependencies) end describe '#dependency_names' do it "lists the names of all the release's dependencies" do expect(no_dependencies.dependency_names).to match_array %w[] expect(one_dependency.dependency_names).to match_array %w[foo] expect(three_dependencies.dependency_names).to match_array %w[foo bar baz] end end describe '#to_s' do let(:name) { 'foobarbaz' } let(:version) { '1.2.3' } subject { make_release(name, version).to_s } it { should =~ /#{name}/ } it { should =~ /#{version}/ } end describe '#<<' do it 'marks matching dependencies as satisfied' do one_dependency << make_release('foo', '1.0.0') expect(one_dependency).to be_satisfied end it 'does not mark mis-matching dependency names as satisfied' do one_dependency << make_release('WAT', '1.0.0') expect(one_dependency).to_not be_satisfied end it 'does not mark mis-matching dependency versions as satisfied' do one_dependency << make_release('foo', '0.0.1') expect(one_dependency).to_not be_satisfied end end describe '#<=>' do it 'considers releases with greater version numbers greater' do expect(make_release('foo', '1.0.0')).to be > make_release('foo', '0.1.0') end it 'considers releases with lesser version numbers lesser' do expect(make_release('foo', '0.1.0')).to be < make_release('foo', '1.0.0') end it 'orders releases with different names lexographically' do expect(make_release('bar', '1.0.0')).to be < make_release('foo', '1.0.0') end it 'orders releases by name first' do expect(make_release('bar', '2.0.0')).to be < make_release('foo', '1.0.0') end end describe '#satisfied?' do it 'returns true when there are no dependencies to satisfy' do expect(no_dependencies).to be_satisfied end it 'returns false when no dependencies have been satisified' do expect(one_dependency).to_not be_satisfied end it 'returns false when not all dependencies have been satisified' do releases = %w[ 0.9.0 1.0.0 1.0.1 ].map { |ver| make_release('foo', ver) } three_dependencies << releases expect(three_dependencies).to_not be_satisfied end it 'returns false when not all dependency versions have been satisified' do releases = %w[ 0.9.0 1.0.1 ].map { |ver| make_release('foo', ver) } one_dependency << releases expect(one_dependency).to_not be_satisfied end it 'returns true when all dependencies have been satisified' do releases = %w[ 0.9.0 1.0.0 1.0.1 ].map { |ver| make_release('foo', ver) } one_dependency << releases expect(one_dependency).to be_satisfied end end describe '#satisfies_dependency?' do it 'returns false when there are no dependencies to satisfy' do release = make_release('foo', '1.0.0') expect(no_dependencies.satisfies_dependency?(release)).to_not be true end it 'returns false when the release does not match the dependency name' do release = make_release('bar', '1.0.0') expect(one_dependency.satisfies_dependency?(release)).to_not be true end it 'returns false when the release does not match the dependency version' do release = make_release('foo', '4.0.0') expect(one_dependency.satisfies_dependency?(release)).to_not be true end it 'returns true when the release matches the dependency' do release = make_release('foo', '1.0.0') expect(one_dependency.satisfies_dependency?(release)).to be true end end end semantic_puppet-0.1.1/spec/unit/semantic_puppet/dependency_spec.rb0000644000004100000410000003337412633261621025543 0ustar www-datawww-datarequire 'spec_helper' require 'semantic_puppet/dependency' describe SemanticPuppet::Dependency do def create_release(source, name, version, deps = {}) SemanticPuppet::Dependency::ModuleRelease.new( source, name, SemanticPuppet::Version.parse(version), Hash[deps.map { |k, v| [k, SemanticPuppet::VersionRange.parse(v) ] }] ) end describe '.sources' do it 'defaults to an empty list' do expect(subject.sources).to be_empty end it 'is frozen' do expect(subject.sources).to be_frozen end it 'can be modified by using #add_source' do subject.add_source(SemanticPuppet::Dependency::Source.new) expect(subject.sources).to_not be_empty end it 'can be emptied by using #clear_sources' do subject.add_source(SemanticPuppet::Dependency::Source.new) subject.clear_sources expect(subject.sources).to be_empty end end describe '.query' do context 'without sources' do it 'returns an unsatisfied ModuleRelease' do expect(subject.query('module_name' => '1.0.0')).to_not be_satisfied end end context 'with one source' do let(:source) { double('Source') } before { SemanticPuppet::Dependency.add_source(source) } it 'queries the source for release information' do source.should_receive(:fetch).with('module_name').and_return([]) SemanticPuppet::Dependency.query('module_name' => '1.0.0') end it 'queries the source for each dependency' do source.should_receive(:fetch).with('module_name').and_return([ create_release(source, 'module_name', '1.0.0', 'bar' => '1.0.0') ]) source.should_receive(:fetch).with('bar').and_return([]) SemanticPuppet::Dependency.query('module_name' => '1.0.0') end it 'queries the source for each dependency only once' do source.should_receive(:fetch).with('module_name').and_return([ create_release( source, 'module_name', '1.0.0', 'bar' => '1.0.0', 'baz' => '0.0.2' ) ]) source.should_receive(:fetch).with('bar').and_return([ create_release(source, 'bar', '1.0.0', 'baz' => '0.0.3') ]) source.should_receive(:fetch).with('baz').once.and_return([]) SemanticPuppet::Dependency.query('module_name' => '1.0.0') end it 'returns a ModuleRelease with the requested dependencies' do source.stub(:fetch).and_return([]) result = SemanticPuppet::Dependency.query('foo' => '1.0.0', 'bar' => '1.0.0') expect(result.dependency_names).to match_array %w[ foo bar ] end it 'populates the returned ModuleRelease with related dependencies' do source.stub(:fetch).and_return( [ foo = create_release(source, 'foo', '1.0.0', 'bar' => '1.0.0') ], [ bar = create_release(source, 'bar', '1.0.0') ] ) result = SemanticPuppet::Dependency.query('foo' => '1.0.0', 'bar' => '1.0.0') expect(result.dependencies['foo']).to eql SortedSet.new([ foo ]) expect(result.dependencies['bar']).to eql SortedSet.new([ bar ]) end it 'populates all returned ModuleReleases with related dependencies' do source.stub(:fetch).and_return( [ foo = create_release(source, 'foo', '1.0.0', 'bar' => '1.0.0') ], [ bar = create_release(source, 'bar', '1.0.0', 'baz' => '0.1.0') ], [ baz = create_release(source, 'baz', '0.1.0', 'baz' => '1.0.0') ] ) result = SemanticPuppet::Dependency.query('foo' => '1.0.0') expect(result.dependencies['foo']).to eql SortedSet.new([ foo ]) expect(foo.dependencies['bar']).to eql SortedSet.new([ bar ]) expect(bar.dependencies['baz']).to eql SortedSet.new([ baz ]) end end context 'with multiple sources' do let(:source1) { double('SourceOne') } let(:source2) { double('SourceTwo') } let(:source3) { double('SourceThree') } before do SemanticPuppet::Dependency.add_source(source1) SemanticPuppet::Dependency.add_source(source2) SemanticPuppet::Dependency.add_source(source3) end it 'queries each source in turn' do source1.should_receive(:fetch).with('module_name').and_return([]) source2.should_receive(:fetch).with('module_name').and_return([]) source3.should_receive(:fetch).with('module_name').and_return([]) SemanticPuppet::Dependency.query('module_name' => '1.0.0') end it 'resolves all dependencies against all sources' do source1.should_receive(:fetch).with('module_name').and_return([ create_release(source1, 'module_name', '1.0.0', 'bar' => '1.0.0') ]) source2.should_receive(:fetch).with('module_name').and_return([]) source3.should_receive(:fetch).with('module_name').and_return([]) source1.should_receive(:fetch).with('bar').and_return([]) source2.should_receive(:fetch).with('bar').and_return([]) source3.should_receive(:fetch).with('bar').and_return([]) SemanticPuppet::Dependency.query('module_name' => '1.0.0') end end end describe '.resolve' do def add_source_modules(name, versions, deps = {}) versions = Array(versions) releases = versions.map { |ver| create_release(source, name, ver, deps) } source.stub(:fetch).with(name).and_return(modules[name].concat(releases)) end def subject(specs) graph = SemanticPuppet::Dependency.query(specs) yield graph if block_given? expect(graph.dependencies).to_not be_empty result = SemanticPuppet::Dependency.resolve(graph) expect(graph.dependencies).to_not be_empty result.map { |rel| [ rel.name, rel.version.to_s ] } end let(:modules) { Hash.new { |h,k| h[k] = [] }} let(:source) { double('Source', :priority => 0) } before { SemanticPuppet::Dependency.add_source(source) } context 'for a module without dependencies' do def foo(range) subject('foo' => range).map { |x| x.last } end it 'returns the greatest release matching the version range' do add_source_modules('foo', %w[ 0.9.0 1.0.0 1.1.0 2.0.0 ]) expect(foo('1.x')).to eql %w[ 1.1.0 ] end context 'when the query includes both stable and prerelease versions' do it 'returns the greatest stable release matching the range' do add_source_modules('foo', %w[ 0.9.0 1.0.0 1.1.0 1.2.0-pre 2.0.0 ]) expect(foo('1.x')).to eql %w[ 1.1.0 ] end end context 'when the query omits all stable versions' do it 'returns the greatest prerelease version matching the range' do add_source_modules('foo', %w[ 1.0.0 1.1.0-a 1.1.0-b 2.0.0 ]) expect(foo('1.1.x')).to eql %w[ 1.1.0-b ] expect(foo('1.1.0-a')).to eql %w[ 1.1.0-a ] end end context 'when the query omits all versions' do it 'fails with an appropriate message' do add_source_modules('foo', %w[ 1.0.0 1.1.0-a 1.1.0 ]) with_message = /Could not find satisfying releases/ expect { foo('2.x') }.to raise_exception with_message expect { foo('2.x') }.to raise_exception /\bfoo\b/ end end end context 'for a module with dependencies' do def foo(range) subject('foo' => range) end it 'returns the greatest releases matching the dependency range' do add_source_modules('foo', '1.1.0', 'bar' => '1.x') add_source_modules('bar', %w[ 0.9.0 1.0.0 1.1.0 1.2.0 2.0.0 ]) expect(foo('1.1.0')).to include %w[ foo 1.1.0 ], %w[ bar 1.2.0 ] end context 'when the dependency has both stable and prerelease versions' do it 'returns the greatest stable release matching the range' do add_source_modules('foo', '1.1.0', 'bar' => '1.x') add_source_modules('bar', %w[ 0.9.0 1.0.0 1.1.0 1.2.0-pre 2.0.0 ]) expect(foo('1.1.0')).to include %w[ foo 1.1.0 ], %w[ bar 1.1.0 ] end end context 'when the dependency has no stable versions' do it 'returns the greatest prerelease version matching the range' do add_source_modules('foo', '1.1.0', 'bar' => '1.1.x') add_source_modules('foo', '1.1.1', 'bar' => '1.1.0-a') add_source_modules('bar', %w[ 1.0.0 1.1.0-a 1.1.0-b 2.0.0 ]) expect(foo('1.1.0')).to include %w[ foo 1.1.0 ], %w[ bar 1.1.0-b ] expect(foo('1.1.1')).to include %w[ foo 1.1.1 ], %w[ bar 1.1.0-a ] end end context 'when the dependency cannot be satisfied' do it 'fails with an appropriate message' do add_source_modules('foo', %w[ 1.1.0 ], 'bar' => '1.x') add_source_modules('bar', %w[ 0.0.1 0.1.0-a 0.1.0 ]) with_message = /Could not find satisfying releases/ expect { foo('1.1.0') }.to raise_exception with_message expect { foo('1.1.0') }.to raise_exception /\bfoo\b/ end end end context 'for a module with competing dependencies' do def foo(range) subject('foo' => range) end context 'that overlap' do it 'returns the greatest release satisfying all dependencies' do add_source_modules('foo', '1.1.0', 'bar' => '1.0.0', 'baz' => '1.0.0') add_source_modules('bar', '1.0.0', 'quxx' => '1.x') add_source_modules('baz', '1.0.0', 'quxx' => '1.1.x') add_source_modules('quxx', %w[ 0.9.0 1.0.0 1.1.0 1.1.1 1.2.0 2.0.0 ]) expect(foo('1.1.0')).to_not include %w[ quxx 1.2.0 ] expect(foo('1.1.0')).to include %w[ quxx 1.1.1 ] end end context 'that do not overlap' do it 'fails with an appropriate message' do add_source_modules('foo','1.1.0', 'bar' => '1.0.0', 'baz' => '1.0.0') add_source_modules('bar','1.0.0', 'quxx' => '1.x') add_source_modules('baz','1.0.0', 'quxx' => '2.x') add_source_modules('quxx', %w[ 0.9.0 1.0.0 1.1.0 1.1.1 1.2.0 2.0.0 ]) with_message = /Could not find satisfying releases/ expect { foo('1.1.0') }.to raise_exception with_message expect { foo('1.1.0') }.to raise_exception /\bfoo\b/ end end end context 'for a module with circular dependencies' do def foo(range) subject('foo' => range) end context 'that can be resolved' do it 'terminates' do add_source_modules('foo', '1.1.0', 'foo' => '1.x') expect(foo('1.1.0')).to include %w[ foo 1.1.0 ] end end context 'that cannot be resolved' do it 'fails with an appropriate message' do add_source_modules('foo', '1.1.0', 'foo' => '1.0.0') with_message = /Could not find satisfying releases/ expect { foo('1.1.0') }.to raise_exception with_message expect { foo('1.1.0') }.to raise_exception /\bfoo\b/ end end end context 'for a module with dependencies' do context 'that violate module constraints on the graph' do def foo(range) subject('foo' => range) do |graph| graph.add_constraint('no downgrade', 'bar', '> 3.0.0') do |node| SemanticPuppet::VersionRange.parse('> 3.0.0') === node.version end end end context 'that can be resolved' do it 'terminates' do add_source_modules('foo', '1.1.0', 'bar' => '1.x') add_source_modules('foo', '1.2.0', 'bar' => '>= 2.0.0') add_source_modules('bar', '1.0.0') add_source_modules('bar', '2.0.0', 'baz' => '>= 1.0.0') add_source_modules('bar', '3.0.0') add_source_modules('bar', '3.0.1') add_source_modules('baz', '1.0.0') expect(foo('1.x')).to include %w[ foo 1.2.0 ], %w[ bar 3.0.1 ] end end context 'that cannot be resolved' do it 'fails with an appropriate message' do add_source_modules('foo', '1.1.0', 'bar' => '1.x') add_source_modules('foo', '1.2.0', 'bar' => '2.x') add_source_modules('bar', '1.0.0', 'baz' => '1.x') add_source_modules('bar', '2.0.0', 'baz' => '1.x') add_source_modules('baz', '1.0.0') add_source_modules('baz', '3.0.0') add_source_modules('baz', '3.0.1') with_message = /Could not find satisfying releases/ expect { foo('1.x') }.to raise_exception with_message expect { foo('1.x') }.to raise_exception /\bfoo\b/ end end end end context 'that violate graph constraints' do def foo(range) subject('foo' => range) do |graph| graph.add_graph_constraint('uniqueness') do |nodes| nodes.none? { |node| node.name =~ /z/ } end end end context 'that can be resolved' do it 'terminates' do add_source_modules('foo', '1.1.0', 'bar' => '1.x') add_source_modules('foo', '1.2.0', 'bar' => '2.x') add_source_modules('bar', '1.0.0') add_source_modules('bar', '2.0.0', 'baz' => '1.0.0') add_source_modules('baz', '1.0.0') expect(foo('1.x')).to include %w[ foo 1.1.0 ], %w[ bar 1.0.0 ] end end context 'that cannot be resolved' do it 'fails with an appropriate message' do add_source_modules('foo', '1.1.0', 'bar' => '1.x') add_source_modules('foo', '1.2.0', 'bar' => '2.x') add_source_modules('bar', '1.0.0', 'baz' => '1.0.0') add_source_modules('bar', '2.0.0', 'baz' => '1.0.0') add_source_modules('baz', '1.0.0') with_message = /Could not find satisfying releases/ expect { foo('1.1.0') }.to raise_exception with_message expect { foo('1.1.0') }.to raise_exception /\bfoo\b/ end end end end end semantic_puppet-0.1.1/lib/0000755000004100000410000000000012633261621015511 5ustar www-datawww-datasemantic_puppet-0.1.1/lib/semantic_puppet/0000755000004100000410000000000012633261621020711 5ustar www-datawww-datasemantic_puppet-0.1.1/lib/semantic_puppet/version_range.rb0000644000004100000410000003376712633261621024117 0ustar www-datawww-datarequire 'semantic_puppet' module SemanticPuppet class VersionRange < Range class << self # Parses a version range string into a comparable {VersionRange} instance. # # Currently parsed version range string may take any of the following: # forms: # # * Regular Semantic Version strings # * ex. `"1.0.0"`, `"1.2.3-pre"` # * Partial Semantic Version strings # * ex. `"1.0.x"`, `"1"`, `"2.X"` # * Inequalities # * ex. `"> 1.0.0"`, `"<3.2.0"`, `">=4.0.0"` # * Approximate Versions # * ex. `"~1.0.0"`, `"~ 3.2.0"`, `"~4.0.0"` # * Inclusive Ranges # * ex. `"1.0.0 - 1.3.9"` # * Range Intersections # * ex. `">1.0.0 <=2.3.0"` # # @param range_str [String] the version range string to parse # @return [VersionRange] a new {VersionRange} instance def parse(range_str) partial = '\d+(?:[.]\d+)?(?:[.][x]|[.]\d+(?:[-][0-9a-z.-]*)?)?' exact = '\d+[.]\d+[.]\d+(?:[-][0-9a-z.-]*)?' range = range_str.gsub(/([(><=~])[ ]+/, '\1') range = range.gsub(/ - /, '#').strip return case range when /\A(#{partial})\Z/i parse_loose_version_expression($1) when /\A([><][=]?)(#{exact})\Z/i parse_inequality_expression($1, $2) when /\A~(#{partial})\Z/i parse_reasonably_close_expression($1) when /\A(#{exact})#(#{exact})\Z/i parse_inclusive_range_expression($1, $2) when /[ ]+/ parse_intersection_expression(range) else raise ArgumentError end rescue ArgumentError raise ArgumentError, "Unparsable version range: #{range_str.inspect}" end private # Creates a new {VersionRange} from a range intersection expression. # # @param expr [String] a range intersection expression # @return [VersionRange] a version range representing `expr` def parse_intersection_expression(expr) expr.split(/[ ]+/).map { |x| parse(x) }.inject { |a,b| a & b } end # Creates a new {VersionRange} from a "loose" description of a Semantic # Version number. # # @see .process_loose_expr # # @param expr [String] a "loose" version expression # @return [VersionRange] a version range representing `expr` def parse_loose_version_expression(expr) start, finish = process_loose_expr(expr) if start.stable? start = start.send(:first_prerelease) end if finish.stable? exclude = true finish = finish.send(:first_prerelease) end self.new(start, finish, exclude) end # Creates an open-ended version range from an inequality expression. # # @overload parse_inequality_expression('<', expr) # {include:.parse_lt_expression} # # @overload parse_inequality_expression('<=', expr) # {include:.parse_lte_expression} # # @overload parse_inequality_expression('>', expr) # {include:.parse_gt_expression} # # @overload parse_inequality_expression('>=', expr) # {include:.parse_gte_expression} # # @param comp ['<', '<=', '>', '>='] an inequality operator # @param expr [String] a "loose" version expression # @return [VersionRange] a range covering all versions in the inequality def parse_inequality_expression(comp, expr) case comp when '>' parse_gt_expression(expr) when '>=' parse_gte_expression(expr) when '<' parse_lt_expression(expr) when '<=' parse_lte_expression(expr) end end # Returns a range covering all versions greater than the given `expr`. # # @param expr [String] the version to be greater than # @return [VersionRange] a range covering all versions greater than the # given `expr` def parse_gt_expression(expr) if expr =~ /^[^+]*-/ start = Version.parse("#{expr}.0") else start = process_loose_expr(expr).last.send(:first_prerelease) end self.new(start, SemanticPuppet::Version::MAX) end # Returns a range covering all versions greater than or equal to the given # `expr`. # # @param expr [String] the version to be greater than or equal to # @return [VersionRange] a range covering all versions greater than or # equal to the given `expr` def parse_gte_expression(expr) if expr =~ /^[^+]*-/ start = Version.parse(expr) else start = process_loose_expr(expr).first.send(:first_prerelease) end self.new(start, SemanticPuppet::Version::MAX) end # Returns a range covering all versions less than the given `expr`. # # @param expr [String] the version to be less than # @return [VersionRange] a range covering all versions less than the # given `expr` def parse_lt_expression(expr) if expr =~ /^[^+]*-/ finish = Version.parse(expr) else finish = process_loose_expr(expr).first.send(:first_prerelease) end self.new(SemanticPuppet::Version::MIN, finish, true) end # Returns a range covering all versions less than or equal to the given # `expr`. # # @param expr [String] the version to be less than or equal to # @return [VersionRange] a range covering all versions less than or equal # to the given `expr` def parse_lte_expression(expr) if expr =~ /^[^+]*-/ finish = Version.parse(expr) self.new(SemanticPuppet::Version::MIN, finish) else finish = process_loose_expr(expr).last.send(:first_prerelease) self.new(SemanticPuppet::Version::MIN, finish, true) end end # The "reasonably close" expression is used to designate ranges that have # a reasonable proximity to the given "loose" version number. These take # the form: # # ~[Version] # # The general semantics of these expressions are that the given version # forms a lower bound for the range, and the upper bound is either the # next version number increment (at whatever precision the expression # provides) or the next stable version (in the case of a prerelease # version). # # @example "Reasonably close" major version # "~1" # => (>=1.0.0 <2.0.0) # @example "Reasonably close" minor version # "~1.2" # => (>=1.2.0 <1.3.0) # @example "Reasonably close" patch version # "~1.2.3" # => (>=1.2.3 <1.3.0) # @example "Reasonably close" prerelease version # "~1.2.3-alpha" # => (>=1.2.3-alpha <1.2.4) # # @param expr [String] a "loose" expression to build the range around # @return [VersionRange] a "reasonably close" version range def parse_reasonably_close_expression(expr) parsed, succ = process_loose_expr(expr) if parsed.stable? parsed = parsed.send(:first_prerelease) # Handle the special case of "~1.2.3" expressions. succ = succ.next(:minor) if ((parsed.major == succ.major) && (parsed.minor == succ.minor)) succ = succ.send(:first_prerelease) self.new(parsed, succ, true) else self.new(parsed, succ.next(:patch).send(:first_prerelease), true) end end # An "inclusive range" expression takes two version numbers (or partial # version numbers) and creates a range that covers all versions between # them. These take the form: # # [Version] - [Version] # # @param start [String] a "loose" expresssion for the start of the range # @param finish [String] a "loose" expression for the end of the range # @return [VersionRange] a {VersionRange} covering `start` to `finish` def parse_inclusive_range_expression(start, finish) start, _ = process_loose_expr(start) _, finish = process_loose_expr(finish) start = start.send(:first_prerelease) if start.stable? if finish.stable? exclude = true finish = finish.send(:first_prerelease) end self.new(start, finish, exclude) end # A "loose expression" is one that takes the form of all or part of a # valid Semantic Version number. Particularly: # # * [Major].[Minor].[Patch]-[Prerelease] # * [Major].[Minor].[Patch] # * [Major].[Minor] # * [Major] # # Various placeholders are also permitted in "loose expressions" # (typically an 'x' or an asterisk). # # This method parses these expressions into a minimal and maximal version # number pair. # # @todo Stabilize whether the second value is inclusive or exclusive # # @param expr [String] a string containing a "loose" version expression # @return [(VersionNumber, VersionNumber)] a minimal and maximal # version pair for the given expression def process_loose_expr(expr) case expr when /^(\d+)(?:[.][xX*])?$/ expr = "#{$1}.0.0" arity = :major when /^(\d+[.]\d+)(?:[.][xX*])?$/ expr = "#{$1}.0" arity = :minor when /^\d+[.]\d+[.]\d+$/ arity = :patch end version = next_version = Version.parse(expr) if arity next_version = version.next(arity) end [ version, next_version ] end end # Computes the intersection of a pair of ranges. If the ranges have no # useful intersection, an empty range is returned. # # @param other [VersionRange] the range to intersect with # @return [VersionRange] the common subset def intersection(other) raise NOT_A_VERSION_RANGE unless other.kind_of?(VersionRange) if self.begin < other.begin return other.intersection(self) end unless include?(other.begin) || other.include?(self.begin) return EMPTY_RANGE end endpoint = ends_before?(other) ? self : other VersionRange.new(self.begin, endpoint.end, endpoint.exclude_end?) end alias :& :intersection # Returns a string representation of this range, prefering simple common # expressions for comprehension. # # @return [String] a range expression representing this VersionRange def to_s start, finish = self.begin, self.end inclusive = exclude_end? ? '' : '=' case when EMPTY_RANGE == self "<0.0.0" when exact_version?, patch_version? "#{ start }" when minor_version? "#{ start }".sub(/.0$/, '.x') when major_version? "#{ start }".sub(/.0.0$/, '.x') when open_end? && start.to_s =~ /-.*[.]0$/ ">#{ start }".sub(/.0$/, '') when open_end? ">=#{ start }" when open_begin? "<#{ inclusive }#{ finish }" else ">=#{ start } <#{ inclusive }#{ finish }" end end alias :inspect :to_s private # Determines whether this {VersionRange} has an earlier endpoint than the # give `other` range. # # @param other [VersionRange] the range to compare against # @return [Boolean] true if the endpoint for this range is less than or # equal to the endpoint of the `other` range. def ends_before?(other) self.end < other.end || (self.end == other.end && self.exclude_end?) end # Describes whether this range has an upper limit. # @return [Boolean] true if this range has no upper limit def open_end? self.end == SemanticPuppet::Version::MAX end # Describes whether this range has a lower limit. # @return [Boolean] true if this range has no lower limit def open_begin? self.begin == SemanticPuppet::Version::MIN end # Describes whether this range follows the patterns for matching all # releases with the same exact version. # @return [Boolean] true if this range matches only a single exact version def exact_version? self.begin == self.end end # Describes whether this range follows the patterns for matching all # releases with the same major version. # @return [Boolean] true if this range matches only a single major version def major_version? start, finish = self.begin, self.end exclude_end? && start.major.next == finish.major && same_minor? && start.minor == 0 && same_patch? && start.patch == 0 && [start.prerelease, finish.prerelease] == ['', ''] end # Describes whether this range follows the patterns for matching all # releases with the same minor version. # @return [Boolean] true if this range matches only a single minor version def minor_version? start, finish = self.begin, self.end exclude_end? && same_major? && start.minor.next == finish.minor && same_patch? && start.patch == 0 && [start.prerelease, finish.prerelease] == ['', ''] end # Describes whether this range follows the patterns for matching all # releases with the same patch version. # @return [Boolean] true if this range matches only a single patch version def patch_version? start, finish = self.begin, self.end exclude_end? && same_major? && same_minor? && start.patch.next == finish.patch && [start.prerelease, finish.prerelease] == ['', ''] end # @return [Boolean] true if `begin` and `end` share the same major verion def same_major? self.begin.major == self.end.major end # @return [Boolean] true if `begin` and `end` share the same minor verion def same_minor? self.begin.minor == self.end.minor end # @return [Boolean] true if `begin` and `end` share the same patch verion def same_patch? self.begin.patch == self.end.patch end undef :to_a NOT_A_VERSION_RANGE = ArgumentError.new("value must be a #{VersionRange}") public # A range that matches no versions EMPTY_RANGE = VersionRange.parse('< 0.0.0').freeze end end semantic_puppet-0.1.1/lib/semantic_puppet/dependency.rb0000644000004100000410000001412712633261621023361 0ustar www-datawww-datarequire 'semantic_puppet' module SemanticPuppet module Dependency extend self autoload :Graph, 'semantic_puppet/dependency/graph' autoload :GraphNode, 'semantic_puppet/dependency/graph_node' autoload :ModuleRelease, 'semantic_puppet/dependency/module_release' autoload :Source, 'semantic_puppet/dependency/source' autoload :UnsatisfiableGraph, 'semantic_puppet/dependency/unsatisfiable_graph' # @!group Sources # @return [Array] a frozen copy of the {Source} list def sources (@sources ||= []).dup.freeze end # Appends a new {Source} to the current list. # @param source [Source] the {Source} to add # @return [void] def add_source(source) sources @sources << source nil end # Clears the current list of {Source}s. # @return [void] def clear_sources sources @sources.clear nil end # @!endgroup # Fetches a graph of modules and their dependencies from the currently # configured list of {Source}s. # # @todo Return a specialized "Graph" object. # @todo Allow for external constraints to be added to the graph. # @see #sources # @see #add_source # @see #clear_sources # # @param modules [{ String => String }] # @return [Graph] the root of a dependency graph def query(modules) constraints = Hash[modules.map { |k, v| [ k, VersionRange.parse(v) ] }] graph = Graph.new(constraints) fetch_dependencies(graph) return graph end # Given a graph result from {#query}, this method will resolve the graph of # dependencies, if possible, into a flat list of the best suited modules. If # the dependency graph does not have a suitable resolution, this method will # raise an exception to that effect. # # @param graph [Graph] the root of a dependency graph # @return [Array] the list of releases to act on def resolve(graph) catch :next do return walk(graph, graph.dependencies.dup) end raise UnsatisfiableGraph.new(graph) end # Fetches all available releases for the given module name. # # @param name [String] the module name to find releases for # @return [Array] the available releases def fetch_releases(name) releases = {} sources.each do |source| source.fetch(name).each do |dependency| releases[dependency.version] ||= dependency end end return releases.values end private # Iterates over a changing set of dependencies in search of the best # solution available. Fitness is specified as meeting all the constraints # placed on it, being {ModuleRelease#satisfied? satisfied}, and having the # greatest version number (with stability being preferred over prereleases). # # @todo Traversal order is not presently guaranteed. # # @param graph [Graph] the root of a dependency graph # @param dependencies [{ String => Array }] the dependencies # @param considering [Array] the set of releases being tested # @return [Array] the list of releases to use, if successful def walk(graph, dependencies, *considering) return considering if dependencies.empty? # Selecting a dependency from the collection... name = dependencies.keys.sort.first deps = dependencies.delete(name) # ... (and stepping over it if we've seen it before) ... unless (deps & considering).empty? return walk(graph, dependencies, *considering) end # ... we'll iterate through the list of possible versions in order. preferred_releases(deps).reverse_each do |dep| # We should skip any releases that violate any module's constraints. unless [graph, *considering].all? { |x| x.satisfies_constraints?(dep) } next end # We should skip over any releases that violate graph-level constraints. potential_solution = considering.dup << dep unless graph.satisfies_graph? potential_solution next end catch :next do # After adding any new dependencies and imposing our own constraints # on existing dependencies, we'll mark ourselves as "under # consideration" and recurse. merged = dependencies.merge(dep.dependencies) { |_,a,b| a & b } # If all subsequent dependencies resolved well, the recursive call # will return a completed dependency list. If there were problems # resolving our dependencies, we'll catch `:next`, which will cause # us to move to the next possibility. return walk(graph, merged, *potential_solution) end end # Once we've exhausted all of our possible versions, we know that our # last choice was unusable, so we'll unwind the stack and make a new # choice. throw :next end # Given a {ModuleRelease}, this method will iterate through the current # list of {Source}s to find the complete list of versions available for its # dependencies. # # @param node [GraphNode] the node to fetch details for # @return [void] def fetch_dependencies(node, cache = {}) node.dependency_names.each do |name| unless cache.key?(name) cache[name] = fetch_releases(name) cache[name].each { |dep| fetch_dependencies(dep, cache) } end node << cache[name] end end # Given a list of potential releases, this method returns the most suitable # releases for exploration. Only {ModuleRelease#satisfied? satisfied} # releases are considered, and releases with stable versions are preferred. # # @param releases [Array] a list of potential releases # @return [Array] releases open for consideration def preferred_releases(releases) satisfied = releases.select { |x| x.satisfied? } if satisfied.any? { |x| x.version.stable? } return satisfied.select { |x| x.version.stable? } else return satisfied end end end end semantic_puppet-0.1.1/lib/semantic_puppet/version.rb0000644000004100000410000001271612633261621022732 0ustar www-datawww-datarequire 'semantic_puppet' module SemanticPuppet # @note SemanticPuppet::Version subclasses Numeric so that it has sane Range # semantics in Ruby 1.9+. class Version < Numeric include Comparable class ValidationFailure < ArgumentError; end class << self # Parse a Semantic Version string. # # @param ver [String] the version string to parse # @return [Version] a comparable {Version} object def parse(ver) match, major, minor, patch, prerelease, build = *ver.match(/\A#{REGEX_FULL}\Z/) if match.nil? raise "Unable to parse '#{ver}' as a semantic version identifier" end prerelease = parse_prerelease(prerelease) if prerelease # Build metadata is not yet supported in semantic_puppet, but we hope to. # The following code prevents build metadata for now. #build = parse_build_metadata(build) if build if !build.nil? raise "'#{ver}' MUST NOT include build identifiers" end self.new(major.to_i, minor.to_i, patch.to_i, prerelease, build) end # Validate a Semantic Version string. # # @param ver [String] the version string to validate # @return [bool] whether or not the string represents a valid Semantic Version def valid?(ver) !!(ver =~ /\A#{REGEX_FULL}\Z/) end private def parse_prerelease(prerelease) subject = 'Prerelease identifiers' prerelease = prerelease.split('.', -1) if prerelease.empty? or prerelease.any? { |x| x.empty? } raise "#{subject} MUST NOT be empty" elsif prerelease.any? { |x| x =~ /[^0-9a-zA-Z-]/ } raise "#{subject} MUST use only ASCII alphanumerics and hyphens" elsif prerelease.any? { |x| x =~ /^0\d+$/ } raise "#{subject} MUST NOT contain leading zeroes" end return prerelease.map { |x| x =~ /^\d+$/ ? x.to_i : x } end def parse_build_metadata(build) subject = 'Build identifiers' build = build.split('.', -1) if build.empty? or build.any? { |x| x.empty? } raise "#{subject} MUST NOT be empty" elsif build.any? { |x| x =~ /[^0-9a-zA-Z-]/ } raise "#{subject} MUST use only ASCII alphanumerics and hyphens" end return build end def raise(msg) super ValidationFailure, msg, caller.drop_while { |x| x !~ /\bparse\b/ } end end attr_reader :major, :minor, :patch def initialize(major, minor, patch, prerelease = nil, build = nil) @major = major @minor = minor @patch = patch @prerelease = prerelease @build = build end def next(part) case part when :major self.class.new(@major.next, 0, 0) when :minor self.class.new(@major, @minor.next, 0) when :patch self.class.new(@major, @minor, @patch.next) end end def prerelease @prerelease && @prerelease.join('.') end # @return [Boolean] true if this is a stable release def stable? @prerelease.nil? end def build @build && @build.join('.') end def <=>(other) return self.major <=> other.major unless self.major == other.major return self.minor <=> other.minor unless self.minor == other.minor return self.patch <=> other.patch unless self.patch == other.patch return compare_prerelease(other) end def to_s "#{major}.#{minor}.#{patch}" + (@prerelease.nil? || prerelease.empty? ? '' : "-" + prerelease) + (@build.nil? || build.empty? ? '' : "+" + build ) end def hash self.to_s.hash end private # This is a hack; tildes sort later than any valid identifier. The # advantage is that we don't need to handle stable vs. prerelease # comparisons separately. @@STABLE_RELEASE = [ '~' ].freeze def compare_prerelease(other) all_mine = @prerelease || @@STABLE_RELEASE all_yours = other.instance_variable_get(:@prerelease) || @@STABLE_RELEASE # Precedence is determined by comparing each dot separated identifier from # left to right... size = [ all_mine.size, all_yours.size ].max Array.new(size).zip(all_mine, all_yours) do |_, mine, yours| # ...until a difference is found. next if mine == yours # Numbers are compared numerically, strings are compared ASCIIbetically. if mine.class == yours.class return mine <=> yours # A larger set of pre-release fields has a higher precedence. elsif mine.nil? return -1 elsif yours.nil? return 1 # Numeric identifiers always have lower precedence than non-numeric. elsif mine.is_a? Numeric return -1 elsif yours.is_a? Numeric return 1 end end return 0 end def first_prerelease self.class.new(@major, @minor, @patch, []) end public # Version string matching regexes REGEX_NUMERIC = "(0|[1-9]\\d*)[.](0|[1-9]\\d*)[.](0|[1-9]\\d*)" # Major . Minor . Patch REGEX_PRE = "(?:[-](.*?))?" # Prerelease REGEX_BUILD = "(?:[+](.*?))?" # Build REGEX_FULL = REGEX_NUMERIC + REGEX_PRE + REGEX_BUILD # The lowest precedence Version possible MIN = self.new(0, 0, 0, []).freeze # The highest precedence Version possible MAX = self.new((1.0/0.0), 0, 0).freeze end end semantic_puppet-0.1.1/lib/semantic_puppet/dependency/0000755000004100000410000000000012633261621023027 5ustar www-datawww-datasemantic_puppet-0.1.1/lib/semantic_puppet/dependency/source.rb0000644000004100000410000000115712633261621024660 0ustar www-datawww-datarequire 'semantic_puppet/dependency' module SemanticPuppet module Dependency class Source def self.priority 0 end def priority self.class.priority end def create_release(name, version, dependencies = {}) version = Version.parse(version) if version.is_a? String dependencies = dependencies.inject({}) do |hash, (key, value)| hash[key] = VersionRange.parse(value || '>= 0.0.0') hash[key] ||= VersionRange::EMPTY_RANGE hash end ModuleRelease.new(self, name, version, dependencies) end end end end semantic_puppet-0.1.1/lib/semantic_puppet/dependency/graph.rb0000644000004100000410000000365012633261621024461 0ustar www-datawww-datarequire 'semantic_puppet/dependency' module SemanticPuppet module Dependency class Graph include GraphNode attr_reader :modules # Create a new instance of a dependency graph. # # @param modules [{String => VersionRange}] the required module # set and their version constraints def initialize(modules = {}) @modules = modules.keys modules.each do |name, range| add_constraint('initialize', name, range.to_s) do |node| range === node.version end add_dependency(name) end end # Constrains graph solutions based on the given block. Graph constraints # are used to describe fundamental truths about the tooling or module # system (e.g.: module names contain a namespace component which is # dropped during install, so module names must be unique excluding the # namespace). # # @example Ensuring a single source for all modules # @graph.add_constraint('installed', mod.name) do |nodes| # nodes.count { |node| node.source } == 1 # end # # @see #considering_solution? # # @param source [String, Symbol] a name describing the source of the # constraint # @yieldparam nodes [Array] the nodes to test the constraint # against # @yieldreturn [Boolean] whether the node passed the constraint # @return [void] def add_graph_constraint(source, &block) constraints[:graph] << [ source, block ] end # Checks the proposed solution (or partial solution) against the graph's # constraints. # # @see #add_graph_constraint # # @return [Boolean] true if none of the graph constraints are violated def satisfies_graph?(solution) constraints[:graph].all? { |_, check| check[solution] } end end end end semantic_puppet-0.1.1/lib/semantic_puppet/dependency/unsatisfiable_graph.rb0000644000004100000410000000116112633261621027365 0ustar www-datawww-datarequire 'semantic_puppet/dependency' module SemanticPuppet module Dependency class UnsatisfiableGraph < StandardError attr_reader :graph def initialize(graph) @graph = graph deps = sentence_from_list(graph.modules) super "Could not find satisfying releases for #{deps}" end private def sentence_from_list(list) case list.length when 1 list.first when 2 list.join(' and ') else list = list.dup list.push("and #{list.pop}") list.join(', ') end end end end end semantic_puppet-0.1.1/lib/semantic_puppet/dependency/module_release.rb0000644000004100000410000000215412633261621026343 0ustar www-datawww-datarequire 'semantic_puppet/dependency' module SemanticPuppet module Dependency class ModuleRelease include GraphNode attr_reader :name, :version # Create a new instance of a module release. # # @param source [SemanticPuppet::Dependency::Source] # @param name [String] # @param version [SemanticPuppet::Version] # @param dependencies [{String => SemanticPuppet::VersionRange}] def initialize(source, name, version, dependencies = {}) @source = source @name = name.freeze @version = version.freeze dependencies.each do |name, range| add_constraint('initialize', name, range.to_s) do |node| range === node.version end add_dependency(name) end end def priority @source.priority end def <=>(oth) our_key = [ priority, name, version ] their_key = [ oth.priority, oth.name, oth.version ] return our_key <=> their_key end def to_s "#<#{self.class} #{name}@#{version}>" end end end end semantic_puppet-0.1.1/lib/semantic_puppet/dependency/graph_node.rb0000644000004100000410000000630012633261621025461 0ustar www-datawww-datarequire 'semantic_puppet/dependency' require 'set' module SemanticPuppet module Dependency module GraphNode include Comparable def name end # Determines whether the modules dependencies are satisfied by the known # releases. # # @return [Boolean] true if all dependencies are satisfied def satisfied? dependencies.none? { |_, v| v.empty? } end def children @_children ||= {} end def populate_children(nodes) if children.empty? nodes = nodes.select { |node| satisfies_dependency?(node) } nodes.each do |node| children[node.name] = node node.populate_children(nodes) end self.freeze end end # @api internal # @return [{ String => SortedSet }] the satisfactory # dependency nodes def dependencies @_dependencies ||= Hash.new { |h, k| h[k] = SortedSet.new } end # Adds the given dependency name to the list of dependencies. # # @param name [String] the dependency name # @return [void] def add_dependency(name) dependencies[name] end # @return [Array] the list of dependency names def dependency_names dependencies.keys end def constraints @_constraints ||= Hash.new { |h, k| h[k] = [] } end def constraints_for(name) return [] unless constraints.has_key?(name) constraints[name].map do |constraint| { :source => constraint[0], :description => constraint[1], :test => constraint[2], } end end # Constrains the named module to suitable releases, as determined by the # given block. # # @example Version-locking currently installed modules # installed_modules.each do |m| # @graph.add_constraint('installed', m.name, m.version) do |node| # m.version == node.version # end # end # # @param source [String, Symbol] a name describing the source of the # constraint # @param mod [String] the name of the module # @param desc [String] a description of the enforced constraint # @yieldparam node [GraphNode] the node to test the constraint against # @yieldreturn [Boolean] whether the node passed the constraint # @return [void] def add_constraint(source, mod, desc, &block) constraints["#{mod}"] << [ source, desc, block ] end def satisfies_dependency?(node) dependencies.key?(node.name) && satisfies_constraints?(node) end # @param release [ModuleRelease] the release to test def satisfies_constraints?(release) constraints_for(release.name).all? { |x| x[:test].call(release) } end def << (nodes) Array(nodes).each do |node| next unless dependencies.key?(node.name) if satisfies_dependency?(node) dependencies[node.name] << node end end return self end def <=>(other) name <=> other.name end end end end semantic_puppet-0.1.1/lib/semantic_puppet.rb0000644000004100000410000000031512633261621021235 0ustar www-datawww-datamodule SemanticPuppet autoload :Version, 'semantic_puppet/version' autoload :VersionRange, 'semantic_puppet/version_range' autoload :Dependency, 'semantic_puppet/dependency' VERSION = '0.1.1' end semantic_puppet-0.1.1/metadata.yml0000644000004100000410000001053212633261621017247 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: semantic_puppet version: !ruby/object:Gem::Version version: 0.1.1 prerelease: platform: ruby authors: - Puppet Labs autorequire: bindir: bin cert_chain: [] date: 2015-04-01 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: simplecov requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: cane requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: yard requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: redcarpet requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' description: Tools used by Puppet to parse, validate, and compare Semantic Versions and Version Ranges and to query and resolve module dependencies. email: - info@puppetlabs.com executables: [] extensions: [] extra_rdoc_files: [] files: - .gitignore - .yardopts - CHANGELOG.md - Gemfile - Gemfile.lock - LICENSE - README.md - Rakefile - lib/semantic_puppet.rb - lib/semantic_puppet/dependency.rb - lib/semantic_puppet/dependency/graph.rb - lib/semantic_puppet/dependency/graph_node.rb - lib/semantic_puppet/dependency/module_release.rb - lib/semantic_puppet/dependency/source.rb - lib/semantic_puppet/dependency/unsatisfiable_graph.rb - lib/semantic_puppet/version.rb - lib/semantic_puppet/version_range.rb - semantic_puppet.gemspec - spec/spec_helper.rb - spec/unit/semantic_puppet/dependency/graph_node_spec.rb - spec/unit/semantic_puppet/dependency/graph_spec.rb - spec/unit/semantic_puppet/dependency/module_release_spec.rb - spec/unit/semantic_puppet/dependency/source_spec.rb - spec/unit/semantic_puppet/dependency/unsatisfiable_graph_spec.rb - spec/unit/semantic_puppet/dependency_spec.rb - spec/unit/semantic_puppet/version_range_spec.rb - spec/unit/semantic_puppet/version_spec.rb homepage: https://github.com/puppetlabs/semantic_puppet-gem licenses: - Apache-2.0 post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.8.7 required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 1.8.23.2 signing_key: specification_version: 3 summary: Useful tools for working with Semantic Versions. test_files: [] has_rdoc: semantic_puppet-0.1.1/.gitignore0000644000004100000410000000003612633261621016732 0ustar www-datawww-data.rbenv-* .yardoc coverage doc semantic_puppet-0.1.1/.yardopts0000644000004100000410000000001412633261621016604 0ustar www-datawww-data-m markdown semantic_puppet-0.1.1/semantic_puppet.gemspec0000644000004100000410000000222612633261621021512 0ustar www-datawww-data# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "semantic_puppet" spec = Gem::Specification.new do |s| # Metadata s.name = "semantic_puppet" s.version = SemanticPuppet::VERSION s.authors = ["Puppet Labs"] s.email = ["info@puppetlabs.com"] s.homepage = "https://github.com/puppetlabs/semantic_puppet-gem" s.summary = "Useful tools for working with Semantic Versions." s.description = %q{Tools used by Puppet to parse, validate, and compare Semantic Versions and Version Ranges and to query and resolve module dependencies.} s.licenses = ['Apache-2.0'] # Manifest s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {spec}/*_spec.rb`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.require_paths = ["lib"] # Dependencies s.required_ruby_version = '>= 1.8.7' s.add_development_dependency "rake" s.add_development_dependency "rspec" s.add_development_dependency "simplecov" s.add_development_dependency "cane" s.add_development_dependency "yard" s.add_development_dependency "redcarpet" end semantic_puppet-0.1.1/LICENSE0000644000004100000410000000121412633261621015746 0ustar www-datawww-data Copyright (C) 2005-2015 Puppet Labs Inc Puppet Labs can be contacted at: info@puppetlabs.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. semantic_puppet-0.1.1/CHANGELOG.md0000644000004100000410000000055112633261621016555 0ustar www-datawww-data# Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## 0.1.1 - 2015-04-01 ### Added - license information ### Removed - template entry from CHANGELOG.md ## 0.1.0 - 2015-03-23 ### Added - initial release in concert with current Puppet Module Tool v4.0.0 behavior semantic_puppet-0.1.1/README.md0000644000004100000410000000346712633261621016234 0ustar www-datawww-dataSemanticPuppet ============== Library of useful tools for working with Semantic Versions and module dependencies. Description ----------- Library of tools used by Puppet to parse, validate, and compare Semantic Versions and Version Ranges and to query and resolve module dependencies. For sparse, but accurate documentation, please see the docs directory. Note that this is a 0 release version, and things can change. Expect that the version and version range code to stay relatively stable, but the module dependency code is expected to change. This library is used by a number of Puppet Labs projects, including [Puppet](https://github.com/puppetlabs/puppet) and [r10k](https://github.com/puppetlabs/r10k). Requirements ------------ Semantic_puppet will work on several ruby versions, including 1.9.3, 2.0.0, and 2.1.0. Ruby 1.8.7 is immediately deprecated as it is in [r10k](https://github.com/puppetlabs/r10k). No gem/library requirements. Installation ------------ ### Rubygems For general use, you should install semantic_puppet from Ruby gems: gem install semantic_puppet ### Github If you have more specific needs or plan on modifying semantic_puppet you can install it out of a git repository: git clone git://github.com/puppetlabs/semantic_puppet Usage ----- SemanticPuppet is intended to be used as a library. ### Verison Range Operator Support SemanticPuppet will support the same version range operators as those used when publishing modules to [Puppet Forge](https://forge.puppetlabs.com) which is documented at [Publishing Modules on the Puppet Forge](https://docs.puppetlabs.com/puppet/latest/reference/modules_publishing.html#dependencies-in-metadatajson). Contributors ------------ Pieter van de Bruggen wrote the library originally, with additions by Alex Dreyer, Jesse Scott and Anderson Mills.