grandprix 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +14 -0
  5. data/Guardfile +24 -0
  6. data/LICENSE +22 -0
  7. data/README.md +118 -0
  8. data/Rakefile +2 -0
  9. data/bin/grandprix +31 -0
  10. data/doc/sample/alongside_elements/elements +4 -0
  11. data/doc/sample/alongside_elements/sample_output +6 -0
  12. data/doc/sample/alongside_elements/topology.yml +11 -0
  13. data/doc/sample/alongside_elements_2/elements +4 -0
  14. data/doc/sample/alongside_elements_2/sample_output +6 -0
  15. data/doc/sample/alongside_elements_2/topology.yml +11 -0
  16. data/doc/sample/annotated_topology/elements +4 -0
  17. data/doc/sample/annotated_topology/sample_output +3 -0
  18. data/doc/sample/annotated_topology/topology.yml +14 -0
  19. data/doc/sample/as_a_library/Gemfile +1 -0
  20. data/doc/sample/as_a_library/sample.rb +29 -0
  21. data/doc/sample/elements_with_extra_info/elements +4 -0
  22. data/doc/sample/elements_with_extra_info/sample_output +3 -0
  23. data/doc/sample/elements_with_extra_info/topology.yml +8 -0
  24. data/doc/sample/simple/elements +4 -0
  25. data/doc/sample/simple/sample_output +4 -0
  26. data/doc/sample/simple/topology.yml +9 -0
  27. data/grandprix.gemspec +17 -0
  28. data/lib/grandprix.rb +9 -0
  29. data/lib/grandprix/elements.rb +98 -0
  30. data/lib/grandprix/graph.rb +105 -0
  31. data/lib/grandprix/planner.rb +62 -0
  32. data/lib/grandprix/runner.rb +6 -0
  33. data/lib/grandprix/version.rb +3 -0
  34. data/spec/lib/grandprix/elements_spec.rb +71 -0
  35. data/spec/lib/grandprix/graph_spec.rb +93 -0
  36. data/spec/lib/grandprix/planner_spec.rb +158 -0
  37. data/spec/lib/grandprix/runner_spec.rb +75 -0
  38. data/spec/matchers_spec.rb +32 -0
  39. data/spec/spec_helper.rb +88 -0
  40. data/version +1 -0
  41. 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,6 @@
1
+ class Grandprix::Runner
2
+ def run!(topology, elements)
3
+ graph = Grandprix::Graph.new
4
+ Grandprix::Planner.new(graph).plan(topology, elements)
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Grandprix
2
+ VERSION = "0.0.1"
3
+ 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