etsy-deployinator 1.0.2 → 1.1.0
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.
- 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
|