Merge lp:~allenap/launchpad/job-status-validator into lp:launchpad

Proposed by Gavin Panella
Status: Work in progress
Proposed branch: lp:~allenap/launchpad/job-status-validator
Merge into: lp:launchpad
Diff against target: 694 lines (+182/-86)
18 files modified
lib/lp/buildmaster/model/builder.py (+2/-2)
lib/lp/buildmaster/model/buildqueue.py (+1/-1)
lib/lp/code/model/branch.py (+2/-2)
lib/lp/code/model/branchjob.py (+2/-2)
lib/lp/code/model/branchmergeproposal.py (+1/-1)
lib/lp/code/model/branchmergeproposaljob.py (+2/-2)
lib/lp/code/model/tests/test_branchjob.py (+1/-1)
lib/lp/code/scripts/tests/test_scan_branches.py (+1/-1)
lib/lp/registry/model/persontransferjob.py (+1/-1)
lib/lp/registry/tests/test_person_merge_job.py (+1/-1)
lib/lp/services/job/model/job.py (+38/-27)
lib/lp/services/job/testing/__init__.py (+37/-0)
lib/lp/services/job/tests/test_job.py (+76/-26)
lib/lp/soyuz/model/distroseriesdifferencejob.py (+1/-1)
lib/lp/soyuz/model/packagecopyjob.py (+2/-2)
lib/lp/soyuz/tests/test_distroseriesdifferencejob.py (+2/-1)
lib/lp/soyuz/tests/test_packagecopyjob.py (+11/-14)
lib/lp/translations/model/translationsharingjob.py (+1/-1)
To merge this branch: bzr merge lp:~allenap/launchpad/job-status-validator
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Disapprove
Jeroen T. Vermeulen (community) code Needs Information
Ian Booth (community) code* Approve
Review via email: mp+62509@code.launchpad.net

Commit message

Consolidate the purpose of Job's status/_status/_set_status code into a single validator.

Description of the change

This removes the status/_status/_set_status issue in Job (where status
is a gettable property, _status represents the database column, and
_set_status is what Job's state transition methods call).

Why?

- I've lost time before wondering why queries aren't working before
  realising I need to use Job._status, so this clears that up.

- It simplifies the code because everything goes via the status
  property, and all transitions are validated.

- Even code within Job can't (accidentally) cheat on status
  transitions, to the extent that a helper, prepare_job(), was
  introduced in the tests in order to get a Job in a desired starting
  state.

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

This is a great change. As you say, everything is much clearer now.

review: Approve (code*)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This one's bitten me too, so I'll be very glad to see it go.

One thing I missed was a pre-imp. Gavin, did you verify that there isn't some good but invisible reason for the current design? I believe Aaron wrote this, and he seems to prefer explaining his code in person over leaving comments.

review: Needs Fixing (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Ahem. Something went wrong with my vote there.

review: Needs Information (code)
Revision history for this message
Gavin Panella (allenap) wrote :

Hi Aaron,

I forgot to ping you today about this, so for now this will have to
do. When you have a chance can you have a gander at this, especially
with respect to Jeroen's comment:

> One thing I missed was a pre-imp. Gavin, did you verify that there
> isn't some good but invisible reason for the current design? I
> believe Aaron wrote this, and he seems to prefer explaining his code
> in person over leaving comments.

I'm very bad at doing pre-implementation calls, so I apologise in
advance if I've done something stupid because of the lack of that.

I'm back on Tuesday, so if you'd like to talk about it let's try to
talk then.

Thanks, Gavin.

Revision history for this message
Aaron Bentley (abentley) wrote :

I didn't make _status private because only some transitions are valid. That would be silly, because you can just make a read/write property for that. And the solution you've given would be even better in that case.

I made status private because status is part of a more complex state that includes the date_started and date_finished. It should not be set in isolation, so I made it private and provided methods for setting it to the desired state. By providing a writable status, you encourage people to set it directly, which will produce inconsistent states (e.g. Job.status == JobStatus.RUNNING, but Job.date_started is None).

For example, prepare_job will apparently produce inconsistent states. IIUC, prepare_job(JobStatus.RUNNING, JobStatus.COMPLETED) will produce a job that is COMPLETED, but has no date_started or date_finished. Now obviously, this is test code and it doesn't matter for those particular tests, but at least in the previous version, it was clear that they were doing something slightly dirty, because they were setting a private variable.

review: Disapprove
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Why not have a public status that you can read and use in queries, and make the validator reject everything because you're not using the mutator methods.

This is what happens on PackageUpload - see lib/lp/soyuz/model/queue.py and search for validate_status.

The current situation is plainly suboptimal and I agree with the intention of this branch.

Revision history for this message
Aaron Bentley (abentley) wrote :

If we can have a readonly "status" attribute and still write to the database column via mutators, that certainly works for me.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

That's exactly what the PackageUpload code is doing.

13127. By Gavin Panella

Test to demonstrate Job's attribute access.

13128. By Gavin Panella

Test access of all attributes at once, so that we get a complete picture on failure.

13129. By Gavin Panella

Merge devel, resolving several conflicts.

13130. By Gavin Panella

Change prepare_job() to automatically figure out how to transition.

13131. By Gavin Panella

Switch to a single argument version of prepare_job().

13132. By Gavin Panella

Move 'invariants' code into validate_status_transition().

13133. By Gavin Panella

Use super(); Exception is now a new-style class.

13134. By Gavin Panella

Merge devel.

13135. By Gavin Panella

Move prepare_job into a testing package.

13136. By Gavin Panella

Merge devel.

13137. By Gavin Panella

Adjust some more parts of Launchpad to the Job._status -> .status change.

13138. By Gavin Panella

Merge devel.

Unmerged revisions

13138. By Gavin Panella

Merge devel.

13137. By Gavin Panella

Adjust some more parts of Launchpad to the Job._status -> .status change.

13136. By Gavin Panella

Merge devel.

13135. By Gavin Panella

Move prepare_job into a testing package.

13134. By Gavin Panella

Merge devel.

13133. By Gavin Panella

Use super(); Exception is now a new-style class.

13132. By Gavin Panella

Move 'invariants' code into validate_status_transition().

13131. By Gavin Panella

Switch to a single argument version of prepare_job().

13130. By Gavin Panella

Change prepare_job() to automatically figure out how to transition.

13129. By Gavin Panella

Merge devel, resolving several conflicts.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/buildmaster/model/builder.py'
--- lib/lp/buildmaster/model/builder.py 2011-09-13 05:23:16 +0000
+++ lib/lp/buildmaster/model/builder.py 2011-10-07 13:40:29 +0000
@@ -854,7 +854,7 @@
854 BuildQueue,854 BuildQueue,
855 BuildQueue.job == Job.id,855 BuildQueue.job == Job.id,
856 BuildQueue.builder == self.id,856 BuildQueue.builder == self.id,
857 Job._status == JobStatus.RUNNING,857 Job.status == JobStatus.RUNNING,
858 Job.date_started != None).one()858 Job.date_started != None).one()
859859
860 def getCurrentBuildFarmJob(self):860 def getCurrentBuildFarmJob(self):
@@ -914,7 +914,7 @@
914 Coalesce(BuildQueue.virtualized, True)),914 Coalesce(BuildQueue.virtualized, True)),
915 Processor.id == BuildQueue.processorID,915 Processor.id == BuildQueue.processorID,
916 Job.id == BuildQueue.jobID,916 Job.id == BuildQueue.jobID,
917 Job._status == JobStatus.WAITING).group_by(917 Job.status == JobStatus.WAITING).group_by(
918 Processor, Coalesce(BuildQueue.virtualized, True))918 Processor, Coalesce(BuildQueue.virtualized, True))
919919
920 result_dict = {'virt': {}, 'nonvirt': {}}920 result_dict = {'virt': {}, 'nonvirt': {}}
921921
=== modified file 'lib/lp/buildmaster/model/buildqueue.py'
--- lib/lp/buildmaster/model/buildqueue.py 2010-08-30 15:00:23 +0000
+++ lib/lp/buildmaster/model/buildqueue.py 2011-10-07 13:40:29 +0000
@@ -553,6 +553,6 @@
553 # Avoid corrupt build jobs where the builder is None.553 # Avoid corrupt build jobs where the builder is None.
554 BuildQueue.builder != None,554 BuildQueue.builder != None,
555 # status is a property. Let's use _status.555 # status is a property. Let's use _status.
556 Job._status == JobStatus.RUNNING,556 Job.status == JobStatus.RUNNING,
557 Job.date_started != None)557 Job.date_started != None)
558 return result_set558 return result_set
559559
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2011-09-27 00:57:13 +0000
+++ lib/lp/code/model/branch.py 2011-10-07 13:40:29 +0000
@@ -1176,8 +1176,8 @@
1176 BranchJob,1176 BranchJob,
1177 BranchJob.branch == self,1177 BranchJob.branch == self,
1178 Job.id == BranchJob.jobID,1178 Job.id == BranchJob.jobID,
1179 Job._status != JobStatus.COMPLETED,1179 Job.status != JobStatus.COMPLETED,
1180 Job._status != JobStatus.FAILED,1180 Job.status != JobStatus.FAILED,
1181 BranchJob.job_type == BranchJobType.UPGRADE_BRANCH)1181 BranchJob.job_type == BranchJobType.UPGRADE_BRANCH)
1182 return jobs.count() > 01182 return jobs.count() > 0
11831183
11841184
=== modified file 'lib/lp/code/model/branchjob.py'
--- lib/lp/code/model/branchjob.py 2011-08-26 00:43:31 +0000
+++ lib/lp/code/model/branchjob.py 2011-10-07 13:40:29 +0000
@@ -926,8 +926,8 @@
926 Job.id == BranchJob.jobID,926 Job.id == BranchJob.jobID,
927 BranchJob.branch == branch,927 BranchJob.branch == branch,
928 BranchJob.job_type == BranchJobType.ROSETTA_UPLOAD,928 BranchJob.job_type == BranchJobType.ROSETTA_UPLOAD,
929 Job._status != JobStatus.COMPLETED,929 Job.status != JobStatus.COMPLETED,
930 Job._status != JobStatus.FAILED)930 Job.status != JobStatus.FAILED)
931 if since is not None:931 if since is not None:
932 match = And(match, Job.date_created > since)932 match = And(match, Job.date_created > since)
933 jobs = store.using(BranchJob, Job).find((BranchJob), match)933 jobs = store.using(BranchJob, Job).find((BranchJob), match)
934934
=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py 2011-09-14 10:19:43 +0000
+++ lib/lp/code/model/branchmergeproposal.py 2011-10-07 13:40:29 +0000
@@ -206,7 +206,7 @@
206 BranchMergeProposalJob.job_type ==206 BranchMergeProposalJob.job_type ==
207 BranchMergeProposalJobType.UPDATE_PREVIEW_DIFF,207 BranchMergeProposalJobType.UPDATE_PREVIEW_DIFF,
208 BranchMergeProposalJob.job == Job.id,208 BranchMergeProposalJob.job == Job.id,
209 Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]))209 Job.status.is_in([JobStatus.WAITING, JobStatus.RUNNING]))
210 job = jobs.order_by(Job.scheduled_start, Job.date_created).first()210 job = jobs.order_by(Job.scheduled_start, Job.date_created).first()
211 if job is not None:211 if job is not None:
212 return BranchMergeProposalJobFactory.create(job)212 return BranchMergeProposalJobFactory.create(job)
213213
=== modified file 'lib/lp/code/model/branchmergeproposaljob.py'
--- lib/lp/code/model/branchmergeproposaljob.py 2011-09-26 20:00:50 +0000
+++ lib/lp/code/model/branchmergeproposaljob.py 2011-10-07 13:40:29 +0000
@@ -779,7 +779,7 @@
779 TargetBranch = ClassAlias(Branch)779 TargetBranch = ClassAlias(Branch)
780 clauses = [780 clauses = [
781 BranchMergeProposalJob.job == Job.id,781 BranchMergeProposalJob.job == Job.id,
782 Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]),782 Job.status.is_in([JobStatus.WAITING, JobStatus.RUNNING]),
783 BranchMergeProposalJob.branch_merge_proposal ==783 BranchMergeProposalJob.branch_merge_proposal ==
784 BranchMergeProposal.id, BranchMergeProposal.source_branch ==784 BranchMergeProposal.id, BranchMergeProposal.source_branch ==
785 SourceBranch.id, BranchMergeProposal.target_branch ==785 SourceBranch.id, BranchMergeProposal.target_branch ==
@@ -794,7 +794,7 @@
794 # the date_created, then job type. This should give us all creation794 # the date_created, then job type. This should give us all creation
795 # jobs before comment jobs.795 # jobs before comment jobs.
796 jobs = jobs.order_by(796 jobs = jobs.order_by(
797 Desc(Job._status), Job.date_created,797 Desc(Job.status), Job.date_created,
798 Desc(BranchMergeProposalJob.job_type))798 Desc(BranchMergeProposalJob.job_type))
799 # Now only return one job for any given merge proposal.799 # Now only return one job for any given merge proposal.
800 ready_jobs = []800 ready_jobs = []
801801
=== modified file 'lib/lp/code/model/tests/test_branchjob.py'
--- lib/lp/code/model/tests/test_branchjob.py 2011-10-06 16:26:36 +0000
+++ lib/lp/code/model/tests/test_branchjob.py 2011-10-07 13:40:29 +0000
@@ -1227,7 +1227,7 @@
1227 job = self._makeRosettaUploadJob()1227 job = self._makeRosettaUploadJob()
1228 job.job.start()1228 job.job.start()
1229 job.job.complete()1229 job.job.complete()
1230 job.job._status = JobStatus.FAILED1230 job.job.status = JobStatus.FAILED
1231 unfinished_jobs = list(RosettaUploadJob.findUnfinishedJobs(1231 unfinished_jobs = list(RosettaUploadJob.findUnfinishedJobs(
1232 self.branch))1232 self.branch))
1233 self.assertEqual([], unfinished_jobs)1233 self.assertEqual([], unfinished_jobs)
12341234
=== modified file 'lib/lp/code/scripts/tests/test_scan_branches.py'
--- lib/lp/code/scripts/tests/test_scan_branches.py 2010-10-26 15:47:24 +0000
+++ lib/lp/code/scripts/tests/test_scan_branches.py 2011-10-07 13:40:29 +0000
@@ -76,7 +76,7 @@
76 result = store.find(76 result = store.find(
77 BranchJob,77 BranchJob,
78 BranchJob.jobID == Job.id,78 BranchJob.jobID == Job.id,
79 Job._status == JobStatus.WAITING,79 Job.status == JobStatus.WAITING,
80 BranchJob.job_type == BranchJobType.REVISION_MAIL,80 BranchJob.job_type == BranchJobType.REVISION_MAIL,
81 BranchJob.branch == db_branch)81 BranchJob.branch == db_branch)
82 self.assertEqual(result.count(), 1)82 self.assertEqual(result.count(), 1)
8383
=== modified file 'lib/lp/registry/model/persontransferjob.py'
--- lib/lp/registry/model/persontransferjob.py 2011-08-12 14:39:51 +0000
+++ lib/lp/registry/model/persontransferjob.py 2011-10-07 13:40:29 +0000
@@ -370,7 +370,7 @@
370 conditions = [370 conditions = [
371 PersonTransferJob.job_type == cls.class_job_type,371 PersonTransferJob.job_type == cls.class_job_type,
372 PersonTransferJob.job_id == Job.id,372 PersonTransferJob.job_id == Job.id,
373 Job._status.is_in(Job.PENDING_STATUSES)]373 Job.status.is_in(Job.PENDING_STATUSES)]
374 arg_conditions = []374 arg_conditions = []
375 if from_person is not None:375 if from_person is not None:
376 arg_conditions.append(376 arg_conditions.append(
377377
=== modified file 'lib/lp/registry/tests/test_person_merge_job.py'
--- lib/lp/registry/tests/test_person_merge_job.py 2011-07-30 09:14:44 +0000
+++ lib/lp/registry/tests/test_person_merge_job.py 2011-10-07 13:40:29 +0000
@@ -174,7 +174,7 @@
174 def test_find_only_pending_or_running(self):174 def test_find_only_pending_or_running(self):
175 # find() only returns jobs that are pending.175 # find() only returns jobs that are pending.
176 for status in JobStatus.items:176 for status in JobStatus.items:
177 removeSecurityProxy(self.job.job)._status = status177 removeSecurityProxy(self.job.job).status = status
178 if status in Job.PENDING_STATUSES:178 if status in Job.PENDING_STATUSES:
179 self.assertEqual([self.job], self.find())179 self.assertEqual([self.job], self.find())
180 else:180 else:
181181
=== modified file 'lib/lp/services/job/model/job.py'
--- lib/lp/services/job/model/job.py 2011-09-21 13:06:53 +0000
+++ lib/lp/services/job/model/job.py 2011-10-07 13:40:29 +0000
@@ -48,9 +48,33 @@
48 """Invalid transition from one job status to another attempted."""48 """Invalid transition from one job status to another attempted."""
4949
50 def __init__(self, current_status, requested_status):50 def __init__(self, current_status, requested_status):
51 Exception.__init__(51 super(InvalidTransition, self).__init__(
52 self, 'Transition from %s to %s is invalid.' %52 'Transition from %s to %s is invalid.' % (
53 (current_status, requested_status))53 current_status, requested_status))
54
55
56def validate_status_transition(job, attr, status):
57 """Validate the status transition and maintain "invariants"."""
58 assert attr == "status"
59 if status not in job._valid_transitions[job.status]:
60 raise InvalidTransition(job.status, status)
61
62 # General invariants.
63 if status == JobStatus.RUNNING:
64 job.date_started = datetime.datetime.now(UTC)
65 job.date_finished = None
66 job.attempt_count += 1
67 if status in (JobStatus.COMPLETED, JobStatus.FAILED):
68 job.date_finished = datetime.datetime.now(UTC)
69
70 # Transition-specific invariants.
71 transition = job.status, status
72 if transition == (JobStatus.RUNNING, JobStatus.WAITING):
73 job.date_finished = datetime.datetime.now(UTC)
74 if transition == (JobStatus.SUSPENDED, JobStatus.WAITING):
75 job.lease_expires = None
76
77 return status
5478
5579
56class Job(SQLBase):80class Job(SQLBase):
@@ -70,8 +94,9 @@
7094
71 log = StringCol()95 log = StringCol()
7296
73 _status = EnumCol(enum=JobStatus, notNull=True, default=JobStatus.WAITING,97 status = EnumCol(
74 dbName='status')98 enum=JobStatus, notNull=True, default=JobStatus.WAITING,
99 dbName='status', storm_validator=validate_status_transition)
75100
76 attempt_count = IntCol(default=0)101 attempt_count = IntCol(default=0)
77102
@@ -102,13 +127,6 @@
102 JobStatus.RUNNING,127 JobStatus.RUNNING,
103 JobStatus.SUSPENDED))128 JobStatus.SUSPENDED))
104129
105 def _set_status(self, status):
106 if status not in self._valid_transitions[self._status]:
107 raise InvalidTransition(self._status, status)
108 self._status = status
109
110 status = property(lambda x: x._status)
111
112 @property130 @property
113 def is_pending(self):131 def is_pending(self):
114 """See `IJob`."""132 """See `IJob`."""
@@ -153,42 +171,35 @@
153171
154 def start(self):172 def start(self):
155 """See `IJob`."""173 """See `IJob`."""
156 self._set_status(JobStatus.RUNNING)174 self.status = JobStatus.RUNNING
157 self.date_started = datetime.datetime.now(UTC)
158 self.date_finished = None
159 self.attempt_count += 1
160175
161 def complete(self):176 def complete(self):
162 """See `IJob`."""177 """See `IJob`."""
163 self._set_status(JobStatus.COMPLETED)178 self.status = JobStatus.COMPLETED
164 self.date_finished = datetime.datetime.now(UTC)
165179
166 def fail(self):180 def fail(self):
167 """See `IJob`."""181 """See `IJob`."""
168 self._set_status(JobStatus.FAILED)182 self.status = JobStatus.FAILED
169 self.date_finished = datetime.datetime.now(UTC)
170183
171 def queue(self):184 def queue(self):
172 """See `IJob`."""185 """See `IJob`."""
173 self._set_status(JobStatus.WAITING)186 self.status = JobStatus.WAITING
174 self.date_finished = datetime.datetime.now(UTC)
175187
176 def suspend(self):188 def suspend(self):
177 """See `IJob`."""189 """See `IJob`."""
178 self._set_status(JobStatus.SUSPENDED)190 self.status = JobStatus.SUSPENDED
179191
180 def resume(self):192 def resume(self):
181 """See `IJob`."""193 """See `IJob`."""
182 if self.status is not JobStatus.SUSPENDED:194 if self.status is not JobStatus.SUSPENDED:
183 raise InvalidTransition(self._status, JobStatus.WAITING)195 raise InvalidTransition(self.status, JobStatus.WAITING)
184 self._set_status(JobStatus.WAITING)196 self.status = JobStatus.WAITING
185 self.lease_expires = None
186197
187198
188Job.ready_jobs = Select(199Job.ready_jobs = Select(
189 Job.id,200 Job.id,
190 And(201 And(
191 Job._status == JobStatus.WAITING,202 Job.status == JobStatus.WAITING,
192 Or(Job.lease_expires == None, Job.lease_expires < UTC_NOW),203 Or(Job.lease_expires == None, Job.lease_expires < UTC_NOW),
193 Or(Job.scheduled_start == None, Job.scheduled_start <= UTC_NOW),204 Or(Job.scheduled_start == None, Job.scheduled_start <= UTC_NOW),
194 ))205 ))
195206
=== added directory 'lib/lp/services/job/testing'
=== added file 'lib/lp/services/job/testing/__init__.py'
--- lib/lp/services/job/testing/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/job/testing/__init__.py 2011-10-07 13:40:29 +0000
@@ -0,0 +1,37 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Testing resources for lp.services.job."""
5
6__metaclass__ = type
7
8from lp.services.job.model.job import Job
9
10
11def find_transition(s_from, s_to):
12 """Find a valid transition route between `s_from` and `s_to`."""
13 if s_to == s_from:
14 return ()
15 else:
16 valid_transitions = Job._valid_transitions.iteritems()
17 for from_status, to_statuses in valid_transitions:
18 if s_to in to_statuses:
19 if s_from == from_status:
20 return (s_to,)
21 else:
22 possible = find_transition(s_from, from_status)
23 if possible is not None:
24 return possible + (s_to,)
25 else:
26 return None
27
28
29def prepare_job(desrired_status, job=None):
30 """Creates a new job and transitions its status to `desrired_status`."""
31 if job is None:
32 job = Job()
33 transitions = find_transition(
34 job.status, desrired_status)
35 for status in transitions:
36 job.status = status
37 return job
038
=== modified file 'lib/lp/services/job/tests/test_job.py'
--- lib/lp/services/job/tests/test_job.py 2011-09-21 13:06:53 +0000
+++ lib/lp/services/job/tests/test_job.py 2011-10-07 13:40:29 +0000
@@ -8,6 +8,12 @@
88
9import pytz9import pytz
10from storm.locals import Store10from storm.locals import Store
11from zope.interface.interface import (
12 Attribute,
13 Method,
14 )
15from zope.security.interfaces import ForbiddenAttribute
16from zope.security.proxy import ProxyFactory
1117
12from canonical.database.constants import UTC_NOW18from canonical.database.constants import UTC_NOW
13from canonical.launchpad.interfaces.lpstorm import IStore19from canonical.launchpad.interfaces.lpstorm import IStore
@@ -22,6 +28,7 @@
22 Job,28 Job,
23 LeaseHeld,29 LeaseHeld,
24 )30 )
31from lp.services.job.testing import prepare_job
25from lp.testing import (32from lp.testing import (
26 TestCase,33 TestCase,
27 TestCaseWithFactory,34 TestCaseWithFactory,
@@ -100,17 +107,17 @@
100107
101 def test_start_when_completed_is_invalid(self):108 def test_start_when_completed_is_invalid(self):
102 """When a job is completed, attempting to start is invalid."""109 """When a job is completed, attempting to start is invalid."""
103 job = Job(_status=JobStatus.COMPLETED)110 job = prepare_job(JobStatus.COMPLETED)
104 self.assertRaises(InvalidTransition, job.start)111 self.assertRaises(InvalidTransition, job.start)
105112
106 def test_start_when_failed_is_invalid(self):113 def test_start_when_failed_is_invalid(self):
107 """When a job is failed, attempting to start is invalid."""114 """When a job is failed, attempting to start is invalid."""
108 job = Job(_status=JobStatus.FAILED)115 job = prepare_job(JobStatus.FAILED)
109 self.assertRaises(InvalidTransition, job.start)116 self.assertRaises(InvalidTransition, job.start)
110117
111 def test_start_when_running_is_invalid(self):118 def test_start_when_running_is_invalid(self):
112 """When a job is running, attempting to start is invalid."""119 """When a job is running, attempting to start is invalid."""
113 job = Job(_status=JobStatus.FAILED)120 job = prepare_job(JobStatus.FAILED)
114 self.assertRaises(InvalidTransition, job.start)121 self.assertRaises(InvalidTransition, job.start)
115122
116 def test_complete(self):123 def test_complete(self):
@@ -118,7 +125,7 @@
118125
119 It should set date_finished and set the job status to COMPLETED.126 It should set date_finished and set the job status to COMPLETED.
120 """127 """
121 job = Job(_status=JobStatus.RUNNING)128 job = Job(status=JobStatus.RUNNING)
122 self.assertEqual(None, job.date_finished)129 self.assertEqual(None, job.date_finished)
123 job.complete()130 job.complete()
124 self.assertNotEqual(None, job.date_finished)131 self.assertNotEqual(None, job.date_finished)
@@ -126,17 +133,17 @@
126133
127 def test_complete_when_waiting_is_invalid(self):134 def test_complete_when_waiting_is_invalid(self):
128 """When a job is waiting, attempting to complete is invalid."""135 """When a job is waiting, attempting to complete is invalid."""
129 job = Job(_status=JobStatus.WAITING)136 job = prepare_job(JobStatus.WAITING)
130 self.assertRaises(InvalidTransition, job.complete)137 self.assertRaises(InvalidTransition, job.complete)
131138
132 def test_complete_when_completed_is_invalid(self):139 def test_complete_when_completed_is_invalid(self):
133 """When a job is completed, attempting to complete is invalid."""140 """When a job is completed, attempting to complete is invalid."""
134 job = Job(_status=JobStatus.COMPLETED)141 job = prepare_job(JobStatus.COMPLETED)
135 self.assertRaises(InvalidTransition, job.complete)142 self.assertRaises(InvalidTransition, job.complete)
136143
137 def test_complete_when_failed_is_invalid(self):144 def test_complete_when_failed_is_invalid(self):
138 """When a job is failed, attempting to complete is invalid."""145 """When a job is failed, attempting to complete is invalid."""
139 job = Job(_status=JobStatus.FAILED)146 job = prepare_job(JobStatus.FAILED)
140 self.assertRaises(InvalidTransition, job.complete)147 self.assertRaises(InvalidTransition, job.complete)
141148
142 def test_fail(self):149 def test_fail(self):
@@ -144,7 +151,7 @@
144151
145 It should set date_finished and set the job status to FAILED.152 It should set date_finished and set the job status to FAILED.
146 """153 """
147 job = Job(_status=JobStatus.RUNNING)154 job = Job(status=JobStatus.RUNNING)
148 self.assertEqual(None, job.date_finished)155 self.assertEqual(None, job.date_finished)
149 job.fail()156 job.fail()
150 self.assertNotEqual(None, job.date_finished)157 self.assertNotEqual(None, job.date_finished)
@@ -152,17 +159,17 @@
152159
153 def test_fail_when_waiting_is_invalid(self):160 def test_fail_when_waiting_is_invalid(self):
154 """When a job is waiting, attempting to fail is invalid."""161 """When a job is waiting, attempting to fail is invalid."""
155 job = Job(_status=JobStatus.WAITING)162 job = prepare_job(JobStatus.WAITING)
156 self.assertRaises(InvalidTransition, job.fail)163 self.assertRaises(InvalidTransition, job.fail)
157164
158 def test_fail_when_completed_is_invalid(self):165 def test_fail_when_completed_is_invalid(self):
159 """When a job is completed, attempting to fail is invalid."""166 """When a job is completed, attempting to fail is invalid."""
160 job = Job(_status=JobStatus.COMPLETED)167 job = prepare_job(JobStatus.COMPLETED)
161 self.assertRaises(InvalidTransition, job.fail)168 self.assertRaises(InvalidTransition, job.fail)
162169
163 def test_fail_when_failed_is_invalid(self):170 def test_fail_when_failed_is_invalid(self):
164 """When a job is failed, attempting to fail is invalid."""171 """When a job is failed, attempting to fail is invalid."""
165 job = Job(_status=JobStatus.FAILED)172 job = prepare_job(JobStatus.FAILED)
166 self.assertRaises(InvalidTransition, job.fail)173 self.assertRaises(InvalidTransition, job.fail)
167174
168 def test_queue(self):175 def test_queue(self):
@@ -170,7 +177,7 @@
170177
171 It should set date_finished, and set status to WAITING.178 It should set date_finished, and set status to WAITING.
172 """179 """
173 job = Job(_status=JobStatus.RUNNING)180 job = Job(status=JobStatus.RUNNING)
174 self.assertEqual(None, job.date_finished)181 self.assertEqual(None, job.date_finished)
175 job.queue()182 job.queue()
176 self.assertNotEqual(None, job.date_finished)183 self.assertNotEqual(None, job.date_finished)
@@ -178,22 +185,22 @@
178185
179 def test_queue_when_completed_is_invalid(self):186 def test_queue_when_completed_is_invalid(self):
180 """When a job is completed, attempting to queue is invalid."""187 """When a job is completed, attempting to queue is invalid."""
181 job = Job(_status=JobStatus.COMPLETED)188 job = prepare_job(JobStatus.COMPLETED)
182 self.assertRaises(InvalidTransition, job.queue)189 self.assertRaises(InvalidTransition, job.queue)
183190
184 def test_queue_when_waiting_is_invalid(self):191 def test_queue_when_waiting_is_invalid(self):
185 """When a job is waiting, attempting to queue is invalid."""192 """When a job is waiting, attempting to queue is invalid."""
186 job = Job(_status=JobStatus.WAITING)193 job = prepare_job(JobStatus.WAITING)
187 self.assertRaises(InvalidTransition, job.queue)194 self.assertRaises(InvalidTransition, job.queue)
188195
189 def test_queue_when_failed_is_invalid(self):196 def test_queue_when_failed_is_invalid(self):
190 """When a job is failed, attempting to queue is invalid."""197 """When a job is failed, attempting to queue is invalid."""
191 job = Job(_status=JobStatus.FAILED)198 job = prepare_job(JobStatus.FAILED)
192 self.assertRaises(InvalidTransition, job.queue)199 self.assertRaises(InvalidTransition, job.queue)
193200
194 def test_suspend(self):201 def test_suspend(self):
195 """A job that is in the WAITING state can be suspended."""202 """A job that is in the WAITING state can be suspended."""
196 job = Job(_status=JobStatus.WAITING)203 job = prepare_job(JobStatus.WAITING)
197 job.suspend()204 job.suspend()
198 self.assertEqual(205 self.assertEqual(
199 job.status,206 job.status,
@@ -201,23 +208,23 @@
201208
202 def test_suspend_when_running(self):209 def test_suspend_when_running(self):
203 """When a job is running, attempting to suspend is valid."""210 """When a job is running, attempting to suspend is valid."""
204 job = Job(_status=JobStatus.RUNNING)211 job = Job(status=JobStatus.RUNNING)
205 job.suspend()212 job.suspend()
206 self.assertEqual(JobStatus.SUSPENDED, job.status)213 self.assertEqual(JobStatus.SUSPENDED, job.status)
207214
208 def test_suspend_when_completed(self):215 def test_suspend_when_completed(self):
209 """When a job is completed, attempting to suspend is invalid."""216 """When a job is completed, attempting to suspend is invalid."""
210 job = Job(_status=JobStatus.COMPLETED)217 job = prepare_job(JobStatus.COMPLETED)
211 self.assertRaises(InvalidTransition, job.suspend)218 self.assertRaises(InvalidTransition, job.suspend)
212219
213 def test_suspend_when_failed(self):220 def test_suspend_when_failed(self):
214 """When a job is failed, attempting to suspend is invalid."""221 """When a job is failed, attempting to suspend is invalid."""
215 job = Job(_status=JobStatus.FAILED)222 job = prepare_job(JobStatus.FAILED)
216 self.assertRaises(InvalidTransition, job.suspend)223 self.assertRaises(InvalidTransition, job.suspend)
217224
218 def test_resume(self):225 def test_resume(self):
219 """A job that is suspended can be resumed."""226 """A job that is suspended can be resumed."""
220 job = Job(_status=JobStatus.SUSPENDED)227 job = Job(status=JobStatus.SUSPENDED)
221 job.resume()228 job.resume()
222 self.assertEqual(229 self.assertEqual(
223 job.status,230 job.status,
@@ -225,30 +232,30 @@
225232
226 def test_resume_clears_lease_expiry(self):233 def test_resume_clears_lease_expiry(self):
227 """A job that resumes should null out the lease_expiry."""234 """A job that resumes should null out the lease_expiry."""
228 job = Job(_status=JobStatus.SUSPENDED)235 job = Job(status=JobStatus.SUSPENDED)
229 job.lease_expires = UTC_NOW236 job.lease_expires = UTC_NOW
230 job.resume()237 job.resume()
231 self.assertIs(None, job.lease_expires)238 self.assertIs(None, job.lease_expires)
232239
233 def test_resume_when_running(self):240 def test_resume_when_running(self):
234 """When a job is running, attempting to resume is invalid."""241 """When a job is running, attempting to resume is invalid."""
235 job = Job(_status=JobStatus.RUNNING)242 job = Job(status=JobStatus.RUNNING)
236 self.assertRaises(InvalidTransition, job.resume)243 self.assertRaises(InvalidTransition, job.resume)
237244
238 def test_resume_when_completed(self):245 def test_resume_when_completed(self):
239 """When a job is completed, attempting to resume is invalid."""246 """When a job is completed, attempting to resume is invalid."""
240 job = Job(_status=JobStatus.COMPLETED)247 job = prepare_job(JobStatus.COMPLETED)
241 self.assertRaises(InvalidTransition, job.resume)248 self.assertRaises(InvalidTransition, job.resume)
242249
243 def test_resume_when_failed(self):250 def test_resume_when_failed(self):
244 """When a job is failed, attempting to resume is invalid."""251 """When a job is failed, attempting to resume is invalid."""
245 job = Job(_status=JobStatus.FAILED)252 job = prepare_job(JobStatus.FAILED)
246 self.assertRaises(InvalidTransition, job.resume)253 self.assertRaises(InvalidTransition, job.resume)
247254
248 def test_is_pending(self):255 def test_is_pending(self):
249 """is_pending is True when the job can possibly complete."""256 """is_pending is True when the job can possibly complete."""
250 for status in JobStatus.items:257 for status in JobStatus.items:
251 job = Job(_status=status)258 job = prepare_job(status)
252 self.assertEqual(259 self.assertEqual(
253 status in Job.PENDING_STATUSES, job.is_pending)260 status in Job.PENDING_STATUSES, job.is_pending)
254261
@@ -272,7 +279,7 @@
272 def test_ready_jobs_started(self):279 def test_ready_jobs_started(self):
273 """Job.ready_jobs should not jobs that have been started."""280 """Job.ready_jobs should not jobs that have been started."""
274 preexisting = self._sampleData()281 preexisting = self._sampleData()
275 job = Job(_status=JobStatus.RUNNING)282 job = Job(status=JobStatus.RUNNING)
276 self.assertEqual(283 self.assertEqual(
277 preexisting, list(Store.of(job).execute(Job.ready_jobs)))284 preexisting, list(Store.of(job).execute(Job.ready_jobs)))
278285
@@ -340,3 +347,46 @@
340 job = Job()347 job = Job()
341 job.acquireLease(-300)348 job.acquireLease(-300)
342 self.assertEqual(0, job.getTimeout())349 self.assertEqual(0, job.getTimeout())
350
351 def test_attribute_access(self):
352 # Job's security proxy allows anyone to its attributes, but no one may
353 # set them.
354 job = ProxyFactory(Job())
355
356 def can_read(name):
357 try:
358 getattr(job, name)
359 except ForbiddenAttribute:
360 return False
361 else:
362 return True
363
364 def can_write(name):
365 try:
366 setattr(job, name, None)
367 except ForbiddenAttribute:
368 return False
369 else:
370 return True
371
372 attributes = set(
373 name for name in IJob
374 if isinstance(IJob[name], Attribute)
375 and not isinstance(IJob[name], Method))
376
377 expected = dict.fromkeys(attributes, "r")
378
379 observed = {}
380 for attribute in attributes:
381 read = can_read(attribute)
382 write = can_write(attribute)
383 if read and write:
384 observed[attribute] = "rw"
385 elif read:
386 observed[attribute] = "r"
387 elif write:
388 observed[attribute] = "w"
389 else:
390 observed[attribute] = "-"
391
392 self.assertEqual(expected, observed)
343393
=== modified file 'lib/lp/soyuz/model/distroseriesdifferencejob.py'
--- lib/lp/soyuz/model/distroseriesdifferencejob.py 2011-09-09 19:34:10 +0000
+++ lib/lp/soyuz/model/distroseriesdifferencejob.py 2011-10-07 13:40:29 +0000
@@ -255,7 +255,7 @@
255 DistributionJob,255 DistributionJob,
256 DistributionJob.job_type == cls.class_job_type,256 DistributionJob.job_type == cls.class_job_type,
257 Job.id == DistributionJob.job_id,257 Job.id == DistributionJob.job_id,
258 Job._status.is_in(Job.PENDING_STATUSES),258 Job.status.is_in(Job.PENDING_STATUSES),
259 DistributionJob.distroseries == derived_series)259 DistributionJob.distroseries == derived_series)
260260
261 parent_series_ids = set(261 parent_series_ids = set(
262262
=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
--- lib/lp/soyuz/model/packagecopyjob.py 2011-09-13 13:41:36 +0000
+++ lib/lp/soyuz/model/packagecopyjob.py 2011-10-07 13:40:29 +0000
@@ -325,7 +325,7 @@
325 PackageCopyJob.job_type == cls.class_job_type,325 PackageCopyJob.job_type == cls.class_job_type,
326 PackageCopyJob.target_archive == target_archive,326 PackageCopyJob.target_archive == target_archive,
327 Job.id == PackageCopyJob.job_id,327 Job.id == PackageCopyJob.job_id,
328 Job._status == JobStatus.WAITING)328 Job.status == JobStatus.WAITING)
329 jobs = jobs.order_by(PackageCopyJob.id)329 jobs = jobs.order_by(PackageCopyJob.id)
330 return DecoratedResultSet(jobs, cls)330 return DecoratedResultSet(jobs, cls)
331331
@@ -337,7 +337,7 @@
337 Job.id == PackageCopyJob.job_id,337 Job.id == PackageCopyJob.job_id,
338 PackageCopyJob.job_type == cls.class_job_type,338 PackageCopyJob.job_type == cls.class_job_type,
339 PackageCopyJob.target_distroseries == target_series,339 PackageCopyJob.target_distroseries == target_series,
340 Job._status.is_in(Job.PENDING_STATUSES))340 Job.status.is_in(Job.PENDING_STATUSES))
341 raw_jobs = raw_jobs.order_by(PackageCopyJob.id)341 raw_jobs = raw_jobs.order_by(PackageCopyJob.id)
342 return DecoratedResultSet(raw_jobs, cls)342 return DecoratedResultSet(raw_jobs, cls)
343343
344344
=== modified file 'lib/lp/soyuz/tests/test_distroseriesdifferencejob.py'
--- lib/lp/soyuz/tests/test_distroseriesdifferencejob.py 2011-08-31 14:12:38 +0000
+++ lib/lp/soyuz/tests/test_distroseriesdifferencejob.py 2011-10-07 13:40:29 +0000
@@ -27,6 +27,7 @@
27from lp.services.database import bulk27from lp.services.database import bulk
28from lp.services.features.testing import FeatureFixture28from lp.services.features.testing import FeatureFixture
29from lp.services.job.interfaces.job import JobStatus29from lp.services.job.interfaces.job import JobStatus
30from lp.services.job.testing import prepare_job
30from lp.soyuz.enums import (31from lp.soyuz.enums import (
31 ArchivePurpose,32 ArchivePurpose,
32 PackagePublishingStatus,33 PackagePublishingStatus,
@@ -506,7 +507,7 @@
506 dsd = self.factory.makeDistroSeriesDifference()507 dsd = self.factory.makeDistroSeriesDifference()
507 job = create_job(508 job = create_job(
508 dsd.derived_series, dsd.source_package_name, dsd.parent_series)509 dsd.derived_series, dsd.source_package_name, dsd.parent_series)
509 removeSecurityProxy(job).job._status = JobStatus.COMPLETED510 prepare_job(JobStatus.COMPLETED, removeSecurityProxy(job))
510 self.assertEqual(511 self.assertEqual(
511 {},512 {},
512 self.getJobSource().getPendingJobsForDifferences(513 self.getJobSource().getPendingJobsForDifferences(
513514
=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_packagecopyjob.py 2011-08-30 14:24:13 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2011-10-07 13:40:29 +0000
@@ -33,6 +33,8 @@
33 JobStatus,33 JobStatus,
34 SuspendJobException,34 SuspendJobException,
35 )35 )
36from lp.services.job.model.job import Job
37from lp.services.job.testing import prepare_job
36from lp.soyuz.adapters.overrides import SourceOverride38from lp.soyuz.adapters.overrides import SourceOverride
37from lp.soyuz.enums import (39from lp.soyuz.enums import (
38 ArchivePurpose,40 ArchivePurpose,
@@ -234,7 +236,7 @@
234 def test_getActiveJobs_only_returns_waiting_jobs(self):236 def test_getActiveJobs_only_returns_waiting_jobs(self):
235 # getActiveJobs ignores jobs that aren't in the WAITING state.237 # getActiveJobs ignores jobs that aren't in the WAITING state.
236 job = self.makeJob()238 job = self.makeJob()
237 removeSecurityProxy(job).job._status = JobStatus.RUNNING239 removeSecurityProxy(job).job.status = JobStatus.RUNNING
238 source = getUtility(IPlainPackageCopyJobSource)240 source = getUtility(IPlainPackageCopyJobSource)
239 self.assertContentEqual([], source.getActiveJobs(job.target_archive))241 self.assertContentEqual([], source.getActiveJobs(job.target_archive))
240242
@@ -480,21 +482,16 @@
480 def test_getPendingJobsPerPackage_only_returns_pending_jobs(self):482 def test_getPendingJobsPerPackage_only_returns_pending_jobs(self):
481 # getPendingJobsPerPackage ignores jobs that have already been483 # getPendingJobsPerPackage ignores jobs that have already been
482 # run.484 # run.
483 dsd = self.factory.makeDistroSeriesDifference()485 series = self.factory.makeDistroSeries()
484 job = self.makeJob(dsd)
485 job_source = getUtility(IPlainPackageCopyJobSource)486 job_source = getUtility(IPlainPackageCopyJobSource)
486 found_by_state = {}
487 for status in JobStatus.items:487 for status in JobStatus.items:
488 removeSecurityProxy(job).job._status = status488 dsd = self.factory.makeDistroSeriesDifference(
489 result = job_source.getPendingJobsPerPackage(dsd.derived_series)489 derived_series=series)
490 if len(result) > 0:490 job = self.makeJob(dsd)
491 found_by_state[status] = result[dsd.source_package_name.name]491 prepare_job(status, removeSecurityProxy(job))
492 expected = {492 jobs = job_source.getPendingJobsPerPackage(series)
493 JobStatus.WAITING: job,493 statuses = set(job.status for job in jobs.itervalues())
494 JobStatus.RUNNING: job,494 self.assertEqual(Job.PENDING_STATUSES, statuses)
495 JobStatus.SUSPENDED: job,
496 }
497 self.assertEqual(expected, found_by_state)
498495
499 def test_getPendingJobsPerPackage_distinguishes_jobs(self):496 def test_getPendingJobsPerPackage_distinguishes_jobs(self):
500 # getPendingJobsPerPackage associates the right job with the497 # getPendingJobsPerPackage associates the right job with the
501498
=== modified file 'lib/lp/translations/model/translationsharingjob.py'
--- lib/lp/translations/model/translationsharingjob.py 2011-08-01 08:12:35 +0000
+++ lib/lp/translations/model/translationsharingjob.py 2011-10-07 13:40:29 +0000
@@ -232,7 +232,7 @@
232 (TranslationSharingJob.productseries_id ==232 (TranslationSharingJob.productseries_id ==
233 packaging.productseries.id),233 packaging.productseries.id),
234 TranslationSharingJob.job_type == cls.class_job_type,234 TranslationSharingJob.job_type == cls.class_job_type,
235 Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]))235 Job.status.is_in([JobStatus.WAITING, JobStatus.RUNNING]))
236 result.order_by(TranslationSharingJob.id)236 result.order_by(TranslationSharingJob.id)
237 job = result.first()237 job = result.first()
238 if job is None:238 if job is None: