Merge lp:~timrchavez/offspring/offspring-sync-milestones-gracefully-handle-buggy-launchpad-api into lp:offspring

Proposed by Timothy R. Chavez
Status: Merged
Approved by: Kevin McDermott
Approved revision: 148
Merged at revision: 147
Proposed branch: lp:~timrchavez/offspring/offspring-sync-milestones-gracefully-handle-buggy-launchpad-api
Merge into: lp:offspring
Diff against target: 286 lines (+172/-33)
3 files modified
lib/offspring/master/scripts/sync_launchpad_project_milestones.py (+53/-25)
lib/offspring/master/tests/helpers.py (+56/-5)
lib/offspring/master/tests/test_utils.py (+63/-3)
To merge this branch: bzr merge lp:~timrchavez/offspring/offspring-sync-milestones-gracefully-handle-buggy-launchpad-api
Reviewer Review Type Date Requested Status
Kevin McDermott Approve
Review via email: mp+109779@code.launchpad.net

Description of the change

Gracefully skip over milestones that are not created correctly due to a buggy Launchpad API.

To post a comment you must log in.
Revision history for this message
Kevin McDermott (bigkevmcd) wrote :

#1

+ def test_sync_project_milestones(self):
+ """
+ Test milestone syncing does not abort with bad milestone objects.
+ """
+
+ def test_sync_project_milestones_handles_bad_milestone(self):
+ """
+ Test milestone syncing does not abort with bad milestone objects.
+ """

Something's not quite right, they both have the same docstring ;-)

#2

I appreciate that this is a hangover from the previous implementation...but...

+ try:
+ credentials.load(open(config.get("master", "launchpad_oauth")))
+ launchpad = Launchpad(credentials, LPNET_SERVICE_ROOT)
+ except:
+ launchpad = Launchpad.get_token_and_login(
+ 'Offspring Image Build System', LPNET_SERVICE_ROOT, cachedir)
+ launchpad.credentials.save(
+ file(config.get("master", "launchpad_oauth"), "w"))

the bare except should be fixed with something a bit more appropriate...

#3

I'd like to see a test with a successful run of the script...that doesn't have to hit Launchpad, but there should be a test with sync_milestones returning something other than None

review: Needs Fixing
148. By Timothy R. Chavez

Update the test to actually test against fake Launchpad milestones
to exercise more of the code.

Revision history for this message
Timothy R. Chavez (timrchavez) wrote :

> #1
>
> + def test_sync_project_milestones(self):
> + """
> + Test milestone syncing does not abort with bad milestone objects.
> + """
> +
> + def test_sync_project_milestones_handles_bad_milestone(self):
> + """
> + Test milestone syncing does not abort with bad milestone objects.
> + """
>
> Something's not quite right, they both have the same docstring ;-)

Oops. Good catch.

>
> #2
>
> I appreciate that this is a hangover from the previous implementation...but...
>
> + try:
> + credentials.load(open(config.get("master", "launchpad_oauth")))
> + launchpad = Launchpad(credentials, LPNET_SERVICE_ROOT)
> + except:
> + launchpad = Launchpad.get_token_and_login(
> + 'Offspring Image Build System', LPNET_SERVICE_ROOT, cachedir)
> + launchpad.credentials.save(
> + file(config.get("master", "launchpad_oauth"), "w"))
>
> the bare except should be fixed with something a bit more appropriate...
>

I agree with you that bare excepts are trouble, but I'm addressing a specific problem with this MP. I've filed a separate bug to address them: https://bugs.launchpad.net/offspring/+bug/1012487

> #3
>
> I'd like to see a test with a successful run of the script...that doesn't have
> to hit Launchpad, but there should be a test with sync_milestones returning
> something other than None

Not sure I follow here. I re-arranged the script at little as possible to write tests that were specifically designed to validate the fix contained in this MP (though one could argue that only the second test is needed to accomplish this). The point is, an invalid milestone (retrieved from Launchpad and simulated in the second test) should not cause the script to abort. By returning 'None', we prove that we did not abort, and the function returned. I think any other tests fall outside the scope of this MP an will require additional changes to the script (if I understand your request correctly).

I pushed a new branch which makes the first test more solid (actually tests a valid milestone scenario).

Revision history for this message
Kevin McDermott (bigkevmcd) wrote :

Looks good to me, +1

review: Approve
149. By Timothy R. Chavez

Get rid if superfulous db commit action and extra space at
the bottom of sync_launchpad_project_milestones.py

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/offspring/master/scripts/sync_launchpad_project_milestones.py'
2--- lib/offspring/master/scripts/sync_launchpad_project_milestones.py 2012-04-19 19:41:36 +0000
3+++ lib/offspring/master/scripts/sync_launchpad_project_milestones.py 2012-06-13 14:30:26 +0000
4@@ -4,6 +4,7 @@
5 # GNU Affero General Public License version 3 (see the file LICENSE).
6
7 import os
8+import sys
9
10 from launchpadlib.credentials import Credentials
11 from launchpadlib.launchpad import Launchpad
12@@ -17,24 +18,7 @@
13 Release
14 )
15
16-def run():
17- # Setup connection to launchpad
18- config = get_configuration()
19- cachedir = os.path.expanduser("~/.launchpadlib/cache/")
20- credentials = Credentials()
21- try:
22- credentials.load(open(config.get("master", "launchpad_oauth")))
23- launchpad = Launchpad(credentials, LPNET_SERVICE_ROOT)
24- except:
25- launchpad = Launchpad.get_token_and_login(
26- 'Offspring Image Build System', LPNET_SERVICE_ROOT, cachedir)
27- launchpad.credentials.save(
28- file(config.get("master", "launchpad_oauth"), "w"))
29-
30- # Setup connection to database
31- database = create_database(config.get("master", "db"))
32- store = Store(database)
33-
34+def sync_milestones(launchpad, store):
35 for launchpad_project in store.find(LaunchpadProject):
36 # Get launchpad instance of the project
37 try:
38@@ -50,10 +34,19 @@
39 all_milestones = p.all_milestones
40
41 # Create new and update existing milestones.
42+ sys.stdout.write("I: Creating / updating existing milestones...\n")
43+ sys.stdout.flush()
44 for m in all_milestones:
45- result = store.find(LaunchpadProjectMilestone,
46- LaunchpadProjectMilestone.launchpad_project == launchpad_project,
47- LaunchpadProjectMilestone.name == unicode(m.name))
48+ try:
49+ result = store.find(LaunchpadProjectMilestone,
50+ LaunchpadProjectMilestone.launchpad_project == launchpad_project,
51+ LaunchpadProjectMilestone.name == unicode(m.name))
52+ except AttributeError:
53+ sys.stdout.write(
54+ "W: Could not retrieve the milestone from Launchpad "
55+ "at '%s'\n" % m)
56+ sys.stdout.flush()
57+ continue
58 if result.is_empty():
59 milestone = LaunchpadProjectMilestone(
60 m.name, m.code_name, launchpad_project, m.date_targeted,
61@@ -71,10 +64,25 @@
62 store.commit()
63
64 # Delete milestones that no longer exist.
65- rescindedMilestones = set(store.find(LaunchpadProjectMilestone,
66- LaunchpadProjectMilestone.launchpad_project == launchpad_project
67- ).values(LaunchpadProjectMilestone.name)
68- ).difference(set([m.name for m in all_milestones]))
69+ sys.stdout.write("I: Deleting milestones that no longer exist...\n")
70+ sys.stdout.flush()
71+
72+ milestones = store.find(
73+ LaunchpadProjectMilestone,
74+ LaunchpadProjectMilestone.launchpad_project == launchpad_project
75+ )
76+
77+ try:
78+ rescindedMilestones = set(
79+ milestones.values(LaunchpadProjectMilestone.name)
80+ ).difference(set([m.name for m in all_milestones]))
81+ except AttributeError:
82+ sys.stdout.write(
83+ "W: Could not retrieve the milestone from Launchpad "
84+ "at '%s'\n" % m)
85+ sys.stdout.flush()
86+ continue
87+
88 for rescindedMilestoneName in rescindedMilestones:
89 result = store.find(
90 LaunchpadProjectMilestone,
91@@ -87,3 +95,23 @@
92 ).set(milestone_id=None)
93 result.remove()
94 store.commit()
95+
96+def run():
97+ # Setup connection to launchpad
98+ config = get_configuration()
99+ cachedir = os.path.expanduser("~/.launchpadlib/cache/")
100+ credentials = Credentials()
101+ try:
102+ credentials.load(open(config.get("master", "launchpad_oauth")))
103+ launchpad = Launchpad(credentials, LPNET_SERVICE_ROOT)
104+ except:
105+ launchpad = Launchpad.get_token_and_login(
106+ 'Offspring Image Build System', LPNET_SERVICE_ROOT, cachedir)
107+ launchpad.credentials.save(
108+ file(config.get("master", "launchpad_oauth"), "w"))
109+
110+ # Setup connection to database
111+ database = create_database(config.get("master", "db"))
112+ store = Store(database)
113+
114+ sync_milestones(launchpad, store)
115
116=== modified file 'lib/offspring/master/tests/helpers.py'
117--- lib/offspring/master/tests/helpers.py 2012-04-10 10:18:46 +0000
118+++ lib/offspring/master/tests/helpers.py 2012-06-13 14:30:26 +0000
119@@ -26,6 +26,26 @@
120 self.mails.append((sender, rcpt, msg))
121
122
123+class FakeLaunchpadMilestone(object):
124+ def __init__(self, name):
125+ self.name = name
126+ self.code_name = name
127+ self.date_targeted = None
128+ self.is_active = True
129+
130+ def __str__(self):
131+ return self.code_name
132+
133+
134+class FakeLaunchpadProject(object):
135+ def __init__(self, title, milestone):
136+ self.title = title
137+ self.all_milestones = [milestone]
138+
139+ def __str__(self):
140+ return self.title
141+
142+
143 class FakeBuilder(object):
144 def __init__(self, current_state, build_result=None):
145 self.name = u"test-builder"
146@@ -170,7 +190,7 @@
147
148 self.connection.execute(
149 "CREATE TABLE buildresults ("
150- "id SERIAL NOT NULL, "
151+ "id serial NOT NULL PRIMARY KEY, "
152 "name VARCHAR(200), "
153 "builder_id INTEGER, "
154 "project_name VARCHAR(200) NOT NULL, "
155@@ -198,6 +218,36 @@
156 "id SERIAL NOT NULL PRIMARY KEY, "
157 "dailybuildorder_id integer NOT NULL, "
158 "project_id VARCHAR(200) NOT NULL)")
159+
160+ self.connection.execute(
161+ "CREATE TABLE launchpad_projects ( "
162+ "name varchar(200) NOT NULL PRIMARY KEY, "
163+ "title varchar(200)) ")
164+
165+ self.connection.execute(
166+ "CREATE TABLE launchpad_project_milestones ("
167+ "id serial NOT NULL PRIMARY KEY, "
168+ "name varchar(200) NOT NULL, "
169+ "launchpad_project_id varchar(200) REFERENCES "
170+ "launchpad_projects (name) DEFERRABLE INITIALLY DEFERRED, "
171+ "title varchar(200) NOT NULL, "
172+ "date_targeted date, "
173+ "active boolean NOT NULL)")
174+
175+ self.connection.execute(
176+ "CREATE TABLE releases ( "
177+ "build_id integer NOT NULL PRIMARY KEY REFERENCES buildresults (id) DEFERRABLE INITIALLY DEFERRED, "
178+ "name varchar(200) NOT NULL, "
179+ "milestone_id integer REFERENCES launchpad_project_milestones (id) DEFERRABLE INITIALLY DEFERRED, "
180+ "tag varchar(5), "
181+ "creator_id integer NOT NULL REFERENCES auth_user (id) DEFERRABLE INITIALLY DEFERRED, "
182+ "created_at timestamp with time zone NOT NULL, "
183+ "updated_at timestamp with time zone NOT NULL, "
184+ "published_at timestamp with time zone, "
185+ "checklist_url varchar(200) NOT NULL, "
186+ "notes text NOT NULL, "
187+ "status smallint CHECK (status >= 0) NOT NULL)")
188+
189 self.connection.commit()
190
191 def create_db_store(self):
192@@ -208,12 +258,13 @@
193 self.db_store.close()
194
195 def drop_tables(self):
196- for table in ["auth_user", "projects", "lexbuilders",
197- "project_notification_subscriptions",
198+ for table in ["auth_user", "releases", "launchpad_projects", "projects",
199+ "lexbuilders", "project_notification_subscriptions",
200 "buildresults", "buildrequests", "dailybuildorders",
201- "dailybuildorders_projects"]:
202+ "dailybuildorders_projects",
203+ "launchpad_project_milestones"]:
204 try:
205- self.connection.execute("DROP TABLE %s" % table)
206+ self.connection.execute("DROP TABLE %s CASCADE" % table)
207 self.connection.commit()
208 except:
209 self.connection.rollback()
210
211=== modified file 'lib/offspring/master/tests/test_utils.py'
212--- lib/offspring/master/tests/test_utils.py 2011-12-02 09:03:14 +0000
213+++ lib/offspring/master/tests/test_utils.py 2012-06-13 14:30:26 +0000
214@@ -7,9 +7,69 @@
215 from offspring.master.utils import (
216 build_active_projects, process_pending_build_orders)
217 from offspring.master.models import (
218- BuildRequest, Project, DailyBuildOrder, DailyBuildOrderProject)
219-
220-from offspring.master.tests.helpers import BaseOffspringMasterTestCase
221+ BuildRequest,
222+ Project,
223+ DailyBuildOrder,
224+ DailyBuildOrderProject,
225+ LaunchpadProject,
226+ LaunchpadProjectMilestone)
227+from offspring.master.scripts import sync_launchpad_project_milestones
228+
229+from offspring.master.tests.helpers import (
230+ BaseOffspringMasterTestCase,
231+ FakeLaunchpadProject,
232+ FakeLaunchpadMilestone
233+)
234+
235+class TestSyncLaunchpadProjectMilestones(BaseOffspringMasterTestCase):
236+
237+ def test_sync_project_milestones(self):
238+ """
239+ Test milestone syncing succeeds.
240+ """
241+ lp_project = LaunchpadProject()
242+ lp_project.name = u"test-project"
243+ self.db_store.add(lp_project)
244+
245+ lp_project_milestone = LaunchpadProjectMilestone(
246+ u"test-project-1.0", u"test-project-1.0", lp_project)
247+ self.db_store.add(lp_project_milestone)
248+
249+ launchpad = self.mocker.mock()
250+ launchpad.projects["test-project"]
251+ fake_lp_milestone = FakeLaunchpadMilestone(u"test-project-1.0")
252+ self.mocker.result(FakeLaunchpadProject(u"test-project", fake_lp_milestone))
253+ self.mocker.replay()
254+
255+ self.assertIs(
256+ sync_launchpad_project_milestones.sync_milestones(
257+ launchpad, self.db_store), None)
258+
259+
260+ def test_sync_project_milestones_handles_bad_milestone(self):
261+ """
262+ Test milestone syncing does not abort with bad milestone objects.
263+ """
264+ lp_project = LaunchpadProject()
265+ lp_project.name = u"test-project"
266+ self.db_store.add(lp_project)
267+
268+ lp_project_milestone = LaunchpadProjectMilestone(
269+ u"test-project-1.0", u"test-project-1.0", lp_project)
270+ self.db_store.add(lp_project_milestone)
271+
272+ launchpad = self.mocker.mock()
273+ launchpad.projects[u"test-project"]
274+ fake_lp_milestone = FakeLaunchpadMilestone(u"test-project-1.0")
275+ delattr(fake_lp_milestone, "name")
276+ fake_lp_project = FakeLaunchpadProject(
277+ u"test-project", fake_lp_milestone)
278+ self.mocker.result(fake_lp_project)
279+ self.mocker.replay()
280+
281+ self.assertIs(
282+ sync_launchpad_project_milestones.sync_milestones(
283+ launchpad, self.db_store), None)
284
285
286 class TestBuildActiveProjects(BaseOffspringMasterTestCase):

Subscribers

People subscribed via source and target branches