Merge lp:~matiasb/locolander/initial-repo-services into lp:locolander
- initial-repo-services
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Natalia Bidart |
Approved revision: | 12 |
Merged at revision: | 9 |
Proposed branch: | lp:~matiasb/locolander/initial-repo-services |
Merge into: | lp:locolander |
Diff against target: |
557 lines (+475/-2) 11 files modified
locolander/locolanderweb/models.py (+10/-1) locolander/locolanderweb/tests/test_models.py (+39/-1) locolander/repos/errors.py (+9/-0) locolander/repos/github.py (+64/-0) locolander/repos/launchpad.py (+58/-0) locolander/repos/services.py (+41/-0) locolander/repos/tests/test_github.py (+118/-0) locolander/repos/tests/test_launchpad.py (+96/-0) locolander/repos/tests/test_services.py (+31/-0) requirements.txt (+2/-0) run_tests.sh (+7/-0) |
To merge this branch: | bzr merge lp:~matiasb/locolander/initial-repo-services |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart | Approve | ||
Review via email: mp+170966@code.launchpad.net |
Commit message
Added projects repo support for launchpad and github.
Description of the change
Added projects repo support for launchpad and github.
Natalia Bidart (nataliabidart) wrote : | # |
The attempt to merge lp:~matiasb/locolander/initial-repo-services into lp:locolander failed. Below is the output from the failed tests.
Traceback (most recent call last):
File "locolander/
from django.
ImportError: No module named django.
Natalia Bidart (nataliabidart) wrote : | # |
The attempt to merge lp:~matiasb/locolander/initial-repo-services into lp:locolander failed. Below is the output from the failed tests.
Traceback (most recent call last):
File "locolander/
execute_
File "/home/
utility.
File "/home/
self.
File "/home/
app_name = get_commands(
File "/home/
apps = settings.
File "/home/
self.
File "/home/
self._wrapped = Settings(
File "/home/
raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_
ImportError: Could not import settings 'locolander.
Natalia Bidart (nataliabidart) wrote : | # |
The attempt to merge lp:~matiasb/locolander/initial-repo-services into lp:locolander failed. Below is the output from the failed tests.
+ python locolander/
Traceback (most recent call last):
File "locolander/
execute_
File "/home/
utility.
File "/home/
self.
File "/home/
app_name = get_commands(
File "/home/
apps = settings.
File "/home/
self.
File "/home/
self._wrapped = Settings(
File "/home/
raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_
ImportError: Could not import settings 'locolander.
Natalia Bidart (nataliabidart) wrote : | # |
The attempt to merge lp:~matiasb/locolander/initial-repo-services into lp:locolander failed. Below is the output from the failed tests.
+ python locolander/
Traceback (most recent call last):
File "locolander/
execute_
File "/home/
utility.
File "/home/
self.
File "/home/
app_name = get_commands(
File "/home/
apps = settings.
File "/home/
self.
File "/home/
self._wrapped = Settings(
File "/home/
raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_
ImportError: Could not import settings 'locolander.
Preview Diff
1 | === modified file 'locolander/locolanderweb/models.py' |
2 | --- locolander/locolanderweb/models.py 2013-06-21 15:34:22 +0000 |
3 | +++ locolander/locolanderweb/models.py 2013-06-23 04:02:25 +0000 |
4 | @@ -1,7 +1,16 @@ |
5 | from datetime import datetime |
6 | |
7 | +from django.core.exceptions import ValidationError |
8 | +from django.contrib.auth.models import User |
9 | from django.db import models |
10 | -from django.contrib.auth.models import User |
11 | + |
12 | +from repos.services import SERVICES |
13 | + |
14 | + |
15 | +def service_url_validator(value): |
16 | + if not any(getattr(s, 'is_valid_url')(value) |
17 | + for s in SERVICES.itervalues()): |
18 | + raise ValidationError('%s is not a valid service url' % value) |
19 | |
20 | |
21 | class Project(models.Model): |
22 | |
23 | === modified file 'locolander/locolanderweb/tests/test_models.py' |
24 | --- locolander/locolanderweb/tests/test_models.py 2013-06-21 15:34:22 +0000 |
25 | +++ locolander/locolanderweb/tests/test_models.py 2013-06-23 04:02:25 +0000 |
26 | @@ -7,7 +7,12 @@ |
27 | from django.core.exceptions import ValidationError |
28 | from django.test import TestCase |
29 | |
30 | -from locolanderweb.models import Project, RunLog |
31 | +from locolanderweb.models import ( |
32 | + Project, |
33 | + RunLog, |
34 | + ValidationError, |
35 | + service_url_validator, |
36 | +) |
37 | |
38 | |
39 | class LocoLanderFactory(object): |
40 | @@ -37,6 +42,39 @@ |
41 | return Project.objects.create(owner=user, url=url) |
42 | |
43 | |
44 | +class ServiceUrlValidatorTestCase(TestCase): |
45 | + |
46 | + def test_invalid(self): |
47 | + urls = ( |
48 | + 'locolander', |
49 | + 'launchpad.net', |
50 | + 'launchpad.net/foo', |
51 | + ###'lp:launchpad.net/foo', |
52 | + 'https://github.com/username', |
53 | + 'git@github.com:foo/dippi-dapi', |
54 | + ) |
55 | + for url in urls: |
56 | + with self.assertRaises(ValidationError) as ctx: |
57 | + service_url_validator(url) |
58 | + |
59 | + e = ctx.exception |
60 | + self.assertIn(url, unicode(e)) |
61 | + |
62 | + def test_valid(self): |
63 | + urls = ( |
64 | + 'http://launchpad.net/locolander', |
65 | + 'http://launchpad.net/~pepe/locolander', |
66 | + 'https://launchpad.net/locolander', |
67 | + 'https://launchpad.net/~pepe/locolander', |
68 | + 'lp:locolander', |
69 | + 'lp:~pepe/locolander/trunk', |
70 | + 'https://github.com/username/yadda-yoda.git', |
71 | + 'git@github.com:foo/dippi-dapi.git', |
72 | + ) |
73 | + for url in urls: |
74 | + service_url_validator(url) |
75 | + |
76 | + |
77 | class BaseTestCase(TestCase): |
78 | |
79 | factory = LocoLanderFactory() |
80 | |
81 | === added directory 'locolander/repos' |
82 | === added file 'locolander/repos/__init__.py' |
83 | === added file 'locolander/repos/errors.py' |
84 | --- locolander/repos/errors.py 1970-01-01 00:00:00 +0000 |
85 | +++ locolander/repos/errors.py 2013-06-23 04:02:25 +0000 |
86 | @@ -0,0 +1,9 @@ |
87 | +"""Common exceptions.""" |
88 | + |
89 | + |
90 | +class RepositoryDoesNotExist(Exception): |
91 | + """Repository does not exist.""" |
92 | + |
93 | + |
94 | +class UnsupportedServiceError(Exception): |
95 | + """Unsupported project url.""" |
96 | |
97 | === added file 'locolander/repos/github.py' |
98 | --- locolander/repos/github.py 1970-01-01 00:00:00 +0000 |
99 | +++ locolander/repos/github.py 2013-06-23 04:02:25 +0000 |
100 | @@ -0,0 +1,64 @@ |
101 | +"""GitHub service wrapper.""" |
102 | + |
103 | +import re |
104 | + |
105 | +from github3 import GitHub |
106 | + |
107 | +from repos import errors |
108 | + |
109 | + |
110 | +GITHUB_SSH_RE = re.compile(r'^git@github.com:([^/]+)/(.+).git$') |
111 | +GITHUB_HTTPS_RE = re.compile(r'^https://github.com/([^/]+)/(.+?)(?:.git)?$') |
112 | + |
113 | + |
114 | +def is_valid_url(url): |
115 | + """Return True if the given url is from a GitHub project.""" |
116 | + return bool(GITHUB_SSH_RE.match(url) or GITHUB_HTTPS_RE.match(url)) |
117 | + |
118 | + |
119 | +class Service(object): |
120 | + |
121 | + def __init__(self): |
122 | + super(Service, self).__init__() |
123 | + # GitHub API access |
124 | + self.gh = GitHub() |
125 | + |
126 | + def _is_approved_comment(self, repo, comment): |
127 | + # this is an 'approved' comment by a commiter |
128 | + lines = comment.body.split('\n') |
129 | + approved = False |
130 | + commit_message = '' |
131 | + if repo.is_collaborator(comment.user.login) and len(lines) > 0: |
132 | + approved = lines[0].strip().lower() == 'approved' |
133 | + commit_message = '\n'.join(lines[1:]) |
134 | + |
135 | + return approved, commit_message |
136 | + |
137 | + def approved_requests_for_project_url(self, project_url): |
138 | + repo = None |
139 | + match = (GITHUB_SSH_RE.match(project_url) or |
140 | + GITHUB_HTTPS_RE.match(project_url)) |
141 | + if match: |
142 | + repo = self.gh.repository(*match.groups()) |
143 | + |
144 | + if repo is None: |
145 | + raise errors.RepositoryDoesNotExist() |
146 | + |
147 | + approved = [] |
148 | + pulls = repo.iter_pulls('open') |
149 | + for pull in pulls: |
150 | + comments = list(pull.iter_issue_comments()) |
151 | + last_comment = comments[-1] |
152 | + is_approved, commit_message = self._is_approved_comment( |
153 | + repo, last_comment) |
154 | + if is_approved: |
155 | + approved.append({ |
156 | + 'date_created': pull.created_at, # XXX: needs UTC processing? |
157 | + ###'source_repo': self.gh.repository(*pull.base.repo), |
158 | + 'source': pull.base.ref, |
159 | + ###'target_repo': self.gh.repository(*pull.head.repo), |
160 | + 'target': pull.head.ref, |
161 | + 'reviewers': [last_comment.user.login], |
162 | + 'commit_message': commit_message, |
163 | + }) |
164 | + return approved |
165 | |
166 | === added file 'locolander/repos/launchpad.py' |
167 | --- locolander/repos/launchpad.py 1970-01-01 00:00:00 +0000 |
168 | +++ locolander/repos/launchpad.py 2013-06-23 04:02:25 +0000 |
169 | @@ -0,0 +1,58 @@ |
170 | +"""Launchpad service wrapper.""" |
171 | + |
172 | +from __future__ import unicode_literals |
173 | + |
174 | +import os |
175 | +import re |
176 | + |
177 | +from launchpadlib.launchpad import Launchpad |
178 | + |
179 | +from repos import errors |
180 | + |
181 | + |
182 | +CACHE_DIR = os.path.join(os.path.expanduser('~'), '.launchpadlib', 'cache') |
183 | +LAUNCHPAD_LP_RE = re.compile(r'^lp:(?:~[^/]+/)?([^/]+)(?:/[^/]+)?$') |
184 | +LAUNCHPAD_HTTPS_RE = re.compile(r'^http(s)?://launchpad.net/(?:~[^/]+/)?([^/]+)$') |
185 | + |
186 | + |
187 | +def is_valid_url(url): |
188 | + """Return True if the given url is from a Launchpad project.""" |
189 | + return bool(LAUNCHPAD_LP_RE.match(url) or LAUNCHPAD_HTTPS_RE.match(url)) |
190 | + |
191 | + |
192 | +class Service(object): |
193 | + |
194 | + def __init__(self): |
195 | + super(Service, self).__init__() |
196 | + # Launchpad API access |
197 | + self.launchpad = Launchpad.login_anonymously( |
198 | + 'locolander', 'production', CACHE_DIR) |
199 | + self.lp_projects = self.launchpad.projects |
200 | + |
201 | + def approved_requests_for_project_url(self, project_url): |
202 | + """Return approved merge proposals.""" |
203 | + repo = None |
204 | + match = (LAUNCHPAD_HTTPS_RE.match(project_url) or |
205 | + LAUNCHPAD_LP_RE.match(project_url)) |
206 | + if match: |
207 | + name = match.groups()[0] |
208 | + try: |
209 | + repo = self.lp_projects[name] |
210 | + except KeyError: |
211 | + repo = None |
212 | + |
213 | + if repo is None: |
214 | + raise errors.RepositoryDoesNotExist() |
215 | + |
216 | + approved = [] |
217 | + proposals = repo.getMergeProposals() |
218 | + for mp in proposals: |
219 | + if mp.queue_status.lower() == 'approved': |
220 | + approved.append({ |
221 | + 'date_created': mp.date_created, # XXX: needs UTC processing? |
222 | + 'source': mp.source_branch.bzr_identity, |
223 | + 'target': mp.target_branch.bzr_identity, |
224 | + 'reviewers': [mp.reviewer.name], |
225 | + 'commit_message': mp.commit_message, |
226 | + }) |
227 | + return approved |
228 | |
229 | === added file 'locolander/repos/services.py' |
230 | --- locolander/repos/services.py 1970-01-01 00:00:00 +0000 |
231 | +++ locolander/repos/services.py 2013-06-23 04:02:25 +0000 |
232 | @@ -0,0 +1,41 @@ |
233 | +"""Repo abstractions.""" |
234 | + |
235 | +from __future__ import unicode_literals |
236 | + |
237 | +from repos import errors, github, launchpad |
238 | + |
239 | + |
240 | +SERVICE_LAUNCHPAD = 'launchpad' |
241 | +SERVICE_GITHUB = 'github' |
242 | + |
243 | +SERVICE_CHOICES = [ |
244 | + (SERVICE_LAUNCHPAD, 'Launchpad'), |
245 | + (SERVICE_GITHUB, 'GitHub'), |
246 | +] |
247 | + |
248 | +SERVICES = { |
249 | + SERVICE_GITHUB: github, |
250 | + SERVICE_LAUNCHPAD: launchpad, |
251 | +} |
252 | + |
253 | +# singletons |
254 | +_SINGLETONS = { |
255 | + SERVICE_GITHUB: None, |
256 | + SERVICE_LAUNCHPAD: None, |
257 | +} |
258 | + |
259 | + |
260 | +def get_service_from_url(url): |
261 | + """Return service from url.""" |
262 | + service = None |
263 | + for s, mod in SERVICES.iteritems(): |
264 | + if getattr(mod, 'is_valid_url')(url): |
265 | + if _SINGLETONS[s] is None: |
266 | + _SINGLETONS[s] = getattr(mod, 'Service')() |
267 | + service = _SINGLETONS[s] |
268 | + break |
269 | + |
270 | + if service is None: |
271 | + raise errors.UnsupportedServiceError() |
272 | + |
273 | + return service |
274 | |
275 | === added directory 'locolander/repos/tests' |
276 | === added file 'locolander/repos/tests/__init__.py' |
277 | === added file 'locolander/repos/tests/test_github.py' |
278 | --- locolander/repos/tests/test_github.py 1970-01-01 00:00:00 +0000 |
279 | +++ locolander/repos/tests/test_github.py 2013-06-23 04:02:25 +0000 |
280 | @@ -0,0 +1,118 @@ |
281 | +from __future__ import unicode_literals |
282 | + |
283 | +from datetime import datetime |
284 | +from unittest import TestCase |
285 | + |
286 | +from mock import Mock, patch |
287 | + |
288 | +from repos import errors, github |
289 | + |
290 | + |
291 | +class FakeComment(object): |
292 | + """Fake GitHub pull request comment.""" |
293 | + |
294 | + def __init__(self, status=None, is_collaborator=False): |
295 | + self.status = status |
296 | + self.body = 'Whatever' |
297 | + username = 'nobody' |
298 | + if is_collaborator: |
299 | + username = 'collaborator' |
300 | + self.user = Mock(login=username) |
301 | + if status == 'approved': |
302 | + self.body = 'approved\ncommit msg' |
303 | + |
304 | + |
305 | +class FakePullRequest(object): |
306 | + """Fake GitHub pull request.""" |
307 | + |
308 | + def __init__(self, status=None, is_approved=False): |
309 | + self.is_approved = is_approved |
310 | + self.status = status |
311 | + self.created_at = datetime.now() |
312 | + self.base = FakeRepo() |
313 | + self.head = FakeRepo() |
314 | + |
315 | + def iter_issue_comments(self): |
316 | + is_collaborator = self.is_approved |
317 | + return [FakeComment(), |
318 | + FakeComment(status=self.status, |
319 | + is_collaborator=is_collaborator)] |
320 | + |
321 | + |
322 | +class FakeRepo(object): |
323 | + """Fake GitHub project repo.""" |
324 | + |
325 | + def __init__(self, is_approved=False): |
326 | + self.is_approved = is_approved |
327 | + self.ref = 'master' |
328 | + self.status = 'no-approved' |
329 | + if is_approved: |
330 | + self.status = 'approved' |
331 | + |
332 | + def iter_pulls(self, state): |
333 | + if self.is_approved: |
334 | + pulls = [FakePullRequest(), |
335 | + FakePullRequest(status='approved', is_approved=True)] |
336 | + else: |
337 | + pulls = [FakePullRequest()] |
338 | + return pulls |
339 | + |
340 | + def is_collaborator(self, username): |
341 | + return username == 'collaborator' |
342 | + |
343 | + |
344 | +class GitHubMock(object): |
345 | + """Fake GitHub API wrapper.""" |
346 | + |
347 | + def __init__(self): |
348 | + self.data = {} |
349 | + repos = { |
350 | + 'test-repo': FakeRepo(is_approved=True), |
351 | + 'another-repo': FakeRepo(), |
352 | + } |
353 | + self.data['test-user'] = repos |
354 | + |
355 | + def repository(self, username, repo_name): |
356 | + return self.data.get(username, {}).get(repo_name, None) |
357 | + |
358 | + |
359 | +class GitHubServiceTestCase(TestCase): |
360 | + |
361 | + def setUp(self): |
362 | + super(GitHubServiceTestCase, self).setUp() |
363 | + self.mock_gh = patch('repos.github.GitHub', GitHubMock) |
364 | + self.mock_gh.start() |
365 | + self.addCleanup(self.mock_gh.stop) |
366 | + self.gh = github.Service() |
367 | + |
368 | + def test_project_not_match(self): |
369 | + with self.assertRaises(errors.RepositoryDoesNotExist) as exc: |
370 | + self.gh.approved_requests_for_project_url('test/not-match') |
371 | + |
372 | + def test_username_not_found(self): |
373 | + with self.assertRaises(errors.RepositoryDoesNotExist) as exc: |
374 | + self.gh.approved_requests_for_project_url( |
375 | + 'git@github.com:test-user/not-found') |
376 | + |
377 | + def test_project_not_found(self): |
378 | + with self.assertRaises(errors.RepositoryDoesNotExist) as exc: |
379 | + self.gh.approved_requests_for_project_url( |
380 | + 'git@github.com:test/not-found') |
381 | + |
382 | + def test_valid_project_without_approved_mp(self): |
383 | + mps = self.gh.approved_requests_for_project_url( |
384 | + 'git@github.com:test-user/another-repo.git') |
385 | + self.assertEqual(len(mps), 0) |
386 | + |
387 | + def test_valid_project_return_approved_mp(self): |
388 | + mps = self.gh.approved_requests_for_project_url( |
389 | + 'git@github.com:test-user/test-repo.git') |
390 | + self.assertEqual(len(mps), 1) |
391 | + mp = mps[0] |
392 | + self.assertIsInstance(mp, dict) |
393 | + |
394 | + expected_keys = ['date_created', 'source', 'target', 'reviewers', |
395 | + 'commit_message'] |
396 | + for key in expected_keys: |
397 | + self.assertIn(key, mp) |
398 | + self.assertEqual(mp['commit_message'], 'commit msg') |
399 | |
400 | === added file 'locolander/repos/tests/test_launchpad.py' |
401 | --- locolander/repos/tests/test_launchpad.py 1970-01-01 00:00:00 +0000 |
402 | +++ locolander/repos/tests/test_launchpad.py 2013-06-23 04:02:25 +0000 |
403 | @@ -0,0 +1,96 @@ |
404 | +from __future__ import unicode_literals |
405 | + |
406 | +from datetime import datetime |
407 | +from unittest import TestCase |
408 | + |
409 | +from mock import Mock, patch |
410 | + |
411 | +from repos import errors, launchpad |
412 | + |
413 | + |
414 | +class FakeMP(object): |
415 | + """Fake launchpadlib Merge Proposal object.""" |
416 | + |
417 | + def __init__(self, status, commit_message=None): |
418 | + self.queue_status = status |
419 | + self.date_created = datetime.now() |
420 | + self.commit_message = commit_message |
421 | + self.source_branch = Mock(bzr_identity='source') |
422 | + self.target_branch = Mock(bzr_identity='target') |
423 | + self.reviewer = Mock(name='reviewer') |
424 | + |
425 | + |
426 | +class FakeProject(object): |
427 | + """Fake launchpadlib Project object.""" |
428 | + |
429 | + def __init__(self, with_approved_mp=False): |
430 | + self.with_approved_mp = with_approved_mp |
431 | + |
432 | + def getMergeProposals(self): |
433 | + proposals = [FakeMP(status='merged', commit_message='merged mp')] |
434 | + if self.with_approved_mp: |
435 | + proposals.append( |
436 | + FakeMP(status='approved', commit_message='approved mp')) |
437 | + return proposals |
438 | + |
439 | + |
440 | +class LaunchpadMock(object): |
441 | + """Fake launchpadlib Launchpad service object.""" |
442 | + |
443 | + def __init__(self, username, env, cache_dir): |
444 | + self.username = username |
445 | + self.env = env |
446 | + self.cache_dir = cache_dir |
447 | + self.projects = { |
448 | + 'test-project': FakeProject(with_approved_mp=True), |
449 | + 'another-project': FakeProject(), |
450 | + } |
451 | + |
452 | + |
453 | +class LaunchpadServiceTestCase(TestCase): |
454 | + |
455 | + def setUp(self): |
456 | + super(LaunchpadServiceTestCase, self).setUp() |
457 | + self.mock_lp_login = patch( |
458 | + 'launchpadlib.launchpad.Launchpad.login_anonymously', |
459 | + LaunchpadMock) |
460 | + self.mock_lp_login.start() |
461 | + self.addCleanup(self.mock_lp_login.stop) |
462 | + self.lp = launchpad.Service() |
463 | + |
464 | + def test_launchpad_login(self): |
465 | + self.assertEqual(self.lp.launchpad.username, 'locolander') |
466 | + self.assertEqual(self.lp.launchpad.env, 'production') |
467 | + self.assertEqual(self.lp.launchpad.cache_dir, launchpad.CACHE_DIR) |
468 | + |
469 | + def test_project_not_match(self): |
470 | + with self.assertRaises(errors.RepositoryDoesNotExist) as exc: |
471 | + self.lp.approved_requests_for_project_url('not-found') |
472 | + |
473 | + def test_project_not_found(self): |
474 | + with self.assertRaises(errors.RepositoryDoesNotExist) as exc: |
475 | + self.lp.approved_requests_for_project_url('lp:not-found') |
476 | + |
477 | + def test_valid_project_without_approved_mp(self): |
478 | + mps = self.lp.approved_requests_for_project_url('lp:test-project') |
479 | + self.assertEqual(len(mps), 1) |
480 | + mp = mps[0] |
481 | + self.assertIsInstance(mp, dict) |
482 | + |
483 | + expected_keys = ['date_created', 'source', 'target', 'reviewers', |
484 | + 'commit_message'] |
485 | + for key in expected_keys: |
486 | + self.assertIn(key, mp) |
487 | + self.assertEqual(mp['commit_message'], 'approved mp') |
488 | + |
489 | + def test_valid_project_return_approved_mp(self): |
490 | + mps = self.lp.approved_requests_for_project_url('lp:test-project') |
491 | + self.assertEqual(len(mps), 1) |
492 | + mp = mps[0] |
493 | + self.assertIsInstance(mp, dict) |
494 | + |
495 | + expected_keys = ['date_created', 'source', 'target', 'reviewers', |
496 | + 'commit_message'] |
497 | + for key in expected_keys: |
498 | + self.assertIn(key, mp) |
499 | + self.assertEqual(mp['commit_message'], 'approved mp') |
500 | |
501 | === added file 'locolander/repos/tests/test_services.py' |
502 | --- locolander/repos/tests/test_services.py 1970-01-01 00:00:00 +0000 |
503 | +++ locolander/repos/tests/test_services.py 2013-06-23 04:02:25 +0000 |
504 | @@ -0,0 +1,31 @@ |
505 | +from __future__ import unicode_literals |
506 | + |
507 | +from unittest import TestCase |
508 | + |
509 | +from repos import errors, services |
510 | + |
511 | + |
512 | +class GetGitHubServiceFromUrlTestCase(TestCase): |
513 | + |
514 | + url = 'git@github.com:pepe/foo.git' |
515 | + service_class = services.github.Service |
516 | + |
517 | + def test_return_a_service_instance(self): |
518 | + service = services.get_service_from_url(self.url) |
519 | + self.assertIsInstance(service, self.service_class) |
520 | + |
521 | + def test_is_a_singleton(self): |
522 | + service1 = services.get_service_from_url(self.url) |
523 | + service2 = services.get_service_from_url(self.url) |
524 | + |
525 | + self.assertIs(service1, service2) |
526 | + |
527 | + def test_unsupported_service(self): |
528 | + with self.assertRaises(errors.UnsupportedServiceError): |
529 | + services.get_service_from_url('zaraza') |
530 | + |
531 | + |
532 | +class GetLaunchpadServiceFromUrlTestCase(TestCase): |
533 | + |
534 | + url = 'lp:foo' |
535 | + service_class = services.launchpad.Service |
536 | |
537 | === modified file 'requirements.txt' |
538 | --- requirements.txt 2013-06-21 23:50:43 +0000 |
539 | +++ requirements.txt 2013-06-23 04:02:25 +0000 |
540 | @@ -3,3 +3,5 @@ |
541 | pep8==1.4.5 |
542 | pyflakes==0.7.2 |
543 | South==0.8.1 |
544 | +github3.py==0.7.0 |
545 | +launchpadlib==1.10.2 |
546 | |
547 | === added file 'run_tests.sh' |
548 | --- run_tests.sh 1970-01-01 00:00:00 +0000 |
549 | +++ run_tests.sh 2013-06-23 04:02:25 +0000 |
550 | @@ -0,0 +1,7 @@ |
551 | +#! /bin/bash |
552 | + |
553 | +set -e |
554 | +set -x |
555 | + |
556 | +python locolander/manage.py test locolanderweb |
557 | +PYTHONPATH=locolander python -m unittest discover -s locolander/repos/ |
Looks good!