solve 1.2.1 → 2.0.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +11 -6
- data/Gemfile +8 -8
- data/NoGecode.gemfile +4 -0
- data/README.md +16 -0
- data/Rakefile +1 -0
- data/Thorfile +5 -0
- data/lib/solve.rb +43 -2
- data/lib/solve/demand.rb +2 -2
- data/lib/solve/errors.rb +6 -0
- data/lib/solve/{solver.rb → gecode_solver.rb} +12 -4
- data/lib/solve/ruby_solver.rb +207 -0
- data/lib/solve/solver/serializer.rb +37 -15
- data/lib/solve/version.rb +1 -1
- data/solve.gemspec +7 -1
- data/spec/acceptance/benchmark.rb +17 -3
- data/spec/acceptance/ruby_solver_solutions_spec.rb +307 -0
- data/spec/acceptance/solutions_spec.rb +6 -1
- data/spec/unit/solve/{solver_spec.rb → gecode_solver_spec.rb} +34 -4
- data/spec/unit/solve/ruby_solver_spec.rb +166 -0
- data/spec/unit/solve/solver/serializer_spec.rb +22 -14
- data/spec/unit/solve_spec.rb +2 -2
- metadata +71 -8
@@ -1,37 +1,59 @@
|
|
1
1
|
require 'json'
|
2
|
+
require 'solve/graph'
|
2
3
|
|
3
4
|
module Solve
|
5
|
+
|
6
|
+
Problem = Struct.new(:graph, :demands)
|
7
|
+
|
8
|
+
# Simple struct class that contains a #graph and #demands (in Array form)
|
9
|
+
#
|
10
|
+
# Can be serialized via Solver::Serializer to create a json representation of
|
11
|
+
# a dependency solving problem.
|
12
|
+
class Problem
|
13
|
+
|
14
|
+
# Create a Problem from a given Solver.
|
15
|
+
#
|
16
|
+
# @param [Solve::GecodeSolver,Solve::RubySolver] dependency solver
|
17
|
+
# @return [Problem]
|
18
|
+
def self.from_solver(solver)
|
19
|
+
demands_data = solver.demands.map do |demand|
|
20
|
+
[ demand.name, demand.constraint.to_s ]
|
21
|
+
end
|
22
|
+
new(solver.graph, demands_data)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
4
26
|
class Solver
|
5
27
|
class Serializer
|
6
|
-
# @param [Solve::
|
28
|
+
# @param [Solve::Problem] problem struct
|
7
29
|
#
|
8
30
|
# @return [String]
|
9
|
-
def serialize(
|
10
|
-
graph =
|
11
|
-
demands =
|
31
|
+
def serialize(problem)
|
32
|
+
graph = problem.graph
|
33
|
+
demands = problem.demands
|
12
34
|
|
13
35
|
graph_hash = format_graph(graph)
|
14
36
|
demands_hash = format_demands(demands)
|
15
37
|
|
16
|
-
|
17
|
-
|
38
|
+
problem_data = graph_hash.merge(demands_hash)
|
39
|
+
problem_data.to_json
|
18
40
|
end
|
19
41
|
|
20
42
|
# @param [Hash, #to_s] solver a json string or a hash representing a solver
|
21
43
|
#
|
22
|
-
# @return [Solve::
|
23
|
-
def deserialize(
|
24
|
-
unless
|
25
|
-
|
44
|
+
# @return [Solve::Problem]
|
45
|
+
def deserialize(problem_data)
|
46
|
+
unless problem_data.is_a?(Hash)
|
47
|
+
problem_data = JSON.parse(problem_data.to_s)
|
26
48
|
end
|
27
49
|
|
28
|
-
graph_spec =
|
29
|
-
demands_spec =
|
50
|
+
graph_spec = problem_data["graph"]
|
51
|
+
demands_spec = problem_data["demands"]
|
30
52
|
|
31
53
|
graph = load_graph(graph_spec)
|
32
54
|
demands = load_demands(demands_spec)
|
33
55
|
|
34
|
-
Solve::
|
56
|
+
Solve::Problem.new(graph, demands)
|
35
57
|
end
|
36
58
|
|
37
59
|
private
|
@@ -71,8 +93,8 @@ module Solve
|
|
71
93
|
|
72
94
|
def format_demand(demand)
|
73
95
|
{
|
74
|
-
"name" => demand
|
75
|
-
"constraint" => demand
|
96
|
+
"name" => demand[0],
|
97
|
+
"constraint" => demand[1]
|
76
98
|
}
|
77
99
|
end
|
78
100
|
|
data/lib/solve/version.rb
CHANGED
data/solve.gemspec
CHANGED
@@ -17,5 +17,11 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.required_ruby_version = ">= 1.9.1"
|
18
18
|
|
19
19
|
s.add_dependency "semverse", "~> 1.1"
|
20
|
-
s.add_dependency "
|
20
|
+
s.add_dependency "molinillo", "~> 0.2.3"
|
21
|
+
|
22
|
+
s.add_development_dependency 'thor', '>= 0.16.0'
|
23
|
+
s.add_development_dependency 'rake', '>= 0.9.2.2'
|
24
|
+
|
25
|
+
s.add_development_dependency 'spork'
|
26
|
+
s.add_development_dependency 'rspec', "~> 3.0"
|
21
27
|
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
require 'benchmark'
|
2
2
|
require 'solve'
|
3
|
+
require 'solve/gecode_solver'
|
3
4
|
require File.expand_path("../large_graph_no_solution", __FILE__)
|
4
5
|
require File.expand_path("../opscode_ci_graph", __FILE__)
|
5
6
|
|
6
7
|
PROBLEM = OpscodeCiGraph
|
7
|
-
|
8
|
+
#PROBLEM = LargeGraphNoSolution
|
9
|
+
N = 100
|
8
10
|
|
9
11
|
def demands
|
10
12
|
PROBLEM::DEMANDS
|
@@ -35,11 +37,23 @@ end
|
|
35
37
|
STATIC_GRAPH = create_graph
|
36
38
|
|
37
39
|
def solve_gecode
|
38
|
-
Solve::
|
39
|
-
rescue Solve::Errors::NoSolutionError
|
40
|
+
Solve::GecodeSolver.new(STATIC_GRAPH, demands).resolve({})
|
41
|
+
rescue Solve::Errors::NoSolutionError => e
|
42
|
+
# Uncomment to look at the error messages. Probably only useful if N == 1
|
43
|
+
#puts e
|
44
|
+
e
|
45
|
+
end
|
46
|
+
|
47
|
+
def solve_ruby
|
48
|
+
Solve::RubySolver.new(STATIC_GRAPH, demands).resolve({})
|
49
|
+
rescue Solve::Errors::NoSolutionError => e
|
50
|
+
# Uncomment to look at the error messages. Probably only useful if N == 1
|
51
|
+
#puts e
|
52
|
+
e
|
40
53
|
end
|
41
54
|
|
42
55
|
Benchmark.bm(12) do |x|
|
43
56
|
x.report("Create graph") { N.times { create_graph } }
|
44
57
|
x.report("Solve Gecode") { N.times { solve_gecode } }
|
58
|
+
x.report("Solve Ruby") { N.times { solve_ruby } }
|
45
59
|
end
|
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Solutions when using the ruby solver" do
|
4
|
+
|
5
|
+
before do
|
6
|
+
Solve.engine = :ruby
|
7
|
+
end
|
8
|
+
|
9
|
+
it "chooses the correct artifact for the demands" do
|
10
|
+
graph = Solve::Graph.new
|
11
|
+
graph.artifact("mysql", "2.0.0")
|
12
|
+
graph.artifact("mysql", "1.2.0")
|
13
|
+
graph.artifact("nginx", "1.0.0").depends("mysql", "= 1.2.0")
|
14
|
+
|
15
|
+
result = Solve.it!(graph, [['nginx', '= 1.0.0'], ['mysql']])
|
16
|
+
|
17
|
+
result.should eql("nginx" => "1.0.0", "mysql" => "1.2.0")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "chooses the best artifact for the demands" do
|
21
|
+
graph = Solve::Graph.new
|
22
|
+
graph.artifact("mysql", "2.0.0")
|
23
|
+
graph.artifact("mysql", "1.2.0")
|
24
|
+
graph.artifact("nginx", "1.0.0").depends("mysql", ">= 1.2.0")
|
25
|
+
|
26
|
+
result = Solve.it!(graph, [['nginx', '= 1.0.0'], ['mysql']])
|
27
|
+
|
28
|
+
result.should eql("nginx" => "1.0.0", "mysql" => "2.0.0")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "raises NoSolutionError when a solution cannot be found" do
|
32
|
+
graph = Solve::Graph.new
|
33
|
+
graph.artifact("mysql", "1.2.0")
|
34
|
+
|
35
|
+
lambda {
|
36
|
+
Solve.it!(graph, ['mysql', '>= 2.0.0'])
|
37
|
+
}.should raise_error(Solve::Errors::NoSolutionError)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "find the correct solution when backtracking in variables introduced via demands" do
|
41
|
+
graph = Solve::Graph.new
|
42
|
+
|
43
|
+
graph.artifact("D", "1.2.0")
|
44
|
+
graph.artifact("D", "1.3.0")
|
45
|
+
graph.artifact("D", "1.4.0")
|
46
|
+
graph.artifact("D", "2.0.0")
|
47
|
+
graph.artifact("D", "2.1.0")
|
48
|
+
|
49
|
+
graph.artifact("C", "2.0.0").depends("D", "= 1.2.0")
|
50
|
+
graph.artifact("C", "2.1.0").depends("D", ">= 2.1.0")
|
51
|
+
graph.artifact("C", "2.2.0").depends("D", "> 2.0.0")
|
52
|
+
|
53
|
+
graph.artifact("B", "1.0.0").depends("D", "= 1.0.0")
|
54
|
+
graph.artifact("B", "1.1.0").depends("D", "= 1.0.0")
|
55
|
+
graph.artifact("B", "2.0.0").depends("D", ">= 1.3.0")
|
56
|
+
graph.artifact("B", "2.1.0").depends("D", ">= 2.0.0")
|
57
|
+
|
58
|
+
graph.artifact("A", "1.0.0").depends("B", "> 2.0.0")
|
59
|
+
graph.artifact("A", "1.0.0").depends("C", "= 2.1.0")
|
60
|
+
graph.artifact("A", "1.0.1").depends("B", "> 1.0.0")
|
61
|
+
graph.artifact("A", "1.0.1").depends("C", "= 2.1.0")
|
62
|
+
graph.artifact("A", "1.0.2").depends("B", "> 1.0.0")
|
63
|
+
graph.artifact("A", "1.0.2").depends("C", "= 2.0.0")
|
64
|
+
|
65
|
+
result = Solve.it!(graph, [['A', '~> 1.0.0'], ['D', ">= 2.0.0"]])
|
66
|
+
|
67
|
+
|
68
|
+
result.should eql("A" => "1.0.1",
|
69
|
+
"B" => "2.1.0",
|
70
|
+
"C" => "2.1.0",
|
71
|
+
"D" => "2.1.0")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "rejects a circular dependency with a circular dep error" do
|
75
|
+
graph = Solve::Graph.new
|
76
|
+
|
77
|
+
graph.artifact("A", "1.0.0").depends("B", "1.0.0")
|
78
|
+
graph.artifact("B", "1.0.0").depends("C", "1.0.0")
|
79
|
+
graph.artifact("C", "1.0.0").depends("A", "1.0.0")
|
80
|
+
|
81
|
+
expect { Solve.it!(graph, [["A", "1.0.0"]]) }.to raise_error(Solve::Errors::NoSolutionError)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "rejects a p shaped depenency chain with a circular dep error" do
|
85
|
+
graph = Solve::Graph.new
|
86
|
+
|
87
|
+
graph.artifact("A", "1.0.0").depends("B", "1.0.0")
|
88
|
+
graph.artifact("B", "1.0.0").depends("C", "1.0.0")
|
89
|
+
graph.artifact("C", "1.0.0").depends("B", "1.0.0")
|
90
|
+
|
91
|
+
expect { Solve.it!(graph, [["A", "1.0.0"]]) }.to raise_error(Solve::Errors::NoSolutionError)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "finds the correct solution when there is a diamond shaped dependency" do
|
95
|
+
graph = Solve::Graph.new
|
96
|
+
|
97
|
+
graph.artifact("A", "1.0.0")
|
98
|
+
.depends("B", "1.0.0")
|
99
|
+
.depends("C", "1.0.0")
|
100
|
+
graph.artifact("B", "1.0.0")
|
101
|
+
.depends("D", "1.0.0")
|
102
|
+
graph.artifact("C", "1.0.0")
|
103
|
+
.depends("D", "1.0.0")
|
104
|
+
graph.artifact("D", "1.0.0")
|
105
|
+
|
106
|
+
result = Solve.it!(graph, [["A", "1.0.0"]])
|
107
|
+
|
108
|
+
result.should eql("A" => "1.0.0",
|
109
|
+
"B" => "1.0.0",
|
110
|
+
"C" => "1.0.0",
|
111
|
+
"D" => "1.0.0")
|
112
|
+
end
|
113
|
+
|
114
|
+
it "solves when packages and constraints have prerelease elements" do
|
115
|
+
graph = Solve::Graph.new
|
116
|
+
|
117
|
+
graph.artifact("A", "1.0.0")
|
118
|
+
.depends("B", ">= 1.0.0-alpha")
|
119
|
+
graph.artifact("B", "1.0.0-alpha")
|
120
|
+
.depends("C", "1.0.0")
|
121
|
+
graph.artifact("C", "1.0.0")
|
122
|
+
|
123
|
+
result = Solve.it!(graph, [["A", "1.0.0"]])
|
124
|
+
|
125
|
+
result.should eql("A" => "1.0.0",
|
126
|
+
"B" => "1.0.0-alpha",
|
127
|
+
"C" => "1.0.0")
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
it "solves when packages and constraints have build elements" do
|
132
|
+
graph = Solve::Graph.new
|
133
|
+
|
134
|
+
graph.artifact("A", "1.0.0")
|
135
|
+
.depends("B", ">= 1.0.0+build")
|
136
|
+
graph.artifact("B", "1.0.0+build")
|
137
|
+
.depends("C", "1.0.0")
|
138
|
+
graph.artifact("C", "1.0.0")
|
139
|
+
|
140
|
+
result = Solve.it!(graph, [["A", "1.0.0"]])
|
141
|
+
|
142
|
+
result.should eql("A" => "1.0.0",
|
143
|
+
"B" => "1.0.0+build",
|
144
|
+
"C" => "1.0.0")
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
it "fails with a self dependency" do
|
149
|
+
graph = Solve::Graph.new
|
150
|
+
|
151
|
+
graph.artifact("bottom", "1.0.0")
|
152
|
+
graph.artifact("middle", "1.0.0").depends("top", "= 1.0.0").depends("middle")
|
153
|
+
|
154
|
+
demands = [["bottom", "1.0.0"],["middle", "1.0.0"]]
|
155
|
+
|
156
|
+
expect { Solve.it!(graph, demands, { :sorted => true } ) }.to raise_error { |error|
|
157
|
+
error.should be_a(Solve::Errors::NoSolutionError)
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
it "gives an empty solution when there are no demands" do
|
162
|
+
graph = Solve::Graph.new
|
163
|
+
result = Solve.it!(graph, [])
|
164
|
+
result.should eql({})
|
165
|
+
end
|
166
|
+
|
167
|
+
it "tries all combinations until it finds a solution" do
|
168
|
+
|
169
|
+
graph = Solve::Graph.new
|
170
|
+
|
171
|
+
graph.artifact("A", "1.0.0").depends("B", "~> 1.0.0")
|
172
|
+
graph.artifact("A", "1.0.1").depends("B", "~> 1.0.0")
|
173
|
+
graph.artifact("A", "1.0.2").depends("B", "~> 1.0.0")
|
174
|
+
|
175
|
+
graph.artifact("B", "1.0.0").depends("C", "~> 1.0.0")
|
176
|
+
graph.artifact("B", "1.0.1").depends("C", "~> 1.0.0")
|
177
|
+
graph.artifact("B", "1.0.2").depends("C", "~> 1.0.0")
|
178
|
+
|
179
|
+
graph.artifact("C", "1.0.0").depends("D", "1.0.0")
|
180
|
+
graph.artifact("C", "1.0.1").depends("D", "1.0.0")
|
181
|
+
graph.artifact("C", "1.0.2").depends("D", "1.0.0")
|
182
|
+
|
183
|
+
# Note:
|
184
|
+
# This test previously used two circular dependencies:
|
185
|
+
# (D 1.0.0) -> A < 0.0.0
|
186
|
+
# (D 0.0.0) -> A = 0.0.0
|
187
|
+
# But Molinillo doesn't support circular dependencies at all.
|
188
|
+
|
189
|
+
# ensure we can't find a solution in the above
|
190
|
+
graph.artifact("D", "1.0.0").depends("E", "< 0.0.0")
|
191
|
+
|
192
|
+
# Add a solution to the graph that should be reached only after all of the
|
193
|
+
# others have been tried
|
194
|
+
graph.artifact("A", "0.0.0").depends("B", "0.0.0")
|
195
|
+
graph.artifact("B", "0.0.0").depends("C", "0.0.0")
|
196
|
+
graph.artifact("C", "0.0.0").depends("D", "0.0.0")
|
197
|
+
graph.artifact("D", "0.0.0")
|
198
|
+
|
199
|
+
demands = [["A"]]
|
200
|
+
|
201
|
+
result = Solve.it!(graph, demands)
|
202
|
+
|
203
|
+
result.should eql({ "A" => "0.0.0",
|
204
|
+
"B" => "0.0.0",
|
205
|
+
"C" => "0.0.0",
|
206
|
+
"D" => "0.0.0"})
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
it "correctly resolves when a resolution exists but it is not the latest" do
|
211
|
+
graph = Solve::Graph.new
|
212
|
+
|
213
|
+
graph.artifact("get-the-old-one", "1.0.0")
|
214
|
+
.depends("locked-mid-1", ">= 0.0.0")
|
215
|
+
.depends("locked-mid-2", ">= 0.0.0")
|
216
|
+
graph.artifact("get-the-old-one", "0.5.0")
|
217
|
+
|
218
|
+
graph.artifact("locked-mid-1", "2.0.0").depends("old-bottom", "= 2.0.0")
|
219
|
+
graph.artifact("locked-mid-1", "1.3.0").depends("old-bottom", "= 0.5.0")
|
220
|
+
graph.artifact("locked-mid-1", "1.0.0")
|
221
|
+
|
222
|
+
graph.artifact("locked-mid-2", "2.0.0").depends("old-bottom", "= 2.1.0")
|
223
|
+
graph.artifact("locked-mid-2", "1.4.0").depends("old-bottom", "= 0.5.0")
|
224
|
+
graph.artifact("locked-mid-2", "1.0.0")
|
225
|
+
|
226
|
+
graph.artifact("old-bottom", "2.1.0")
|
227
|
+
graph.artifact("old-bottom", "2.0.0")
|
228
|
+
graph.artifact("old-bottom", "1.0.0")
|
229
|
+
graph.artifact("old-bottom", "0.5.0")
|
230
|
+
|
231
|
+
demands = [["get-the-old-one"]]
|
232
|
+
|
233
|
+
result = Solve.it!(graph, demands)
|
234
|
+
|
235
|
+
# Note: Gecode solver is different. It picks:
|
236
|
+
#
|
237
|
+
# "get-the-old-one" => "1.0.0",
|
238
|
+
# "locked-mid-1" => "1.0.0",
|
239
|
+
# "locked-mid-2" => "2.0.0",
|
240
|
+
# "old-bottom" => "2.1.0"
|
241
|
+
|
242
|
+
result.should eql({
|
243
|
+
"get-the-old-one" => "1.0.0",
|
244
|
+
"locked-mid-1" => "2.0.0",
|
245
|
+
"locked-mid-2" => "1.0.0",
|
246
|
+
"old-bottom" => "2.0.0"
|
247
|
+
})
|
248
|
+
end
|
249
|
+
|
250
|
+
describe "when options[:sorted] is true" do
|
251
|
+
describe "with a simple list of dependencies" do
|
252
|
+
it "returns a sorted list of dependencies" do
|
253
|
+
graph = Solve::Graph.new
|
254
|
+
|
255
|
+
graph.artifact("A", "1.0.0").depends("B", "= 1.0.0")
|
256
|
+
graph.artifact("B", "1.0.0").depends("C", "= 1.0.0")
|
257
|
+
graph.artifact("C", "1.0.0")
|
258
|
+
|
259
|
+
demands = [["A"]]
|
260
|
+
|
261
|
+
result = Solve.it!(graph, demands, { :sorted => true })
|
262
|
+
|
263
|
+
result.should eql([
|
264
|
+
["C", "1.0.0"],
|
265
|
+
["B", "1.0.0"],
|
266
|
+
["A", "1.0.0"]
|
267
|
+
])
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# The order that the demands come in determines the order of artifacts
|
272
|
+
# in the solver's variable_table. This must not determine the sort order
|
273
|
+
describe "with a constraint that depends upon an earlier constrained artifact" do
|
274
|
+
it "returns a sorted list of dependencies" do
|
275
|
+
graph = Solve::Graph.new
|
276
|
+
|
277
|
+
graph.artifact("B", "1.0.0").depends("A", "= 1.0.0")
|
278
|
+
graph.artifact("A", "1.0.0").depends("C", "= 1.0.0")
|
279
|
+
graph.artifact("C", "1.0.0")
|
280
|
+
|
281
|
+
demands = [["A"],["B"]]
|
282
|
+
|
283
|
+
result = Solve.it!(graph, demands, { :sorted => true } )
|
284
|
+
|
285
|
+
result.should eql([
|
286
|
+
["C", "1.0.0"],
|
287
|
+
["A", "1.0.0"],
|
288
|
+
["B", "1.0.0"]
|
289
|
+
])
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
describe "when the solution is cyclic" do
|
294
|
+
it "raises a Solve::Errors::NoSolutionError because Molinillo doesn't support circular deps" do
|
295
|
+
graph = Solve::Graph.new
|
296
|
+
|
297
|
+
graph.artifact("A", "1.0.0").depends("B", "= 1.0.0")
|
298
|
+
graph.artifact("B", "1.0.0").depends("C", "= 1.0.0")
|
299
|
+
graph.artifact("C", "1.0.0").depends("A", "= 1.0.0")
|
300
|
+
|
301
|
+
demands = [["A"]]
|
302
|
+
|
303
|
+
expect { Solve.it!(graph, demands, { :sorted => true } ) }.to raise_error(Solve::Errors::NoSolutionError)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|