Merge lp:~matiasb/locolander/initial-repo-services into lp:locolander

Proposed by Matias Bordese
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
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.

To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good!

review: Approve
Revision history for this message
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/manage.py", line 8, in <module>
    from django.core.management import execute_from_command_line
ImportError: No module named django.core.management

Revision history for this message
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/manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 453, in execute_from_command_line
    utility.execute()
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 392, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 263, in fetch_command
    app_name = get_commands()[subcommand]
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 109, in get_commands
    apps = settings.INSTALLED_APPS
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 53, in __getattr__
    self._setup(name)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 48, in _setup
    self._wrapped = Settings(settings_module)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 134, in __init__
    raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_MODULE, e))
ImportError: Could not import settings 'locolander.settings' (Is it on sys.path?): No module named compat

Revision history for this message
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/manage.py test locolanderweb
Traceback (most recent call last):
  File "locolander/manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 453, in execute_from_command_line
    utility.execute()
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 392, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 263, in fetch_command
    app_name = get_commands()[subcommand]
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 109, in get_commands
    apps = settings.INSTALLED_APPS
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 53, in __getattr__
    self._setup(name)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 48, in _setup
    self._wrapped = Settings(settings_module)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 134, in __init__
    raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_MODULE, e))
ImportError: Could not import settings 'locolander.settings' (Is it on sys.path?): No module named compat

Revision history for this message
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/manage.py test locolanderweb
Traceback (most recent call last):
  File "locolander/manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 453, in execute_from_command_line
    utility.execute()
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 392, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 263, in fetch_command
    app_name = get_commands()[subcommand]
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 109, in get_commands
    apps = settings.INSTALLED_APPS
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 53, in __getattr__
    self._setup(name)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 48, in _setup
    self._wrapped = Settings(settings_module)
  File "/home/nessita/projects/envs/locolander-env/local/lib/python2.7/site-packages/django/conf/__init__.py", line 134, in __init__
    raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_MODULE, e))
ImportError: Could not import settings 'locolander.settings' (Is it on sys.path?): No module named compat

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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/

Subscribers

People subscribed via source and target branches

to all changes: