grandprix 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +14 -0
- data/Guardfile +24 -0
- data/LICENSE +22 -0
- data/README.md +118 -0
- data/Rakefile +2 -0
- data/bin/grandprix +31 -0
- data/doc/sample/alongside_elements/elements +4 -0
- data/doc/sample/alongside_elements/sample_output +6 -0
- data/doc/sample/alongside_elements/topology.yml +11 -0
- data/doc/sample/alongside_elements_2/elements +4 -0
- data/doc/sample/alongside_elements_2/sample_output +6 -0
- data/doc/sample/alongside_elements_2/topology.yml +11 -0
- data/doc/sample/annotated_topology/elements +4 -0
- data/doc/sample/annotated_topology/sample_output +3 -0
- data/doc/sample/annotated_topology/topology.yml +14 -0
- data/doc/sample/as_a_library/Gemfile +1 -0
- data/doc/sample/as_a_library/sample.rb +29 -0
- data/doc/sample/elements_with_extra_info/elements +4 -0
- data/doc/sample/elements_with_extra_info/sample_output +3 -0
- data/doc/sample/elements_with_extra_info/topology.yml +8 -0
- data/doc/sample/simple/elements +4 -0
- data/doc/sample/simple/sample_output +4 -0
- data/doc/sample/simple/topology.yml +9 -0
- data/grandprix.gemspec +17 -0
- data/lib/grandprix.rb +9 -0
- data/lib/grandprix/elements.rb +98 -0
- data/lib/grandprix/graph.rb +105 -0
- data/lib/grandprix/planner.rb +62 -0
- data/lib/grandprix/runner.rb +6 -0
- data/lib/grandprix/version.rb +3 -0
- data/spec/lib/grandprix/elements_spec.rb +71 -0
- data/spec/lib/grandprix/graph_spec.rb +93 -0
- data/spec/lib/grandprix/planner_spec.rb +158 -0
- data/spec/lib/grandprix/runner_spec.rb +75 -0
- data/spec/matchers_spec.rb +32 -0
- data/spec/spec_helper.rb +88 -0
- data/version +1 -0
- metadata +92 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
class Grandprix::Graph
|
2
|
+
def sort(graph)
|
3
|
+
Sort.new(graph).solve
|
4
|
+
end
|
5
|
+
|
6
|
+
class Sort
|
7
|
+
def initialize(graph)
|
8
|
+
@graph = graph
|
9
|
+
@preds_count = PredecessorCount.new graph
|
10
|
+
@successors = SuccessorTable.new graph
|
11
|
+
end
|
12
|
+
|
13
|
+
def solve
|
14
|
+
def visit(queue, ordered_vertices)
|
15
|
+
return ordered_vertices if queue.empty?
|
16
|
+
current, *rest = queue
|
17
|
+
|
18
|
+
successors = @successors.of(current)
|
19
|
+
@preds_count.decrement_all successors
|
20
|
+
|
21
|
+
new_queue = rest + @preds_count.zeroes_among(successors)
|
22
|
+
visit new_queue, ordered_vertices.push(current)
|
23
|
+
end
|
24
|
+
|
25
|
+
check_for_cycles visit(initial_queue, [])
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def initial_queue
|
30
|
+
@preds_count.zeroes
|
31
|
+
end
|
32
|
+
|
33
|
+
def num_vertices
|
34
|
+
@preds_count.size
|
35
|
+
end
|
36
|
+
|
37
|
+
def check_for_cycles(seq)
|
38
|
+
raise CycleDetected if seq.size < num_vertices
|
39
|
+
seq
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class PredecessorCount
|
44
|
+
def initialize(edges)
|
45
|
+
@counts = {}
|
46
|
+
edges.each do |j, k|
|
47
|
+
@counts[j] ||= 0
|
48
|
+
@counts[k] ||= 0
|
49
|
+
|
50
|
+
@counts[k] += 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def decrement_all(vertices)
|
55
|
+
vertices.each do |suc|
|
56
|
+
@counts[suc] -= 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def zeroes
|
61
|
+
@counts.select {|vertex,count| count == 0 }.keys
|
62
|
+
end
|
63
|
+
|
64
|
+
def zeroes_among(vertices)
|
65
|
+
vertices.select do |v|
|
66
|
+
@counts[v] == 0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def size
|
71
|
+
@counts.size
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
ks = @counts.keys.map{|k| k.to_s.rjust 2}.join(" ")
|
76
|
+
vs = @counts.values.map{|v| v.to_s.rjust 2}.join(" ")
|
77
|
+
"Predecessors:\n#{ks}\n#{vs}\n"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class SuccessorTable
|
82
|
+
def initialize(edges)
|
83
|
+
@successors = {}
|
84
|
+
edges.each do |j, k|
|
85
|
+
@successors[j] ||= []
|
86
|
+
@successors[k] ||= []
|
87
|
+
|
88
|
+
@successors[j].push k
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def of(origin)
|
93
|
+
@successors[origin]
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_s
|
97
|
+
ks = @successors.keys.map{|k| k.to_s.rjust 6}.join(" ")
|
98
|
+
vs = @successors.values.map{|v| v.inspect.rjust 6}.join(" ")
|
99
|
+
"Successors :\n#{ks}\n#{vs}\n"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class CycleDetected < Exception
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class Grandprix::Planner
|
2
|
+
def initialize(graph)
|
3
|
+
@graph = graph
|
4
|
+
end
|
5
|
+
|
6
|
+
def plan(topology, elements_array)
|
7
|
+
elements = Grandprix::Elements.build elements_array
|
8
|
+
|
9
|
+
nested_dependencies = project_array(topology, "after")
|
10
|
+
alongside = project_array(topology, "alongside")
|
11
|
+
|
12
|
+
dependencies = flatten_edges nested_dependencies
|
13
|
+
|
14
|
+
full_dependencies = dependencies.flat_map do |from, to|
|
15
|
+
extended_from = alongside[from] + [from]
|
16
|
+
extended_to = alongside[ to] + [to ]
|
17
|
+
new_deps = extended_from.product extended_to
|
18
|
+
|
19
|
+
eliminate_self_loops compact_pairs(new_deps)
|
20
|
+
end
|
21
|
+
|
22
|
+
before_relation = invert full_dependencies
|
23
|
+
|
24
|
+
in_order = @graph.sort before_relation
|
25
|
+
|
26
|
+
full_elements = elements.alongside alongside
|
27
|
+
independent_elements = elements.except in_order
|
28
|
+
elements_in_order = full_elements.reorder in_order
|
29
|
+
|
30
|
+
all = independent_elements + elements_in_order
|
31
|
+
all.annotate project(topology, "annotation")
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def project_array(hash, key)
|
36
|
+
projected = project(hash, key)
|
37
|
+
projected.tap { |h| h.default = [] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def project(hash, key)
|
41
|
+
Hash[ compact_pairs hash.map{|element, config| [element, config[key]]} ]
|
42
|
+
end
|
43
|
+
|
44
|
+
def invert(edges)
|
45
|
+
edges.map {|k, v| [v, k]}
|
46
|
+
end
|
47
|
+
|
48
|
+
def compact_pairs(pairs)
|
49
|
+
pairs.reject do |pair|
|
50
|
+
pair.nil? || pair[0].nil? || pair[1].nil?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def eliminate_self_loops(pairs)
|
55
|
+
pairs.reject {|x, y| x == y }
|
56
|
+
end
|
57
|
+
|
58
|
+
def flatten_edges(vertex_to_successors)
|
59
|
+
vertex_to_successors.flat_map {|vertex_j, successors| successors.map {|vertex_k| [vertex_j, vertex_k] } }
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grandprix::Elements do
|
4
|
+
def make(elements_array)
|
5
|
+
Grandprix::Elements.build elements_array
|
6
|
+
end
|
7
|
+
|
8
|
+
it { (make(["a","b"]) + make(["c","d"])).should == make(["a","b","c","d"]) }
|
9
|
+
|
10
|
+
it { (make(["a","b","c","d"]).except ["c","d"]).should == make(["a","b"]) }
|
11
|
+
|
12
|
+
|
13
|
+
it "should store extra information for each name" do
|
14
|
+
elements = make ["first=0.1", "second=0.2"]
|
15
|
+
elements.underlying.should == [["first", "0.1"], ["second", "0.2"]]
|
16
|
+
end
|
17
|
+
|
18
|
+
describe :reorder do
|
19
|
+
it "should reorder the elements according to the given names sequence" do
|
20
|
+
(make(["a","c"]).reorder ["d","c","b","a"]).should == make(["c","a"])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should remove elements whose names are absent from the ordering" do
|
24
|
+
elements = make ["prost"]
|
25
|
+
order = ["prost_externo", "prost", "assets", "schumi"]
|
26
|
+
|
27
|
+
result = elements.reorder order
|
28
|
+
result.should == elements
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe :alongside do
|
33
|
+
it "should store the extra names" do
|
34
|
+
(make(["a","c"]).alongside "a" => ["aa"], "c" => ["cc"]).should == make(["a", "c", "aa", "cc"])
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should copy extra data from the stored names to the names defined alongside" do
|
38
|
+
elements = make ["first=0.1", "second=0.2", "third"]
|
39
|
+
res = elements.alongside "first" => ["one"], "second" => ["two"], "third" => ["three"]
|
40
|
+
res.strings.should == ["first=0.1", "second=0.2", "third", "one=0.1", "two=0.2", "three"]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "REGRESSION: it should not add unrelated names" do
|
44
|
+
elements = make ["prost=1.2.3"]
|
45
|
+
alongside = {"schumi"=>["assets"], "prost"=>["prost_externo"]}
|
46
|
+
|
47
|
+
res = elements.alongside alongside
|
48
|
+
res.should == make(["prost=1.2.3","prost_externo=1.2.3"])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe :annotate do
|
53
|
+
it "should add extra string information to the appropriate names" do
|
54
|
+
elements = make ["first=1", "second=2", "third", "fourth=4", "fifth"]
|
55
|
+
res = elements.annotate "first" => "10", "second" => "20", "third" => "30"
|
56
|
+
res.strings.should == ["first=1=10", "second=2=20", "third==30", "fourth=4", "fifth"]
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should add extra object information to the appropriate names" do
|
60
|
+
elements = make ["first=1"]
|
61
|
+
res = elements.annotate "first" => {:a => "some string", "b" => ["other"]}
|
62
|
+
res.strings.should == [%|first=1={"a":"some string","b":["other"]}|]
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should format properly when the element has no prior extra information but is annotated" do
|
66
|
+
elements = make ["backend"]
|
67
|
+
res = elements.annotate "backend" => "this is the backend"
|
68
|
+
res.strings.should == ['backend==this is the backend']
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe Grandprix::Graph do
|
5
|
+
describe :topological_sort do
|
6
|
+
it "should sort a linked list" do
|
7
|
+
edges = [[:a, :b],[:b, :c]]
|
8
|
+
subject.sort(edges).should be_an_ordering_of(edges)
|
9
|
+
subject.sort(edges).should == [:a, :b, :c]
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should sort a small graph" do
|
13
|
+
# d
|
14
|
+
# ^^
|
15
|
+
# / \
|
16
|
+
# a -> b -> c <- e
|
17
|
+
edges = [
|
18
|
+
[:a, :b],
|
19
|
+
[:b, :c],
|
20
|
+
[:b, :d],
|
21
|
+
[:c, :d],
|
22
|
+
[:e, :c],
|
23
|
+
]
|
24
|
+
subject.sort(edges).should be_an_ordering_of(edges)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should sort a larger graph" do
|
28
|
+
#
|
29
|
+
# 4 -> 6 <- 8 <- 2
|
30
|
+
# ^ ^ ^
|
31
|
+
# | | |
|
32
|
+
# 1 -> 3 -> 7 ---> 5 <- 9
|
33
|
+
edges = [
|
34
|
+
[9, 2], [4, 6],
|
35
|
+
[3, 7], [1, 3],
|
36
|
+
[7, 5], [7, 4],
|
37
|
+
[5, 8], [9, 5],
|
38
|
+
[8, 6], [2, 8],
|
39
|
+
]
|
40
|
+
subject.sort(edges).should be_an_ordering_of(edges)
|
41
|
+
end
|
42
|
+
|
43
|
+
context "invalid input" do
|
44
|
+
it "should do nothing on empty imput" do
|
45
|
+
subject.sort([]).should == []
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should raise an exception when a cycle is found" do
|
49
|
+
#
|
50
|
+
# A -> B
|
51
|
+
# ^ |
|
52
|
+
# | v
|
53
|
+
# D <- C
|
54
|
+
#
|
55
|
+
expect { subject.sort([[:a, :b], [:b, :c], [:c, :d], [:d, :a]]) }.to raise_error
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should raise an exception when a cycle is found in a small graph" do
|
59
|
+
#
|
60
|
+
# Z -> A -> B
|
61
|
+
# ^ |
|
62
|
+
# | v
|
63
|
+
# D <- C
|
64
|
+
#
|
65
|
+
expect { subject.sort([[:z, :a], [:a, :b], [:b, :c], [:c, :d], [:d, :a]]) }.to raise_error
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should raise an exception when a cycle is found inside a larger graph" do
|
69
|
+
#
|
70
|
+
# D <- E <- F -> H <-
|
71
|
+
# | ^ \
|
72
|
+
# | | J
|
73
|
+
# v | /
|
74
|
+
# A -> B -> C -> G -> I <-
|
75
|
+
#
|
76
|
+
expect { subject.sort([
|
77
|
+
[:a, :b],
|
78
|
+
[:b, :c],
|
79
|
+
[:c, :g],
|
80
|
+
[:g, :i],
|
81
|
+
[:e, :d],
|
82
|
+
[:f, :e],
|
83
|
+
[:f, :h],
|
84
|
+
[:d, :b],
|
85
|
+
[:g, :f],
|
86
|
+
[:j, :h],
|
87
|
+
[:j, :i],
|
88
|
+
]) }.to raise_error
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grandprix::Planner do
|
4
|
+
let (:graph) { mock("graph") }
|
5
|
+
subject { described_class.new graph }
|
6
|
+
|
7
|
+
it "should understand dependencies" do
|
8
|
+
topology = {
|
9
|
+
"frontend" => {
|
10
|
+
"after" => ["backend"]
|
11
|
+
},
|
12
|
+
"backend" => {
|
13
|
+
"after" => ["db", "mq"]
|
14
|
+
},
|
15
|
+
"client" => {
|
16
|
+
"after" => ["frontend"]
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
elements = ["frontend", "db", "client"]
|
21
|
+
|
22
|
+
graph.should_receive(:sort).with([
|
23
|
+
["backend", "frontend"],
|
24
|
+
["db", "backend"],
|
25
|
+
["mq", "backend"],
|
26
|
+
["frontend", "client"],
|
27
|
+
]).and_return(["mq", "db", "backend", "frontend", "client"])
|
28
|
+
|
29
|
+
subject.plan(topology, elements).names.should == ["db", "frontend", "client"]
|
30
|
+
end
|
31
|
+
|
32
|
+
context " - alongside - " do
|
33
|
+
it "should include elements that must always be deployed together" do
|
34
|
+
topology = {
|
35
|
+
"frontend" => {
|
36
|
+
"after" => ["backend"],
|
37
|
+
"alongside" => ["assets", "images"]
|
38
|
+
},
|
39
|
+
"backend" => {
|
40
|
+
"alongside" => ["external_backend"]
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
elements = ["frontend", "backend"]
|
45
|
+
|
46
|
+
expected_deps = [
|
47
|
+
["backend", "frontend"],
|
48
|
+
["backend", "assets"],
|
49
|
+
["backend", "images"],
|
50
|
+
["external_backend", "frontend"],
|
51
|
+
["external_backend", "assets"],
|
52
|
+
["external_backend", "images"],
|
53
|
+
]
|
54
|
+
|
55
|
+
graph.should_receive(:sort) do |arg|
|
56
|
+
arg.should =~ expected_deps
|
57
|
+
["external_backend", "backend", "frontend", "assets", "images"]
|
58
|
+
end
|
59
|
+
|
60
|
+
result = subject.plan(topology, elements)
|
61
|
+
|
62
|
+
result.names.should =~ ["assets", "images", "frontend", "backend", "external_backend"]
|
63
|
+
result.should beOrderedHaving("backend", "external_backend").before("frontend","assets", "images")
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should allow for alongside dependencies to declare their own dependencies" do
|
67
|
+
topology = {
|
68
|
+
"frontend" => {
|
69
|
+
"after" => ["backend", "assets"], # frontend depends on assets in addxition to
|
70
|
+
"alongside" => ["assets", "images"] # having it declared alongside
|
71
|
+
},
|
72
|
+
"backend" => {
|
73
|
+
"alongside" => ["external_backend"]
|
74
|
+
},
|
75
|
+
"images" => {
|
76
|
+
"after" => ["client"] #images that is an alongside dep of frontend depends on client
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
elements = ["frontend", "client", "backend"]
|
81
|
+
expected_deps = [
|
82
|
+
["assets", "frontend"],
|
83
|
+
["assets", "images"],
|
84
|
+
["client", "images"],
|
85
|
+
["backend", "frontend"],
|
86
|
+
["backend", "assets"],
|
87
|
+
["backend", "images"],
|
88
|
+
["external_backend", "frontend"],
|
89
|
+
["external_backend", "assets"],
|
90
|
+
["external_backend", "images"],
|
91
|
+
]
|
92
|
+
|
93
|
+
graph.should_receive(:sort) do |arg|
|
94
|
+
arg.should =~ expected_deps
|
95
|
+
["client", "backend", "external_backend", "assets", "images", "frontend"]
|
96
|
+
end
|
97
|
+
|
98
|
+
result = subject.plan(topology, elements)
|
99
|
+
|
100
|
+
result.names.should =~ ["client", "backend", "external_backend", "frontend", "assets", "images"]
|
101
|
+
result.should beOrderedHaving("backend", "assets").before("frontend")
|
102
|
+
result.should beOrderedHaving("backend", "assets").before("frontend", "assets", "images")
|
103
|
+
result.should beOrderedHaving("client").before("images")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context " - input handling - " do
|
108
|
+
it "should consider elements that are mentioned but not fully specified" do
|
109
|
+
topology = {
|
110
|
+
"frontend" => {
|
111
|
+
"after" => ["backend"]
|
112
|
+
},
|
113
|
+
"backend" => {
|
114
|
+
"after" => ["db", "mq"]
|
115
|
+
},
|
116
|
+
}
|
117
|
+
|
118
|
+
elements = ["frontend", "db", "client"]
|
119
|
+
|
120
|
+
graph.should_receive(:sort).with([
|
121
|
+
["backend", "frontend"],
|
122
|
+
["db", "backend"],
|
123
|
+
["mq", "backend"],
|
124
|
+
]).and_return(["mq", "db", "backend", "frontend"])
|
125
|
+
|
126
|
+
subject.plan(topology, elements).names.should == ["client","db", "frontend"]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context " - output - " do
|
131
|
+
it "should annotate the elements with provided metadata, if given" do
|
132
|
+
topology = {
|
133
|
+
"frontend" => {
|
134
|
+
"after" => ["backend"],
|
135
|
+
"annotation" => "this is the frontend"
|
136
|
+
},
|
137
|
+
"backend" => {
|
138
|
+
"after" => ["db"]
|
139
|
+
},
|
140
|
+
"db" => {
|
141
|
+
"annotation" => {"type" => "document-oriented"}
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
elements = ["frontend=1.0.0", "backend=2.0.0", "db"]
|
146
|
+
|
147
|
+
graph.should_receive(:sort).with([
|
148
|
+
["backend", "frontend"],
|
149
|
+
["db", "backend"],
|
150
|
+
]).and_return(["db", "backend", "frontend"])
|
151
|
+
|
152
|
+
subject.plan(topology, elements).strings.should == ['db=={"type":"document-oriented"}', 'backend=2.0.0', 'frontend=1.0.0=this is the frontend']
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
end
|