etsy-deployinator 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.gitignore +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +0 -1
- data/README.md +47 -10
- data/Rakefile +0 -1
- data/bin/deployinator-tailer.rb +2 -2
- data/deployinator.gemspec +5 -1
- data/lib/deployinator.rb +20 -1
- data/lib/deployinator/app.rb +53 -12
- data/lib/deployinator/controller.rb +8 -3
- data/lib/deployinator/helpers.rb +109 -13
- data/lib/deployinator/helpers/concurrency.rb +70 -0
- data/lib/deployinator/helpers/dsh.rb +15 -5
- data/lib/deployinator/helpers/git.rb +16 -9
- data/lib/deployinator/helpers/version.rb +52 -52
- data/lib/deployinator/static/css/maintenance.css +9 -0
- data/lib/deployinator/static/css/style.css +25 -5
- data/lib/deployinator/static/images/maintenance.gif +0 -0
- data/lib/deployinator/static/js/stats_load.js +10 -0
- data/lib/deployinator/templates/exception.mustache +11 -0
- data/lib/deployinator/templates/generic_single_push.mustache +2 -1
- data/lib/deployinator/templates/layout.mustache +5 -4
- data/lib/deployinator/templates/log_table.mustache +18 -5
- data/lib/deployinator/templates/maintenance.mustache +5 -0
- data/lib/deployinator/templates/stats.mustache +180 -0
- data/lib/deployinator/version.rb +1 -1
- data/lib/deployinator/views/log_table.rb +36 -0
- data/lib/deployinator/views/maintenance.rb +15 -0
- data/lib/deployinator/views/stats.rb +96 -0
- data/test/unit/concurrency_test.rb +72 -0
- data/test/unit/git_test.rb +70 -0
- data/test/unit/helpers_test.rb +61 -2
- data/test/unit/version_test.rb +12 -12
- metadata +74 -17
@@ -0,0 +1,180 @@
|
|
1
|
+
<!-- content -->
|
2
|
+
<section id="main" class="info stats">
|
3
|
+
<!-- heading -->
|
4
|
+
<div class="heading clearfix">
|
5
|
+
<h2>Deployments Per Day (US/Eastern)</h2>
|
6
|
+
</div>
|
7
|
+
<div class="stats-main">
|
8
|
+
<button id="scatterOrLine">Line</button>
|
9
|
+
<label><input type="checkbox" id="combined"> Combined</label>
|
10
|
+
<div id="placeholder" style="width:940px;height:300px;"></div>
|
11
|
+
<div id="overview" style="width:940px;height:120px;"></div>
|
12
|
+
<script>var data = {}</script>
|
13
|
+
|
14
|
+
<p id="choices"></p>
|
15
|
+
|
16
|
+
<div class="holder">
|
17
|
+
|
18
|
+
{{# per_day}}
|
19
|
+
<div class="stats">
|
20
|
+
<table class="stats">
|
21
|
+
<tr>
|
22
|
+
<th>Day</th>
|
23
|
+
<th>Count</th>
|
24
|
+
</tr>
|
25
|
+
{{# data}}
|
26
|
+
<tr>
|
27
|
+
<td>{{date}}</td>
|
28
|
+
<td class="count">{{count}}</td>
|
29
|
+
</tr>
|
30
|
+
{{/ data}}
|
31
|
+
</table>
|
32
|
+
<h3>{{stack}}</h3>
|
33
|
+
<script type="text/javascript" charset="utf-8">
|
34
|
+
data["{{stack}}"] = {data: {{json}}, label: "{{stack}}"}
|
35
|
+
</script>
|
36
|
+
</div>
|
37
|
+
{{/ per_day}}
|
38
|
+
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
</section>
|
43
|
+
|
44
|
+
<script type="text/javascript" charset="utf-8">
|
45
|
+
var x1, x2;
|
46
|
+
|
47
|
+
function getData(ignoreX) {
|
48
|
+
var d = [];
|
49
|
+
var c = { data: [], label: "combined" };
|
50
|
+
|
51
|
+
$("#choices").find("input:checked").each(function() {
|
52
|
+
if ($(this).attr("id") == "combined") { return; }
|
53
|
+
|
54
|
+
var key = $(this).attr("name");
|
55
|
+
|
56
|
+
if (key && data[key]) {
|
57
|
+
// combine all the data points if requested
|
58
|
+
if ($("#combined")[0].checked) {
|
59
|
+
for (var i=0; i<data[key].data.length; i++) {
|
60
|
+
var found = false;
|
61
|
+
var m = data[key].data[i];
|
62
|
+
for (var j=0; j<c.data.length; j++) {
|
63
|
+
var n = c.data[j];
|
64
|
+
if (n[0] == m[0]) {
|
65
|
+
n[1] += m[1];
|
66
|
+
found = true;
|
67
|
+
break;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
if (! found) { c.data[i] = [m[0], m[1]]; }
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
d.push(data[key]);
|
76
|
+
}
|
77
|
+
});
|
78
|
+
|
79
|
+
if ($("#combined")[0].checked) { d = [c]; }
|
80
|
+
|
81
|
+
var newD = [];
|
82
|
+
for (var i=0; i<d.length; i++) {
|
83
|
+
var dAr = [];
|
84
|
+
for (var j=0; j<d[i].data.length; j++) {
|
85
|
+
n = d[i].data[j];
|
86
|
+
dAr.push(n);
|
87
|
+
}
|
88
|
+
newD[i] = { label: d[i].label, data: dAr };
|
89
|
+
}
|
90
|
+
|
91
|
+
if ($("#combined")[0].checked) { outputTable(c); }
|
92
|
+
return newD;
|
93
|
+
}
|
94
|
+
|
95
|
+
function outputTable(c) {
|
96
|
+
$("#holderCombined").remove();
|
97
|
+
|
98
|
+
var table = $("<table id='holderCombined'>")
|
99
|
+
.html("<tr><th>Date</th><th>Combined</th></tr>")
|
100
|
+
.appendTo(".stats-main");
|
101
|
+
|
102
|
+
for (var i=0; i<c.data.length; i++) {
|
103
|
+
var m = c.data[i];
|
104
|
+
var dO = new Date(m[0]);
|
105
|
+
var dateS = (dO.getYear() + 1900) + "-" + dO.getMonth() + "-" + dO.getDate();
|
106
|
+
$("<tr><td>" + dateS + "</td><td>" + m[1] + "</td></tr>")
|
107
|
+
.appendTo(table);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
function options() {
|
112
|
+
return {
|
113
|
+
xaxis: { mode: "time", timeformat: "%y/%m/%d", minTickSize: [1, "day"] },
|
114
|
+
legend: { position: "nw" },
|
115
|
+
points: { show: ($("#scatterOrLine").html() == "Scatter") },
|
116
|
+
lines: { show: ($("#scatterOrLine").html() == "Line") },
|
117
|
+
selection: { mode: "x" }
|
118
|
+
}
|
119
|
+
};
|
120
|
+
|
121
|
+
function sliceD(d, msg) {
|
122
|
+
return d.slice(d.slice(d.indexOf(x1), d.indexOf(x2)));
|
123
|
+
}
|
124
|
+
|
125
|
+
function drawGraphs() {
|
126
|
+
var d = getData();
|
127
|
+
|
128
|
+
var plot = $.plot($("#placeholder"), sliceD(d, "main"), options());
|
129
|
+
|
130
|
+
var overview = $.plot($("#overview"), d, {
|
131
|
+
legend: { show: false },
|
132
|
+
selection: { mode: "x" },
|
133
|
+
lines: { show: true },
|
134
|
+
xaxis: { mode: "time", timeformat: "%y/%m/%d" }
|
135
|
+
});
|
136
|
+
|
137
|
+
$("#placeholder").unbind("plotselected");
|
138
|
+
$("#placeholder").bind("plotselected", function (event, cRanges) {
|
139
|
+
ranges = cRanges;
|
140
|
+
x1 = Math.floor(ranges.xaxis.from);
|
141
|
+
x2 = Math.floor(ranges.xaxis.to);
|
142
|
+
|
143
|
+
// do the zooming
|
144
|
+
plot = $.plot($("#placeholder"), sliceD(d, "zoom"),
|
145
|
+
$.extend(true, {}, options(), {
|
146
|
+
xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to },
|
147
|
+
yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to }
|
148
|
+
}));
|
149
|
+
|
150
|
+
// don't fire event on the overview to prevent eternal loop
|
151
|
+
overview.setSelection(ranges, true);
|
152
|
+
});
|
153
|
+
|
154
|
+
$("#overview").unbind("plotselected");
|
155
|
+
$("#overview").bind("plotselected", function (event, ranges) {
|
156
|
+
plot.setSelection(ranges);
|
157
|
+
|
158
|
+
x1 = Math.floor(ranges.xaxis.from);
|
159
|
+
x2 = Math.floor(ranges.xaxis.to);
|
160
|
+
});
|
161
|
+
|
162
|
+
if (x1 && x2) {
|
163
|
+
ranges = {xaxis: {from: x1, to: x2}};
|
164
|
+
|
165
|
+
overview.setSelection(ranges);
|
166
|
+
plot.setSelection(ranges);
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
$("#scatterOrLine").live("click", function () {
|
171
|
+
$(this).html(($(this).html() == "Line") ? "Scatter" : "Line");
|
172
|
+
|
173
|
+
drawGraphs();
|
174
|
+
});
|
175
|
+
|
176
|
+
$("#combined").live("click", function () {
|
177
|
+
drawGraphs();
|
178
|
+
});
|
179
|
+
</script>
|
180
|
+
<script src="/js/stats_load.js"></script>
|
data/lib/deployinator/version.rb
CHANGED
@@ -17,6 +17,42 @@ module Deployinator::Views
|
|
17
17
|
@params[:show_counts] == "true"
|
18
18
|
end
|
19
19
|
|
20
|
+
def next_page_params
|
21
|
+
params = [
|
22
|
+
{
|
23
|
+
:name => "page",
|
24
|
+
:value => next_page
|
25
|
+
}
|
26
|
+
]
|
27
|
+
|
28
|
+
unless @params[:stack].nil?
|
29
|
+
params << {
|
30
|
+
:name => "stack",
|
31
|
+
:value => @params[:stack]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
return params
|
36
|
+
end
|
37
|
+
|
38
|
+
def prev_page_params
|
39
|
+
params = [
|
40
|
+
{
|
41
|
+
:name => "page",
|
42
|
+
:value => prev_page
|
43
|
+
}
|
44
|
+
]
|
45
|
+
|
46
|
+
unless @params[:stack].nil?
|
47
|
+
params << {
|
48
|
+
:name => "stack",
|
49
|
+
:value => @params[:stack]
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
return params
|
54
|
+
end
|
55
|
+
|
20
56
|
def prev_page
|
21
57
|
return unless @params && @params[:page]
|
22
58
|
page = @params[:page].to_i
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Deployinator::Views
|
2
|
+
class Maintenance < Layout
|
3
|
+
self.template_file = "#{File.dirname(__FILE__)}/../templates/maintenance.mustache"
|
4
|
+
def additional_header_html
|
5
|
+
<<-EOS
|
6
|
+
#{super}
|
7
|
+
<link rel="stylesheet" href="/css/maintenance.css" type="text/css" media="screen">
|
8
|
+
EOS
|
9
|
+
end
|
10
|
+
|
11
|
+
def maintenance_contact
|
12
|
+
Deployinator.maintenance_contact
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Deployinator::Views
|
5
|
+
class Stats < Layout
|
6
|
+
|
7
|
+
self.template_file = "#{File.dirname(__FILE__)}/../templates/stats.mustache"
|
8
|
+
|
9
|
+
@@ignored_stacks = Deployinator.stats_ignored_stacks || []
|
10
|
+
|
11
|
+
def deploys
|
12
|
+
@deploys ||= begin
|
13
|
+
log_to_hash({
|
14
|
+
:no_global => false,
|
15
|
+
:stack => Deployinator.stats_included_stacks,
|
16
|
+
:env => "production|search|prod",
|
17
|
+
:extragrep => Deployinator.stats_extra_grep,
|
18
|
+
:no_limit => true,
|
19
|
+
:limit => 10000
|
20
|
+
})
|
21
|
+
end
|
22
|
+
# note that the stack param will help but will send bring back extra lines that matched
|
23
|
+
end
|
24
|
+
|
25
|
+
def timings
|
26
|
+
deploys
|
27
|
+
end
|
28
|
+
|
29
|
+
def inject_renamed_stacks(renamed_stacks)
|
30
|
+
return if nil == renamed_stacks
|
31
|
+
|
32
|
+
renamed_stacks.each do |ops|
|
33
|
+
previous_stack = ops[:previous_stack]
|
34
|
+
new_stack_name = ops[:new_name]
|
35
|
+
|
36
|
+
renamed_stack_data = log_to_hash(previous_stack)
|
37
|
+
|
38
|
+
renamed_stack_data.each do |data|
|
39
|
+
data[:stack] = new_stack_name
|
40
|
+
deploys.push(data)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def per_day
|
46
|
+
inject_renamed_stacks(Deployinator.stats_renamed_stacks)
|
47
|
+
|
48
|
+
original_zone = ENV["TZ"]
|
49
|
+
ENV["TZ"] = "US/Eastern"
|
50
|
+
|
51
|
+
early_day = Time.now.strftime("%Y-%m-%d")
|
52
|
+
stack_days = deploys.inject({}) do |h, deploy|
|
53
|
+
if deploy[:time] && deploy[:stack]
|
54
|
+
if @@ignored_stacks.include?(deploy[:stack])
|
55
|
+
# puts "SKIPPING " + deploy[:stack]
|
56
|
+
# something breaks if you just next here so don't
|
57
|
+
else
|
58
|
+
day = Date.parse(deploy[:time].localtime.strftime("%Y-%m-%d"))
|
59
|
+
early_day = day if day.to_s < early_day.to_s
|
60
|
+
h[deploy[:stack]] ||= {}
|
61
|
+
h[deploy[:stack]][day] ||= 0
|
62
|
+
h[deploy[:stack]][day] += 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
h
|
66
|
+
end
|
67
|
+
|
68
|
+
# fill in zero days
|
69
|
+
day_seconds = 24 * 60 * 60
|
70
|
+
(0..((Time.now - Time.parse(early_day.to_s)) / day_seconds).to_i).each do |days_ago|
|
71
|
+
stack_days.keys.each do |stack|
|
72
|
+
|
73
|
+
next if @@ignored_stacks.include?(stack)
|
74
|
+
day = Date.parse((Time.now - (day_seconds * days_ago)).strftime("%Y-%m-%d"))
|
75
|
+
stack_days[stack][day] ||= 0
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
n = stack_days.keys.map do |stack|
|
80
|
+
next if @@ignored_stacks.include?(stack)
|
81
|
+
|
82
|
+
data = []
|
83
|
+
json = []
|
84
|
+
stack_days[stack].sort.reverse.each do |d,c|
|
85
|
+
data << {:date => d, :count => c}
|
86
|
+
json << [d.strftime("%s").to_i * 1000, c]
|
87
|
+
end
|
88
|
+
{:stack => stack, :data => data, :json => json.to_json}
|
89
|
+
end
|
90
|
+
|
91
|
+
ENV["TZ"] = original_zone
|
92
|
+
|
93
|
+
n
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'deployinator'
|
2
|
+
require 'deployinator/helpers'
|
3
|
+
require 'deployinator/helpers/concurrency'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'mocha/setup'
|
6
|
+
|
7
|
+
Celluloid.logger = nil
|
8
|
+
class ConcurrencyTest < Test::Unit::TestCase
|
9
|
+
# Celluloid recommends doing this before each test
|
10
|
+
# https://github.com/celluloid/celluloid/wiki/Gotchas#testing
|
11
|
+
include Deployinator::Helpers::ConcurrencyHelpers
|
12
|
+
def setup
|
13
|
+
Celluloid.boot
|
14
|
+
@@futures = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
Celluloid.shutdown
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_spawn_off_concurrent_thread
|
22
|
+
run_parallel(:test) do
|
23
|
+
"Running inside thread"
|
24
|
+
end
|
25
|
+
assert_equal(get_value(:test), "Running inside thread")
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_future_name_type
|
29
|
+
assert_raise NoMethodError do
|
30
|
+
run_parallel({:test => 'going crazy'}) do
|
31
|
+
"Running inside thread"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_fibers_with_same_reference
|
37
|
+
run_parallel(:test) do
|
38
|
+
"Running inside thread"
|
39
|
+
end
|
40
|
+
assert_raise Deployinator::Helpers::ConcurrencyHelpers::DuplicateReferenceError do
|
41
|
+
run_parallel(:test) do
|
42
|
+
"Running inside thread"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_get_non_symbol_value
|
48
|
+
run_parallel(:test) do
|
49
|
+
"Running inside thread"
|
50
|
+
end
|
51
|
+
assert_raise NoMethodError do
|
52
|
+
get_value({:test => 'going crazy - jpaul'})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_reference_taken
|
57
|
+
assert_equal(reference_taken?(:test), false)
|
58
|
+
@@futures[:test] = 'a value'
|
59
|
+
assert_equal(reference_taken?(:test), true)
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_multiple_futures_return
|
63
|
+
run_parallel(:test1) do
|
64
|
+
"future1"
|
65
|
+
end
|
66
|
+
run_parallel(:test2) do
|
67
|
+
"future2"
|
68
|
+
end
|
69
|
+
assert_equal(get_values(:test1, :test2), {:test1 => "future1", :test2 => "future2"})
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'deployinator'
|
2
|
+
require 'deployinator/helpers'
|
3
|
+
require 'deployinator/helpers/git'
|
4
|
+
|
5
|
+
include Deployinator::Helpers::GitHelpers
|
6
|
+
|
7
|
+
class HelpersTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_git_url_https
|
10
|
+
GitHelpers.expects(:which_github_host).returns("www.testmagic.com")
|
11
|
+
GitHelpers.expects(:git_info_for_stack).returns({:stack => {:repository => 'drills', :user => 'construction'}})
|
12
|
+
GitHelpers.expects(:git_info_for_stack).returns({:stack => {:repository => 'drills', :user => 'construction'}})
|
13
|
+
assert_equal('https://www.testmagic.com/construction/drills.git', GitHelpers.git_url(:stack, 'https', false))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_git_url_default
|
17
|
+
GitHelpers.expects(:which_github_host).returns("www.testmagic.com")
|
18
|
+
GitHelpers.expects(:git_info_for_stack).returns({:stack => {:repository => 'drills', :user => 'construction'}})
|
19
|
+
GitHelpers.expects(:git_info_for_stack).returns({:stack => {:repository => 'drills', :user => 'construction'}})
|
20
|
+
assert_equal('git://www.testmagic.com/construction/drills', GitHelpers.git_url(:stack))
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_git_url_read_write
|
24
|
+
GitHelpers.expects(:which_github_host).returns("www.testmagic.com")
|
25
|
+
GitHelpers.expects(:git_info_for_stack).returns({:stack => {:repository => 'drills', :user => 'construction'}})
|
26
|
+
GitHelpers.expects(:git_info_for_stack).returns({:stack => {:repository => 'drills', :user => 'construction'}})
|
27
|
+
assert_equal('git@www.testmagic.com:construction/drills', GitHelpers.git_url(:stack, "git", true))
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_git_freshen_or_clone_https_passthrough
|
31
|
+
GitHelpers.expects(:git_checkout_path).returns("/dev/null")
|
32
|
+
GitHelpers.expects(:is_git_repo).with("/dev/null", "extra-ssh").returns(:missing)
|
33
|
+
GitHelpers.expects(:log_and_stream).returns(nil)
|
34
|
+
GitHelpers.expects(:git_url).with(:stack, "https", false).returns("https://www.testmagic.com/construction/drills.git")
|
35
|
+
GitHelpers.expects(:git_clone).with(:stack, "https://www.testmagic.com/construction/drills.git", "extra-ssh", "/dev/null", "merge99")
|
36
|
+
GitHelpers.git_freshen_or_clone(:stack, "extra-ssh", "/dev/null", "merge99", false, "https")
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_git_freshen_or_clone_git_update
|
40
|
+
GitHelpers.expects(:git_checkout_path).returns("/dev/null")
|
41
|
+
GitHelpers.expects(:is_git_repo).with("/dev/null", "extra-ssh").returns(:true)
|
42
|
+
GitHelpers.expects(:log_and_stream).returns(nil)
|
43
|
+
GitHelpers.expects(:git_freshen_clone).with(:stack, "extra-ssh", "/dev/null", "merge99", false)
|
44
|
+
GitHelpers.git_freshen_or_clone(:stack, "extra-ssh", "/dev/null", "merge99", false, "https")
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_git_freshen_or_clone_git_update_force
|
48
|
+
GitHelpers.expects(:git_checkout_path).returns("/dev/null")
|
49
|
+
GitHelpers.expects(:is_git_repo).with("/dev/null", "extra-ssh").returns(:true)
|
50
|
+
GitHelpers.expects(:log_and_stream).returns(nil)
|
51
|
+
GitHelpers.expects(:git_freshen_clone).with(:stack, "extra-ssh", "/dev/null", "merge99", true)
|
52
|
+
GitHelpers.git_freshen_or_clone(:stack, "extra-ssh", "/dev/null", "merge99", false, "https", true)
|
53
|
+
end
|
54
|
+
def test_git_freshen_or_clone_git_bad_repo
|
55
|
+
GitHelpers.expects(:git_checkout_path).returns("/dev/null")
|
56
|
+
GitHelpers.expects(:is_git_repo).with("/dev/null", "extra-ssh").returns(:false)
|
57
|
+
GitHelpers.expects(:log_and_stream).returns(nil)
|
58
|
+
GitHelpers.git_freshen_or_clone(:stack, "extra-ssh", "/dev/null", "merge99", false, "https")
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_git_head_rev_should_cache_results
|
62
|
+
FileUtils.rm_f('/tmp/rev_head_cache_some_stack')
|
63
|
+
|
64
|
+
head_rev_sha = 'ba83f60523008e48950f77bd0d3a773f9cb2805c'
|
65
|
+
GitHelpers.expects(:get_git_head_rev).with('some_stack', 'master').returns(head_rev_sha).once
|
66
|
+
assert_equal(head_rev_sha, GitHelpers.git_head_rev('some_stack'))
|
67
|
+
# Calling it a second time should just use the cached result on disk
|
68
|
+
assert_equal(head_rev_sha, GitHelpers.git_head_rev('some_stack'))
|
69
|
+
end
|
70
|
+
end
|