ruby-graph-walker 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 098a0fa35d5773837d9b335237aa86d664835da3
4
+ data.tar.gz: 2450e77e9c9de04c866daeaad447f81f34f2d58b
5
+ SHA512:
6
+ metadata.gz: 43d80e28ce884734f03b408099f8ee123e558ed631300d36f4342e5443606f45b9e94c9d22d8af579a3724313d453a55f7f2614e8e5c79081f942e4188b273a5
7
+ data.tar.gz: ab81246bc8d2c9f36f3aa49e598fe4086e33d89c604f29e3e1f49dee49b1528e346ef959c8f8004cb90bd7362c0191eca3ee76a2bdcb04466e9fbf0d6f1cbb3c
data/lib/colorize.rb ADDED
@@ -0,0 +1,28 @@
1
+ module RubyGraphWalker
2
+ def colorize(text, color_code)
3
+ "\e[#{color_code}m#{text}\e[0m"
4
+ end
5
+
6
+ def red(text); colorize(text, 31); end
7
+ def green(text); colorize(text, 32); end
8
+ def yellow(text); colorize(text, 33); end
9
+ def blue(text); colorize(text, 34); end
10
+ def magenta(text); colorize(text, 35); end
11
+ def cyan(text); colorize(text, 36); end
12
+ def yellow_background(text); colorize(text, 43); end
13
+ def magenta_background(text); colorize(text, 45); end
14
+ def exchange_foreground_and_background(text); colorize(text, 7); end
15
+ def light_red(text); colorize(text, 91); end
16
+
17
+ def log_warning(msg)
18
+ puts magenta(msg)
19
+ end
20
+
21
+ def log_error(msg)
22
+ puts light_red(msg)
23
+ end
24
+
25
+ def log_info(msg)
26
+ puts green(msg)
27
+ end
28
+ end
data/lib/dijkstra.rb ADDED
@@ -0,0 +1,57 @@
1
+ require_relative 'pq'
2
+
3
+ class Dijkstra
4
+ def initialize(graph, source_vertex)
5
+ @graph = graph
6
+ @vertices = @graph.vertices
7
+ @source_vertex = source_vertex
8
+ @path_to = {}
9
+ @distance_to = {}
10
+ @pq = PriorityQueue.new
11
+ compute_shortest_path
12
+ end
13
+
14
+ def shortest_path_to(vertex)
15
+ return [] unless reachable?(vertex)
16
+ return [] if @distance_to[vertex] == 0
17
+ path = []
18
+ while vertex != @source_vertex
19
+ path.unshift(vertex)
20
+ vertex = @path_to[vertex]
21
+ end
22
+ path.unshift(@source_vertex)
23
+ end
24
+
25
+ private
26
+
27
+ def reachable?(vertex)
28
+ not @distance_to[vertex] == Float::INFINITY
29
+ end
30
+
31
+ def compute_shortest_path
32
+ update_distance_of_all_edges_to(Float::INFINITY)
33
+ @distance_to[@source_vertex] = 0
34
+
35
+ @pq.insert(@source_vertex, 0)
36
+ while @pq.any?
37
+ vertex = @pq.remove_min
38
+ @vertices[vertex].edges.each do |edge|
39
+ update(edge)
40
+ end
41
+ end
42
+ end
43
+
44
+ def update_distance_of_all_edges_to(distance)
45
+ @vertices.each do |key, value|
46
+ @distance_to[key] = distance
47
+ end
48
+ end
49
+
50
+ def update(edge)
51
+ raise "#{edge.to} doesn't exist, check edge[:to] in #{edge.inspect}" if @distance_to[edge.to].nil?
52
+ return if @distance_to[edge.to] <= @distance_to[edge.from] + edge.weight
53
+ @distance_to[edge.to] = @distance_to[edge.from] + edge.weight
54
+ @path_to[edge.to] = edge.from
55
+ @pq.insert(edge.to, @distance_to[edge.to])
56
+ end
57
+ end
data/lib/graph.rb ADDED
@@ -0,0 +1,153 @@
1
+ require 'dijkstra'
2
+ require 'colorize'
3
+
4
+ module RubyGraphWalker
5
+ class Vertex
6
+ attr_accessor :name, :weight, :edges, :trait, :visited, :zindex
7
+ def initialize(args = {})
8
+ [:name, :edges, :trait].each { |key| raise "#{key} is not defined for Vertex #{args}" unless args[key] }
9
+ @name = args[:name]
10
+ @weight = args[:weight] || 1
11
+ @trait = args[:trait]
12
+ @visited = args[:visited] || false
13
+ @weight = args[:zindex] || 0
14
+ @edges = []
15
+
16
+ args[:edges].each do |edge|
17
+ e = Edge.new(edge)
18
+ e.from = @name
19
+ e.to = edge[:to]
20
+ add_edge(e)
21
+ end
22
+ end
23
+
24
+ private
25
+ def add_edge(e)
26
+ @edges << e
27
+ end
28
+ end
29
+
30
+ class Edge
31
+ attr_accessor :name, :from, :to, :weight, :visited, :proc, :error_count
32
+ def initialize(args = {})
33
+ [:name, :to, :proc].each { |key| raise "#{key} is not defined for Edge #{args}" unless args[key] }
34
+ @name = args[:name]
35
+ @from = args[:from]
36
+ @to = args[:to]
37
+ @weight = args[:weight] || 1
38
+ @visited = args[:visited] || false
39
+ @proc = args[:proc]
40
+ @error_count = 0
41
+ end
42
+
43
+ def run
44
+ @proc.call
45
+ @visited = true
46
+ end
47
+ end
48
+
49
+ class Graph
50
+ attr_accessor :vertices, :edges, :edges_by_name
51
+ def initialize(vertices)
52
+ @vertices = {}
53
+ @edges_by_name = {}
54
+ @edges = []
55
+ vertices.each do |v_params|
56
+ v = Vertex.new(v_params)
57
+ @vertices[v_params[:name]] = v
58
+ v.edges.each do |edge|
59
+ puts "Warning: multiple edges named '#{edge.name}'" if @edges_by_name[edge.name]
60
+ @edges_by_name[edge.name] = edge
61
+ @edges << edge
62
+ end
63
+ end
64
+ end
65
+
66
+ def find_path_via_edge(from, to, edge_name)
67
+ log_info "#{from} -> #{to} via: (#{edge_name}): "
68
+ matched_edges = @edges.select { |edge| edge.name == edge_name }
69
+ log_error "no edge found for '#{edge_name}'" if matched_edges.size == 0
70
+ log_error "multiple edges matched for '#{edge_name}'" if matched_edges.size > 1
71
+ via_edge = matched_edges.first
72
+ plan = []
73
+
74
+ first_path = Dijkstra.new(self, from).shortest_path_to(via_edge.from)
75
+
76
+ first_path.each_cons(2) do |path|
77
+ start, dest = path
78
+ vertex = @vertices[start]
79
+ edge = vertex.edges.select { |e| e.to == dest}.first
80
+ plan << {v: vertex, e: edge}
81
+ end
82
+
83
+ via_v = @vertices[via_edge.from]
84
+ plan << {v: via_v, e: via_edge}
85
+
86
+ second_path = Dijkstra.new(self, via_edge.to).shortest_path_to(to)
87
+
88
+ second_path.each_cons(2) do |path|
89
+ start, dest = path
90
+ vertex = @vertices[start]
91
+ edge = vertex.edges.select { |e| e.to == dest }.first
92
+ plan << {v: vertex, e: edge}
93
+ end
94
+ plan
95
+ end
96
+
97
+ def find_path(from, to, args = {})
98
+ vertex_path = []
99
+ plan = []
100
+ via_type = args.keys.join
101
+
102
+ case via_type
103
+ when "via"
104
+ via = args[:via]
105
+ vertex_path = Dijkstra.new(self, from).shortest_path_to(via) + Dijkstra.new(self, via).shortest_path_to(to).drop(1)
106
+ vertex_path.each_cons(2) do |path|
107
+ f, d = path
108
+ vertex = @vertices[d]
109
+ edge = vertex.edges.select { |e| e.to == d}.first
110
+ plan << {v: vertex, e: edge}
111
+ end
112
+ when "via_edge"
113
+ via_edge = args[:via_edge]
114
+ plan = find_path_via_edge(from, to, via_edge)
115
+
116
+ # when "via_edges"
117
+ # edges = args[:via_edges]
118
+ # if edges.size < 2
119
+ # raise "please specify multiple edges"
120
+ # end
121
+ # v = nil
122
+ # edges[0..-2].each do |edge|
123
+ # start = (plan.last[:v].name if plan.last) || from
124
+ # v = @edges_by_name[edge].to
125
+ # plan += find_path_via_edge(start, v, edge)
126
+ # end
127
+ # plan += find_path_via_edge(v, to, edges.last)
128
+ when ""
129
+ vertex_path = Dijkstra.new(self, from).shortest_path_to(to)
130
+
131
+ vertex_path.each_cons(2) do |path|
132
+ from, d = path
133
+ vertex = @vertices[from]
134
+ edge = vertex.edges.select { |e| e.to == d}.first
135
+ plan << {v: vertex, e: edge}
136
+ end
137
+ if plan.empty? and from == to
138
+ return :on_the_spot
139
+ end
140
+ end
141
+
142
+ if plan.any?
143
+ puts plan.map {|p| "#{p[:v].name if p[:v]} (#{p[:e].name if p[:e]})" }.join(" -> ") + " >> #{@vertices[to].name}"
144
+ else
145
+ raise "No path from #{from} to #{to}"
146
+ end
147
+ plan
148
+ end
149
+ end
150
+
151
+ end
152
+
153
+
data/lib/pq.rb ADDED
@@ -0,0 +1,23 @@
1
+ class PriorityQueue
2
+ def initialize
3
+ @queue = {}
4
+ end
5
+
6
+ def any?
7
+ @queue.any?
8
+ end
9
+
10
+ def insert(key, value)
11
+ @queue[key] = value
12
+ order_queue
13
+ end
14
+
15
+ def remove_min
16
+ @queue.shift.first
17
+ end
18
+
19
+ private
20
+ def order_queue
21
+ @queue.sort_by {|_key, value| value }
22
+ end
23
+ end
@@ -0,0 +1,2 @@
1
+ require 'graph'
2
+ require 'test_planner'
@@ -0,0 +1,98 @@
1
+ require 'colorize'
2
+
3
+ module RubyGraphWalker
4
+ module TestPlanner
5
+ def search_vertex(query_method = 'query')
6
+ vertices = @graph.vertices.values
7
+ candidates = vertices.select do |v|
8
+ trait = v.trait
9
+ if trait.is_a?(Proc)
10
+ trait.call
11
+ elsif trait.is_a?(Array)
12
+ trait.any? { |t| (send query_method, t).any? }
13
+ else
14
+ send(query_method, trait).any?
15
+ end
16
+ end
17
+
18
+ if candidates.size > 1
19
+ log_warning "Warning: multiple vertices found: #{candidates.map {|c| c[:name]}.join(' ') }"
20
+ elsif candidates.size == 0
21
+ if send(query_method, "*").any?
22
+ raise "No vertex found"
23
+ else
24
+ raise "Connection refused"
25
+ end
26
+ end
27
+ candidates.max_by {|v| v.zindex}
28
+ end
29
+
30
+ def path_to(to, args = {})
31
+ v = search_vertex
32
+ @graph.find_path(v.name, to, args)
33
+ end
34
+
35
+ def run(plan, args = {})
36
+ raise "on the spot" if plan == :on_the_spot
37
+
38
+ plan.each do |p|
39
+ edge = p[:e]
40
+ raise "edge is nil" if edge.nil?
41
+ edge.run
42
+ @graph.vertices[edge.from].visited = true
43
+ end
44
+ end
45
+
46
+ def text_logger(args = {})
47
+ log_file_name = "#{Time.now.strftime('%Y_%m_%d_%H_%M_%S')}.txt"
48
+ @log_file ||= File.open(log_file_name, 'w')
49
+ end
50
+
51
+ def log(msg)
52
+ text_logger.puts(msg)
53
+ end
54
+
55
+ def start(graph, args = {})
56
+ @graph = graph
57
+ unvisited_edges = @graph.edges_by_name.select { |k, v| v.visited == false and v.weight > 1 }
58
+ starting_point = args[:start] || @graph.vertices.keys.first
59
+ while unvisited_edges.any?
60
+ edge_name = largest_weight(unvisited_edges).first
61
+ edge = @graph.edges_by_name[edge_name]
62
+ plan = path_to(starting_point, via_edge: edge_name)
63
+ begin
64
+ run(plan)
65
+ rescue => e
66
+ edge.error_count += 1
67
+ log edge.name + " failed! #{edge.error_count}"
68
+ e.backtrace.each do |text|
69
+ log text
70
+ end
71
+
72
+ retries = 3
73
+ begin
74
+ sleep 10
75
+ new_plan = path_to(edge.from)
76
+ run(new_plan, args)
77
+ rescue
78
+ edge.error_count += 1
79
+ retry unless (retries -= 1).zero?
80
+ end
81
+ end
82
+ unvisited_edges = @graph.edges_by_name.select { |k, v| v.visited == false and v.weight > 1 and v.error_count < 3}
83
+ end
84
+
85
+ @graph.edges_by_name.each do |k, v|
86
+ log "#{k} error count: #{v.error_count}"
87
+ end
88
+
89
+ text_logger.close
90
+ end
91
+
92
+ private
93
+ def largest_weight(hash)
94
+ hash.max_by{|k,v| v.weight}
95
+ end
96
+
97
+ end
98
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-graph-walker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - astro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-09 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: ''
14
+ email: astro2linus@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/colorize.rb
20
+ - lib/dijkstra.rb
21
+ - lib/graph.rb
22
+ - lib/pq.rb
23
+ - lib/ruby_graph_walker.rb
24
+ - lib/test_planner.rb
25
+ homepage: ''
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubyforge_project:
45
+ rubygems_version: 2.4.8
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Ruby Graph Walker
49
+ test_files: []