ruby-graph-walker 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|