molinillo 0.4.5 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5471c5978d4fcbf76eb45959379323cb82e267c5
4
- data.tar.gz: 7fdf053d634d953fd63625c8954ead0f36d7b9cd
3
+ metadata.gz: 1b84ed4e59212748f00d03e6af1c8efa14a50312
4
+ data.tar.gz: c23774f7ef7ceb3641ae3f34357fbc4894c9afb5
5
5
  SHA512:
6
- metadata.gz: 7eebcc4b974f45733fbf034330958c5add2ad31b0dfd674f2442d6811761bbbf7a4ae160d3f1c6a98e19483d2d5511d715c724c15e07eff52dbc8da92234a257
7
- data.tar.gz: ab9a7d6eb33b8f40cf1aa4a9a5495da77c4ee2eeac629d128cffb5e9e54fbf5f1acc06d8a23f2af20960e05dbb7f58c3eebd723f8b58c5623bec9040640adb70
6
+ metadata.gz: 748d0fcec60150be7b9b33b0585ee1bd52e08ba19bf1ac7d2be6889ad2b4669a29d15a5d4de21e22823f9900f8e2f1b364b1ac40406cff35d4261a8a7c783103
7
+ data.tar.gz: 1a09bbe11d5b9c1d27e26e5f7b0dfc4848f28a87729d999fea11e784b6ba788a98146108572c6c8b340774360815bc0e3c44ae2f478c9373a85a85be1bbec8c8
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ module Molinillo
3
+ # @!visibility private
4
+ module Delegates
5
+ # Delegates all {Molinillo::ResolutionState} methods to a `#state` property.
6
+ module ResolutionState
7
+ # (see Molinillo::ResolutionState#name)
8
+ def name
9
+ current_state = state || Molinillo::ResolutionState.empty
10
+ current_state.name
11
+ end
12
+
13
+ # (see Molinillo::ResolutionState#requirements)
14
+ def requirements
15
+ current_state = state || Molinillo::ResolutionState.empty
16
+ current_state.requirements
17
+ end
18
+
19
+ # (see Molinillo::ResolutionState#activated)
20
+ def activated
21
+ current_state = state || Molinillo::ResolutionState.empty
22
+ current_state.activated
23
+ end
24
+
25
+ # (see Molinillo::ResolutionState#requirement)
26
+ def requirement
27
+ current_state = state || Molinillo::ResolutionState.empty
28
+ current_state.requirement
29
+ end
30
+
31
+ # (see Molinillo::ResolutionState#possibilities)
32
+ def possibilities
33
+ current_state = state || Molinillo::ResolutionState.empty
34
+ current_state.possibilities
35
+ end
36
+
37
+ # (see Molinillo::ResolutionState#depth)
38
+ def depth
39
+ current_state = state || Molinillo::ResolutionState.empty
40
+ current_state.depth
41
+ end
42
+
43
+ # (see Molinillo::ResolutionState#conflicts)
44
+ def conflicts
45
+ current_state = state || Molinillo::ResolutionState.empty
46
+ current_state.conflicts
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+ module Molinillo
3
+ module Delegates
4
+ # Delegates all {Molinillo::SpecificationProvider} methods to a
5
+ # `#specification_provider` property.
6
+ module SpecificationProvider
7
+ # (see Molinillo::SpecificationProvider#search_for)
8
+ def search_for(dependency)
9
+ with_no_such_dependency_error_handling do
10
+ specification_provider.search_for(dependency)
11
+ end
12
+ end
13
+
14
+ # (see Molinillo::SpecificationProvider#dependencies_for)
15
+ def dependencies_for(specification)
16
+ with_no_such_dependency_error_handling do
17
+ specification_provider.dependencies_for(specification)
18
+ end
19
+ end
20
+
21
+ # (see Molinillo::SpecificationProvider#requirement_satisfied_by?)
22
+ def requirement_satisfied_by?(requirement, activated, spec)
23
+ with_no_such_dependency_error_handling do
24
+ specification_provider.requirement_satisfied_by?(requirement, activated, spec)
25
+ end
26
+ end
27
+
28
+ # (see Molinillo::SpecificationProvider#name_for)
29
+ def name_for(dependency)
30
+ with_no_such_dependency_error_handling do
31
+ specification_provider.name_for(dependency)
32
+ end
33
+ end
34
+
35
+ # (see Molinillo::SpecificationProvider#name_for_explicit_dependency_source)
36
+ def name_for_explicit_dependency_source
37
+ with_no_such_dependency_error_handling do
38
+ specification_provider.name_for_explicit_dependency_source
39
+ end
40
+ end
41
+
42
+ # (see Molinillo::SpecificationProvider#name_for_locking_dependency_source)
43
+ def name_for_locking_dependency_source
44
+ with_no_such_dependency_error_handling do
45
+ specification_provider.name_for_locking_dependency_source
46
+ end
47
+ end
48
+
49
+ # (see Molinillo::SpecificationProvider#sort_dependencies)
50
+ def sort_dependencies(dependencies, activated, conflicts)
51
+ with_no_such_dependency_error_handling do
52
+ specification_provider.sort_dependencies(dependencies, activated, conflicts)
53
+ end
54
+ end
55
+
56
+ # (see Molinillo::SpecificationProvider#allow_missing?)
57
+ def allow_missing?(dependency)
58
+ with_no_such_dependency_error_handling do
59
+ specification_provider.allow_missing?(dependency)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Ensures any raised {NoSuchDependencyError} has its
66
+ # {NoSuchDependencyError#required_by} set.
67
+ # @yield
68
+ def with_no_such_dependency_error_handling
69
+ yield
70
+ rescue NoSuchDependencyError => error
71
+ if state
72
+ vertex = activated.vertex_named(name_for(error.dependency))
73
+ error.required_by += vertex.incoming_edges.map { |e| e.origin.name }
74
+ error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty?
75
+ end
76
+ raise
77
+ end
78
+ end
79
+ end
80
+ end
@@ -2,6 +2,9 @@
2
2
  require 'set'
3
3
  require 'tsort'
4
4
 
5
+ require 'molinillo/dependency_graph/log'
6
+ require 'molinillo/dependency_graph/vertex'
7
+
5
8
  module Molinillo
6
9
  # A directed acyclic graph that is tuned to hold named dependencies
7
10
  class DependencyGraph
@@ -10,15 +13,16 @@ module Molinillo
10
13
  # Enumerates through the vertices of the graph.
11
14
  # @return [Array<Vertex>] The graph's vertices.
12
15
  def each
16
+ return vertices.values.each unless block_given?
13
17
  vertices.values.each { |v| yield v }
14
18
  end
15
19
 
16
20
  include TSort
17
21
 
18
- # @visibility private
22
+ # @!visibility private
19
23
  alias tsort_each_node each
20
24
 
21
- # @visibility private
25
+ # @!visibility private
22
26
  def tsort_each_child(vertex, &block)
23
27
  vertex.successors.each(&block)
24
28
  end
@@ -44,9 +48,27 @@ module Molinillo
44
48
  # by {Vertex#name}
45
49
  attr_reader :vertices
46
50
 
51
+ # @return [Log] the op log for this graph
52
+ attr_reader :log
53
+
47
54
  # Initializes an empty dependency graph
48
55
  def initialize
49
56
  @vertices = {}
57
+ @log = Log.new
58
+ end
59
+
60
+ # Tags the current state of the dependency as the given tag
61
+ # @param [Object] tag an opaque tag for the current state of the graph
62
+ # @return [Void]
63
+ def tag(tag)
64
+ log.tag(self, tag)
65
+ end
66
+
67
+ # Rewinds the graph to the state tagged as `tag`
68
+ # @param [Object] tag the tag to rewind to
69
+ # @return [Void]
70
+ def rewind_to(tag)
71
+ log.rewind_to(self, tag)
50
72
  end
51
73
 
52
74
  # Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices}
@@ -55,6 +77,7 @@ module Molinillo
55
77
  def initialize_copy(other)
56
78
  super
57
79
  @vertices = {}
80
+ @log = other.log.dup
58
81
  traverse = lambda do |new_v, old_v|
59
82
  return if new_v.outgoing_edges.size == old_v.outgoing_edges.size
60
83
  old_v.outgoing_edges.each do |edge|
@@ -75,6 +98,22 @@ module Molinillo
75
98
  "#{self.class}:#{vertices.values.inspect}"
76
99
  end
77
100
 
101
+ # @return [String] Returns a dot format representation of the graph
102
+ def to_dot
103
+ dot_vertices = []
104
+ dot_edges = []
105
+ vertices.each do |n, v|
106
+ dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]"
107
+ v.outgoing_edges.each do |e|
108
+ dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=\"#{e.requirement}\"]"
109
+ end
110
+ end
111
+ dot_vertices.sort!
112
+ dot_edges.sort!
113
+ dot = dot_vertices.unshift('digraph G {').push('') + dot_edges.push('}')
114
+ dot.join("\n")
115
+ end
116
+
78
117
  # @return [Boolean] whether the two dependency graphs are equal, determined
79
118
  # by a recursive traversal of each {#root_vertices} and its
80
119
  # {Vertex#successors}
@@ -93,12 +132,9 @@ module Molinillo
93
132
  # @param [Object] requirement the requirement that is requiring the child
94
133
  # @return [void]
95
134
  def add_child_vertex(name, payload, parent_names, requirement)
96
- vertex = add_vertex(name, payload)
135
+ root = !parent_names.delete(nil) { true }
136
+ vertex = add_vertex(name, payload, root)
97
137
  parent_names.each do |parent_name|
98
- unless parent_name
99
- vertex.root = true
100
- next
101
- end
102
138
  parent_node = vertex_named(parent_name)
103
139
  add_edge(parent_node, vertex, requirement)
104
140
  end
@@ -110,10 +146,7 @@ module Molinillo
110
146
  # @param [Object] payload
111
147
  # @return [Vertex] the vertex that was added to `self`
112
148
  def add_vertex(name, payload, root = false)
113
- vertex = vertices[name] ||= Vertex.new(name, payload)
114
- vertex.payload ||= payload
115
- vertex.root ||= root
116
- vertex
149
+ log.add_vertex(self, name, payload, root)
117
150
  end
118
151
 
119
152
  # Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively
@@ -121,16 +154,7 @@ module Molinillo
121
154
  # @param [String] name
122
155
  # @return [void]
123
156
  def detach_vertex_named(name)
124
- return unless vertex = vertices.delete(name)
125
- vertex.outgoing_edges.each do |e|
126
- v = e.destination
127
- v.incoming_edges.delete(e)
128
- detach_vertex_named(v.name) unless v.root? || v.predecessors.any?
129
- end
130
- vertex.incoming_edges.each do |e|
131
- v = e.origin
132
- v.outgoing_edges.delete(e)
133
- end
157
+ log.detach_vertex_named(self, name)
134
158
  end
135
159
 
136
160
  # @param [String] name
@@ -158,134 +182,22 @@ module Molinillo
158
182
  add_edge_no_circular(origin, destination, requirement)
159
183
  end
160
184
 
185
+ # Sets the payload of the vertex with the given name
186
+ # @param [String] name the name of the vertex
187
+ # @param [Object] payload the payload
188
+ # @return [Void]
189
+ def set_payload(name, payload)
190
+ log.set_payload(self, name, payload)
191
+ end
192
+
161
193
  private
162
194
 
163
195
  # Adds a new {Edge} to the dependency graph without checking for
164
196
  # circularity.
197
+ # @param (see #add_edge)
198
+ # @return (see #add_edge)
165
199
  def add_edge_no_circular(origin, destination, requirement)
166
- edge = Edge.new(origin, destination, requirement)
167
- origin.outgoing_edges << edge
168
- destination.incoming_edges << edge
169
- edge
170
- end
171
-
172
- # A vertex in a {DependencyGraph} that encapsulates a {#name} and a
173
- # {#payload}
174
- class Vertex
175
- # @return [String] the name of the vertex
176
- attr_accessor :name
177
-
178
- # @return [Object] the payload the vertex holds
179
- attr_accessor :payload
180
-
181
- # @return [Arrary<Object>] the explicit requirements that required
182
- # this vertex
183
- attr_reader :explicit_requirements
184
-
185
- # @return [Boolean] whether the vertex is considered a root vertex
186
- attr_accessor :root
187
- alias root? root
188
-
189
- # Initializes a vertex with the given name and payload.
190
- # @param [String] name see {#name}
191
- # @param [Object] payload see {#payload}
192
- def initialize(name, payload)
193
- @name = name
194
- @payload = payload
195
- @explicit_requirements = []
196
- @outgoing_edges = []
197
- @incoming_edges = []
198
- end
199
-
200
- # @return [Array<Object>] all of the requirements that required
201
- # this vertex
202
- def requirements
203
- incoming_edges.map(&:requirement) + explicit_requirements
204
- end
205
-
206
- # @return [Array<Edge>] the edges of {#graph} that have `self` as their
207
- # {Edge#origin}
208
- attr_accessor :outgoing_edges
209
-
210
- # @return [Array<Edge>] the edges of {#graph} that have `self` as their
211
- # {Edge#destination}
212
- attr_accessor :incoming_edges
213
-
214
- # @return [Array<Vertex>] the vertices of {#graph} that have an edge with
215
- # `self` as their {Edge#destination}
216
- def predecessors
217
- incoming_edges.map(&:origin)
218
- end
219
-
220
- # @return [Array<Vertex>] the vertices of {#graph} where `self` is a
221
- # {#descendent?}
222
- def recursive_predecessors
223
- vertices = predecessors
224
- vertices += vertices.map(&:recursive_predecessors).flatten(1)
225
- vertices.uniq!
226
- vertices
227
- end
228
-
229
- # @return [Array<Vertex>] the vertices of {#graph} that have an edge with
230
- # `self` as their {Edge#origin}
231
- def successors
232
- outgoing_edges.map(&:destination)
233
- end
234
-
235
- # @return [Array<Vertex>] the vertices of {#graph} where `self` is an
236
- # {#ancestor?}
237
- def recursive_successors
238
- vertices = successors
239
- vertices += vertices.map(&:recursive_successors).flatten(1)
240
- vertices.uniq!
241
- vertices
242
- end
243
-
244
- # @return [String] a string suitable for debugging
245
- def inspect
246
- "#{self.class}:#{name}(#{payload.inspect})"
247
- end
248
-
249
- # @return [Boolean] whether the two vertices are equal, determined
250
- # by a recursive traversal of each {Vertex#successors}
251
- def ==(other)
252
- shallow_eql?(other) &&
253
- successors.to_set == other.successors.to_set
254
- end
255
-
256
- # @param [Vertex] other the other vertex to compare to
257
- # @return [Boolean] whether the two vertices are equal, determined
258
- # solely by {#name} and {#payload} equality
259
- def shallow_eql?(other)
260
- other &&
261
- name == other.name &&
262
- payload == other.payload
263
- end
264
-
265
- alias eql? ==
266
-
267
- # @return [Fixnum] a hash for the vertex based upon its {#name}
268
- def hash
269
- name.hash
270
- end
271
-
272
- # Is there a path from `self` to `other` following edges in the
273
- # dependency graph?
274
- # @return true iff there is a path following edges within this {#graph}
275
- def path_to?(other)
276
- equal?(other) || successors.any? { |v| v.path_to?(other) }
277
- end
278
-
279
- alias descendent? path_to?
280
-
281
- # Is there a path from `other` to `self` following edges in the
282
- # dependency graph?
283
- # @return true iff there is a path following edges within this {#graph}
284
- def ancestor?(other)
285
- other.path_to?(self)
286
- end
287
-
288
- alias is_reachable_from? ancestor?
200
+ log.add_edge_no_circular(self, origin.name, destination.name, requirement)
289
201
  end
290
202
  end
291
203
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module Molinillo
3
+ class DependencyGraph
4
+ # An action that modifies a {DependencyGraph} that is reversible.
5
+ # @abstract
6
+ class Action
7
+ # rubocop:disable Lint/UnusedMethodArgument
8
+
9
+ # @return [Symbol] The name of the action.
10
+ def self.name
11
+ raise 'Abstract'
12
+ end
13
+
14
+ # Performs the action on the given graph.
15
+ # @param [DependencyGraph] graph the graph to perform the action on.
16
+ # @return [Void]
17
+ def up(graph)
18
+ raise 'Abstract'
19
+ end
20
+
21
+ # Reverses the action on the given graph.
22
+ # @param [DependencyGraph] graph the graph to reverse the action on.
23
+ # @return [Void]
24
+ def down(graph)
25
+ raise 'Abstract'
26
+ end
27
+
28
+ # @return [Action,Nil] The previous action
29
+ attr_accessor :previous
30
+
31
+ # @return [Action,Nil] The next action
32
+ attr_accessor :next
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ require 'molinillo/dependency_graph/action'
3
+ module Molinillo
4
+ class DependencyGraph
5
+ # @!visibility private
6
+ # (see DependencyGraph#add_edge_no_circular)
7
+ class AddEdgeNoCircular < Action
8
+ # @!group Action
9
+
10
+ # (see Action.name)
11
+ def self.name
12
+ :add_vertex
13
+ end
14
+
15
+ # (see Action#up)
16
+ def up(graph)
17
+ edge = make_edge(graph)
18
+ edge.origin.outgoing_edges << edge
19
+ edge.destination.incoming_edges << edge
20
+ edge
21
+ end
22
+
23
+ # (see Action#down)
24
+ def down(graph)
25
+ edge = make_edge(graph)
26
+ edge.origin.outgoing_edges.delete(edge)
27
+ edge.destination.incoming_edges.delete(edge)
28
+ end
29
+
30
+ # @!group AddEdgeNoCircular
31
+
32
+ # @return [String] the name of the origin of the edge
33
+ attr_reader :origin
34
+
35
+ # @return [String] the name of the destination of the edge
36
+ attr_reader :destination
37
+
38
+ # @return [Object] the requirement that the edge represents
39
+ attr_reader :requirement
40
+
41
+ # @param [DependencyGraph] graph the graph to find vertices from
42
+ # @return [Edge] The edge this action adds
43
+ def make_edge(graph)
44
+ Edge.new(graph.vertex_named(origin), graph.vertex_named(destination), requirement)
45
+ end
46
+
47
+ # Initialize an action to add an edge to a dependency graph
48
+ # @param [String] origin the name of the origin of the edge
49
+ # @param [String] destination the name of the destination of the edge
50
+ # @param [Object] requirement the requirement that the edge represents
51
+ def initialize(origin, destination, requirement)
52
+ @origin = origin
53
+ @destination = destination
54
+ @requirement = requirement
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require 'molinillo/dependency_graph/action'
3
+ module Molinillo
4
+ class DependencyGraph
5
+ # @!visibility private
6
+ # (see DependencyGraph#add_vertex)
7
+ class AddVertex < Action # :nodoc:
8
+ # @!group Action
9
+
10
+ # (see Action.name)
11
+ def self.name
12
+ :add_vertex
13
+ end
14
+
15
+ # (see Action#up)
16
+ def up(graph)
17
+ if existing = graph.vertices[name]
18
+ @existing_payload = existing.payload
19
+ @existing_root = existing.root
20
+ end
21
+ vertex = existing || Vertex.new(name, payload)
22
+ graph.vertices[vertex.name] = vertex
23
+ vertex.payload ||= payload
24
+ vertex.root ||= root
25
+ vertex
26
+ end
27
+
28
+ # (see Action#down)
29
+ def down(graph)
30
+ if defined?(@existing_payload)
31
+ vertex = graph.vertices[name]
32
+ vertex.payload = @existing_payload
33
+ vertex.root = @existing_root
34
+ else
35
+ graph.vertices.delete(name)
36
+ end
37
+ end
38
+
39
+ # @!group AddVertex
40
+
41
+ # @return [String] the name of the vertex
42
+ attr_reader :name
43
+
44
+ # @return [Object] the payload for the vertex
45
+ attr_reader :payload
46
+
47
+ # @return [Boolean] whether the vertex is root or not
48
+ attr_reader :root
49
+
50
+ # Initialize an action to add a vertex to a dependency graph
51
+ # @param [String] name the name of the vertex
52
+ # @param [Object] payload the payload for the vertex
53
+ # @param [Boolean] root whether the vertex is root or not
54
+ def initialize(name, payload, root)
55
+ @name = name
56
+ @payload = payload
57
+ @root = root
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require 'molinillo/dependency_graph/action'
3
+ module Molinillo
4
+ class DependencyGraph
5
+ # @!visibility private
6
+ # @see DependencyGraph#detach_vertex_named
7
+ class DetachVertexNamed < Action
8
+ # @!group Action
9
+
10
+ # (see Action#name)
11
+ def self.name
12
+ :add_vertex
13
+ end
14
+
15
+ # (see Action#up)
16
+ def up(graph)
17
+ return unless @vertex = graph.vertices.delete(name)
18
+ @vertex.outgoing_edges.each do |e|
19
+ v = e.destination
20
+ v.incoming_edges.delete(e)
21
+ graph.detach_vertex_named(v.name) unless v.root? || v.predecessors.any?
22
+ end
23
+ @vertex.incoming_edges.each do |e|
24
+ v = e.origin
25
+ v.outgoing_edges.delete(e)
26
+ end
27
+ end
28
+
29
+ # (see Action#down)
30
+ def down(graph)
31
+ return unless @vertex
32
+ graph.vertices[@vertex.name] = @vertex
33
+ @vertex.outgoing_edges.each do |e|
34
+ e.destination.incoming_edges << e
35
+ end
36
+ @vertex.incoming_edges.each do |e|
37
+ e.origin.outgoing_edges << e
38
+ end
39
+ end
40
+
41
+ # @!group DetachVertexNamed
42
+
43
+ # @return [String] the name of the vertex to detach
44
+ attr_reader :name
45
+
46
+ # Initialize an action to detach a vertex from a dependency graph
47
+ # @param [String] name the name of the vertex to detach
48
+ def initialize(name)
49
+ @name = name
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require 'molinillo/dependency_graph/add_edge_no_circular'
3
+ require 'molinillo/dependency_graph/add_vertex'
4
+ require 'molinillo/dependency_graph/detach_vertex_named'
5
+ require 'molinillo/dependency_graph/set_payload'
6
+ require 'molinillo/dependency_graph/tag'
7
+
8
+ module Molinillo
9
+ class DependencyGraph
10
+ # A log for dependency graph actions
11
+ class Log
12
+ # Initializes an empty log
13
+ def initialize
14
+ @current_action = @first_action = nil
15
+ end
16
+
17
+ # @!macro [new] action
18
+ # {include:DependencyGraph#$0}
19
+ # @param [Graph] graph the graph to perform the action on
20
+ # @param (see DependencyGraph#$0)
21
+ # @return (see DependencyGraph#$0)
22
+
23
+ # @macro action
24
+ def tag(graph, tag)
25
+ push_action(graph, Tag.new(tag))
26
+ end
27
+
28
+ # @macro action
29
+ def add_vertex(graph, name, payload, root)
30
+ push_action(graph, AddVertex.new(name, payload, root))
31
+ end
32
+
33
+ # @macro action
34
+ def detach_vertex_named(graph, name)
35
+ push_action(graph, DetachVertexNamed.new(name))
36
+ end
37
+
38
+ # @macro action
39
+ def add_edge_no_circular(graph, origin, destination, requirement)
40
+ push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement))
41
+ end
42
+
43
+ # @macro action
44
+ def set_payload(graph, name, payload)
45
+ push_action(graph, SetPayload.new(name, payload))
46
+ end
47
+
48
+ # Pops the most recent action from the log and undoes the action
49
+ # @param [DependencyGraph] graph
50
+ # @return [Action] the action that was popped off the log
51
+ def pop!(graph)
52
+ return unless action = @current_action
53
+ unless @current_action = action.previous
54
+ @first_action = nil
55
+ end
56
+ action.down(graph)
57
+ action
58
+ end
59
+
60
+ extend Enumerable
61
+
62
+ # @!visibility private
63
+ # Enumerates each action in the log
64
+ # @yield [Action]
65
+ def each
66
+ return enum_for unless block_given?
67
+ action = @first_action
68
+ loop do
69
+ break unless action
70
+ yield action
71
+ action = action.next
72
+ end
73
+ self
74
+ end
75
+
76
+ # @!visibility private
77
+ # Enumerates each action in the log in reverse order
78
+ # @yield [Action]
79
+ def reverse_each
80
+ return enum_for(:reverse_each) unless block_given?
81
+ action = @current_action
82
+ loop do
83
+ break unless action
84
+ yield action
85
+ action = action.previous
86
+ end
87
+ self
88
+ end
89
+
90
+ # @macro action
91
+ def rewind_to(graph, tag)
92
+ loop do
93
+ action = pop!(graph)
94
+ raise "No tag #{tag.inspect} found" unless action
95
+ break if action.class.name == :tag && action.tag == tag
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ # Adds the given action to the log, running the action
102
+ # @param [DependencyGraph] graph
103
+ # @param [Action] action
104
+ # @return The value returned by `action.up`
105
+ def push_action(graph, action)
106
+ action.previous = @current_action
107
+ @current_action.next = action if @current_action
108
+ @current_action = action
109
+ @first_action ||= action
110
+ action.up(graph)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ require 'molinillo/dependency_graph/action'
3
+ module Molinillo
4
+ class DependencyGraph
5
+ # @!visibility private
6
+ # @see DependencyGraph#set_payload
7
+ class SetPayload < Action # :nodoc:
8
+ # @!group Action
9
+
10
+ # (see Action.name)
11
+ def self.name
12
+ :set_payload
13
+ end
14
+
15
+ # (see Action#up)
16
+ def up(graph)
17
+ vertex = graph.vertex_named(name)
18
+ @old_payload = vertex.payload
19
+ vertex.payload = payload
20
+ end
21
+
22
+ # (see Action#down)
23
+ def down(graph)
24
+ graph.vertex_named(name).payload = @old_payload
25
+ end
26
+
27
+ # @!group SetPayload
28
+
29
+ # @return [String] the name of the vertex
30
+ attr_reader :name
31
+
32
+ # @return [Object] the payload for the vertex
33
+ attr_reader :payload
34
+
35
+ # Initialize an action to add set the payload for a vertex in a dependency
36
+ # graph
37
+ # @param [String] name the name of the vertex
38
+ # @param [Object] payload the payload for the vertex
39
+ def initialize(name, payload)
40
+ @name = name
41
+ @payload = payload
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ require 'molinillo/dependency_graph/action'
3
+ module Molinillo
4
+ class DependencyGraph
5
+ # @!visibility private
6
+ # @see DependencyGraph#tag
7
+ class Tag < Action
8
+ # @!group Action
9
+
10
+ # (see Action.name)
11
+ def self.name
12
+ :tag
13
+ end
14
+
15
+ # (see Action#up)
16
+ def up(_graph)
17
+ end
18
+
19
+ # (see Action#down)
20
+ def down(_graph)
21
+ end
22
+
23
+ # @!group Tag
24
+
25
+ # @return [Object] An opaque tag
26
+ attr_reader :tag
27
+
28
+ # Initialize an action to tag a state of a dependency graph
29
+ # @param [Object] tag an opaque tag
30
+ def initialize(tag)
31
+ @tag = tag
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+ module Molinillo
3
+ class DependencyGraph
4
+ # A vertex in a {DependencyGraph} that encapsulates a {#name} and a
5
+ # {#payload}
6
+ class Vertex
7
+ # @return [String] the name of the vertex
8
+ attr_accessor :name
9
+
10
+ # @return [Object] the payload the vertex holds
11
+ attr_accessor :payload
12
+
13
+ # @return [Arrary<Object>] the explicit requirements that required
14
+ # this vertex
15
+ attr_reader :explicit_requirements
16
+
17
+ # @return [Boolean] whether the vertex is considered a root vertex
18
+ attr_accessor :root
19
+ alias root? root
20
+
21
+ # Initializes a vertex with the given name and payload.
22
+ # @param [String] name see {#name}
23
+ # @param [Object] payload see {#payload}
24
+ def initialize(name, payload)
25
+ @name = name.frozen? ? name : name.dup.freeze
26
+ @payload = payload
27
+ @explicit_requirements = []
28
+ @outgoing_edges = []
29
+ @incoming_edges = []
30
+ end
31
+
32
+ # @return [Array<Object>] all of the requirements that required
33
+ # this vertex
34
+ def requirements
35
+ incoming_edges.map(&:requirement) + explicit_requirements
36
+ end
37
+
38
+ # @return [Array<Edge>] the edges of {#graph} that have `self` as their
39
+ # {Edge#origin}
40
+ attr_accessor :outgoing_edges
41
+
42
+ # @return [Array<Edge>] the edges of {#graph} that have `self` as their
43
+ # {Edge#destination}
44
+ attr_accessor :incoming_edges
45
+
46
+ # @return [Array<Vertex>] the vertices of {#graph} that have an edge with
47
+ # `self` as their {Edge#destination}
48
+ def predecessors
49
+ incoming_edges.map(&:origin)
50
+ end
51
+
52
+ # @return [Array<Vertex>] the vertices of {#graph} where `self` is a
53
+ # {#descendent?}
54
+ def recursive_predecessors
55
+ vertices = predecessors
56
+ vertices += vertices.map(&:recursive_predecessors).flatten(1)
57
+ vertices.uniq!
58
+ vertices
59
+ end
60
+
61
+ # @return [Array<Vertex>] the vertices of {#graph} that have an edge with
62
+ # `self` as their {Edge#origin}
63
+ def successors
64
+ outgoing_edges.map(&:destination)
65
+ end
66
+
67
+ # @return [Array<Vertex>] the vertices of {#graph} where `self` is an
68
+ # {#ancestor?}
69
+ def recursive_successors
70
+ vertices = successors
71
+ vertices += vertices.map(&:recursive_successors).flatten(1)
72
+ vertices.uniq!
73
+ vertices
74
+ end
75
+
76
+ # @return [String] a string suitable for debugging
77
+ def inspect
78
+ "#{self.class}:#{name}(#{payload.inspect})"
79
+ end
80
+
81
+ # @return [Boolean] whether the two vertices are equal, determined
82
+ # by a recursive traversal of each {Vertex#successors}
83
+ def ==(other)
84
+ shallow_eql?(other) &&
85
+ successors.to_set == other.successors.to_set
86
+ end
87
+
88
+ # @param [Vertex] other the other vertex to compare to
89
+ # @return [Boolean] whether the two vertices are equal, determined
90
+ # solely by {#name} and {#payload} equality
91
+ def shallow_eql?(other)
92
+ other &&
93
+ name == other.name &&
94
+ payload == other.payload
95
+ end
96
+
97
+ alias eql? ==
98
+
99
+ # @return [Fixnum] a hash for the vertex based upon its {#name}
100
+ def hash
101
+ name.hash
102
+ end
103
+
104
+ # Is there a path from `self` to `other` following edges in the
105
+ # dependency graph?
106
+ # @return true iff there is a path following edges within this {#graph}
107
+ def path_to?(other)
108
+ equal?(other) || successors.any? { |v| v.path_to?(other) }
109
+ end
110
+
111
+ alias descendent? path_to?
112
+
113
+ # Is there a path from `other` to `self` following edges in the
114
+ # dependency graph?
115
+ # @return true iff there is a path following edges within this {#graph}
116
+ def ancestor?(other)
117
+ other.path_to?(self)
118
+ end
119
+
120
+ alias is_reachable_from? ancestor?
121
+ end
122
+ end
123
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module Molinillo
3
3
  # The version of Molinillo.
4
- VERSION = '0.4.5'.freeze
4
+ VERSION = '0.5.0'.freeze
5
5
  end
@@ -52,6 +52,7 @@ module Molinillo
52
52
  @base = base
53
53
  @states = []
54
54
  @iteration_counter = 0
55
+ @parent_of = {}
55
56
  end
56
57
 
57
58
  # Resolves the {#original_requested} dependencies into a full dependency
@@ -67,7 +68,12 @@ module Molinillo
67
68
  indicate_progress
68
69
  if state.respond_to?(:pop_possibility_state) # DependencyState
69
70
  debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" }
70
- state.pop_possibility_state.tap { |s| states.push(s) if s }
71
+ state.pop_possibility_state.tap do |s|
72
+ if s
73
+ states.push(s)
74
+ activated.tag(s)
75
+ end
76
+ end
71
77
  end
72
78
  process_topmost_state
73
79
  end
@@ -118,27 +124,11 @@ module Molinillo
118
124
  require 'molinillo/state'
119
125
  require 'molinillo/modules/specification_provider'
120
126
 
121
- ResolutionState.new.members.each do |member|
122
- define_method member do |*args, &block|
123
- current_state = state || ResolutionState.empty
124
- current_state.send(member, *args, &block)
125
- end
126
- end
127
+ require 'molinillo/delegates/resolution_state'
128
+ require 'molinillo/delegates/specification_provider'
127
129
 
128
- SpecificationProvider.instance_methods(false).each do |instance_method|
129
- define_method instance_method do |*args, &block|
130
- begin
131
- specification_provider.send(instance_method, *args, &block)
132
- rescue NoSuchDependencyError => error
133
- if state
134
- vertex = activated.vertex_named(name_for(error.dependency))
135
- error.required_by += vertex.incoming_edges.map { |e| e.origin.name }
136
- error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty?
137
- end
138
- raise
139
- end
140
- end
141
- end
130
+ include Molinillo::Delegates::ResolutionState
131
+ include Molinillo::Delegates::SpecificationProvider
142
132
 
143
133
  # Processes the topmost available {RequirementState} on the stack
144
134
  # @return [void]
@@ -169,6 +159,7 @@ module Molinillo
169
159
  def initial_state
170
160
  graph = DependencyGraph.new.tap do |dg|
171
161
  original_requested.each { |r| dg.add_vertex(name_for(r), nil, true).tap { |v| v.explicit_requirements << r } }
162
+ dg.tag(:initial_state)
172
163
  end
173
164
 
174
165
  requirements = sort_dependencies(original_requested, graph, {})
@@ -189,8 +180,9 @@ module Molinillo
189
180
  def unwind_for_conflict
190
181
  debug(depth) { "Unwinding for conflict: #{requirement}" }
191
182
  conflicts.tap do |c|
192
- states.slice!((state_index_for_unwind + 1)..-1)
183
+ sliced_states = states.slice!((state_index_for_unwind + 1)..-1)
193
184
  raise VersionConflict.new(c) unless state
185
+ activated.rewind_to(sliced_states.first || :initial_state) if sliced_states
194
186
  state.conflicts = c
195
187
  end
196
188
  end
@@ -217,20 +209,14 @@ module Molinillo
217
209
  # @return [Object] the requirement that led to `requirement` being added
218
210
  # to the list of requirements.
219
211
  def parent_of(requirement)
220
- return nil unless requirement
221
- seen = false
222
- state = states.reverse_each.find do |s|
223
- seen ||= s.requirement == requirement || s.requirements.include?(requirement)
224
- seen && s.requirement != requirement && !s.requirements.include?(requirement)
225
- end
226
- state && state.requirement
212
+ @parent_of[requirement]
227
213
  end
228
214
 
229
215
  # @return [Object] the requirement that led to a version of a possibility
230
216
  # with the given name being activated.
231
217
  def requirement_for_existing_name(name)
232
218
  return nil unless activated.vertex_named(name).payload
233
- states.reverse_each.find { |s| !s.activated.vertex_named(name).payload }.requirement
219
+ states.find { |s| s.name == name }.requirement
234
220
  end
235
221
 
236
222
  # @return [ResolutionState] the state whose `requirement` is the given
@@ -250,19 +236,25 @@ module Molinillo
250
236
  # the {#possibility} in conjunction with the current {#state}
251
237
  def create_conflict
252
238
  vertex = activated.vertex_named(name)
253
- requirements = {
254
- name_for_explicit_dependency_source => vertex.explicit_requirements,
255
- name_for_locking_dependency_source => Array(locked_requirement_named(name)),
256
- }
239
+ locked_requirement = locked_requirement_named(name)
240
+
241
+ requirements = {}
242
+ unless vertex.explicit_requirements.empty?
243
+ requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements
244
+ end
245
+ requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement
257
246
  vertex.incoming_edges.each { |edge| (requirements[edge.origin.payload] ||= []).unshift(edge.requirement) }
247
+
248
+ activated_by_name = {}
249
+ activated.each { |v| activated_by_name[v.name] = v.payload if v.payload }
258
250
  conflicts[name] = Conflict.new(
259
251
  requirement,
260
- Hash[requirements.select { |_, r| !r.empty? }],
252
+ requirements,
261
253
  vertex.payload,
262
254
  possibility,
263
- locked_requirement_named(name),
255
+ locked_requirement,
264
256
  requirement_trees,
265
- Hash[activated.map { |v| [v.name, v.payload] }.select(&:last)]
257
+ activated_by_name
266
258
  )
267
259
  end
268
260
 
@@ -341,15 +333,16 @@ module Molinillo
341
333
  # spec with the given name
342
334
  # @return [Boolean] Whether the possibility was swapped into {#activated}
343
335
  def attempt_to_swap_possibility
344
- swapped = activated.dup
345
- vertex = swapped.vertex_named(name)
346
- vertex.payload = possibility
347
- return unless vertex.requirements.
348
- all? { |r| requirement_satisfied_by?(r, swapped, possibility) }
349
- return unless new_spec_satisfied?
350
- actual_vertex = activated.vertex_named(name)
351
- actual_vertex.payload = possibility
352
- fixup_swapped_children(actual_vertex)
336
+ activated.tag(:swap)
337
+ vertex = activated.vertex_named(name)
338
+ activated.set_payload(name, possibility)
339
+ if !vertex.requirements.
340
+ all? { |r| requirement_satisfied_by?(r, activated, possibility) } ||
341
+ !new_spec_satisfied?
342
+ activated.rewind_to(:swap)
343
+ return
344
+ end
345
+ fixup_swapped_children(vertex)
353
346
  activate_spec
354
347
  end
355
348
 
@@ -412,8 +405,7 @@ module Molinillo
412
405
  def activate_spec
413
406
  conflicts.delete(name)
414
407
  debug(depth) { 'Activated ' + name + ' at ' + possibility.to_s }
415
- vertex = activated.vertex_named(name)
416
- vertex.payload = possibility
408
+ activated.set_payload(name, possibility)
417
409
  require_nested_dependencies_for(possibility)
418
410
  end
419
411
 
@@ -424,7 +416,10 @@ module Molinillo
424
416
  def require_nested_dependencies_for(activated_spec)
425
417
  nested_dependencies = dependencies_for(activated_spec)
426
418
  debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" }
427
- nested_dependencies.each { |d| activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d) }
419
+ nested_dependencies.each do |d|
420
+ activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d)
421
+ @parent_of[d] = requirement
422
+ end
428
423
 
429
424
  push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?)
430
425
  end
@@ -436,7 +431,7 @@ module Molinillo
436
431
  def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated)
437
432
  new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort
438
433
  new_requirement = new_requirements.shift
439
- new_name = new_requirement ? name_for(new_requirement) : ''
434
+ new_name = new_requirement ? name_for(new_requirement) : ''.freeze
440
435
  possibilities = new_requirement ? search_for(new_requirement) : []
441
436
  handle_missing_or_push_dependency_state DependencyState.new(
442
437
  new_name, new_requirements, new_activated,
@@ -457,7 +452,7 @@ module Molinillo
457
452
  state.activated.detach_vertex_named(state.name)
458
453
  push_state_for_requirements(state.requirements.dup, false, state.activated)
459
454
  else
460
- states.push state
455
+ states.push(state).tap { activated.tag(state) }
461
456
  end
462
457
  end
463
458
  end
@@ -36,12 +36,14 @@ module Molinillo
36
36
  PossibilityState.new(
37
37
  name,
38
38
  requirements.dup,
39
- activated.dup,
39
+ activated,
40
40
  requirement,
41
41
  [possibilities.pop],
42
42
  depth + 1,
43
43
  conflicts.dup
44
- )
44
+ ).tap do |state|
45
+ state.activated.tag(state)
46
+ end
45
47
  end
46
48
  end
47
49
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe Molinillo::DependencyGraph::Log do
5
+ shared_examples_for 'replay' do |steps|
6
+ it 'replays the log' do
7
+ copy = Molinillo::DependencyGraph.new
8
+ graph = Molinillo::DependencyGraph.new.tap { |g| steps.each { |s| s.call(g) } }
9
+ graph.log.each { |a| a.up(copy) }
10
+ expect(copy).to eq(graph)
11
+ end
12
+
13
+ it 'can undo to an empty graph' do
14
+ graph = Molinillo::DependencyGraph.new
15
+ graph.tag(self)
16
+ steps.each { |s| s.call(graph) }
17
+ graph.log.rewind_to(graph, self)
18
+ expect(graph).to eq(Molinillo::DependencyGraph.new)
19
+ end
20
+ end
21
+
22
+ it_behaves_like 'replay', []
23
+ it_behaves_like 'replay', [
24
+ proc { |g| g.add_child_vertex('Foo', 1, [nil], 4) },
25
+ proc { |g| g.add_child_vertex('Bar', 2, ['Foo', nil], 3) },
26
+ proc { |g| g.add_child_vertex('Baz', 3, %w(Foo Bar), 2) },
27
+ proc { |g| g.add_child_vertex('Foo', 4, [], 1) },
28
+ ]
29
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require File.expand_path('../spec_helper', __FILE__)
2
3
 
3
4
  module Molinillo
data/spec/errors_spec.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require File.expand_path('../spec_helper', __FILE__)
2
3
 
3
4
  module Molinillo
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require File.expand_path('../spec_helper', __FILE__)
2
3
  require 'json'
3
4
  require 'pathname'
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'bundler/setup'
2
3
 
3
4
  # Set up coverage analysis
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Molinillo
2
3
  class TestIndex
3
4
  attr_accessor :specs
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Molinillo
2
3
  class TestSpecification
3
4
  attr_accessor :name, :version, :dependencies
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module Molinillo
2
3
  class TestUI
3
4
  include UI
4
5
 
5
6
  def output
6
- @output ||= File.open('/dev/null', 'w')
7
+ @output ||= if debug?
8
+ $stderr
9
+ else
10
+ File.open('/dev/null', 'w')
11
+ end
7
12
  end
8
13
  end
9
14
  end
data/spec/state_spec.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require File.expand_path('../spec_helper', __FILE__)
2
3
 
3
4
  module Molinillo
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: molinillo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel E. Giddins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-30 00:00:00.000000000 Z
11
+ date: 2016-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -48,7 +48,17 @@ files:
48
48
  - LICENSE
49
49
  - README.md
50
50
  - lib/molinillo.rb
51
+ - lib/molinillo/delegates/resolution_state.rb
52
+ - lib/molinillo/delegates/specification_provider.rb
51
53
  - lib/molinillo/dependency_graph.rb
54
+ - lib/molinillo/dependency_graph/action.rb
55
+ - lib/molinillo/dependency_graph/add_edge_no_circular.rb
56
+ - lib/molinillo/dependency_graph/add_vertex.rb
57
+ - lib/molinillo/dependency_graph/detach_vertex_named.rb
58
+ - lib/molinillo/dependency_graph/log.rb
59
+ - lib/molinillo/dependency_graph/set_payload.rb
60
+ - lib/molinillo/dependency_graph/tag.rb
61
+ - lib/molinillo/dependency_graph/vertex.rb
52
62
  - lib/molinillo/errors.rb
53
63
  - lib/molinillo/gem_metadata.rb
54
64
  - lib/molinillo/modules/specification_provider.rb
@@ -56,6 +66,7 @@ files:
56
66
  - lib/molinillo/resolution.rb
57
67
  - lib/molinillo/resolver.rb
58
68
  - lib/molinillo/state.rb
69
+ - spec/dependency_graph/log_spec.rb
59
70
  - spec/dependency_graph_spec.rb
60
71
  - spec/errors_spec.rb
61
72
  - spec/resolver_integration_specs/index_from_rubygems.rb
@@ -85,11 +96,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
96
  version: '0'
86
97
  requirements: []
87
98
  rubyforge_project:
88
- rubygems_version: 2.6.3
99
+ rubygems_version: 2.6.4
89
100
  signing_key:
90
101
  specification_version: 4
91
102
  summary: Provides support for dependency resolution
92
103
  test_files:
104
+ - spec/dependency_graph/log_spec.rb
93
105
  - spec/dependency_graph_spec.rb
94
106
  - spec/errors_spec.rb
95
107
  - spec/resolver_integration_specs/index_from_rubygems.rb