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 +7 -0
- data/lib/colorize.rb +28 -0
- data/lib/dijkstra.rb +57 -0
- data/lib/graph.rb +153 -0
- data/lib/pq.rb +23 -0
- data/lib/ruby_graph_walker.rb +2 -0
- data/lib/test_planner.rb +98 -0
- metadata +49 -0
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
|
data/lib/test_planner.rb
ADDED
@@ -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: []
|