solve 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +5 -0
- data/README.md +8 -7
- data/lib/solve.rb +10 -29
- data/lib/solve/artifact.rb +91 -52
- data/lib/solve/constraint.rb +109 -24
- data/lib/solve/demand.rb +26 -17
- data/lib/solve/dependency.rb +15 -1
- data/lib/solve/errors.rb +23 -20
- data/lib/solve/gem_version.rb +1 -1
- data/lib/solve/graph.rb +76 -84
- data/lib/solve/solver.rb +246 -0
- data/lib/solve/solver/constraint_row.rb +17 -0
- data/lib/solve/solver/constraint_table.rb +27 -0
- data/lib/solve/solver/variable.rb +41 -0
- data/lib/solve/solver/variable_table.rb +50 -0
- data/lib/solve/version.rb +53 -16
- data/solve.gemspec +2 -3
- data/spec/acceptance/solutions_spec.rb +81 -8
- data/spec/unit/solve/artifact_spec.rb +73 -49
- data/spec/unit/solve/constraint_spec.rb +36 -30
- data/spec/unit/solve/demand_spec.rb +22 -13
- data/spec/unit/solve/dependency_spec.rb +15 -0
- data/spec/unit/solve/graph_spec.rb +98 -142
- data/spec/unit/solve/solver_spec.rb +302 -0
- data/spec/unit/solve/version_spec.rb +110 -0
- metadata +42 -96
- data/lib/solve/core_ext.rb +0 -3
- data/lib/solve/core_ext/kernel.rb +0 -33
@@ -0,0 +1,17 @@
|
|
1
|
+
module Solve
|
2
|
+
class Solver
|
3
|
+
# @author Andrew Garson <andrew.garson@gmail.com>
|
4
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
5
|
+
class ConstraintRow
|
6
|
+
attr_reader :package
|
7
|
+
attr_reader :constraint
|
8
|
+
attr_reader :source
|
9
|
+
|
10
|
+
def initialize(package, constraint, source)
|
11
|
+
@package = package
|
12
|
+
@constraint = constraint
|
13
|
+
@source = source
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Solve
|
2
|
+
class Solver
|
3
|
+
# @author Andrew Garson <andrew.garson@gmail.com>
|
4
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
5
|
+
class ConstraintTable
|
6
|
+
attr_reader :rows
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@rows = Array.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(package, constraint, source)
|
13
|
+
@rows << ConstraintRow.new(package, constraint, source)
|
14
|
+
end
|
15
|
+
|
16
|
+
def constraints_on_package(package)
|
17
|
+
@rows.select do |row|
|
18
|
+
row.package == package
|
19
|
+
end.map { |row| row.constraint }
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_constraints_from_source!(source)
|
23
|
+
@rows.reject! { |row| row.source == source }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Solve
|
2
|
+
class Solver
|
3
|
+
# @author Andrew Garson <andrew.garson@gmail.com>
|
4
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
5
|
+
class Variable
|
6
|
+
attr_reader :package
|
7
|
+
attr_reader :value
|
8
|
+
attr_reader :sources
|
9
|
+
|
10
|
+
def initialize(package, source)
|
11
|
+
@package = package
|
12
|
+
@value = nil
|
13
|
+
@sources = Array(source)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_source(source)
|
17
|
+
@sources << source
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_source
|
21
|
+
@sources.last
|
22
|
+
end
|
23
|
+
|
24
|
+
def bind(value)
|
25
|
+
@value = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def unbind
|
29
|
+
@value = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def bound?
|
33
|
+
!@value.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove_source(source)
|
37
|
+
@sources.delete(source)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Solve
|
2
|
+
class Solver
|
3
|
+
# @author Andrew Garson <andrew.garson@gmail.com>
|
4
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
5
|
+
class VariableTable
|
6
|
+
attr_reader :rows
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@rows = Array.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(package, source)
|
13
|
+
row = rows.detect { |row| row.package == package }
|
14
|
+
if row.nil?
|
15
|
+
@rows << Variable.new(package, source)
|
16
|
+
else
|
17
|
+
row.add_source(source)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def first_unbound
|
22
|
+
@rows.detect { |row| row.bound? == false }
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_package(package)
|
26
|
+
@rows.detect { |row| row.package == package }
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_all_with_only_this_source!(source)
|
30
|
+
with_only_this_source, others = @rows.partition { |row| row.sources == [source] }
|
31
|
+
@rows = others
|
32
|
+
with_only_this_source
|
33
|
+
end
|
34
|
+
|
35
|
+
def all_from_source(source)
|
36
|
+
@rows.select { |row| row.sources.include?(source) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def before(package)
|
40
|
+
package_index = @rows.index { |row| row.package == package }
|
41
|
+
(package_index == 0) ? nil : @rows[package_index - 1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def all_after(package)
|
45
|
+
package_index = @rows.index { |row| row.package == package }
|
46
|
+
@rows[(package_index+1)..-1]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/solve/version.rb
CHANGED
@@ -1,16 +1,63 @@
|
|
1
1
|
module Solve
|
2
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
2
3
|
class Version
|
4
|
+
class << self
|
5
|
+
# @param [#to_s] version_string
|
6
|
+
#
|
7
|
+
# @raise [InvalidVersionFormat]
|
8
|
+
#
|
9
|
+
# @return [Array]
|
10
|
+
def split(version_string)
|
11
|
+
major, minor, patch = case version_string.to_s
|
12
|
+
when /^(\d+)\.(\d+)\.(\d+)$/
|
13
|
+
[ $1.to_i, $2.to_i, $3.to_i ]
|
14
|
+
when /^(\d+)\.(\d+)$/
|
15
|
+
[ $1.to_i, $2.to_i, nil ]
|
16
|
+
else
|
17
|
+
raise Errors::InvalidVersionFormat.new(version_string)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
3
22
|
include Comparable
|
4
23
|
|
5
24
|
attr_reader :major
|
6
25
|
attr_reader :minor
|
7
26
|
attr_reader :patch
|
8
27
|
|
9
|
-
# @
|
10
|
-
|
11
|
-
|
28
|
+
# @overload initialize(version_array)
|
29
|
+
# @param [Array] version_array
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# Version.new([1, 2, 3]) => #<Version: @major=1, @minor=2, @patch=3>
|
33
|
+
#
|
34
|
+
# @overload initialize(version_string)
|
35
|
+
# @param [#to_s] version_string
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# Version.new("1.2.3") => #<Version: @major=1, @minor=2, @patch=3>
|
39
|
+
#
|
40
|
+
# @overload initialize(version)
|
41
|
+
# @param [Solve::Version] version
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# Version.new(Version.new("1.2.3")) => #<Version: @major=1, @minor=2, @patch=3>
|
45
|
+
#
|
46
|
+
def initialize(*args)
|
47
|
+
if args.first.is_a?(Array)
|
48
|
+
@major, @minor, @patch = args.first
|
49
|
+
else
|
50
|
+
@major, @minor, @patch = self.class.split(args.first.to_s)
|
51
|
+
end
|
52
|
+
|
53
|
+
@major ||= 0
|
54
|
+
@minor ||= 0
|
55
|
+
@patch ||= 0
|
12
56
|
end
|
13
57
|
|
58
|
+
# @param [Solve::Version] other
|
59
|
+
#
|
60
|
+
# @return [Integer]
|
14
61
|
def <=>(other)
|
15
62
|
[:major, :minor, :patch].each do |method|
|
16
63
|
ans = (self.send(method) <=> other.send(method))
|
@@ -19,6 +66,9 @@ module Solve
|
|
19
66
|
0
|
20
67
|
end
|
21
68
|
|
69
|
+
# @param [Solve::Version] other
|
70
|
+
#
|
71
|
+
# @return [Boolean]
|
22
72
|
def eql?(other)
|
23
73
|
other.is_a?(Version) && self == other
|
24
74
|
end
|
@@ -30,18 +80,5 @@ module Solve
|
|
30
80
|
def to_s
|
31
81
|
"#{major}.#{minor}.#{patch}"
|
32
82
|
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def parse(ver_str = String.new)
|
37
|
-
@major, @minor, @patch = case ver_str.to_s
|
38
|
-
when /^(\d+)\.(\d+)\.(\d+)$/
|
39
|
-
[ $1.to_i, $2.to_i, $3.to_i ]
|
40
|
-
when /^(\d+)\.(\d+)$/
|
41
|
-
[ $1.to_i, $2.to_i, 0 ]
|
42
|
-
else
|
43
|
-
raise InvalidVersionFormat.new(ver_str)
|
44
|
-
end
|
45
|
-
end
|
46
83
|
end
|
47
84
|
end
|
data/solve.gemspec
CHANGED
@@ -16,9 +16,8 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.version = Solve::VERSION
|
17
17
|
s.required_ruby_version = ">= 1.9.1"
|
18
18
|
|
19
|
-
s.
|
20
|
-
|
21
|
-
s.add_development_dependency 'thor'
|
19
|
+
s.add_development_dependency 'thor', '>= 0.16.0'
|
20
|
+
s.add_development_dependency 'rake', '>= 0.9.2.2'
|
22
21
|
s.add_development_dependency 'rspec'
|
23
22
|
s.add_development_dependency 'fuubar'
|
24
23
|
s.add_development_dependency 'spork'
|
@@ -1,25 +1,98 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "Solutions" do
|
4
|
-
|
4
|
+
|
5
|
+
it "chooses the correct artifact for the demands" do
|
5
6
|
graph = Solve::Graph.new
|
6
7
|
graph.artifacts("mysql", "2.0.0")
|
7
8
|
graph.artifacts("mysql", "1.2.0")
|
8
9
|
graph.artifacts("nginx", "1.0.0").depends("mysql", "= 1.2.0")
|
9
|
-
graph.demands('nginx')
|
10
|
-
graph.demands('mysql')
|
11
10
|
|
12
|
-
Solve.it(graph
|
11
|
+
result = Solve.it!(graph, [['nginx', '= 1.0.0'], ['mysql']])
|
12
|
+
|
13
|
+
result.should eql("nginx" => "1.0.0", "mysql" => "1.2.0")
|
13
14
|
end
|
14
15
|
|
15
|
-
it "
|
16
|
+
it "chooses the best artifact for the demands" do
|
16
17
|
graph = Solve::Graph.new
|
18
|
+
graph.artifacts("mysql", "2.0.0")
|
17
19
|
graph.artifacts("mysql", "1.2.0")
|
20
|
+
graph.artifacts("nginx", "1.0.0").depends("mysql", ">= 1.2.0")
|
21
|
+
|
22
|
+
result = Solve.it!(graph, [['nginx', '= 1.0.0'], ['mysql']])
|
18
23
|
|
19
|
-
|
24
|
+
result.should eql("nginx" => "1.0.0", "mysql" => "2.0.0")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises NoSolutionError when a solution cannot be found" do
|
28
|
+
graph = Solve::Graph.new
|
29
|
+
graph.artifacts("mysql", "1.2.0")
|
20
30
|
|
21
31
|
lambda {
|
22
|
-
Solve.it!(graph)
|
23
|
-
}.should raise_error(Solve::NoSolutionError)
|
32
|
+
Solve.it!(graph, ['mysql', '>= 2.0.0'])
|
33
|
+
}.should raise_error(Solve::Errors::NoSolutionError)
|
24
34
|
end
|
35
|
+
|
36
|
+
it "find the correct solution when backtracking in variables introduced via demands" do
|
37
|
+
graph = Solve::Graph.new
|
38
|
+
|
39
|
+
graph.artifacts("D", "1.2.0")
|
40
|
+
graph.artifacts("D", "1.3.0")
|
41
|
+
graph.artifacts("D", "1.4.0")
|
42
|
+
graph.artifacts("D", "2.0.0")
|
43
|
+
graph.artifacts("D", "2.1.0")
|
44
|
+
|
45
|
+
graph.artifacts("C", "2.0.0").depends("D", "= 1.2.0")
|
46
|
+
graph.artifacts("C", "2.1.0").depends("D", ">= 2.1.0")
|
47
|
+
graph.artifacts("C", "2.2.0").depends("D", "> 2.0.0")
|
48
|
+
|
49
|
+
graph.artifacts("B", "1.0.0").depends("D", "= 1.0.0")
|
50
|
+
graph.artifacts("B", "1.1.0").depends("D", "= 1.0.0")
|
51
|
+
graph.artifacts("B", "2.0.0").depends("D", ">= 1.3.0")
|
52
|
+
graph.artifacts("B", "2.1.0").depends("D", ">= 2.0.0")
|
53
|
+
|
54
|
+
graph.artifacts("A", "1.0.0").depends("B", "> 1.0.0")
|
55
|
+
graph.artifacts("A", "1.0.0").depends("C", "= 2.0.0")
|
56
|
+
graph.artifacts("A", "1.0.1").depends("B", "> 1.0.0")
|
57
|
+
graph.artifacts("A", "1.0.1").depends("C", "= 2.1.0")
|
58
|
+
graph.artifacts("A", "1.0.2").depends("B", "> 2.0.0")
|
59
|
+
graph.artifacts("A", "1.0.2").depends("C", "= 2.1.0")
|
60
|
+
|
61
|
+
result = Solve.it!(graph, [['A', '~> 1.0.0'], ['D', ">= 2.0.0"]])
|
62
|
+
|
63
|
+
|
64
|
+
result.should eql("A" => "1.0.2",
|
65
|
+
"B" => "2.1.0",
|
66
|
+
"C" => "2.1.0",
|
67
|
+
"D" => "2.1.0")
|
68
|
+
end
|
69
|
+
|
70
|
+
it "finds the correct solution when there is a circular dependency" do
|
71
|
+
graph = Solve::Graph.new
|
72
|
+
|
73
|
+
graph.artifacts("A", "1.0.0").depends("B", "1.0.0")
|
74
|
+
graph.artifacts("B", "1.0.0").depends("C", "1.0.0")
|
75
|
+
graph.artifacts("C", "1.0.0").depends("A", "1.0.0")
|
76
|
+
|
77
|
+
result = Solve.it!(graph, [["A", "1.0.0"]])
|
78
|
+
|
79
|
+
result.should eql("A" => "1.0.0",
|
80
|
+
"B" => "1.0.0",
|
81
|
+
"C" => "1.0.0")
|
82
|
+
end
|
83
|
+
|
84
|
+
it "finds the correct solution when there is a p shaped depenency chain" do
|
85
|
+
graph = Solve::Graph.new
|
86
|
+
|
87
|
+
graph.artifacts("A", "1.0.0").depends("B", "1.0.0")
|
88
|
+
graph.artifacts("B", "1.0.0").depends("C", "1.0.0")
|
89
|
+
graph.artifacts("C", "1.0.0").depends("B", "1.0.0")
|
90
|
+
|
91
|
+
result = Solve.it!(graph, [["A", "1.0.0"]])
|
92
|
+
|
93
|
+
result.should eql("A" => "1.0.0",
|
94
|
+
"B" => "1.0.0",
|
95
|
+
"C" => "1.0.0")
|
96
|
+
end
|
97
|
+
|
25
98
|
end
|
@@ -13,44 +13,58 @@ describe Solve::Artifact do
|
|
13
13
|
let(:version) { "1.0.0" }
|
14
14
|
subject { Solve::Artifact.new(graph, name, version) }
|
15
15
|
|
16
|
-
describe "
|
17
|
-
context "given
|
18
|
-
let(:
|
19
|
-
let(:
|
16
|
+
describe "equality" do
|
17
|
+
context "given an artifact with the same name and version" do
|
18
|
+
let(:one) { Solve::Artifact.new(graph, "riot", "1.0.0") }
|
19
|
+
let(:two) { Solve::Artifact.new(graph, "riot", "1.0.0") }
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
it "the dependency has the given name" do
|
27
|
-
subject.dependencies(name, constraint).name.should eql(name)
|
28
|
-
end
|
21
|
+
it "is equal" do
|
22
|
+
one.should be_eql(two)
|
23
|
+
end
|
24
|
+
end
|
29
25
|
|
30
|
-
|
31
|
-
|
32
|
-
|
26
|
+
context "given an artifact with the same name but different version" do
|
27
|
+
let(:one) { Solve::Artifact.new(graph, "riot", "1.0.0") }
|
28
|
+
let(:two) { Solve::Artifact.new(graph, "riot", "2.0.0") }
|
33
29
|
|
34
|
-
|
35
|
-
|
30
|
+
it "is not equal" do
|
31
|
+
one.should_not be_eql(two)
|
32
|
+
end
|
33
|
+
end
|
36
34
|
|
37
|
-
|
38
|
-
|
35
|
+
context "given an artifact with the same version but different name" do
|
36
|
+
let(:one) { Solve::Artifact.new(graph, "riot", "1.0.0") }
|
37
|
+
let(:two) { Solve::Artifact.new(graph, "league", "1.0.0") }
|
39
38
|
|
40
|
-
|
41
|
-
|
39
|
+
it "is not equal" do
|
40
|
+
one.should_not be_eql(two)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
+
describe "sorting" do
|
46
|
+
let(:one) { Solve::Artifact.new(graph, "riot", "1.0.0") }
|
47
|
+
let(:two) { Solve::Artifact.new(graph, "riot", "2.0.0") }
|
48
|
+
let(:three) { Solve::Artifact.new(graph, "riot", "3.0.0") }
|
49
|
+
|
50
|
+
let(:artifacts) do
|
51
|
+
[
|
52
|
+
one,
|
53
|
+
two,
|
54
|
+
three
|
55
|
+
].shuffle
|
56
|
+
end
|
45
57
|
|
46
|
-
|
47
|
-
|
58
|
+
it "orders artifacts by their version number" do
|
59
|
+
sorted = artifacts.sort
|
48
60
|
|
49
|
-
|
50
|
-
|
51
|
-
|
61
|
+
sorted[0].should eql(one)
|
62
|
+
sorted[1].should eql(two)
|
63
|
+
sorted[2].should eql(three)
|
52
64
|
end
|
65
|
+
end
|
53
66
|
|
67
|
+
describe "#dependencies" do
|
54
68
|
context "given no arguments" do
|
55
69
|
it "returns an array" do
|
56
70
|
subject.dependencies.should be_a(Array)
|
@@ -59,38 +73,48 @@ describe Solve::Artifact do
|
|
59
73
|
it "returns an empty array if no dependencies have been accessed" do
|
60
74
|
subject.dependencies.should have(0).items
|
61
75
|
end
|
76
|
+
end
|
77
|
+
end
|
62
78
|
|
63
|
-
|
64
|
-
|
79
|
+
describe "#depends" do
|
80
|
+
context "given a name and constraint argument" do
|
81
|
+
let(:name) { "nginx" }
|
82
|
+
let(:constraint) { "~> 0.101.5" }
|
65
83
|
|
66
|
-
|
84
|
+
context "given the dependency of the given name and constraint does not exist" do
|
85
|
+
it "returns a Solve::Artifact" do
|
86
|
+
subject.depends(name, constraint).should be_a(Solve::Artifact)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "adds a dependency with the given name and constraint to the list of dependencies" do
|
90
|
+
subject.depends(name, constraint)
|
91
|
+
|
92
|
+
subject.dependencies.should have(1).item
|
93
|
+
subject.dependencies.first.name.should eql(name)
|
94
|
+
subject.dependencies.first.constraint.to_s.should eql(constraint)
|
95
|
+
end
|
67
96
|
end
|
68
97
|
end
|
69
98
|
|
70
99
|
context "given only a name argument" do
|
71
|
-
it "
|
72
|
-
subject.
|
100
|
+
it "adds a dependency with a all constraint (>= 0.0.0)" do
|
101
|
+
subject.depends("nginx")
|
102
|
+
|
103
|
+
subject.dependencies.should have(1).item
|
104
|
+
subject.dependencies.first.constraint.to_s.should eql(">= 0.0.0")
|
73
105
|
end
|
74
106
|
end
|
107
|
+
end
|
75
108
|
|
76
|
-
|
77
|
-
|
78
|
-
lambda {
|
79
|
-
subject.dependencies(1, 2, 3)
|
80
|
-
}.should raise_error(ArgumentError, "Unexpected number of arguments. You gave: 3. Expected: 2 or less.")
|
81
|
-
end
|
109
|
+
describe "::get_dependency" do
|
110
|
+
before(:each) { subject.depends("nginx", "~> 1.2.3") }
|
82
111
|
|
83
|
-
|
84
|
-
|
85
|
-
subject.dependencies(nil)
|
86
|
-
}.should raise_error(ArgumentError, "A name must be specified. You gave: [nil].")
|
87
|
-
end
|
112
|
+
it "returns an instance of Solve::Dependency matching the given name and constraint" do
|
113
|
+
dependency = subject.get_dependency("nginx", "~> 1.2.3")
|
88
114
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
}.should raise_error(ArgumentError, 'A name must be specified. You gave: [nil, "= 1.0.0"].')
|
93
|
-
end
|
115
|
+
dependency.should be_a(Solve::Dependency)
|
116
|
+
dependency.name.should eql("nginx")
|
117
|
+
dependency.constraint.to_s.should eql("~> 1.2.3")
|
94
118
|
end
|
95
119
|
end
|
96
120
|
|