solve 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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::Solver] solver
28
+ # @param [Solve::Problem] problem struct
7
29
  #
8
30
  # @return [String]
9
- def serialize(solver)
10
- graph = solver.graph
11
- demands = solver.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
- problem = graph_hash.merge(demands_hash)
17
- problem.to_json
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::Solver]
23
- def deserialize(solver)
24
- unless solver.is_a?(Hash)
25
- solver = JSON.parse(solver.to_s)
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 = solver["graph"]
29
- demands_spec = solver["demands"]
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::Solver.new(graph, demands)
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.name,
75
- "constraint" => demand.constraint.to_s
96
+ "name" => demand[0],
97
+ "constraint" => demand[1]
76
98
  }
77
99
  end
78
100
 
@@ -1,3 +1,3 @@
1
1
  module Solve
2
- VERSION = "1.2.1"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -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 "dep_selector", "~> 1.0"
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
- N = 1
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::Solver.new(STATIC_GRAPH, demands, {}).resolve({})
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