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.
@@ -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