grandprix 0.0.4
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.
- 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
|