-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtest-piehole.py
executable file
·386 lines (328 loc) · 12.1 KB
/
test-piehole.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#!/usr/bin/env python3
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
import contextlib
import os
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import unittest
import urllib.parse
import urllib.request
import uuid
sys.path.append('.')
from piehole import run_git, etcd_read, etcd_write, GitFailure, BLANK, \
invoke_daemon, reporef, DAEMON_PORT
TEST_REPO_COUNT = 3
class RunError(Exception):
pass
def cleanup_directory(path):
"Remove a directory and contents even if written into by another process"
while True:
try:
shutil.rmtree(path)
break
except OSError as err:
if 39 != err.errno: # 39: directory not empty
raise
def run(command):
try:
return subprocess.check_output(command,
shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as err:
raise RunError(err.output.decode('utf-8'))
@contextlib.contextmanager
def in_directory(path):
oldcwd = os.getcwd()
try:
path = path.root
except:
pass
os.chdir(path)
try:
yield
finally:
os.chdir(oldcwd)
class TemporaryGitRepo:
def __init__(self, arg="--quiet"):
self.root = tempfile.mkdtemp()
with in_directory(self.root):
self.run_git('init', arg)
def run_git(self, *args):
with in_directory(self.root):
return run_git(*args)
def run(self, command):
with in_directory(self.root):
return run(command)
@property
def url(self):
return urllib.parse.urljoin("file:///",
urllib.request.pathname2url(self.root))
def __repr__(self):
return("git repo at %s" % self.url)
def cleanup(self):
cleanup_directory(self.root)
def commit(self, filename=None, message=None):
if filename is None:
filename = 'README'
if message is None:
message = 'message'
with in_directory(self.root):
with open(filename, 'a', encoding='utf-8') as fh:
fh.write(message)
self.run_git('add', filename)
self.run_git('commit', '--all', "--message=%s" % message)
def log(self):
res = self.run_git('log', '--oneline')
return res.strip().split('\n')
def add_remote(self, repo, name):
return self.run_git('remote', 'add', name, repo.url)
def push(self, reponame, branch='master'):
return self.run_git('push', reponame, branch)
def repeat_push(self, reponame, branch='master', repeat=3):
count = 0
while True:
count += 1
try:
return self.run_git('push', reponame, branch)
except GitFailure as err:
if count < repeat and "Please try" in str(err):
time.sleep(0.25)
else:
raise
def reporef(self, ref='refs/heads/master'):
with in_directory(self):
return reporef(ref)
def register(self):
self.run("piehole.py check")
class TemporaryEtcdServer:
def __init__ (self):
self.root = tempfile.mkdtemp()
self.etcd = subprocess.Popen("./etcd -d %s -n node0" % self.root,
shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def cleanup(self):
self.etcd.terminate()
self.etcd.wait()
cleanup_directory(self.root)
class TemporaryPieholeDaemon:
def __init__(self):
self.returncode = None
self.root = tempfile.mkdtemp()
self.logfile = os.path.join(self.root, 'piehole.log')
count = 0
self.daemon = subprocess.Popen(["piehole.py", "daemon", "--logfile=%s" % self.logfile])
run("curl --connect-timeout 2 -s -d action=ping http://localhost:%d" % DAEMON_PORT)
if self.daemon.poll() is None:
return
raise RuntimeError("Failed to start daemon")
def cleanup(self):
while self.returncode is None:
os.killpg(self.daemon.pid, signal.SIGKILL)
self.returncode = self.daemon.wait()
cleanup_directory(self.root)
def log(self):
with open(self.logfile) as fh:
fh.seek(0)
return fh.read()
class PieholeTest(unittest.TestCase):
def __init__(self, methodname):
super(PieholeTest, self).__init__(methodname)
self.repos = []
@classmethod
def setUpClass(cls):
cls.etcd = TemporaryEtcdServer()
@classmethod
def tearDownClass(cls):
cls.etcd.cleanup()
def get_repo(self, i):
try:
return self.repos[i]
except IndexError:
newrepo = TemporaryGitRepo('--bare')
with in_directory(newrepo):
run("piehole.py install --repogroup=%s" % self.repogroup)
self.repos.append(newrepo)
return self.get_repo(i)
def register(self, omit=None):
for repo in self.repos:
if repo is omit:
continue
repo.register()
@property
def repoa(self):
return self.get_repo(0)
@property
def repob(self):
return self.get_repo(TEST_REPO_COUNT -1)
def setUp(self):
self.repogroup = uuid.uuid4().hex
os.environ['PATH'] = "%s:%s" % (os.getcwd(), os.environ['PATH'])
shutil.rmtree('__pycache__', ignore_errors=True)
self.pieholed = TemporaryPieholeDaemon()
self.workrepo = TemporaryGitRepo()
self.workrepo.add_remote(self.repoa, "a")
self.workrepo.add_remote(self.repob, "b")
def tearDown(self):
self.pieholed.cleanup()
self.workrepo.cleanup()
while self.repos:
self.repos.pop().cleanup()
def current_ref(self, ref='refs/heads/master'):
with in_directory(self.repoa):
return etcd_read("%s %s" % (self.repogroup, ref))
def clobber_ref(self, value, ref='refs/heads/master'):
while self.current_ref(ref) != value:
with in_directory(self.repoa):
while True:
if etcd_write("%s refs/heads/master" % self.repogroup,
value, self.current_ref(ref)):
break
def wait_for_replication(self, ref='refs/heads/master'):
failtime = time.time() + 10 + len(self.repos)
while time.time() < failtime:
target = self.current_ref(ref)
for repo in self.repos:
if repo.reporef(ref) != target:
time.sleep(0.1)
break
else:
return True
repostat = [ "%s:%s" % (r.root, r.reporef(ref)) for r in self.repos ]
raise AssertionError("failed to replicate %s %s " % (self.current_ref(ref), repostat))
def commit(self, repo):
repo.commit()
def __repr__(self):
return("%s" % self.etcd)
def test_existing_update_hook(self):
"Don't overwrite existing hooks if present."
with self.assertRaisesRegex(RunError, 'Hook already exists'):
with in_directory(self.repoa):
run('date > hooks/update')
run("piehole.py install")
def test_reflog_config(self):
"Fail if the reflog is turned off."
with in_directory(self.repoa):
run('git config --local core.logAllRefUpdates false')
with self.assertRaisesRegex(RunError,
'core.logAllRefUpdates is off'):
run('piehole.py check')
def test_bad_hook_perms(self):
with self.assertRaisesRegex(RunError, 'not executable'):
with in_directory(self.repoa):
run('chmod 400 hooks/update')
run("piehole.py check")
def test_daemon_down(self):
self.workrepo.commit()
self.pieholed.cleanup()
try:
self.workrepo.push('a')
except GitFailure as err:
self.assertIn('Cannot connect to piehole daemon', str(err))
def test_daemon(self):
self.workrepo.commit()
self.workrepo.push('a')
invoke_daemon(self.repoa.root, 'refs/heads/master', 'push')
with in_directory(self.repoa):
self.assertIn(b'Error', run("curl -s -d monkey=yes http://localhost:3690"))
self.assertIn('Transferring refs/heads/master', self.pieholed.log())
def test_basics(self):
for i in range(3):
self.workrepo.commit()
self.workrepo.push('a')
self.wait_for_replication()
def test_register(self):
"Drop repo b's URL from etcd and see that it can re-register itself"
self.workrepo.commit()
self.workrepo.push('a')
self.wait_for_replication()
with in_directory(self.repoa):
etcd_write(self.repogroup, self.repoa.url)
self.register(omit=self.repob)
self.workrepo.commit()
self.workrepo.repeat_push('b')
self.workrepo.commit()
self.workrepo.repeat_push('a')
self.wait_for_replication()
def test_ssh(self):
with in_directory(self.repoa):
etcd_write(self.repogroup, self.repoa.url)
with in_directory(self.repob):
run("git config piehole.repourl git+ssh://localhost%s" % self.repob.root)
self.register()
self.workrepo.commit()
self.workrepo.repeat_push('b')
for i in range(2):
self.workrepo.commit()
self.workrepo.repeat_push('a')
self.wait_for_replication()
def test_lockout(self):
"Impossible consensus ref prevents push."
self.clobber_ref('fail')
self.workrepo.commit()
for i in range(20):
try:
res = self.workrepo.push('a')
raise AssertionError("push should fail, got %s" % res)
except GitFailure as err:
self.assertIn("Failed to update", str(err))
with self.assertRaisesRegex(AssertionError, 'failed to replicate'):
self.wait_for_replication()
def test_out_of_date(self):
"Push to an out of date repo succeeds eventually."
self.workrepo.commit()
self.workrepo.push('a')
with in_directory(self.repob):
run('rm -rf *')
run('git init --bare')
run("piehole.py install --repogroup=%s" % self.repogroup)
self.workrepo.commit()
for failcount in range(20):
try:
res = self.workrepo.push('b')
self.assertGreater(failcount, 0, "first push should fail, got %s" % res)
break
except GitFailure as err:
assert("try your push again" in str(err))
self.wait_for_replication()
else:
raise AssertionError("Out of date repo failed to catch up")
def test_clobber(self):
"Get stuck, then unstick with clobber from one repo"
self.workrepo.commit()
self.workrepo.push('a')
self.wait_for_replication()
self.clobber_ref('dead')
for failcount in range(10):
try:
self.workrepo.commit()
res = self.workrepo.push('a')
raise AssertionError("Hopeless push should fail, got %s" % res)
except GitFailure as err:
self.assertIn("failed", str(err))
with in_directory(self.repob):
run("piehole.py clobber")
self.workrepo.push('a')
self.wait_for_replication()
def test_tag(self):
"Replicate a tag."
self.workrepo.commit()
self.workrepo.run_git('tag', 'fun')
self.workrepo.push('a', 'fun')
self.assertIn('fun', self.repoa.run_git('tag'))
self.wait_for_replication('refs/tags/fun')
self.assertIn('fun', self.repob.run_git('tag'))
def test_overrun_push(self):
"Rewind the consensus to an earlier known commit and catch up."
self.workrepo.commit()
self.workrepo.push('a')
orig = self.workrepo.reporef()
self.workrepo.commit()
self.workrepo.push('a')
self.clobber_ref(orig)
self.workrepo.commit()
self.workrepo.repeat_push('a')
if __name__ == '__main__':
unittest.main()