Merge lp:~mrazik/jenkins-launchpad-plugin/stale-locks-take2 into lp:~private-ps-quality-team/jenkins-launchpad-plugin/trunk

Proposed by Martin Mrazik
Status: Merged
Approved by: Francis Ginther
Approved revision: 138
Merged at revision: 112
Proposed branch: lp:~mrazik/jenkins-launchpad-plugin/stale-locks-take2
Merge into: lp:~private-ps-quality-team/jenkins-launchpad-plugin/trunk
Diff against target: 1526 lines (+820/-274)
11 files modified
jlp.config (+0/-3)
jlp/__init__.py (+0/-2)
jlp/commands/autoland.py (+1/-3)
jlp/jenkinsutils.py (+315/-10)
jlp/launchpadutils.py (+6/-13)
jlp/mergeproposalreview.py (+0/-55)
tests/test_MergeProposalReview.py (+0/-29)
tests/test_autoland.py (+3/-24)
tests/test_jenkinsutils.py (+484/-6)
tests/test_launchpadutils.py (+11/-12)
tests/test_socketlock.py (+0/-117)
To merge this branch: bzr merge lp:~mrazik/jenkins-launchpad-plugin/stale-locks-take2
Reviewer Review Type Date Requested Status
Francis Ginther Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+166452@code.launchpad.net

Commit message

This removes the locking mechanism and instead queries jenkins if a specific job is running (with specific params).

Description of the change

Take 2 on removing locks and querying jenkins instead.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
134. By mrazik

pep8 fixed

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Martin Mrazik (mrazik) wrote :

I accidentally removed test_socketlock and then added it back. There were not changes in that file.

Revision history for this message
Martin Mrazik (mrazik) wrote :

One thing that concerns me a bit is how much load this checking will generate. I'll probably need to do some testing first on a real jenkins (by changing the triggering job to use this branch or something the like).

Revision history for this message
Francis Ginther (fginther) wrote :

I still need to review the tests, but I have the following comments (as discussed in our 1 on 1):

In trigger_ci_build() and trigger_al_build(), the
    if launchpadutils.testing_in_progress(mp, jenkins_job):
        continue

check should be the last check before proceeding to the trigger as it is likely the most time and resource intensive check.

review: Needs Fixing
135. By mrazik

moving testing_in_progress check to the very end of trigger_al_build/trigger_ci_build

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
136. By mrazik

fixed tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Martin Mrazik (mrazik) wrote :

the triggering job is now using this new branch. So far I didn't see anything strange and the build time of trigger-ci-and-autolanding-job-NG is roughly the same as before.

There is one issue, though -- fasttrack merges. I just noticed a pointless merge and as far as I can see it is because the autolanding was triggered while the other generic-land (which I started manually due to some unrelated bug) was still running.
OTOH the problem is probably not introduced by this branch and explains why I'm seeing these pointless merges every now and then.

Revision history for this message
Martin Mrazik (mrazik) wrote :

Just for the reference. This was the first trigger-ci-and-autolanding-job-NG build configured with this branch:
  #8425 Jun 4, 2013 6:17:18 AM

Revision history for this message
Martin Mrazik (mrazik) wrote :

> There is one issue, though -- fasttrack merges. I just noticed a pointless
> merge and as far as I can see it is because the autolanding was triggered
> while the other generic-land (which I started manually due to some unrelated
> bug) was still running.
> OTOH the problem is probably not introduced by this branch and explains why
> I'm seeing these pointless merges every now and then.

I was wrong about this. It is a newly introduced regression. OTOH solving this would most likely add yet another level of complexity and:
1. even if it happens nothing really bad happens (just the pointless merge error)
2. it shouldn't be happening very often as the generic-land is usually finished in 15 mins (the time between to triggering operations)

Revision history for this message
Francis Ginther (fginther) wrote :

> > There is one issue, though -- fasttrack merges. I just noticed a pointless
> > merge and as far as I can see it is because the autolanding was triggered
> > while the other generic-land (which I started manually due to some unrelated
> > bug) was still running.
> > OTOH the problem is probably not introduced by this branch and explains why
> > I'm seeing these pointless merges every now and then.
>
> I was wrong about this. It is a newly introduced regression. OTOH solving this
> would most likely add yet another level of complexity and:
> 1. even if it happens nothing really bad happens (just the pointless merge
> error)
> 2. it shouldn't be happening very often as the generic-land is usually
> finished in 15 mins (the time between to triggering operations)

Right. The fasttrack MPs never call the autolanding job, they go straight to generic-land. As this new method looks for the MPs that were started from the autolanding job, it won't find a downstream build due to the upstream check.

I agree that we are safe for now as long as the second generic-land does not change the state of the MP from Merged to something else.

Revision history for this message
Francis Ginther (fginther) wrote :

I think a testcase is needed with a different MP in a downstream job in TestIsJobOrDownstreamBuildingScenarios:
        ('downstream_job_building_with_different_mp',
         {'expected': False,
          'job_params': {'merge_proposal': 'http://mp'},
          'jenkins_data': {
              'builds': [{
                  'building': True,
                  'actions': [{
                      'causes': [{
                          'upstreamProject': 'job',
                          'upstreamBuild': 22
                      }]
                  }],
              'result': None}],
              'upstreamProjects': [{
                  'name': 'job',
                  'builds': [
                      {
                          'number': 22,
                          'actions': [{'parameters': [{
                              'name': 'merge_proposal',
                              'value': 'http://mp3'}]}]}
                  ]}]}}),

The scenario is that job was started with "http://mp3" and it is still active in a downstream build (like generic-land). A new MP, "http://mp" is being checked, no matching builds should be found. I'm guessing at the correct scenario, it may not be quite right.

Some comments appear to be out of place:
- In is_job_or_downstream_building(), the docstring refers to "the json request below" which is actually in is_downstream_job_running().
- is_job_or_downstream_building() and _is_item_queued_by_upstream() are missing parameters and return value in the docstring.
- In _get_build_actions(), there is a comment, "# get the jenkins queue" which does not belong.

Other then that, this looks good.

review: Needs Fixing
137. By mrazik

added a downstream_job_building_with_different_mp scenario for is_job_or_downstream_building test

138. By mrazik

fixed some docstrings

Revision history for this message
Martin Mrazik (mrazik) wrote :

> I think a testcase is needed with a different MP in a downstream job in
> TestIsJobOrDownstreamBuildingScenarios:
> ('downstream_job_building_with_different_mp',
> {'expected': False,
> 'job_params': {'merge_proposal': 'http://mp'},
> 'jenkins_data': {
> 'builds': [{
> 'building': True,
> 'actions': [{
> 'causes': [{
> 'upstreamProject': 'job',
> 'upstreamBuild': 22
> }]
> }],
> 'result': None}],
> 'upstreamProjects': [{
> 'name': 'job',
> 'builds': [
> {
> 'number': 22,
> 'actions': [{'parameters': [{
> 'name': 'merge_proposal',
> 'value': 'http://mp3'}]}]}
> ]}]}}),
>
> The scenario is that job was started with "http://mp3" and it is still active
> in a downstream build (like generic-land). A new MP, "http://mp" is being
> checked, no matching builds should be found. I'm guessing at the correct
> scenario, it may not be quite right.

Added.

> Some comments appear to be out of place:
> - In is_job_or_downstream_building(), the docstring refers to "the json
> request below" which is actually in is_downstream_job_running().
> - is_job_or_downstream_building() and _is_item_queued_by_upstream() are
> missing parameters and return value in the docstring.
> - In _get_build_actions(), there is a comment, "# get the jenkins queue" which
> does not belong.

Fixed.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Francis Ginther (fginther) wrote :

Thanks for the updates, approve.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'jlp.config'
--- jlp.config 2013-05-16 08:20:42 +0000
+++ jlp.config 2013-06-05 09:44:54 +0000
@@ -40,9 +40,6 @@
40#Usually you don't need to change this40#Usually you don't need to change this
41launchpad_review_type: continuous-integration41launchpad_review_type: continuous-integration
4242
43# directory containing lockfiles for Launchpad merge proposals
44launchpadlocks_dir: /tmp/jenkins-launchpad-plugin/locks
45
46#lock file that is being used to limit the number of parallel launchpad43#lock file that is being used to limit the number of parallel launchpad
47#connections44#connections
48lock_name: launchpad-trigger-lock45lock_name: launchpad-trigger-lock
4946
=== modified file 'jlp/__init__.py'
--- jlp/__init__.py 2013-05-16 08:23:40 +0000
+++ jlp/__init__.py 2013-06-05 09:44:54 +0000
@@ -89,8 +89,6 @@
89assert Branch # silence pyflakes89assert Branch # silence pyflakes
90from .bzrrecipe import BzrRecipe90from .bzrrecipe import BzrRecipe
91assert BzrRecipe # silence pyflakes91assert BzrRecipe # silence pyflakes
92from .mergeproposalreview import MergeProposalReview
93assert MergeProposalReview # silence pyflakes
94from .launchpadagent import get_launchpad92from .launchpadagent import get_launchpad
95assert get_launchpad # silence pyflakes93assert get_launchpad # silence pyflakes
96from .dputrunner import DputRunner94from .dputrunner import DputRunner
9795
=== modified file 'jlp/commands/autoland.py'
--- jlp/commands/autoland.py 2013-05-17 15:53:01 +0000
+++ jlp/commands/autoland.py 2013-06-05 09:44:54 +0000
@@ -2,7 +2,7 @@
2from argparse import ArgumentParser2from argparse import ArgumentParser
3import argparse3import argparse
4from jlp.launchpadutils import build_state, LaunchpadVote4from jlp.launchpadutils import build_state, LaunchpadVote
5from jlp import (launchpadutils, MergeProposalReview, Branch,5from jlp import (launchpadutils, Branch,
6 DputRunner, jenkinsutils, get_launchpad,6 DputRunner, jenkinsutils, get_launchpad,
7 get_config_option, logger)7 get_config_option, logger)
8from tarmac.exceptions import TarmacMergeError, BranchHasConflicts8from tarmac.exceptions import TarmacMergeError, BranchHasConflicts
@@ -156,8 +156,6 @@
156 return 1156 return 1
157 finally:157 finally:
158 target.cleanup()158 target.cleanup()
159 review = MergeProposalReview(mp)
160 review.unlock()
161159
162 return 0160 return 0
163161
164162
=== modified file 'jlp/jenkinsutils.py'
--- jlp/jenkinsutils.py 2013-05-20 11:44:39 +0000
+++ jlp/jenkinsutils.py 2013-06-05 09:44:54 +0000
@@ -6,7 +6,6 @@
6from textwrap import dedent6from textwrap import dedent
7from . import get_json_jenkins7from . import get_json_jenkins
8from lazr.restfulclient.errors import Unauthorized8from lazr.restfulclient.errors import Unauthorized
9from .mergeproposalreview import MergeProposalReview
10from jlp import logger, get_config_option9from jlp import logger, get_config_option
1110
1211
@@ -15,6 +14,315 @@
15 urlparse.urlparse(url).path.replace('//', '/'))14 urlparse.urlparse(url).path.replace('//', '/'))
1615
1716
17def _actions_has_param(actions, param, value):
18 """ Internal method to see if actions json object (as returned by jenkins)
19 includes a given parameter (and value).
20
21 It returns True or False based on whether a parameter/value pair is
22 included in the given jenkins job actions ("actions" is where jenkins
23 stores the parameters)
24
25 :param actions: json result (list of dictionaries) as returned by jenkins
26 :param param: name of the jenkins job parameter to check
27 (e.g. 'merge_proposal')
28 :param value: value that we want to check
29
30 Returns True/False.
31 """
32 for action in actions:
33 if 'parameters' not in action:
34 continue
35 for parameter in action['parameters']:
36 if parameter['name'] == param and 'value' in parameter and \
37 parameter['value'] == value:
38 return True
39 return False
40
41
42def _actions_has_all_params(actions, job_params):
43 """ Internal method to see if actions json object (as returned by jenkins)
44 includes all given job_params. See also _actions_has_param.
45
46 The main difference between this and _actions_has_param is that this
47 method checks for all params in a given dictionary (job_params).
48
49 :param actions: json result (list of dictionaries) as returned by jenkins
50 :param job_params: dictionary of parameters to check in the form of:
51 {'parameter': 'value', 'parameter2': 'value2'}
52
53 Returns True/False.
54 """
55 all_params = True
56 for param in job_params:
57 all_params = all_params and \
58 _actions_has_param(actions, param, job_params[param])
59 return all_params
60
61
62def get_running_builds(job, job_params={}):
63 """ For a given jenkins job return a list of currently active builds
64 :param job: name of the jenkins job
65 :param job_params: dictionary of job parameters
66
67 Returns an empty list if there is no active build with given
68 parameters. If job_params is empty (default) it is assumed no check
69 for job_params is required.
70 """
71
72 json_jenkins = get_json_jenkins()
73 jenkins_url = get_config_option('jenkins_url')
74 request = \
75 '{}/job/{}/api/json'.format(jenkins_url, job) +\
76 '?depth=1&tree=builds[number,building,actions[parameters[name,value]]]'
77 data = json_jenkins.get_json_data(request, append_api=False)
78 if 'builds' not in data:
79 return []
80 result = []
81
82 for build in data['builds']:
83 if build['building'] and \
84 _actions_has_all_params(build['actions'], job_params):
85 result.append(build['number'])
86 return result
87
88
89def _get_build_actions(job, build):
90 """Internal method. For a given job and build return the "actions" object
91 as returned by jenkins. This object holds the list of parameters of a
92 given build.
93
94 :param: job: jenkins job name
95 :param: build: jenkins build number
96 """
97
98 json_jenkins = get_json_jenkins()
99 jenkins_url = get_config_option('jenkins_url')
100
101 job = json_jenkins.get_json_data('{}/job/{}/{}'.format(
102 jenkins_url, job, build))
103 if 'actions' not in job:
104 return []
105 return job['actions']
106
107
108def _is_item_queued_by_upstream(item, upstream_job, upstream_params):
109 """Internal method to check if a given queued item
110 (as returned by jenkins; see the "$jenkins_url/queue/json/api" API) was
111 queued by an upstream job with a given upstream_job parameters.
112
113 The idea here is to check e.g. if generic_land (item) was queued by
114 unity-autolanding (upstream_job) with {'merge_proposal': 'http://'}
115 (upstream_params). If there is such a generic-land queued then this method
116 returns True otherwise it returns False
117
118 :param item: "item" as returned in the json jenkins reply (see the queue
119 API)
120 :param: upstream_job: name of the upstream_job
121 :param upstream_params: dictionary of the upstream job params we are
122 interested in
123 """
124 upstream_project = None
125 upstream_build = None
126
127 if 'actions' not in item:
128 return False
129
130 for action in item['actions']:
131 upstream_project = None
132 if 'causes' not in action:
133 continue
134 for cause in action['causes']:
135 if isinstance(cause, dict) and 'upstreamProject' in cause:
136 upstream_project = cause['upstreamProject']
137 # if the upstreamProject differes we can return False right
138 # away
139 if upstream_project != upstream_job:
140 return False
141
142 upstream_build = cause['upstreamBuild']
143 if upstream_params:
144 upstream_actions = _get_build_actions(
145 upstream_project, upstream_build)
146 if _actions_has_all_params(upstream_actions,
147 upstream_params):
148 return True
149 else:
150 return True
151 return False
152
153
154def is_job_queued(job, queue=None, job_params={}, upstream_job=None,
155 upstream_params={}):
156 """ Return if a job is queued in a given jenkins queue.
157 If upstream_job is supplied the job in queue must have been triggered
158 by upstream_job.
159 If upstream_params are given then the triggering upstream_job must
160 include the given upstream_params (dictionary of the form of
161 {'param1': 'value1'}).
162
163 :param job: jenkins job name
164 :param queue: jenkins queue (as returned by $jenkinsurl/queue/api/json)
165 :param upstream_job: name of the upstream job that was supposed to
166 queue job
167 :param upstream_params: parameters to check in the upstream_job
168 """
169 if queue is None:
170 json_jenkins = get_json_jenkins()
171 jenkins_url = get_config_option('jenkins_url')
172 queue = json_jenkins.get_json_data('{}/queue'.format(jenkins_url))
173
174 for item in queue['items']:
175 if 'task' in item and 'name' in item['task']:
176 # we can ignore jobs that are different from the job we are
177 # looking for
178 if item['task']['name'] != job:
179 continue
180
181 # check if the required parameters are present
182 if job_params and 'actions' in item and \
183 not _actions_has_all_params(item['actions'], job_params):
184 continue
185
186 # if we have a match and we don't need to check
187 # for upstream_job then we are done
188 if upstream_job is None:
189 return True
190
191 if _is_item_queued_by_upstream(item,
192 upstream_job,
193 upstream_params):
194 return True
195
196 return False
197
198
199def is_downstream_job_running(downstream_project,
200 upstream_job, upstream_params):
201 """Check if downstream job is running.
202 It returns True only if the downstream_project was triggered by
203 upstream_job and upstream_job has upstream_params (e.g. a given merge
204 proposal).
205
206 This is unfortunately not 100% reliable. For some reasons the json request
207 in (&tree=builds[number,url,...]) sometimes doesn't return what it should
208 (maybe the jenkins needs to update some internal state). Once the
209 relationship between jobs is somehow recorded the subsequent calls always
210 return them correctly.
211
212 :param downstream_project: name of the jenkins downstream_project (e.g.
213 generic-land)
214 :param upstream_job: name of the upstream_job (e.g. unity-autolanding)
215 :param upstream_params: dictionary of the parameters (e.g.
216 {'merge_proposal': 'http://...'})
217 """
218 json_jenkins = get_json_jenkins()
219 jenkins_url = get_config_option('jenkins_url')
220 json_request = \
221 '/api/json?depth=2&tree=builds[number,url,building,' + \
222 'actions[causes[upstreamBuild,upstreamProject]]],' + \
223 'upstreamProjects[name,builds[number,actions[parameters[name,value]]]]'
224
225 request = '{}job/{}{}'.format(
226 jenkins_url, downstream_project, json_request)
227
228 data = json_jenkins.get_json_data(request, append_api=False)
229 for build in data['builds']:
230 if 'actions' not in build:
231 continue
232 for action in build['actions']:
233 if 'causes' not in action:
234 continue
235 for cause in action['causes']:
236 upstream_project = None
237 if isinstance(cause, dict) and 'upstreamProject' in cause:
238 upstream_project = cause['upstreamProject']
239 upstream_build = cause['upstreamBuild']
240
241 if build['building'] and upstream_project == upstream_job:
242 #now we need to find the builds of the right upstream_job
243 #in data['upstreamProjects']
244 builds = {}
245 for upstream_project in data['upstreamProjects']:
246 if upstream_project['name'] == upstream_job:
247 builds = upstream_project['builds']
248
249 #find the upstream build in the data
250 for upstreamBuild in builds:
251 if upstreamBuild['number'] != upstream_build:
252 continue
253 if _actions_has_all_params(upstreamBuild['actions'],
254 upstream_params):
255 return True
256 return False
257
258
259def is_job_or_downstream_building(job, job_params):
260 """Check if a job (with given parameters) or its downstream job is
261 currently building or not.
262
263 Usually this will be called like this:
264 is_job_or_downstream_building(
265 'my-project-ci',
266 {'merge_proposal': 'http://my-merge-proposal/...'})
267 It will return True iff:
268 - my-project-ci build is running or queued and it has the correct
269 merge_proposal param
270 - a downstream project (e.g. generic-land) is running (or queued) and
271 my-project-ci which triggered the downstream has the right
272 merge_proposal param
273
274 This is unfortunately not 100% reliable because is_downstream_job_running
275 is not 100% reliable. See the docstring there for more info.
276
277 :param job: name of the jenkins job
278 :param job_params: parameters of the jenkins job we want to check
279 """
280
281 json_jenkins = get_json_jenkins()
282 jenkins_url = get_config_option('jenkins_url')
283
284 # get the jenkins queue. This is to prevent a case when a job is queued
285 # while we are preforming the other checks. It might be the other way
286 # round (i.e. a job will get out of the queue and finish its execution)
287 # and our result won't be correct. Doing it this way is however safer. The
288 # worst thing that can happen is that our job will be scheduled with the
289 # next poll
290 queue = json_jenkins.get_json_data('{}/queue'.format(jenkins_url))
291
292 # first check if a job with a given name and given params isn't already
293 # running
294 if get_running_builds(job, job_params):
295 logger.debug('{} is currently building'.format(job))
296 return True
297
298 # next check if such job (including the params) is not queued
299 if is_job_queued(job, queue, job_params=job_params):
300 logger.debug('{} is currently queued'.format(job))
301 return True
302
303 # now do similar checks like the above for the (directly) downstream
304 # projects
305 downstream_projects = get_downstream_projects(
306 json_jenkins, '{}/job/{}'.format(jenkins_url, job))
307
308 for downstream_project in downstream_projects:
309 # first check the queue; If the downstream_project is in the queue
310 # and was triggered by job then we are done
311 if is_job_queued(downstream_project, queue, upstream_job=job,
312 upstream_params=job_params):
313 logger.debug('Downstream project "{}" is queued.'.format(
314 downstream_project))
315 return True
316
317 # build is not queued. Lets check if it is not running
318 if is_downstream_job_running(downstream_project, job, job_params):
319 logger.debug('Downstream project "{}" is building'.format(
320 downstream_project))
321 return True
322
323 return False
324
325
18def is_job_published(job):326def is_job_published(job):
19 if not job:327 if not job:
20 logger.debug('is_job_published: no job specified')328 logger.debug('is_job_published: no job specified')
@@ -353,14 +661,11 @@
353 else:661 else:
354 logger.debug('Doing a regular build')662 logger.debug('Doing a regular build')
355663
356 review = MergeProposalReview(mp)
357 try:664 try:
358 logger.debug('Starting job: ' + str(jenkins_params))665 logger.debug('Starting job: ' + str(jenkins_params))
359 review.lock(jenkins_job)
360 j.build_job(jenkins_job, jenkins_params)666 j.build_job(jenkins_job, jenkins_params)
361 except jenkins.JenkinsException:667 except jenkins.JenkinsException:
362 logger.debug('Starting a job failed')668 logger.debug('Starting a job failed')
363 review.unlock()
364 message = "ERROR: failed to start a jenkins job"669 message = "ERROR: failed to start a jenkins job"
365 mp.createComment(670 mp.createComment(
366 review_type=get_config_option('launchpad_review_type'),671 review_type=get_config_option('launchpad_review_type'),
@@ -498,12 +803,12 @@
498 if not launchpadutils.user_allowed_to_trigger_jobs(mp.registrant):803 if not launchpadutils.user_allowed_to_trigger_jobs(mp.registrant):
499 continue804 continue
500805
501 if launchpadutils.testing_in_progress(mp):
502 continue
503
504 if launchpadutils.latest_candidate_validated(launchpad_user, mp):806 if launchpadutils.latest_candidate_validated(launchpad_user, mp):
505 continue807 continue
506808
809 if launchpadutils.testing_in_progress(mp, jenkins_job):
810 continue
811
507 ret = start_jenkins_job(launchpad_user, jenkins_url, jenkins_job, mp)812 ret = start_jenkins_job(launchpad_user, jenkins_url, jenkins_job, mp)
508 result = ret and result813 result = ret and result
509 return result814 return result
@@ -544,13 +849,13 @@
544 launchpadutils.LaunchpadVote.NEEDS_FIXING)849 launchpadutils.LaunchpadVote.NEEDS_FIXING)
545 continue850 continue
546851
547 if launchpadutils.testing_in_progress(mp):
548 continue
549
550 if launchpadutils.unapproved_prerequisite_exists(mp):852 if launchpadutils.unapproved_prerequisite_exists(mp):
551 logger.debug('Unapproved prerequisite exists for ' + str(mp))853 logger.debug('Unapproved prerequisite exists for ' + str(mp))
552 continue854 continue
553855
856 if launchpadutils.testing_in_progress(mp, jenkins_job):
857 continue
858
554 logger.debug('Going to trigger merge: ' + str(mp.source_branch))859 logger.debug('Going to trigger merge: ' + str(mp.source_branch))
555860
556 launchpad_user = lp_handle.people(861 launchpad_user = lp_handle.people(
557862
=== modified file 'jlp/launchpadutils.py'
--- jlp/launchpadutils.py 2013-05-10 14:10:52 +0000
+++ jlp/launchpadutils.py 2013-06-05 09:44:54 +0000
@@ -1,6 +1,5 @@
1import re1import re
2from jlp import logger, get_config_option2from jlp import logger, get_config_option
3from .mergeproposalreview import MergeProposalReview
4import jenkinsutils3import jenkinsutils
5from . import get_json_jenkins4from . import get_json_jenkins
65
@@ -42,8 +41,6 @@
42 mp.createComment(41 mp.createComment(
43 review_type=get_config_option('launchpad_review_type'),42 review_type=get_config_option('launchpad_review_type'),
44 subject=subject, content=message, vote=vote)43 subject=subject, content=message, vote=vote)
45 review = MergeProposalReview(mp)
46 review.unlock()
47 else:44 else:
48 mp.createComment(45 mp.createComment(
49 review_type=get_config_option('launchpad_review_type'),46 review_type=get_config_option('launchpad_review_type'),
@@ -172,17 +169,17 @@
172 return False169 return False
173170
174171
175def testing_in_progress(mp):172def testing_in_progress(mp, jenkins_job):
176 """ Returns whether testing is in progress.173 """ Returns whether testing is in progress.
177174
178 We are using a file (represented by MergeProposalReview class) to indicate175 This method queries jenkins to find out if we are not running a similar job
179 a testing is in progress. This file is created when the jenkins job is176 (or downstream such as generic_land) already.
180 started and cleaned up either by generic-update-mp job or generic-land.
181177
182 :param mp: launchpad merge proposal handle178 :param mp: launchpad merge proposal handle
179 :param jenkins_job: name of the jenkins job that is going to be triggered
183 """180 """
184 review = MergeProposalReview(mp)181 if jenkinsutils.is_job_or_downstream_building(
185 if review.is_locked():182 jenkins_job, job_params={'merge_proposal': mp.web_link}):
186 logger.debug('Skipping this MP. It is curently being ' +183 logger.debug('Skipping this MP. It is curently being ' +
187 'tested by Jenkins.')184 'tested by Jenkins.')
188 return True185 return True
@@ -279,8 +276,6 @@
279 mp.createComment(review_type=get_config_option('launchpad_review_type'),276 mp.createComment(review_type=get_config_option('launchpad_review_type'),
280 vote=LaunchpadVote.APPROVE, subject=state,277 vote=LaunchpadVote.APPROVE, subject=state,
281 content=content)278 content=content)
282 review = MergeProposalReview(mp)
283 review.unlock()
284279
285280
286def disapprove_mp(mp, revision, build_url, reason=None):281def disapprove_mp(mp, revision, build_url, reason=None):
@@ -305,8 +300,6 @@
305 mp.createComment(review_type=get_config_option('launchpad_review_type'),300 mp.createComment(review_type=get_config_option('launchpad_review_type'),
306 vote=LaunchpadVote.NEEDS_FIXING, subject=subject,301 vote=LaunchpadVote.NEEDS_FIXING, subject=subject,
307 content=content)302 content=content)
308 review = MergeProposalReview(mp)
309 review.unlock()
310303
311304
312class UpdateMergeProposalException(Exception):305class UpdateMergeProposalException(Exception):
313306
=== removed file 'jlp/mergeproposalreview.py'
--- jlp/mergeproposalreview.py 2013-05-10 14:10:52 +0000
+++ jlp/mergeproposalreview.py 1970-01-01 00:00:00 +0000
@@ -1,55 +0,0 @@
1import os
2import hashlib
3from jlp import logger, get_config_option
4
5
6class MergeProposalReview():
7 _mp = None
8
9 def __init__(self, mp):
10 self._mp = mp
11
12 @property
13 def mp(self):
14 """Merge proposal."""
15
16 return self._mp
17
18 def _get_hash(self):
19 return hashlib.sha224(self.mp.web_link).hexdigest()
20
21 def get_lockfile(self):
22 return '{dir}/{hash}'.format(
23 dir=get_config_option('launchpadlocks_dir'),
24 hash=self._get_hash())
25
26 def lock(self, job_name):
27 locks_dir = get_config_option('launchpadlocks_dir')
28 if not os.path.exists(locks_dir):
29 os.makedirs(locks_dir)
30 lockfile = open(self.get_lockfile(), 'w')
31 lock_info = "{jenkins_job}\n{merge_proposal}\n"
32 lock_info = lock_info.format(jenkins_job=job_name,
33 merge_proposal=self.mp.web_link)
34 lockfile.write(lock_info)
35 lockfile.close()
36
37 def unlock(self):
38 try:
39 os.remove(self.get_lockfile())
40 return True
41 except OSError as error:
42 if error.errno == 2:
43 logger.debug("mp lock doesn't exists. Ignoring this unlock.")
44 return False
45
46 def get_jenkins_job(self):
47 if self.is_locked():
48 lockfile = open(self.get_lockfile(), 'r')
49 job_name = lockfile.readline().rstrip()
50 return job_name
51 else:
52 return None
53
54 def is_locked(self):
55 return os.path.exists(self.get_lockfile())
560
=== removed file 'tests/test_MergeProposalReview.py'
--- tests/test_MergeProposalReview.py 2013-04-26 11:24:46 +0000
+++ tests/test_MergeProposalReview.py 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
1import unittest
2from jlp import MergeProposalReview
3from mock import MagicMock
4
5
6class MergeProposalReviewTestCase(unittest.TestCase):
7 review = None
8 job_name = 'test-job'
9
10 def setUp(self):
11 mp = MagicMock()
12 mp.web_link = 'http://my-example-url.com'
13 self.review = MergeProposalReview(mp)
14
15 def test_is_unlocked(self):
16 self.review.unlock()
17 self.assertFalse(self.review.is_locked())
18
19 def test_is_locked(self):
20 self.review.lock(self.job_name)
21 self.assertTrue(self.review.is_locked())
22
23 def test_get_job_name(self):
24 self.review.lock(self.job_name)
25 self.assertEquals(self.review.get_jenkins_job(), self.job_name)
26
27 def test_get_job_name_with_unlocked_mp(self):
28 self.review.unlock()
29 self.assertEquals(self.review.get_jenkins_job(), None)
300
=== modified file 'tests/test_autoland.py'
--- tests/test_autoland.py 2013-05-17 15:53:01 +0000
+++ tests/test_autoland.py 2013-06-05 09:44:54 +0000
@@ -172,16 +172,10 @@
172 self.branch_patch = patch('jlp.commands.autoland.Branch', new=branch)172 self.branch_patch = patch('jlp.commands.autoland.Branch', new=branch)
173 self.target = target173 self.target = target
174 self.Branch = self.branch_patch.start()174 self.Branch = self.branch_patch.start()
175 self.MergeProposalReview = MagicMock()
176 self.merge_proposal_review_patch = patch(
177 'jlp.commands.autoland.MergeProposalReview',
178 new=MagicMock(return_value=self.MergeProposalReview))
179 self.merge_proposal_review_patch.start()
180175
181 def tearDown(self):176 def tearDown(self):
182 super(TestAutolandForMergeAndCommit, self).tearDown()177 super(TestAutolandForMergeAndCommit, self).tearDown()
183 self.branch_patch.stop()178 self.branch_patch.stop()
184 self.merge_proposal_review_patch.stop()
185179
186180
187class TestAutolandMergeAndCommit(TestAutolandForMergeAndCommit):181class TestAutolandMergeAndCommit(TestAutolandForMergeAndCommit):
@@ -194,7 +188,6 @@
194 self.assertTrue(self.target.merge.call_count == 1)188 self.assertTrue(self.target.merge.call_count == 1)
195 self.assertTrue(self.target.commit.call_count == 1)189 self.assertTrue(self.target.commit.call_count == 1)
196 self.assertTrue(close_bugs_mock.call_count == 1)190 self.assertTrue(close_bugs_mock.call_count == 1)
197 self.MergeProposalReview.unlock.assert_called_once_with()
198191
199192
200class TestAutolandVoting(TestAutolandForMergeAndCommit):193class TestAutolandVoting(TestAutolandForMergeAndCommit):
@@ -244,7 +237,6 @@
244 self.assertTrue(self.target.merge.call_count == 1)237 self.assertTrue(self.target.merge.call_count == 1)
245 self.assertTrue(self.target.commit.call_count == 1)238 self.assertTrue(self.target.commit.call_count == 1)
246 self.assertTrue(close_bugs_mock.call_count == 1)239 self.assertTrue(close_bugs_mock.call_count == 1)
247 self.MergeProposalReview.unlock.assert_called_once_with()
248240
249241
250class TestAutolandWithMultipleDputs(TestAutolandForMergeAndCommit):242class TestAutolandWithMultipleDputs(TestAutolandForMergeAndCommit):
@@ -326,7 +318,6 @@
326 self.assertTrue(self.target.merge.call_count == 1)318 self.assertTrue(self.target.merge.call_count == 1)
327 self.assertTrue(self.target.commit.call_count == 1)319 self.assertTrue(self.target.commit.call_count == 1)
328 self.assertTrue(close_bugs_mock.call_count == 1)320 self.assertTrue(close_bugs_mock.call_count == 1)
329 self.MergeProposalReview.unlock.assert_called_once_with()
330 self.assertTrue(self.dputRunner.dput.call_count == 1)321 self.assertTrue(self.dputRunner.dput.call_count == 1)
331322
332323
@@ -347,15 +338,11 @@
347 super(TestAutolandMergeAndCommitMergeConflict, self).tearDown()338 super(TestAutolandMergeAndCommitMergeConflict, self).tearDown()
348 self.branch_patch.stop()339 self.branch_patch.stop()
349340
350 @patch('jlp.commands.autoland.MergeProposalReview')341 def test_merge_and_commit_merge_conflict(self):
351 def test_merge_and_commit_merge_conflict(self, MergeProposalReview):
352 sys_argv = ['autoland.py', '-r', 'PASSED',342 sys_argv = ['autoland.py', '-r', 'PASSED',
353 '-v', '123', '-m', 'url']343 '-v', '123', '-m', 'url']
354 review = MagicMock()
355 MergeProposalReview.return_value = review
356 with patch('sys.argv', sys_argv):344 with patch('sys.argv', sys_argv):
357 self.assertTrue(autoland.autoland() == 1)345 self.assertTrue(autoland.autoland() == 1)
358 self.assertEqual(review.unlock.call_count, 1)
359346
360347
361class TestAutolandMergeAndCommitPushFailed(TestAutoland):348class TestAutolandMergeAndCommitPushFailed(TestAutoland):
@@ -374,15 +361,11 @@
374 super(TestAutolandMergeAndCommitPushFailed, self).tearDown()361 super(TestAutolandMergeAndCommitPushFailed, self).tearDown()
375 self.branch_patch.stop()362 self.branch_patch.stop()
376363
377 @patch('jlp.commands.autoland.MergeProposalReview')364 def test_merge_and_commit_push_error(self):
378 def test_merge_and_commit_push_error(self, MergeProposalReview):
379 sys_argv = ['autoland.py', '-r', 'PASSED',365 sys_argv = ['autoland.py', '-r', 'PASSED',
380 '-v', '123', '-m', 'url']366 '-v', '123', '-m', 'url']
381 review = MagicMock()
382 MergeProposalReview.return_value = review
383 with patch('sys.argv', sys_argv):367 with patch('sys.argv', sys_argv):
384 self.assertTrue(autoland.autoland() == 1)368 self.assertTrue(autoland.autoland() == 1)
385 self.assertEqual(review.unlock.call_count, 1)
386369
387370
388class TestAutolandMergeAndCommitLockFailed(TestAutoland):371class TestAutolandMergeAndCommitLockFailed(TestAutoland):
@@ -401,15 +384,11 @@
401 super(TestAutolandMergeAndCommitLockFailed, self).tearDown()384 super(TestAutolandMergeAndCommitLockFailed, self).tearDown()
402 self.branch_patch.stop()385 self.branch_patch.stop()
403386
404 @patch('jlp.commands.autoland.MergeProposalReview')387 def test_merge_and_commit_lock_failed(self):
405 def test_merge_and_commit_lock_failed(self, MergeProposalReview):
406 sys_argv = ['autoland.py', '-r', 'PASSED',388 sys_argv = ['autoland.py', '-r', 'PASSED',
407 '-v', '123', '-m', 'url']389 '-v', '123', '-m', 'url']
408 review = MagicMock()
409 MergeProposalReview.return_value = review
410 with patch('sys.argv', sys_argv):390 with patch('sys.argv', sys_argv):
411 self.assertTrue(autoland.autoland() == 1)391 self.assertTrue(autoland.autoland() == 1)
412 self.assertEqual(review.unlock.call_count, 1)
413392
414393
415class TestAutolandMergeAndCommitUnapprovedChange(TestAutoland):394class TestAutolandMergeAndCommitUnapprovedChange(TestAutoland):
416395
=== modified file 'tests/test_jenkinsutils.py'
--- tests/test_jenkinsutils.py 2013-05-20 11:44:39 +0000
+++ tests/test_jenkinsutils.py 2013-06-05 09:44:54 +0000
@@ -70,6 +70,476 @@
70 "%s" % (err.exception))70 "%s" % (err.exception))
7171
7272
73class TestIsJobOrDownstreamBuilding(unittest.TestCase):
74 def setUp(self):
75 self.get_config_option_patch = patch(
76 'jlp.jenkinsutils.get_config_option', new=lambda option: '')
77 self.get_config_option_patch.start()
78 self.get_json_jenkins = self.get_json_jenkins_patch = patch(
79 'jlp.jenkinsutils.get_json_jenkins')
80 self.get_json_jenkins_patch.start()
81
82 def tearDown(self):
83 self.get_config_option_patch.stop()
84 self.get_json_jenkins_patch.stop()
85
86 @patch('jlp.jenkinsutils.get_running_builds',
87 new=lambda job, job_params: [1])
88 def test_job_is_building(self):
89 self.assertTrue(
90 jenkinsutils.is_job_or_downstream_building('job', {}))
91
92 @patch('jlp.jenkinsutils.get_running_builds',
93 new=lambda job, job_params: [])
94 @patch('jlp.jenkinsutils.is_job_queued',
95 new=lambda job, queue, job_params: True)
96 def test_job_is_queued(self):
97 self.assertTrue(
98 jenkinsutils.is_job_or_downstream_building('job', {}))
99
100 @patch('jlp.jenkinsutils.get_running_builds',
101 new=lambda x, job_params: [])
102 @patch('jlp.jenkinsutils.get_downstream_projects', new=lambda x, z: [1])
103 def test_downstream_job_is_queued(self):
104 def is_job_queued(job, queue, job_params={}, upstream_job=None,
105 upstream_params={}):
106 if upstream_job:
107 return True
108 return False
109 with patch('jlp.jenkinsutils.is_job_queued', new=is_job_queued):
110 self.assertTrue(jenkinsutils.is_job_or_downstream_building(
111 'job', {}))
112
113
114class TestIsJobOrDownstreamBuildingScenarios(TestWithScenarios):
115 scenarios = [
116 ('downstream_job_building_no_params_check',
117 {'expected': True,
118 'job_params': {},
119 'jenkins_data': {
120 'builds': [{
121 'building': True,
122 'actions': [{
123 'causes': [{
124 'upstreamProject': 'job',
125 'upstreamBuild': 22
126 }]
127 }],
128 'result': None}],
129 'upstreamProjects': [{
130 'name': 'job',
131 'builds': [{
132 'number': 22,
133 'actions': [{'parameters': [{
134 'name': 'merge_proposal',
135 'value': 'http://mp3'}]}]}]}]}}),
136 ('downstream_job_building_with_params_check',
137 {'expected': True,
138 'job_params': {'merge_proposal': 'http://mp'},
139 'jenkins_data': {
140 'builds': [{
141 'building': True,
142 'actions': [{
143 'causes': [{
144 'upstreamProject': 'job',
145 'upstreamBuild': 22
146 }]
147 }],
148 'result': None}],
149 'upstreamProjects': [{
150 'name': 'job',
151 'builds': [
152 {
153 'number': 23,
154 'actions': [{'parameters': [{
155 'name': 'merge_proposal',
156 'value': 'http://different-mp'}]}]},
157 {
158 'number': 22,
159 'actions': [{'parameters': [{
160 'name': 'merge_proposal',
161 'value': 'http://mp'}]}]}
162 ]}]}}),
163 ('downstream_job_building_with_different_mp',
164 {'expected': False,
165 'job_params': {'merge_proposal': 'http://mp'},
166 'jenkins_data': {
167 'builds': [{
168 'building': True,
169 'actions': [{
170 'causes': [{
171 'upstreamProject': 'job',
172 'upstreamBuild': 22
173 }]
174 }],
175 'result': None}],
176 'upstreamProjects': [{
177 'name': 'job',
178 'builds': [
179 {
180 'number': 22,
181 'actions': [{'parameters': [{
182 'name': 'merge_proposal',
183 'value': 'http://different-mp'}]}]}
184 ]}]}}),
185
186 ('no_action',
187 {'expected': False,
188 'job_params': {},
189 'jenkins_data': {
190 'builds': [{
191 'result': None
192 }]}}),
193 ('different_upstream',
194 {'expected': False,
195 'job_params': {},
196 'jenkins_data': {
197 'builds': [{
198 'building': True,
199 'actions': [{
200 'causes': [{
201 'upstreamProject': 'different_job',
202 'upstreamBuild': 23
203 }]
204 }],
205 'result': None
206 }]}}),
207 ('no_causes',
208 {'expected': False,
209 'job_params': {},
210 'jenkins_data': {
211 'builds': [{
212 'actions': [{
213 }],
214 'result': None
215 }]}}),
216 ('no_upstream',
217 {'expected': False,
218 'job_params': {},
219 'jenkins_data': {
220 'builds': [{
221 'building': True,
222 'actions': [{
223 'causes': [{
224 }]
225 }],
226 'result': None
227 }]}}),
228 ]
229
230 @patch('jlp.jenkinsutils.get_config_option', new=lambda x: '')
231 @patch('jlp.jenkinsutils.get_running_builds',
232 new=lambda job, job_params={}: [])
233 @patch('jlp.jenkinsutils.get_downstream_projects',
234 new=lambda x, z: ['downstream_job'])
235 @patch('jlp.jenkinsutils.is_job_queued',
236 new=lambda x, y, upstream_job=None,
237 job_params={}, upstream_params={}: False)
238 def test_is_job_or_downstream_building(self):
239
240 jenkins = MagicMock()
241 jenkins.get_json_data = lambda x, append_api=True: self.jenkins_data
242 with patch('jlp.jenkinsutils.get_json_jenkins',
243 new=MagicMock(return_value=jenkins)):
244 self.assertEquals(
245 jenkinsutils.is_job_or_downstream_building(
246 'job', self.job_params),
247 self.expected)
248
249
250class TestGetRunningBuilds(TestWithScenarios):
251 scenarios = [
252 ('single_job_running',
253 {'expected': [1],
254 'job_params': {},
255 'jenkins_data': {
256 'builds': [{
257 'building': True,
258 'number': 1,
259 'actions': []
260 }]}}),
261 ('single_job_running_different_params',
262 {'expected': [],
263 'job_params': {'merge_proposal': 'http://mp'},
264 'jenkins_data': {
265 'builds': [{
266 'building': True,
267 'number': 1,
268 'actions': [{
269 'parameters': [{
270 'name': 'merge_proposal',
271 'value': 'http://different-mp'
272 }]}]
273 }]}}),
274 ('single_job_running_same_params',
275 {'expected': [1],
276 'job_params': {'merge_proposal': 'http://mp'},
277 'jenkins_data': {
278 'builds': [{
279 'building': True,
280 'number': 1,
281 'actions': [{
282 'parameters': [{
283 'name': 'merge_proposal',
284 'value': 'http://mp'
285 }]}]
286 }]}}),
287 ('two_jobs_running',
288 {'expected': [1, 2],
289 'job_params': {},
290 'jenkins_data': {
291 'builds': [{
292 'building': True,
293 'actions': [],
294 'number': 1},
295 {'building': True,
296 'actions': [],
297 'number': 2}
298 ]}}),
299 ('two_jobs_running_only_one_matches',
300 {'expected': [2],
301 'job_params': {'merge_proposal': 'http://mp'},
302 'jenkins_data': {
303 'builds': [{
304 'building': True,
305 'actions': [],
306 'number': 1},
307 {'building': True,
308 'actions': [{
309 'parameters': [{
310 'name': 'merge_proposal',
311 'value': 'http://mp'
312 }]}],
313 'number': 2}
314 ]}}),
315 ('one_job_running_out_of_two',
316 {'expected': [2],
317 'job_params': {},
318 'jenkins_data': {
319 'builds': [{
320 'building': False,
321 'actions': [],
322 'number': 1},
323 {'building': True,
324 'actions': [],
325 'number': 2}
326 ]}}),
327 ('no_job_running',
328 {'expected': [],
329 'job_params': {},
330 'jenkins_data': {
331 'builds': [{
332 'building': False,
333 'actions': [],
334 'number': 1},
335 {'building': False,
336 'actions': [],
337 'number': 2}
338 ]}}),
339 ('no_builds',
340 {'expected': [],
341 'job_params': {},
342 'jenkins_data': {}}),
343 ]
344
345 @patch('jlp.jenkinsutils.get_config_option', new=lambda x: '')
346 def test_get_running_builds(self):
347
348 jenkins = MagicMock()
349 jenkins.get_json_data = lambda x, append_api=True: self.jenkins_data
350 with patch('jlp.jenkinsutils.get_json_jenkins',
351 new=MagicMock(return_value=jenkins)):
352 self.assertEquals(
353 jenkinsutils.get_running_builds('job', self.job_params),
354 self.expected)
355
356
357class TestIsJobQueued(TestWithScenarios):
358 scenarios = [
359 ('no_items',
360 {'expected': False,
361 'upstream_job': None,
362 'job_params': {},
363 'queue': {'items': []}}),
364 ('different_job_in_queue',
365 {'expected': False,
366 'upstream_job': None,
367 'job_params': {},
368 'queue': {'items': [{
369 'task': {'name': 'different-job'}
370 }]}}),
371 ('no_upstream_job_and_match',
372 {'expected': True,
373 'upstream_job': None,
374 'job_params': {},
375 'queue': {'items': [{
376 'task': {'name': 'job'}
377 }]}}),
378 ('no_upstream_job_and_param_match',
379 {'expected': True,
380 'upstream_job': None,
381 'job_params': {'merge_proposal': 'http://mp'},
382 'queue': {'items': [{
383 'task': {'name': 'job'},
384 'actions': [{
385 'parameters': [{
386 'name': 'merge_proposal',
387 'value': 'http://mp'}]}]
388 }]}}),
389 ('no_upstream_job_and_param_mismatch',
390 {'expected': False,
391 'upstream_job': None,
392 'job_params': {'merge_proposal': 'http://mp'},
393 'queue': {'items': [{
394 'task': {'name': 'job'},
395 'actions': [{
396 'parameters': [{
397 'name': 'merge_proposal',
398 'value': 'http://different-mp'}]}]
399 }]}}),
400 ('no_actions',
401 {'expected': False,
402 'upstream_job': 'upstream-job',
403 'job_params': {},
404 'queue': {'items': [{
405 'task': {'name': 'job'}
406 }]}}),
407 ('no_causes',
408 {'expected': False,
409 'upstream_job': 'upstream-job',
410 'job_params': {},
411 'queue': {'items': [{
412 'task': {'name': 'job'},
413 'actions': [{}]
414 }]}}),
415 ('triggered_by_upstream',
416 {'expected': True,
417 'upstream_job': 'upstream-job',
418 'job_params': {},
419 'queue': {'items': [{
420 'task': {'name': 'job'},
421 'actions': [{
422 'causes': [{
423 'upstreamProject': 'upstream-job',
424 'upstreamBuild': 22,
425 }]
426 }]
427 }]}}),
428 ('triggered_by_upstream_with_the_right_mp',
429 {'expected': True,
430 'upstream_job': 'upstream-job',
431 'job_params': {},
432 'upstream_params': {'merge_proposal': 'http://mp'},
433 'get_build_actions': [{
434 'parameters': [{
435 'name': 'merge_proposal',
436 'value': 'http://mp'
437 }]}],
438 'queue': {'items': [{
439 'task': {'name': 'job'},
440 'actions': [{
441 'causes': [{
442 'upstreamProject': 'upstream-job',
443 'upstreamBuild': 22,
444 }]
445 }]
446 }]}}),
447 ('triggered_by_upstream_with_no_params',
448 {'expected': False,
449 'upstream_job': 'upstream-job',
450 'job_params': {},
451 'upstream_params': {'merge_proposal': 'http://mp'},
452 'get_build_actions': [{}],
453 'queue': {'items': [{
454 'task': {'name': 'job'},
455 'actions': [{
456 'causes': [{
457 'upstreamProject': 'upstream-job',
458 'upstreamBuild': 22,
459 }]
460 }]
461 }]}}),
462
463 ('triggered_by_upstream_but_different_mp',
464 {'expected': False,
465 'upstream_job': 'upstream-job',
466 'job_params': {},
467 'upstream_params': {'merge_proposal': 'http://mp'},
468 'get_build_actions': [{
469 'parameters': [{
470 'name': 'merge_proposal',
471 'value': 'http://different-mp'
472 }]}],
473 'queue': {'items': [{
474 'task': {'name': 'job'},
475 'actions': [{
476 'causes': [{
477 'upstreamProject': 'upstream-job',
478 'upstreamBuild': 22,
479 }]
480 }]
481 }]}}),
482 ('triggered_by_different_upstream',
483 {'expected': False,
484 'upstream_job': 'upstream-job',
485 'job_params': {},
486 'queue': {'items': [{
487 'task': {'name': 'job'},
488 'actions': [{
489 'causes': [{
490 'upstreamProject': 'different-upstream-job',
491 'upstreamBuild': 23
492 }]
493 }]
494 }]}}),
495 ('no_queue_provided',
496 {'expected': False,
497 'upstream_job': 'upstream-job',
498 'job_params': {},
499 'queue': None}),
500
501 ]
502
503 @patch('jlp.jenkinsutils._get_build_actions')
504 @patch('jlp.jenkinsutils.get_json_jenkins')
505 def test_is_job_queued(self, get_json_jenkins, get_build_actions):
506 json_jenkins = MagicMock()
507 json_jenkins.get_json_data.return_value = {'items': []}
508 get_json_jenkins.return_value = json_jenkins
509
510 if 'get_build_actions' in dir(self):
511 get_build_actions.return_value = self.get_build_actions
512 if 'upstream_params' in dir(self):
513 upstream_params = self.upstream_params
514 else:
515 upstream_params = {}
516 self.assertEquals(
517 jenkinsutils.is_job_queued(
518 'job', queue=self.queue, job_params=self.job_params,
519 upstream_job=self.upstream_job,
520 upstream_params=upstream_params),
521 self.expected)
522
523
524class TestGetBuildActions(unittest.TestCase):
525 @patch('jlp.jenkinsutils.get_json_jenkins')
526 def test_no_actions(self, get_json_jenkins):
527 json_jenkins = MagicMock()
528 json_jenkins.get_json_data.return_value = {}
529 get_json_jenkins.return_value = json_jenkins
530 self.assertEquals(jenkinsutils._get_build_actions('job', 23),
531 [])
532
533 @patch('jlp.jenkinsutils.get_json_jenkins')
534 def test_actions_are_returned(self, get_json_jenkins):
535 actions = object()
536 json_jenkins = MagicMock()
537 json_jenkins.get_json_data.return_value = {'actions': actions}
538 get_json_jenkins.return_value = json_jenkins
539 ret = jenkinsutils._get_build_actions('job', 23)
540 self.assertTrue(actions == ret)
541
542
73class JenkinsUtilsGetDownstreamBuilds(unittest.TestCase):543class JenkinsUtilsGetDownstreamBuilds(unittest.TestCase):
74544
75 def setUp(self):545 def setUp(self):
@@ -663,7 +1133,8 @@
663 jenkinsutils.trigger_ci_build(lp_handle, [], 'job', 'url')1133 jenkinsutils.trigger_ci_build(lp_handle, [], 'job', 'url')
664 self.assertEqual(start_jenkins_job.call_count, 0)1134 self.assertEqual(start_jenkins_job.call_count, 0)
6651135
666 @patch('jlp.launchpadutils.testing_in_progress', new=lambda x: True)1136 @patch('jlp.launchpadutils.testing_in_progress',
1137 new=lambda mp, jenkins_job: True)
667 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs')1138 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs')
668 @patch('jlp.jenkinsutils.start_jenkins_job')1139 @patch('jlp.jenkinsutils.start_jenkins_job')
669 def test_trigger_ci_while_testing_in_progress(self,1140 def test_trigger_ci_while_testing_in_progress(self,
@@ -676,7 +1147,8 @@
6761147
677 @patch('jlp.launchpadutils.latest_candidate_validated',1148 @patch('jlp.launchpadutils.latest_candidate_validated',
678 new=lambda x, y: True)1149 new=lambda x, y: True)
679 @patch('jlp.launchpadutils.testing_in_progress', new=lambda x: False)1150 @patch('jlp.launchpadutils.testing_in_progress',
1151 new=lambda mp, jenkins_job: False)
680 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs')1152 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs')
681 @patch('jlp.jenkinsutils.start_jenkins_job')1153 @patch('jlp.jenkinsutils.start_jenkins_job')
682 def test_trigger_ci_while_Revision_validated(self,1154 def test_trigger_ci_while_Revision_validated(self,
@@ -690,7 +1162,8 @@
6901162
691 @patch('jlp.launchpadutils.latest_candidate_validated',1163 @patch('jlp.launchpadutils.latest_candidate_validated',
692 new=lambda x, y: False)1164 new=lambda x, y: False)
693 @patch('jlp.launchpadutils.testing_in_progress', new=lambda x: False)1165 @patch('jlp.launchpadutils.testing_in_progress',
1166 new=lambda mp, jenkins_job: False)
694 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs')1167 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs')
695 @patch('jlp.jenkinsutils.start_jenkins_job')1168 @patch('jlp.jenkinsutils.start_jenkins_job')
696 def test_trigger_ci_while_Revision_not_validated(self,1169 def test_trigger_ci_while_Revision_not_validated(self,
@@ -743,7 +1216,7 @@
743 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs',1216 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs',
744 new=lambda x: True)1217 new=lambda x: True)
745 @patch('jlp.launchpadutils.testing_in_progress',1218 @patch('jlp.launchpadutils.testing_in_progress',
746 new=lambda mp: True)1219 new=lambda mp, jenkins_job: True)
747 @patch('jlp.jenkinsutils.start_jenkins_job')1220 @patch('jlp.jenkinsutils.start_jenkins_job')
748 def test_trigger_al_while_testing_in_progress(self, start_jenkins_job):1221 def test_trigger_al_while_testing_in_progress(self, start_jenkins_job):
749 """When testing is in progress jenkins job must not be started"""1222 """When testing is in progress jenkins job must not be started"""
@@ -756,7 +1229,7 @@
756 @patch('jlp.launchpadutils.unapproved_prerequisite_exists',1229 @patch('jlp.launchpadutils.unapproved_prerequisite_exists',
757 new=lambda x: True)1230 new=lambda x: True)
758 @patch('jlp.launchpadutils.testing_in_progress',1231 @patch('jlp.launchpadutils.testing_in_progress',
759 new=lambda x: False)1232 new=lambda x, y: False)
760 @patch('jlp.launchpadutils.is_commit_message_set',1233 @patch('jlp.launchpadutils.is_commit_message_set',
761 new=lambda x, y, z: True)1234 new=lambda x, y, z: True)
762 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs',1235 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs',
@@ -777,7 +1250,7 @@
777 @patch('jlp.launchpadutils.unapproved_prerequisite_exists',1250 @patch('jlp.launchpadutils.unapproved_prerequisite_exists',
778 new=lambda x: False)1251 new=lambda x: False)
779 @patch('jlp.launchpadutils.testing_in_progress',1252 @patch('jlp.launchpadutils.testing_in_progress',
780 new=lambda x: False)1253 new=lambda x, y: False)
781 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs',1254 @patch('jlp.launchpadutils.user_allowed_to_trigger_jobs',
782 new=lambda x: True)1255 new=lambda x: True)
783 @patch('jlp.jenkinsutils.start_jenkins_job')1256 @patch('jlp.jenkinsutils.start_jenkins_job')
@@ -804,6 +1277,10 @@
804 self.start_jenkins_job_patch = patch(1277 self.start_jenkins_job_patch = patch(
805 'jlp.jenkinsutils.start_jenkins_job')1278 'jlp.jenkinsutils.start_jenkins_job')
806 self.start_jenkins_job_patch.start()1279 self.start_jenkins_job_patch.start()
1280 self.unapproved_prerequisite_exists_patch = patch(
1281 'jlp.launchpadutils.unapproved_prerequisite_exists',
1282 new=lambda mp: False)
1283 self.unapproved_prerequisite_exists_patch.start()
807 #return True so we stop the flow; We just want to test the permissions1284 #return True so we stop the flow; We just want to test the permissions
808 #above and not the whole trigger_al_build method1285 #above and not the whole trigger_al_build method
809 test_in_progress = MagicMock(return_value=True)1286 test_in_progress = MagicMock(return_value=True)
@@ -817,6 +1294,7 @@
817 self.testing_in_progress_patch.stop()1294 self.testing_in_progress_patch.stop()
818 self.user_allowed_to_trigger_jobs_patch.stop()1295 self.user_allowed_to_trigger_jobs_patch.stop()
819 self.start_jenkins_job_patch.stop()1296 self.start_jenkins_job_patch.stop()
1297 self.unapproved_prerequisite_exists_patch.stop()
8201298
821 def test_trigger_al_with_trusted_reviewer(self):1299 def test_trigger_al_with_trusted_reviewer(self):
822 mp = MagicMock()1300 mp = MagicMock()
8231301
=== modified file 'tests/test_launchpadutils.py'
--- tests/test_launchpadutils.py 2013-05-10 14:10:52 +0000
+++ tests/test_launchpadutils.py 2013-06-05 09:44:54 +0000
@@ -1,5 +1,5 @@
1import unittest1import unittest
2from jlp import launchpadutils, MergeProposalReview, get_config_option2from jlp import launchpadutils, get_config_option
3from mock import MagicMock, patch3from mock import MagicMock, patch
4from testscenarios import TestWithScenarios4from testscenarios import TestWithScenarios
5from textwrap import dedent5from textwrap import dedent
@@ -301,27 +301,26 @@
301 self.assertTrue(ret)301 self.assertTrue(ret)
302 self._cleanup()302 self._cleanup()
303303
304 @patch('jlp.launchpadutils.jenkinsutils.is_job_or_downstream_building',
305 new=lambda jenkins_job, job_params: False)
304 def test_testing_not_in_progress_approved(self):306 def test_testing_not_in_progress_approved(self):
305 """ If there is no MergeProposalReview lock then testing_in_progress307 """ If there is no job running then testing_in_progress
306 must return False.308 must return False.
307 """309 """
308 mp = MagicMock()310 mp = MagicMock()
309 mp.web_link = 'http://my-example-url.com'311 mp.web_link = 'http://my-example-url.com'
310 review = MergeProposalReview(mp)312 self.assertFalse(launchpadutils.testing_in_progress(mp, 'job'))
311 review.unlock()
312 self.assertFalse(launchpadutils.testing_in_progress(mp))
313313
314 @patch('jlp.launchpadutils.jenkinsutils.is_job_or_downstream_building',
315 new=lambda jenkins_job, job_params: True)
314 def test_testing_not_in_progress_abstain(self):316 def test_testing_not_in_progress_abstain(self):
315 """ If a MergeProposalReview lock exists then testing_in_progress317 """ If there is job running
316 must return True and it must report the correct jenkins job which318 then testing_in_progress must return True and it must report the
317 holds the lock.319 correct jenkins job which holds the lock.
318 """320 """
319 mp = MagicMock()321 mp = MagicMock()
320 mp.web_link = 'http://my-example-url.com'322 mp.web_link = 'http://my-example-url.com'
321 review = MergeProposalReview(mp)323 self.assertTrue(launchpadutils.testing_in_progress(mp, 'job'))
322 review.lock('my-job')
323 self.assertTrue(launchpadutils.testing_in_progress(mp))
324 self.assertEquals(review.get_jenkins_job(), 'my-job')
325324
326325
327class TestLatestReview(unittest.TestCase):326class TestLatestReview(unittest.TestCase):
328327
=== added file 'tests/test_socketlock.py'
--- tests/test_socketlock.py 1970-01-01 00:00:00 +0000
+++ tests/test_socketlock.py 2013-06-05 09:44:54 +0000
@@ -0,0 +1,117 @@
1from testtools import TestCase
2from testtools.matchers import Equals
3from jlp import SocketLock
4import socket
5from lockfile import LockTimeout, AlreadyLocked
6from multiprocessing import Process
7from autopilot.matchers import Eventually
8import sys
9import os
10import time
11from jlp import get_config_option
12
13LOCK_STATUS_ACQUIRED = 0
14LOCK_STATUS_TIMEOUT = 1
15LOCK_STATUS_ALREADYLOCKED = 2
16
17
18def run_lock_process(timeout=None, sleeptime=5, kill_process=False):
19 try:
20 with SocketLock(get_config_option('lock_name'), timeout=timeout):
21 time.sleep(sleeptime)
22 if kill_process:
23 os.kill(os.getpid(), 9)
24 except LockTimeout:
25 sys.exit(LOCK_STATUS_TIMEOUT)
26 except AlreadyLocked:
27 sys.exit(LOCK_STATUS_ALREADYLOCKED)
28 sys.exit(LOCK_STATUS_ACQUIRED)
29
30
31def is_locked():
32 try:
33 mysocket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
34 mysocket.bind('\0' + get_config_option('lock_name'))
35 mysocket.close()
36 return False
37 except socket.error:
38 return True
39
40
41class TestLocking(TestCase):
42
43 def setUp(self):
44 super(TestLocking, self).setUp()
45 self.assertThat(is_locked(), Equals(False))
46
47 def test_lock_can_be_acquired(self):
48 """Create a process and check the lock exists """
49 lock_process = Process(target=run_lock_process, args=())
50 lock_process.start()
51 self.assertThat(is_locked, Eventually(Equals(True)))
52 lock_process.join()
53 self.assertThat(lock_process.exitcode, Equals(LOCK_STATUS_ACQUIRED))
54
55 def test_lock_timeouts(self):
56 """Create a process that holds the lock for 20s. Then create another
57 process that timeouts after trying to acquire the lock for 1s.
58 Make sure the timeout happens."""
59
60 # first create a process that holds the lock for 10 seconds
61 holder = Process(target=run_lock_process, args=(120, 20))
62 holder.start()
63 #give the holder some time to acquire the lock
64 self.assertThat(is_locked, Eventually(Equals(True)))
65 #now create a process that will try to acquire the lock with 1s
66 #timeout
67 acquirer = Process(target=run_lock_process, args=(1, 5))
68 acquirer.start()
69 acquirer.join()
70 self.assertThat(acquirer.exitcode, Equals(LOCK_STATUS_TIMEOUT))
71 holder.join()
72 self.assertThat(holder.exitcode, Equals(LOCK_STATUS_ACQUIRED))
73
74 def test_consecutive_holders(self):
75 """spawn 5 process each holding the lock for 1s and make sure
76 they all exit with LOCK_STATUS_ACQUIRED"""
77 holders = []
78 for i in range(0, 5):
79 holders.append(Process(target=run_lock_process, args=(120, 1)))
80 holders[i].start()
81 for i in range(0, 5):
82 holders[i].join()
83 for i in range(0, 5):
84 self.assertThat(holders[i].exitcode, Equals(LOCK_STATUS_ACQUIRED))
85
86 def test_double_locking(self):
87 """spawn 1 holder and then 5 process trying to lock the same lock
88 with 0 timeout. They should all exit with
89 LOCK_STATUS_ALREADYLOCKED"""
90 holder = Process(target=run_lock_process, args=(120, 20))
91 holder.start()
92 #give the holder some time to acquire the lock
93 self.assertThat(is_locked, Eventually(Equals(True)))
94 acquirers = []
95 for i in range(0, 5):
96 acquirers.append(Process(target=run_lock_process, args=(0, 1)))
97 acquirers[i].start()
98 for i in range(0, 5):
99 acquirers[i].join()
100 for i in range(0, 5):
101 self.assertThat(acquirers[i].exitcode,
102 Equals(LOCK_STATUS_ALREADYLOCKED))
103 holder.join()
104 self.assertThat(holder.exitcode, Equals(LOCK_STATUS_ACQUIRED))
105
106 def test_nonrunning_holder(self):
107 """Create a lock which is not released properly and check if the
108 consequtive process can reclaim it."""
109 for i in range(0, 5):
110 holder = Process(target=run_lock_process, args=(120, 1, True))
111 holder.start()
112 holder.join()
113 self.assertThat(-9, Equals(holder.exitcode))
114 holder = Process(target=run_lock_process, args=(0, 1))
115 holder.start()
116 holder.join()
117 self.assertThat(holder.exitcode, Equals(LOCK_STATUS_ACQUIRED))
0118
=== removed file 'tests/test_socketlock.py'
--- tests/test_socketlock.py 2013-05-10 14:10:52 +0000
+++ tests/test_socketlock.py 1970-01-01 00:00:00 +0000
@@ -1,117 +0,0 @@
1from testtools import TestCase
2from testtools.matchers import Equals
3from jlp import SocketLock
4import socket
5from lockfile import LockTimeout, AlreadyLocked
6from multiprocessing import Process
7from autopilot.matchers import Eventually
8import sys
9import os
10import time
11from jlp import get_config_option
12
13LOCK_STATUS_ACQUIRED = 0
14LOCK_STATUS_TIMEOUT = 1
15LOCK_STATUS_ALREADYLOCKED = 2
16
17
18def run_lock_process(timeout=None, sleeptime=5, kill_process=False):
19 try:
20 with SocketLock(get_config_option('lock_name'), timeout=timeout):
21 time.sleep(sleeptime)
22 if kill_process:
23 os.kill(os.getpid(), 9)
24 except LockTimeout:
25 sys.exit(LOCK_STATUS_TIMEOUT)
26 except AlreadyLocked:
27 sys.exit(LOCK_STATUS_ALREADYLOCKED)
28 sys.exit(LOCK_STATUS_ACQUIRED)
29
30
31def is_locked():
32 try:
33 mysocket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
34 mysocket.bind('\0' + get_config_option('lock_name'))
35 mysocket.close()
36 return False
37 except socket.error:
38 return True
39
40
41class TestLocking(TestCase):
42
43 def setUp(self):
44 super(TestLocking, self).setUp()
45 self.assertThat(is_locked(), Equals(False))
46
47 def test_lock_can_be_acquired(self):
48 """Create a process and check the lock exists """
49 lock_process = Process(target=run_lock_process, args=())
50 lock_process.start()
51 self.assertThat(is_locked, Eventually(Equals(True)))
52 lock_process.join()
53 self.assertThat(lock_process.exitcode, Equals(LOCK_STATUS_ACQUIRED))
54
55 def test_lock_timeouts(self):
56 """Create a process that holds the lock for 20s. Then create another
57 process that timeouts after trying to acquire the lock for 1s.
58 Make sure the timeout happens."""
59
60 # first create a process that holds the lock for 10 seconds
61 holder = Process(target=run_lock_process, args=(120, 20))
62 holder.start()
63 #give the holder some time to acquire the lock
64 self.assertThat(is_locked, Eventually(Equals(True)))
65 #now create a process that will try to acquire the lock with 1s
66 #timeout
67 acquirer = Process(target=run_lock_process, args=(1, 5))
68 acquirer.start()
69 acquirer.join()
70 self.assertThat(acquirer.exitcode, Equals(LOCK_STATUS_TIMEOUT))
71 holder.join()
72 self.assertThat(holder.exitcode, Equals(LOCK_STATUS_ACQUIRED))
73
74 def test_consecutive_holders(self):
75 """spawn 5 process each holding the lock for 1s and make sure
76 they all exit with LOCK_STATUS_ACQUIRED"""
77 holders = []
78 for i in range(0, 5):
79 holders.append(Process(target=run_lock_process, args=(120, 1)))
80 holders[i].start()
81 for i in range(0, 5):
82 holders[i].join()
83 for i in range(0, 5):
84 self.assertThat(holders[i].exitcode, Equals(LOCK_STATUS_ACQUIRED))
85
86 def test_double_locking(self):
87 """spawn 1 holder and then 5 process trying to lock the same lock
88 with 0 timeout. They should all exit with
89 LOCK_STATUS_ALREADYLOCKED"""
90 holder = Process(target=run_lock_process, args=(120, 20))
91 holder.start()
92 #give the holder some time to acquire the lock
93 self.assertThat(is_locked, Eventually(Equals(True)))
94 acquirers = []
95 for i in range(0, 5):
96 acquirers.append(Process(target=run_lock_process, args=(0, 1)))
97 acquirers[i].start()
98 for i in range(0, 5):
99 acquirers[i].join()
100 for i in range(0, 5):
101 self.assertThat(acquirers[i].exitcode,
102 Equals(LOCK_STATUS_ALREADYLOCKED))
103 holder.join()
104 self.assertThat(holder.exitcode, Equals(LOCK_STATUS_ACQUIRED))
105
106 def test_nonrunning_holder(self):
107 """Create a lock which is not released properly and check if the
108 consequtive process can reclaim it."""
109 for i in range(0, 5):
110 holder = Process(target=run_lock_process, args=(120, 1, True))
111 holder.start()
112 holder.join()
113 self.assertThat(-9, Equals(holder.exitcode))
114 holder = Process(target=run_lock_process, args=(0, 1))
115 holder.start()
116 holder.join()
117 self.assertThat(holder.exitcode, Equals(LOCK_STATUS_ACQUIRED))

Subscribers

People subscribed via source and target branches

to all changes: