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