molinillo 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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