flash_flow 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +81 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +10 -0
- data/bin/flash_flow +23 -0
- data/flash_flow.gemspec +28 -0
- data/flash_flow.yml.erb.example +83 -0
- data/lib/flash_flow.rb +7 -0
- data/lib/flash_flow/branch_merger.rb +52 -0
- data/lib/flash_flow/cmd_runner.rb +37 -0
- data/lib/flash_flow/config.rb +71 -0
- data/lib/flash_flow/data.rb +6 -0
- data/lib/flash_flow/data/base.rb +58 -0
- data/lib/flash_flow/data/branch.rb +131 -0
- data/lib/flash_flow/data/collection.rb +181 -0
- data/lib/flash_flow/data/github.rb +129 -0
- data/lib/flash_flow/data/store.rb +33 -0
- data/lib/flash_flow/deploy.rb +184 -0
- data/lib/flash_flow/git.rb +248 -0
- data/lib/flash_flow/install.rb +19 -0
- data/lib/flash_flow/issue_tracker.rb +52 -0
- data/lib/flash_flow/issue_tracker/pivotal.rb +160 -0
- data/lib/flash_flow/lock.rb +25 -0
- data/lib/flash_flow/lock/github.rb +91 -0
- data/lib/flash_flow/notifier.rb +24 -0
- data/lib/flash_flow/notifier/hipchat.rb +36 -0
- data/lib/flash_flow/options.rb +36 -0
- data/lib/flash_flow/time_helper.rb +11 -0
- data/lib/flash_flow/version.rb +3 -0
- data/test/lib/data/test_base.rb +10 -0
- data/test/lib/data/test_branch.rb +203 -0
- data/test/lib/data/test_collection.rb +238 -0
- data/test/lib/data/test_github.rb +23 -0
- data/test/lib/data/test_store.rb +53 -0
- data/test/lib/issue_tracker/test_pivotal.rb +221 -0
- data/test/lib/lock/test_github.rb +70 -0
- data/test/lib/test_branch_merger.rb +76 -0
- data/test/lib/test_config.rb +84 -0
- data/test/lib/test_deploy.rb +175 -0
- data/test/lib/test_git.rb +73 -0
- data/test/lib/test_issue_tracker.rb +43 -0
- data/test/lib/test_notifier.rb +33 -0
- data/test/minitest_helper.rb +38 -0
- metadata +217 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
module FlashFlow
|
4
|
+
module Data
|
5
|
+
class TestGithub < Minitest::Test
|
6
|
+
|
7
|
+
class SomeFakeError < RuntimeError; end
|
8
|
+
|
9
|
+
def setup
|
10
|
+
# @github = Github.new('fake_repo')
|
11
|
+
# @octokit = Minitest::Mock.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_cant_initialize_without_token
|
15
|
+
# val = ENV.delete('GH_TOKEN')
|
16
|
+
# assert_raises(RuntimeError) do
|
17
|
+
# Github.new('fake_repo')
|
18
|
+
# end
|
19
|
+
# ENV['GH_TOKEN'] = val
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'flash_flow/data/store'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Data
|
6
|
+
class TestStore < Minitest::Test
|
7
|
+
def sample_branches
|
8
|
+
{
|
9
|
+
'origin/branch 1' => {'branch' => 'branch 1', 'remote' => 'origin', 'status' => 'success', 'stories' => ['123']},
|
10
|
+
'other_origin/branch 2' => {'branch' => 'branch 2', 'remote' => 'origin', 'status' => 'success', 'stories' => ['456']}
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@mock_git = MockGit.new
|
16
|
+
@collection = Collection.new({ 'origin' => 'the_origin_url' })
|
17
|
+
@branch = Branch.new('origin', 'the_origin_url', 'some_branch')
|
18
|
+
@storage = Store.new('/dev/null', @mock_git)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_get
|
22
|
+
@mock_git.stub(:read_file_from_merge_branch, JSON.pretty_generate(old_branches)) do
|
23
|
+
assert_equal(@storage.get, old_branches)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_write
|
28
|
+
str = StringIO.new
|
29
|
+
@storage.write(old_branches, str)
|
30
|
+
|
31
|
+
assert_equal(str.string.strip, JSON.pretty_generate(old_branches).strip)
|
32
|
+
end
|
33
|
+
|
34
|
+
private # Helpers
|
35
|
+
|
36
|
+
def old_branches
|
37
|
+
@old_branches ||= {
|
38
|
+
'the_origin_url/some_old_branch' => {'ref' => 'some_old_branch', 'remote_url' => 'the_origin_url', 'remote' => 'origin', 'created_at' => (Time.now - 3600).to_s, 'stories' => ['111']},
|
39
|
+
'the_origin_url/some_branch' => {'ref' => 'some_branch', 'remote_url' => 'the_origin_url', 'remote' => 'origin', 'status' => 'success', 'created_at' => (Time.now - 1800).to_s, 'stories' => ['222']}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class MockGit
|
45
|
+
def read_file_from_merge_branch; end
|
46
|
+
def add_and_commit(_,_,_=nil); end
|
47
|
+
|
48
|
+
def in_temp_merge_branch
|
49
|
+
yield
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'flash_flow/issue_tracker/pivotal'
|
3
|
+
require 'flash_flow/time_helper'
|
4
|
+
|
5
|
+
module FlashFlow
|
6
|
+
module IssueTracker
|
7
|
+
class TestPivotal < Minitest::Test
|
8
|
+
include TimeHelper
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@project_mock = MiniTest::Mock.new
|
12
|
+
@stories = MiniTest::Mock.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_stories_pushed_only_marks_success_branches
|
16
|
+
stub_tracker_gem(@project_mock) do
|
17
|
+
[[0,'111'],[1,'222']].each do |branch, story|
|
18
|
+
mock_find(nil, story)
|
19
|
+
git = mock_working_branch(branch)
|
20
|
+
|
21
|
+
Pivotal.new(sample_branches, git).stories_pushed
|
22
|
+
@stories.verify
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_stories_delivered_gets_success_and_removed_stories
|
28
|
+
stub_tracker_gem(@project_mock) do
|
29
|
+
mock_find(nil, '111')
|
30
|
+
mock_find(nil, '222')
|
31
|
+
mock_find(nil, '555')
|
32
|
+
|
33
|
+
Pivotal.new(sample_branches, nil).stories_delivered
|
34
|
+
@stories.verify
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_stories_delivered_only_delivers_finished_stories
|
39
|
+
stub_tracker_gem(@project_mock) do
|
40
|
+
story1_mock = MiniTest::Mock.new
|
41
|
+
.expect(:id, '111')
|
42
|
+
.expect(:current_state, 'finished')
|
43
|
+
.expect(:current_state=, true, ['delivered'])
|
44
|
+
.expect(:update, true)
|
45
|
+
story2_mock = MiniTest::Mock.new
|
46
|
+
.expect(:id, '222')
|
47
|
+
.expect(:current_state, 'delivered')
|
48
|
+
story3_mock = MiniTest::Mock.new
|
49
|
+
.expect(:id, '555')
|
50
|
+
.expect(:current_state, 'removed')
|
51
|
+
mock_find(story1_mock)
|
52
|
+
mock_find(story2_mock)
|
53
|
+
mock_find(story3_mock)
|
54
|
+
|
55
|
+
Pivotal.new(sample_branches, nil).stories_delivered
|
56
|
+
story1_mock.verify
|
57
|
+
story2_mock.verify
|
58
|
+
story3_mock.verify
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_stories_pushed_only_finishes_started_stories
|
63
|
+
stub_tracker_gem(@project_mock) do
|
64
|
+
story1_mock = MiniTest::Mock.new
|
65
|
+
.expect(:id, '111')
|
66
|
+
.expect(:current_state, 'started')
|
67
|
+
.expect(:current_state=, true, ['finished'])
|
68
|
+
.expect(:update, true)
|
69
|
+
story2_mock = MiniTest::Mock.new
|
70
|
+
.expect(:id, '222')
|
71
|
+
.expect(:current_state, 'finished')
|
72
|
+
mock_find(story1_mock)
|
73
|
+
mock_find(story2_mock)
|
74
|
+
|
75
|
+
[[0,story1_mock],[1,story2_mock]].each do |branch, story|
|
76
|
+
git = mock_working_branch(branch)
|
77
|
+
|
78
|
+
Pivotal.new(sample_branches, git).stories_pushed
|
79
|
+
story.verify
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_stories_pushed_only_finishes_stories_of_current_branch
|
85
|
+
stub_tracker_gem(@project_mock) do
|
86
|
+
story1_mock = MiniTest::Mock.new
|
87
|
+
.expect(:id, '111')
|
88
|
+
.expect(:current_state, 'started')
|
89
|
+
.expect(:current_state=, true, ['finished'])
|
90
|
+
.expect(:update, true)
|
91
|
+
story2_mock = MiniTest::Mock.new
|
92
|
+
.expect(:id, '222')
|
93
|
+
mock_find(story1_mock)
|
94
|
+
mock_find(story2_mock)
|
95
|
+
|
96
|
+
git = mock_working_branch(0)
|
97
|
+
|
98
|
+
Pivotal.new(sample_branches, git).stories_pushed
|
99
|
+
story1_mock.verify
|
100
|
+
story2_mock.verify
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_production_deploy_only_comments_on_shipped_branches
|
105
|
+
stub_tracker_gem(@project_mock) do
|
106
|
+
mock_find(nil, '111')
|
107
|
+
|
108
|
+
Pivotal.new(sample_branches, mock_git).production_deploy
|
109
|
+
@stories.verify
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_production_deploy_comments
|
114
|
+
shipped_text = with_time_zone("US/Pacific") { Time.now.strftime("Shipped to production on %m/%d/%Y at %H:%M") }
|
115
|
+
fake_notes = Minitest::Mock.new
|
116
|
+
.expect(:all, [mock_comment('Some random comment'), mock_comment('Some other random comment')])
|
117
|
+
.expect(:create, true, [{ text: shipped_text }])
|
118
|
+
story_mock = MiniTest::Mock.new
|
119
|
+
.expect(:id, '111')
|
120
|
+
.expect(:notes, fake_notes)
|
121
|
+
.expect(:notes, fake_notes)
|
122
|
+
|
123
|
+
stub_tracker_gem(@project_mock) do
|
124
|
+
mock_find(story_mock)
|
125
|
+
|
126
|
+
Pivotal.new(sample_branches, mock_git, {'timezone' => "US/Pacific"}).production_deploy
|
127
|
+
end
|
128
|
+
|
129
|
+
story_mock.verify
|
130
|
+
fake_notes.verify
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_production_deploy_only_comments_if_no_existing_comment
|
134
|
+
fake_notes = Minitest::Mock.new
|
135
|
+
.expect(:all, [mock_comment('Some random comment'), mock_comment('Shipped to production on')])
|
136
|
+
story_mock = MiniTest::Mock.new
|
137
|
+
.expect(:id, '111')
|
138
|
+
.expect(:notes, fake_notes)
|
139
|
+
|
140
|
+
stub_tracker_gem(@project_mock) do
|
141
|
+
mock_find(story_mock)
|
142
|
+
|
143
|
+
Pivotal.new(sample_branches, mock_git).production_deploy
|
144
|
+
end
|
145
|
+
|
146
|
+
story_mock.verify
|
147
|
+
fake_notes.verify
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_list_release_notes_with_time_scope
|
151
|
+
time = Time.now
|
152
|
+
time -= time.sec
|
153
|
+
time_now = time.strftime("%m/%d/%Y at %H:%M")
|
154
|
+
fake_notes = Minitest::Mock.new
|
155
|
+
.expect(:all, [mock_comment('Some random comment'), mock_comment("Shipped to production on #{time_now}")])
|
156
|
+
story_mock = MiniTest::Mock.new
|
157
|
+
.expect(:id, '111')
|
158
|
+
.expect(:name, 'fake_name')
|
159
|
+
.expect(:notes, fake_notes)
|
160
|
+
fake_file = MiniTest::Mock.new
|
161
|
+
.expect(:puts, nil, ["PT#111 fake_name (#{time})"])
|
162
|
+
|
163
|
+
stub_tracker_gem(@project_mock) do
|
164
|
+
pivotal = Pivotal.new(sample_branches, mock_git)
|
165
|
+
pivotal.stub(:done_and_current_stories, [story_mock]) do
|
166
|
+
pivotal.release_notes(24, fake_file)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
story_mock.verify
|
171
|
+
fake_notes.verify
|
172
|
+
fake_file.verify
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def stub_tracker_gem(project)
|
178
|
+
PivotalTracker::Client.stub(:token=, true) do
|
179
|
+
PivotalTracker::Client.stub(:use_ssl=, true) do
|
180
|
+
PivotalTracker::Project.stub(:find, project) do
|
181
|
+
yield
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def mock_git
|
188
|
+
Minitest::Mock.new
|
189
|
+
.expect(:master_branch_contains?, true, [sample_branches[0].sha])
|
190
|
+
.expect(:master_branch_contains?, false, [sample_branches[1].sha])
|
191
|
+
.expect(:master_branch_contains?, false, [sample_branches[2].sha])
|
192
|
+
.expect(:master_branch_contains?, false, [sample_branches[3].sha])
|
193
|
+
.expect(:master_branch_contains?, false, [sample_branches[4].sha])
|
194
|
+
end
|
195
|
+
|
196
|
+
def mock_working_branch(index)
|
197
|
+
git = Minitest::Mock.new
|
198
|
+
5.times { git.expect(:working_branch, sample_branches[index].ref) }
|
199
|
+
git
|
200
|
+
end
|
201
|
+
|
202
|
+
def mock_comment(comment)
|
203
|
+
Minitest::Mock.new.expect(:text, comment)
|
204
|
+
end
|
205
|
+
|
206
|
+
def mock_find(story, story_id=nil)
|
207
|
+
story_id ||= story.id
|
208
|
+
@project_mock.expect(:stories, @stories.expect(:find, story, [story_id]))
|
209
|
+
end
|
210
|
+
|
211
|
+
def sample_branches
|
212
|
+
@sample_branches ||= [Data::Branch.from_hash({'ref' => 'branch1', 'remote' => 'origin', 'sha' => 'sha1', 'status' => 'success', 'created_at' => (Time.now - 3600), 'stories' => ['111']}),
|
213
|
+
Data::Branch.from_hash({'ref' => 'branch2', 'remote' => 'origin', 'sha' => 'sha2', 'status' => 'success', 'created_at' => (Time.now - 1800), 'stories' => ['222']}),
|
214
|
+
Data::Branch.from_hash({'ref' => 'branch3', 'remote' => 'origin', 'sha' => 'sha3', 'status' => 'fail', 'created_at' => (Time.now - 1800), 'stories' => ['333']}),
|
215
|
+
Data::Branch.from_hash({'ref' => 'branch4', 'remote' => 'origin', 'sha' => 'sha4', 'status' => nil, 'created_at' => (Time.now - 1800), 'stories' => ['444']}),
|
216
|
+
Data::Branch.from_hash({'ref' => 'branch5', 'remote' => 'origin', 'sha' => 'sha5', 'status' => 'removed', 'created_at' => (Time.now - 1800), 'stories' => ['555']})
|
217
|
+
]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'minitest/stub_any_instance'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Lock
|
6
|
+
class TestGithub < Minitest::Test
|
7
|
+
class SomeFakeError < RuntimeError;
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@lock = Lock::Github.new(params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_raises_without_required_params
|
15
|
+
config = params.select { |k, _| k != 'token' }
|
16
|
+
assert_raises(Lock::Error) { Lock::Github.new(config) }
|
17
|
+
|
18
|
+
config = params.select { |k, _| k != 'repo' }
|
19
|
+
assert_raises(Lock::Error) { Lock::Github.new(config) }
|
20
|
+
|
21
|
+
config = params.select { |k, _| k != 'issue_id' }
|
22
|
+
assert_raises(Lock::Error) { Lock::Github.new(config) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_error_message_when_issue_opened
|
26
|
+
@lock.stub(:issue_open?, true) do
|
27
|
+
@lock.stub(:get_last_event, {actor: {login: 'anonymous'}, created_at: Time.now}) do
|
28
|
+
assert_raises(FlashFlow::Lock::Error) do
|
29
|
+
@lock.with_lock
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_with_lock_calls_the_block
|
36
|
+
my_mock = Minitest::Mock.new.expect(:block_call, true).expect(:close_issue, true)
|
37
|
+
|
38
|
+
@lock.stub(:issue_open?, false) do
|
39
|
+
@lock.stub(:open_issue, nil) do
|
40
|
+
@lock.stub(:close_issue, -> { my_mock.close_issue }) do
|
41
|
+
@lock.with_lock { my_mock.block_call }
|
42
|
+
my_mock.verify
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_with_lock_closes_issue_no_matter_what
|
49
|
+
my_mock = Minitest::Mock.new.expect(:some_method, true)
|
50
|
+
|
51
|
+
@lock.stub(:issue_open?, false) do
|
52
|
+
@lock.stub(:open_issue, nil) do
|
53
|
+
@lock.stub(:close_issue, -> { my_mock.some_method }) do
|
54
|
+
assert_raises(SomeFakeError) do
|
55
|
+
@lock.with_lock { raise SomeFakeError }
|
56
|
+
my_mock.verify
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def params
|
66
|
+
{'token' => '1234567890', 'repo' => 'f/f', 'issue_id' => '123'}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'minitest/stub_any_instance'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
class TestBranchMerger < Minitest::Test
|
6
|
+
|
7
|
+
def setup
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_deleted_branch
|
11
|
+
merger.stub(:sha, nil) do
|
12
|
+
assert_equal(merger.do_merge(true), :deleted)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_successful_merge
|
17
|
+
git.expect(:last_success?, true)
|
18
|
+
|
19
|
+
merger.stub(:sha, 'some_sha') do
|
20
|
+
assert_equal(merger.do_merge(false), :success)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_successful_rerere
|
25
|
+
git.expect(:last_success?, false)
|
26
|
+
.expect(:rerere_resolve!, true)
|
27
|
+
|
28
|
+
merger.stub(:sha, 'some_sha') do
|
29
|
+
assert_equal(merger.do_merge(false), :success)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_rerere_forget
|
34
|
+
git.expect(:last_success?, false)
|
35
|
+
.expect(:run, true, [ 'rerere forget' ])
|
36
|
+
.expect(:run, true, [ 'reset --hard HEAD' ])
|
37
|
+
.expect(:run, true, [ 'rev-parse HEAD' ])
|
38
|
+
.expect(:last_stdout, 'conflict sha', )
|
39
|
+
|
40
|
+
merger.stub(:sha, 'some_sha') do
|
41
|
+
assert_equal(merger.do_merge(true), :conflict)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_failed_rerere
|
46
|
+
git.expect(:last_success?, false)
|
47
|
+
.expect(:rerere_resolve!, false)
|
48
|
+
.expect(:run, true, [ 'reset --hard HEAD' ])
|
49
|
+
.expect(:run, true, [ 'rev-parse HEAD' ])
|
50
|
+
.expect(:last_stdout, 'conflict sha', )
|
51
|
+
|
52
|
+
merger.stub(:sha, 'some_sha') do
|
53
|
+
assert_equal(merger.do_merge(false), :conflict)
|
54
|
+
assert_equal(merger.conflict_sha, 'conflict sha')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def merger
|
61
|
+
@merger ||= BranchMerger.new(git, branch)
|
62
|
+
end
|
63
|
+
|
64
|
+
def branch
|
65
|
+
@branch ||= Data::Branch.from_hash({'ref' => 'pushing_branch', 'remote' => 'origin', 'status' => 'fail', 'stories' => []})
|
66
|
+
end
|
67
|
+
|
68
|
+
def git
|
69
|
+
return @git if @git
|
70
|
+
|
71
|
+
@git = Minitest::Mock.new
|
72
|
+
@git.expect(:run, true, ["merge --no-ff #{branch.remote}/#{branch.ref}"])
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|