blob: 9c76d4371730542eddf379eceb59f15b502aaee6 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
Christian Berendtffba5df2014-06-07 21:30:22 +020017from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070018import gc
19import hashlib
20import json
21import logging
22import os
23import pprint
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070031import socket
32import string
33import subprocess
34import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080035import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070036import threading
37import time
Clark Boylanb640e052014-04-03 16:41:46 -070038
39import git
40import gear
41import fixtures
Clark Boylanb640e052014-04-03 16:41:46 -070042import statsd
43import testtools
Clint Byrum3343e3e2016-11-15 16:05:03 -080044from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070045
Joshua Hesketh352264b2015-08-11 23:42:08 +100046import zuul.connection.gerrit
47import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070048import zuul.scheduler
49import zuul.webapp
50import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100051import zuul.launcher.server
52import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070053import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080054import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070056import zuul.merger.merger
57import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070058import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.reporter.gerrit
60import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110061import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.trigger.gerrit
63import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070064import zuul.trigger.zuultrigger
Clark Boylanb640e052014-04-03 16:41:46 -070065
66FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
67 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070068USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070069
70logging.basicConfig(level=logging.DEBUG,
71 format='%(asctime)s %(name)-32s '
72 '%(levelname)-8s %(message)s')
73
74
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700106 categories = {'approved': ('Approved', -1, 1),
107 'code-review': ('Code-Review', -2, 2),
108 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700263 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
Joshua Hesketh642824b2014-07-01 17:54:59 +1000269 def addApproval(self, category, value, username='reviewer_john',
270 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700271 if not granted_on:
272 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000273 approval = {
274 'description': self.categories[category][0],
275 'type': category,
276 'value': str(value),
277 'by': {
278 'username': username,
279 'email': username + '@example.com',
280 },
281 'grantedOn': int(granted_on)
282 }
Clark Boylanb640e052014-04-03 16:41:46 -0700283 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
284 if x['by']['username'] == username and x['type'] == category:
285 del self.patchsets[-1]['approvals'][i]
286 self.patchsets[-1]['approvals'].append(approval)
287 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000288 'author': {'email': 'author@example.com',
289 'name': 'Patchset Author',
290 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700291 'change': {'branch': self.branch,
292 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
293 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000294 'owner': {'email': 'owner@example.com',
295 'name': 'Change Owner',
296 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700297 'project': self.project,
298 'subject': self.subject,
299 'topic': 'master',
300 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000301 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700302 'patchSet': self.patchsets[-1],
303 'type': 'comment-added'}
304 self.data['submitRecords'] = self.getSubmitRecords()
305 return json.loads(json.dumps(event))
306
307 def getSubmitRecords(self):
308 status = {}
309 for cat in self.categories.keys():
310 status[cat] = 0
311
312 for a in self.patchsets[-1]['approvals']:
313 cur = status[a['type']]
314 cat_min, cat_max = self.categories[a['type']][1:]
315 new = int(a['value'])
316 if new == cat_min:
317 cur = new
318 elif abs(new) > abs(cur):
319 cur = new
320 status[a['type']] = cur
321
322 labels = []
323 ok = True
324 for typ, cat in self.categories.items():
325 cur = status[typ]
326 cat_min, cat_max = cat[1:]
327 if cur == cat_min:
328 value = 'REJECT'
329 ok = False
330 elif cur == cat_max:
331 value = 'OK'
332 else:
333 value = 'NEED'
334 ok = False
335 labels.append({'label': cat[0], 'status': value})
336 if ok:
337 return [{'status': 'OK'}]
338 return [{'status': 'NOT_READY',
339 'labels': labels}]
340
341 def setDependsOn(self, other, patchset):
342 self.depends_on_change = other
343 d = {'id': other.data['id'],
344 'number': other.data['number'],
345 'ref': other.patchsets[patchset - 1]['ref']
346 }
347 self.data['dependsOn'] = [d]
348
349 other.needed_by_changes.append(self)
350 needed = other.data.get('neededBy', [])
351 d = {'id': self.data['id'],
352 'number': self.data['number'],
353 'ref': self.patchsets[patchset - 1]['ref'],
354 'revision': self.patchsets[patchset - 1]['revision']
355 }
356 needed.append(d)
357 other.data['neededBy'] = needed
358
359 def query(self):
360 self.queried += 1
361 d = self.data.get('dependsOn')
362 if d:
363 d = d[0]
364 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
365 d['isCurrentPatchSet'] = True
366 else:
367 d['isCurrentPatchSet'] = False
368 return json.loads(json.dumps(self.data))
369
370 def setMerged(self):
371 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000372 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700373 return
374 if self.fail_merge:
375 return
376 self.data['status'] = 'MERGED'
377 self.open = False
378
379 path = os.path.join(self.upstream_root, self.project)
380 repo = git.Repo(path)
381 repo.heads[self.branch].commit = \
382 repo.commit(self.patchsets[-1]['revision'])
383
384 def setReported(self):
385 self.reported += 1
386
387
Joshua Hesketh352264b2015-08-11 23:42:08 +1000388class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700389 """A Fake Gerrit connection for use in tests.
390
391 This subclasses
392 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
393 ability for tests to add changes to the fake Gerrit it represents.
394 """
395
Joshua Hesketh352264b2015-08-11 23:42:08 +1000396 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700397
Joshua Hesketh352264b2015-08-11 23:42:08 +1000398 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700399 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000400 super(FakeGerritConnection, self).__init__(connection_name,
401 connection_config)
402
James E. Blair7fc8daa2016-08-08 15:37:15 -0700403 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700404 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
405 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000406 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700407 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200408 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700409
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700410 def addFakeChange(self, project, branch, subject, status='NEW',
411 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700412 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700413 self.change_number += 1
414 c = FakeChange(self, self.change_number, project, branch, subject,
415 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700416 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700417 self.changes[self.change_number] = c
418 return c
419
Clark Boylanb640e052014-04-03 16:41:46 -0700420 def review(self, project, changeid, message, action):
421 number, ps = changeid.split(',')
422 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000423
424 # Add the approval back onto the change (ie simulate what gerrit would
425 # do).
426 # Usually when zuul leaves a review it'll create a feedback loop where
427 # zuul's review enters another gerrit event (which is then picked up by
428 # zuul). However, we can't mimic this behaviour (by adding this
429 # approval event into the queue) as it stops jobs from checking what
430 # happens before this event is triggered. If a job needs to see what
431 # happens they can add their own verified event into the queue.
432 # Nevertheless, we can update change with the new review in gerrit.
433
James E. Blair8b5408c2016-08-08 15:37:46 -0700434 for cat in action.keys():
435 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000436 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000437
James E. Blair8b5408c2016-08-08 15:37:46 -0700438 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000439 if 'label' in action:
440 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000441 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000442
Clark Boylanb640e052014-04-03 16:41:46 -0700443 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000444
Clark Boylanb640e052014-04-03 16:41:46 -0700445 if 'submit' in action:
446 change.setMerged()
447 if message:
448 change.setReported()
449
450 def query(self, number):
451 change = self.changes.get(int(number))
452 if change:
453 return change.query()
454 return {}
455
James E. Blairc494d542014-08-06 09:23:52 -0700456 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700457 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700458 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800459 if query.startswith('change:'):
460 # Query a specific changeid
461 changeid = query[len('change:'):]
462 l = [change.query() for change in self.changes.values()
463 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700464 elif query.startswith('message:'):
465 # Query the content of a commit message
466 msg = query[len('message:'):].strip()
467 l = [change.query() for change in self.changes.values()
468 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800469 else:
470 # Query all open changes
471 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700472 return l
James E. Blairc494d542014-08-06 09:23:52 -0700473
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700475 pass
476
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 def getGitUrl(self, project):
478 return os.path.join(self.upstream_root, project.name)
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480
481class BuildHistory(object):
482 def __init__(self, **kw):
483 self.__dict__.update(kw)
484
485 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700486 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
487 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700488
489
490class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200491 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700492 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700493 self.url = url
494
495 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700496 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700497 path = res.path
498 project = '/'.join(path.split('/')[2:-2])
499 ret = '001e# service=git-upload-pack\n'
500 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
501 'multi_ack thin-pack side-band side-band-64k ofs-delta '
502 'shallow no-progress include-tag multi_ack_detailed no-done\n')
503 path = os.path.join(self.upstream_root, project)
504 repo = git.Repo(path)
505 for ref in repo.refs:
506 r = ref.object.hexsha + ' ' + ref.path + '\n'
507 ret += '%04x%s' % (len(r) + 4, r)
508 ret += '0000'
509 return ret
510
511
Clark Boylanb640e052014-04-03 16:41:46 -0700512class FakeStatsd(threading.Thread):
513 def __init__(self):
514 threading.Thread.__init__(self)
515 self.daemon = True
516 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
517 self.sock.bind(('', 0))
518 self.port = self.sock.getsockname()[1]
519 self.wake_read, self.wake_write = os.pipe()
520 self.stats = []
521
522 def run(self):
523 while True:
524 poll = select.poll()
525 poll.register(self.sock, select.POLLIN)
526 poll.register(self.wake_read, select.POLLIN)
527 ret = poll.poll()
528 for (fd, event) in ret:
529 if fd == self.sock.fileno():
530 data = self.sock.recvfrom(1024)
531 if not data:
532 return
533 self.stats.append(data[0])
534 if fd == self.wake_read:
535 return
536
537 def stop(self):
538 os.write(self.wake_write, '1\n')
539
540
James E. Blaire1767bc2016-08-02 10:00:27 -0700541class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700542 log = logging.getLogger("zuul.test")
543
James E. Blair34776ee2016-08-25 13:53:54 -0700544 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700545 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700546 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700547 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700548 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700549 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700551 # TODOv3(jeblair): self.node is really "the image of the node
552 # assigned". We should rename it (self.node_image?) if we
553 # keep using it like this, or we may end up exposing more of
554 # the complexity around multi-node jobs here
555 # (self.nodes[0].image?)
556 self.node = None
557 if len(self.parameters.get('nodes')) == 1:
558 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700559 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700560 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700561 self.wait_condition = threading.Condition()
562 self.waiting = False
563 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500564 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700565 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700566 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700567 self.changes = None
568 if 'ZUUL_CHANGE_IDS' in self.parameters:
569 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700570
James E. Blair3158e282016-08-19 09:34:11 -0700571 def __repr__(self):
572 waiting = ''
573 if self.waiting:
574 waiting = ' [waiting]'
575 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
576
Clark Boylanb640e052014-04-03 16:41:46 -0700577 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700578 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700579 self.wait_condition.acquire()
580 self.wait_condition.notify()
581 self.waiting = False
582 self.log.debug("Build %s released" % self.unique)
583 self.wait_condition.release()
584
585 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700586 """Return whether this build is being held.
587
588 :returns: Whether the build is being held.
589 :rtype: bool
590 """
591
Clark Boylanb640e052014-04-03 16:41:46 -0700592 self.wait_condition.acquire()
593 if self.waiting:
594 ret = True
595 else:
596 ret = False
597 self.wait_condition.release()
598 return ret
599
600 def _wait(self):
601 self.wait_condition.acquire()
602 self.waiting = True
603 self.log.debug("Build %s waiting" % self.unique)
604 self.wait_condition.wait()
605 self.wait_condition.release()
606
607 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700608 self.log.debug('Running build %s' % self.unique)
609
James E. Blaire1767bc2016-08-02 10:00:27 -0700610 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700611 self.log.debug('Holding build %s' % self.unique)
612 self._wait()
613 self.log.debug("Build %s continuing" % self.unique)
614
Clark Boylanb640e052014-04-03 16:41:46 -0700615 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700616 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700617 result = 'FAILURE'
618 if self.aborted:
619 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500620 if self.requeue:
621 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700622
623 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700624 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700625
James E. Blaire1767bc2016-08-02 10:00:27 -0700626 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700627
James E. Blaira5dba232016-08-08 15:53:24 -0700628 def shouldFail(self):
629 changes = self.launch_server.fail_tests.get(self.name, [])
630 for change in changes:
631 if self.hasChanges(change):
632 return True
633 return False
634
James E. Blaire7b99a02016-08-05 14:27:34 -0700635 def hasChanges(self, *changes):
636 """Return whether this build has certain changes in its git repos.
637
638 :arg FakeChange changes: One or more changes (varargs) that
639 are expected to be present (in order) in the git repository of
640 the active project.
641
642 :returns: Whether the build has the indicated changes.
643 :rtype: bool
644
645 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800646 for change in changes:
647 path = os.path.join(self.jobdir.git_root, change.project)
648 try:
649 repo = git.Repo(path)
650 except NoSuchPathError as e:
651 self.log.debug('%s' % e)
652 return False
653 ref = self.parameters['ZUUL_REF']
654 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
655 commit_message = '%s-1' % change.subject
656 self.log.debug("Checking if build %s has changes; commit_message "
657 "%s; repo_messages %s" % (self, commit_message,
658 repo_messages))
659 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700660 self.log.debug(" messages do not match")
661 return False
662 self.log.debug(" OK")
663 return True
664
Clark Boylanb640e052014-04-03 16:41:46 -0700665
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000666class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700667 """An Ansible launcher to be used in tests.
668
669 :ivar bool hold_jobs_in_build: If true, when jobs are launched
670 they will report that they have started but then pause until
671 released before reporting completion. This attribute may be
672 changed at any time and will take effect for subsequently
673 launched builds, but previously held builds will still need to
674 be explicitly released.
675
676 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800677 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700678 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800679 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700680 self.hold_jobs_in_build = False
681 self.lock = threading.Lock()
682 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700683 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700684 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700685 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800686
James E. Blaira5dba232016-08-08 15:53:24 -0700687 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700688 """Instruct the launcher to report matching builds as failures.
689
690 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700691 :arg Change change: The :py:class:`~tests.base.FakeChange`
692 instance which should cause the job to fail. This job
693 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700694
695 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700696 l = self.fail_tests.get(name, [])
697 l.append(change)
698 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800699
James E. Blair962220f2016-08-03 11:22:38 -0700700 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700701 """Release a held build.
702
703 :arg str regex: A regular expression which, if supplied, will
704 cause only builds with matching names to be released. If
705 not supplied, all builds will be released.
706
707 """
James E. Blair962220f2016-08-03 11:22:38 -0700708 builds = self.running_builds[:]
709 self.log.debug("Releasing build %s (%s)" % (regex,
710 len(self.running_builds)))
711 for build in builds:
712 if not regex or re.match(regex, build.name):
713 self.log.debug("Releasing build %s" %
714 (build.parameters['ZUUL_UUID']))
715 build.release()
716 else:
717 self.log.debug("Not releasing build %s" %
718 (build.parameters['ZUUL_UUID']))
719 self.log.debug("Done releasing builds %s (%s)" %
720 (regex, len(self.running_builds)))
721
James E. Blair17302972016-08-10 16:11:42 -0700722 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700723 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700724 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700725 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700726 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700727 super(RecordingLaunchServer, self).launchJob(job)
728
729 def stopJob(self, job):
730 self.log.debug("handle stop")
731 parameters = json.loads(job.arguments)
732 uuid = parameters['uuid']
733 for build in self.running_builds:
734 if build.unique == uuid:
735 build.aborted = True
736 build.release()
737 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700738
739 def runAnsible(self, jobdir, job):
740 build = self.job_builds[job.unique]
741 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700742
743 if self._run_ansible:
744 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
745 else:
746 result = build.run()
747
748 self.lock.acquire()
749 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700750 BuildHistory(name=build.name, result=result, changes=build.changes,
751 node=build.node, uuid=build.unique,
752 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700753 pipeline=build.parameters['ZUUL_PIPELINE'])
754 )
James E. Blairab7132b2016-08-05 12:36:22 -0700755 self.running_builds.remove(build)
756 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700757 self.lock.release()
758 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800759
760
Clark Boylanb640e052014-04-03 16:41:46 -0700761class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700762 """A Gearman server for use in tests.
763
764 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
765 added to the queue but will not be distributed to workers
766 until released. This attribute may be changed at any time and
767 will take effect for subsequently enqueued jobs, but
768 previously held jobs will still need to be explicitly
769 released.
770
771 """
772
Clark Boylanb640e052014-04-03 16:41:46 -0700773 def __init__(self):
774 self.hold_jobs_in_queue = False
775 super(FakeGearmanServer, self).__init__(0)
776
777 def getJobForConnection(self, connection, peek=False):
778 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
779 for job in queue:
780 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500781 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700782 job.waiting = self.hold_jobs_in_queue
783 else:
784 job.waiting = False
785 if job.waiting:
786 continue
787 if job.name in connection.functions:
788 if not peek:
789 queue.remove(job)
790 connection.related_jobs[job.handle] = job
791 job.worker_connection = connection
792 job.running = True
793 return job
794 return None
795
796 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700797 """Release a held job.
798
799 :arg str regex: A regular expression which, if supplied, will
800 cause only jobs with matching names to be released. If
801 not supplied, all jobs will be released.
802 """
Clark Boylanb640e052014-04-03 16:41:46 -0700803 released = False
804 qlen = (len(self.high_queue) + len(self.normal_queue) +
805 len(self.low_queue))
806 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
807 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500808 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700809 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500810 parameters = json.loads(job.arguments)
811 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700812 self.log.debug("releasing queued job %s" %
813 job.unique)
814 job.waiting = False
815 released = True
816 else:
817 self.log.debug("not releasing queued job %s" %
818 job.unique)
819 if released:
820 self.wakeConnections()
821 qlen = (len(self.high_queue) + len(self.normal_queue) +
822 len(self.low_queue))
823 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
824
825
826class FakeSMTP(object):
827 log = logging.getLogger('zuul.FakeSMTP')
828
829 def __init__(self, messages, server, port):
830 self.server = server
831 self.port = port
832 self.messages = messages
833
834 def sendmail(self, from_email, to_email, msg):
835 self.log.info("Sending email from %s, to %s, with msg %s" % (
836 from_email, to_email, msg))
837
838 headers = msg.split('\n\n', 1)[0]
839 body = msg.split('\n\n', 1)[1]
840
841 self.messages.append(dict(
842 from_email=from_email,
843 to_email=to_email,
844 msg=msg,
845 headers=headers,
846 body=body,
847 ))
848
849 return True
850
851 def quit(self):
852 return True
853
854
855class FakeSwiftClientConnection(swiftclient.client.Connection):
856 def post_account(self, headers):
857 # Do nothing
858 pass
859
860 def get_auth(self):
861 # Returns endpoint and (unused) auth token
862 endpoint = os.path.join('https://storage.example.org', 'V1',
863 'AUTH_account')
864 return endpoint, ''
865
866
Maru Newby3fe5f852015-01-13 04:22:14 +0000867class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700868 log = logging.getLogger("zuul.test")
869
870 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000871 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700872 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
873 try:
874 test_timeout = int(test_timeout)
875 except ValueError:
876 # If timeout value is invalid do not set a timeout.
877 test_timeout = 0
878 if test_timeout > 0:
879 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
880
881 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
882 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
883 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
884 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
885 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
886 os.environ.get('OS_STDERR_CAPTURE') == '1'):
887 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
888 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
889 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
890 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -0700891 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100892 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
893 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -0700894 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
895 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100896 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
897 log_level = logging.WARNING
898 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
899 log_level = logging.ERROR
900 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
901 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -0700902 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100903 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -0700904 format='%(asctime)s %(name)-32s '
905 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000906
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700907 # NOTE(notmorgan): Extract logging overrides for specific libraries
908 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
909 # each. This is used to limit the output during test runs from
910 # libraries that zuul depends on such as gear.
911 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
912
913 if log_defaults_from_env:
914 for default in log_defaults_from_env.split(','):
915 try:
916 name, level_str = default.split('=', 1)
917 level = getattr(logging, level_str, logging.DEBUG)
918 self.useFixture(fixtures.FakeLogger(
919 name=name,
920 level=level,
921 format='%(asctime)s %(name)-32s '
922 '%(levelname)-8s %(message)s'))
923 except ValueError:
924 # NOTE(notmorgan): Invalid format of the log default,
925 # skip and don't try and apply a logger for the
926 # specified module
927 pass
928
Maru Newby3fe5f852015-01-13 04:22:14 +0000929
930class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700931 """A test case with a functioning Zuul.
932
933 The following class variables are used during test setup and can
934 be overidden by subclasses but are effectively read-only once a
935 test method starts running:
936
937 :cvar str config_file: This points to the main zuul config file
938 within the fixtures directory. Subclasses may override this
939 to obtain a different behavior.
940
941 :cvar str tenant_config_file: This is the tenant config file
942 (which specifies from what git repos the configuration should
943 be loaded). It defaults to the value specified in
944 `config_file` but can be overidden by subclasses to obtain a
945 different tenant/project layout while using the standard main
946 configuration.
947
948 The following are instance variables that are useful within test
949 methods:
950
951 :ivar FakeGerritConnection fake_<connection>:
952 A :py:class:`~tests.base.FakeGerritConnection` will be
953 instantiated for each connection present in the config file
954 and stored here. For instance, `fake_gerrit` will hold the
955 FakeGerritConnection object for a connection named `gerrit`.
956
957 :ivar FakeGearmanServer gearman_server: An instance of
958 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
959 server that all of the Zuul components in this test use to
960 communicate with each other.
961
962 :ivar RecordingLaunchServer launch_server: An instance of
963 :py:class:`~tests.base.RecordingLaunchServer` which is the
964 Ansible launch server used to run jobs for this test.
965
966 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
967 representing currently running builds. They are appended to
968 the list in the order they are launched, and removed from this
969 list upon completion.
970
971 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
972 objects representing completed builds. They are appended to
973 the list in the order they complete.
974
975 """
976
James E. Blair83005782015-12-11 14:46:03 -0800977 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700978 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700979
980 def _startMerger(self):
981 self.merge_server = zuul.merger.server.MergeServer(self.config,
982 self.connections)
983 self.merge_server.start()
984
Maru Newby3fe5f852015-01-13 04:22:14 +0000985 def setUp(self):
986 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700987 if USE_TEMPDIR:
988 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000989 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
990 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700991 else:
992 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700993 self.test_root = os.path.join(tmp_root, "zuul-test")
994 self.upstream_root = os.path.join(self.test_root, "upstream")
995 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700996 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700997
998 if os.path.exists(self.test_root):
999 shutil.rmtree(self.test_root)
1000 os.makedirs(self.test_root)
1001 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001002 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001003
1004 # Make per test copy of Configuration.
1005 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001006 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001007 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001008 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001009 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001010 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001011
1012 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001013 # TODOv3(jeblair): remove these and replace with new git
1014 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001015 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001016 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001017 self.init_repo("org/project5")
1018 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001019 self.init_repo("org/one-job-project")
1020 self.init_repo("org/nonvoting-project")
1021 self.init_repo("org/templated-project")
1022 self.init_repo("org/layered-project")
1023 self.init_repo("org/node-project")
1024 self.init_repo("org/conflict-project")
1025 self.init_repo("org/noop-project")
1026 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001027 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001028
1029 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001030 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1031 # see: https://github.com/jsocol/pystatsd/issues/61
1032 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001033 os.environ['STATSD_PORT'] = str(self.statsd.port)
1034 self.statsd.start()
1035 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001036 reload_module(statsd)
1037 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001038
1039 self.gearman_server = FakeGearmanServer()
1040
1041 self.config.set('gearman', 'port', str(self.gearman_server.port))
1042
Joshua Hesketh352264b2015-08-11 23:42:08 +10001043 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1044 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1045 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001046
Joshua Hesketh352264b2015-08-11 23:42:08 +10001047 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001048
1049 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1050 FakeSwiftClientConnection))
1051 self.swift = zuul.lib.swift.Swift(self.config)
1052
Jan Hruban6b71aff2015-10-22 16:58:08 +02001053 self.event_queues = [
1054 self.sched.result_event_queue,
1055 self.sched.trigger_event_queue
1056 ]
1057
James E. Blairfef78942016-03-11 16:28:56 -08001058 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001059 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001060
Clark Boylanb640e052014-04-03 16:41:46 -07001061 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001062 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001063 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001064 return FakeURLOpener(self.upstream_root, *args, **kw)
1065
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001066 old_urlopen = urllib.request.urlopen
1067 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001068
James E. Blair3f876d52016-07-22 13:07:14 -07001069 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001070
James E. Blaire1767bc2016-08-02 10:00:27 -07001071 self.launch_server = RecordingLaunchServer(
1072 self.config, self.connections, _run_ansible=self.run_ansible)
1073 self.launch_server.start()
1074 self.history = self.launch_server.build_history
1075 self.builds = self.launch_server.running_builds
1076
1077 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001078 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001079 self.merge_client = zuul.merger.client.MergeClient(
1080 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001081 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001082
James E. Blaire1767bc2016-08-02 10:00:27 -07001083 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001084 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001085 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001086
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001087 self.webapp = zuul.webapp.WebApp(
1088 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001089 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001090
1091 self.sched.start()
1092 self.sched.reconfigure(self.config)
1093 self.sched.resume()
1094 self.webapp.start()
1095 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001096 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001097
1098 self.addCleanup(self.assertFinalState)
1099 self.addCleanup(self.shutdown)
1100
James E. Blairfef78942016-03-11 16:28:56 -08001101 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001102 # Register connections from the config
1103 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001104
Joshua Hesketh352264b2015-08-11 23:42:08 +10001105 def FakeSMTPFactory(*args, **kw):
1106 args = [self.smtp_messages] + list(args)
1107 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001108
Joshua Hesketh352264b2015-08-11 23:42:08 +10001109 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001110
Joshua Hesketh352264b2015-08-11 23:42:08 +10001111 # Set a changes database so multiple FakeGerrit's can report back to
1112 # a virtual canonical database given by the configured hostname
1113 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001114 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001115
Joshua Hesketh352264b2015-08-11 23:42:08 +10001116 for section_name in self.config.sections():
1117 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1118 section_name, re.I)
1119 if not con_match:
1120 continue
1121 con_name = con_match.group(2)
1122 con_config = dict(self.config.items(section_name))
1123
1124 if 'driver' not in con_config:
1125 raise Exception("No driver specified for connection %s."
1126 % con_name)
1127
1128 con_driver = con_config['driver']
1129
1130 # TODO(jhesketh): load the required class automatically
1131 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001132 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1133 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001134 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001135 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001136 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001137 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001138 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001139 self.event_queues.append(
1140 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001141 setattr(self, 'fake_' + con_name,
1142 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001143 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001144 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001145 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1146 else:
1147 raise Exception("Unknown driver, %s, for connection %s"
1148 % (con_config['driver'], con_name))
1149
1150 # If the [gerrit] or [smtp] sections still exist, load them in as a
1151 # connection named 'gerrit' or 'smtp' respectfully
1152
1153 if 'gerrit' in self.config.sections():
1154 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001155 self.event_queues.append(
1156 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001157 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001158 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001159 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001160
1161 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001162 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001163 zuul.connection.smtp.SMTPConnection(
1164 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001165
James E. Blair83005782015-12-11 14:46:03 -08001166 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001167 # This creates the per-test configuration object. It can be
1168 # overriden by subclasses, but should not need to be since it
1169 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001170 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001171 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001172 if hasattr(self, 'tenant_config_file'):
1173 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001174 git_path = os.path.join(
1175 os.path.dirname(
1176 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1177 'git')
1178 if os.path.exists(git_path):
1179 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001180 project = reponame.replace('_', '/')
1181 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001182 os.path.join(git_path, reponame))
1183
1184 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001185 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001186
1187 files = {}
1188 for (dirpath, dirnames, filenames) in os.walk(source_path):
1189 for filename in filenames:
1190 test_tree_filepath = os.path.join(dirpath, filename)
1191 common_path = os.path.commonprefix([test_tree_filepath,
1192 source_path])
1193 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1194 with open(test_tree_filepath, 'r') as f:
1195 content = f.read()
1196 files[relative_filepath] = content
1197 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001198 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001199
Clark Boylanb640e052014-04-03 16:41:46 -07001200 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001201 # Make sure that git.Repo objects have been garbage collected.
1202 repos = []
1203 gc.collect()
1204 for obj in gc.get_objects():
1205 if isinstance(obj, git.Repo):
1206 repos.append(obj)
1207 self.assertEqual(len(repos), 0)
1208 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001209 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001210 for tenant in self.sched.abide.tenants.values():
1211 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001212 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001213 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001214
1215 def shutdown(self):
1216 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001217 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001218 self.merge_server.stop()
1219 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001220 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001221 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001222 self.sched.stop()
1223 self.sched.join()
1224 self.statsd.stop()
1225 self.statsd.join()
1226 self.webapp.stop()
1227 self.webapp.join()
1228 self.rpc.stop()
1229 self.rpc.join()
1230 self.gearman_server.shutdown()
1231 threads = threading.enumerate()
1232 if len(threads) > 1:
1233 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001234
1235 def init_repo(self, project):
1236 parts = project.split('/')
1237 path = os.path.join(self.upstream_root, *parts[:-1])
1238 if not os.path.exists(path):
1239 os.makedirs(path)
1240 path = os.path.join(self.upstream_root, project)
1241 repo = git.Repo.init(path)
1242
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001243 with repo.config_writer() as config_writer:
1244 config_writer.set_value('user', 'email', 'user@example.com')
1245 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001246
Clark Boylanb640e052014-04-03 16:41:46 -07001247 repo.index.commit('initial commit')
1248 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001249
James E. Blair97d902e2014-08-21 13:25:56 -07001250 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001251 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001252 repo.git.clean('-x', '-f', '-d')
1253
James E. Blair97d902e2014-08-21 13:25:56 -07001254 def create_branch(self, project, branch):
1255 path = os.path.join(self.upstream_root, project)
1256 repo = git.Repo.init(path)
1257 fn = os.path.join(path, 'README')
1258
1259 branch_head = repo.create_head(branch)
1260 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001261 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001262 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001263 f.close()
1264 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001265 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001266
James E. Blair97d902e2014-08-21 13:25:56 -07001267 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001268 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001269 repo.git.clean('-x', '-f', '-d')
1270
Sachi King9f16d522016-03-16 12:20:45 +11001271 def create_commit(self, project):
1272 path = os.path.join(self.upstream_root, project)
1273 repo = git.Repo(path)
1274 repo.head.reference = repo.heads['master']
1275 file_name = os.path.join(path, 'README')
1276 with open(file_name, 'a') as f:
1277 f.write('creating fake commit\n')
1278 repo.index.add([file_name])
1279 commit = repo.index.commit('Creating a fake commit')
1280 return commit.hexsha
1281
James E. Blairb8c16472015-05-05 14:55:26 -07001282 def orderedRelease(self):
1283 # Run one build at a time to ensure non-race order:
1284 while len(self.builds):
1285 self.release(self.builds[0])
1286 self.waitUntilSettled()
1287
Clark Boylanb640e052014-04-03 16:41:46 -07001288 def release(self, job):
1289 if isinstance(job, FakeBuild):
1290 job.release()
1291 else:
1292 job.waiting = False
1293 self.log.debug("Queued job %s released" % job.unique)
1294 self.gearman_server.wakeConnections()
1295
1296 def getParameter(self, job, name):
1297 if isinstance(job, FakeBuild):
1298 return job.parameters[name]
1299 else:
1300 parameters = json.loads(job.arguments)
1301 return parameters[name]
1302
1303 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001304 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001305 while True:
1306 done = True
1307 for connection in self.gearman_server.active_connections:
1308 if (connection.functions and
1309 connection.client_id not in ['Zuul RPC Listener',
1310 'Zuul Merger']):
1311 done = False
1312 if done:
1313 break
1314 time.sleep(0)
1315 self.gearman_server.functions = set()
1316 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001317
1318 def haveAllBuildsReported(self):
1319 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001320 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001321 return False
1322 # Find out if every build that the worker has completed has been
1323 # reported back to Zuul. If it hasn't then that means a Gearman
1324 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001325 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001326 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001327 if not zbuild:
1328 # It has already been reported
1329 continue
1330 # It hasn't been reported yet.
1331 return False
1332 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001333 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001334 if connection.state == 'GRAB_WAIT':
1335 return False
1336 return True
1337
1338 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001339 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001340 for build in builds:
1341 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001342 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001343 for j in conn.related_jobs.values():
1344 if j.unique == build.uuid:
1345 client_job = j
1346 break
1347 if not client_job:
1348 self.log.debug("%s is not known to the gearman client" %
1349 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001350 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001351 if not client_job.handle:
1352 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001353 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001354 server_job = self.gearman_server.jobs.get(client_job.handle)
1355 if not server_job:
1356 self.log.debug("%s is not known to the gearman server" %
1357 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001358 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001359 if not hasattr(server_job, 'waiting'):
1360 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001361 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001362 if server_job.waiting:
1363 continue
James E. Blair17302972016-08-10 16:11:42 -07001364 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001365 self.log.debug("%s has not reported start" % build)
1366 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001367 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001368 if worker_build:
1369 if worker_build.isWaiting():
1370 continue
1371 else:
1372 self.log.debug("%s is running" % worker_build)
1373 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001374 else:
James E. Blair962220f2016-08-03 11:22:38 -07001375 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001376 return False
1377 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001378
Jan Hruban6b71aff2015-10-22 16:58:08 +02001379 def eventQueuesEmpty(self):
1380 for queue in self.event_queues:
1381 yield queue.empty()
1382
1383 def eventQueuesJoin(self):
1384 for queue in self.event_queues:
1385 queue.join()
1386
Clark Boylanb640e052014-04-03 16:41:46 -07001387 def waitUntilSettled(self):
1388 self.log.debug("Waiting until settled...")
1389 start = time.time()
1390 while True:
1391 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001392 self.log.debug("Queue status:")
1393 for queue in self.event_queues:
1394 self.log.debug(" %s: %s" % (queue, queue.empty()))
1395 self.log.debug("All builds waiting: %s" %
1396 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001397 self.log.debug("All builds reported: %s" %
1398 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001399 raise Exception("Timeout waiting for Zuul to settle")
1400 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001401
James E. Blaire1767bc2016-08-02 10:00:27 -07001402 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001403 # have all build states propogated to zuul?
1404 if self.haveAllBuildsReported():
1405 # Join ensures that the queue is empty _and_ events have been
1406 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001407 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001408 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001409 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001410 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001411 self.haveAllBuildsReported() and
1412 self.areAllBuildsWaiting()):
1413 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001414 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001415 self.log.debug("...settled.")
1416 return
1417 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001418 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001419 self.sched.wake_event.wait(0.1)
1420
1421 def countJobResults(self, jobs, result):
1422 jobs = filter(lambda x: x.result == result, jobs)
1423 return len(jobs)
1424
James E. Blair96c6bf82016-01-15 16:20:40 -08001425 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001426 for job in self.history:
1427 if (job.name == name and
1428 (project is None or
1429 job.parameters['ZUUL_PROJECT'] == project)):
1430 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001431 raise Exception("Unable to find job %s in history" % name)
1432
1433 def assertEmptyQueues(self):
1434 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001435 for tenant in self.sched.abide.tenants.values():
1436 for pipeline in tenant.layout.pipelines.values():
1437 for queue in pipeline.queues:
1438 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001439 print('pipeline %s queue %s contents %s' % (
1440 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001441 self.assertEqual(len(queue.queue), 0,
1442 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001443
1444 def assertReportedStat(self, key, value=None, kind=None):
1445 start = time.time()
1446 while time.time() < (start + 5):
1447 for stat in self.statsd.stats:
1448 pprint.pprint(self.statsd.stats)
1449 k, v = stat.split(':')
1450 if key == k:
1451 if value is None and kind is None:
1452 return
1453 elif value:
1454 if value == v:
1455 return
1456 elif kind:
1457 if v.endswith('|' + kind):
1458 return
1459 time.sleep(0.1)
1460
1461 pprint.pprint(self.statsd.stats)
1462 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001463
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001464 def assertBuilds(self, builds):
1465 """Assert that the running builds are as described.
1466
1467 The list of running builds is examined and must match exactly
1468 the list of builds described by the input.
1469
1470 :arg list builds: A list of dictionaries. Each item in the
1471 list must match the corresponding build in the build
1472 history, and each element of the dictionary must match the
1473 corresponding attribute of the build.
1474
1475 """
James E. Blair3158e282016-08-19 09:34:11 -07001476 try:
1477 self.assertEqual(len(self.builds), len(builds))
1478 for i, d in enumerate(builds):
1479 for k, v in d.items():
1480 self.assertEqual(
1481 getattr(self.builds[i], k), v,
1482 "Element %i in builds does not match" % (i,))
1483 except Exception:
1484 for build in self.builds:
1485 self.log.error("Running build: %s" % build)
1486 else:
1487 self.log.error("No running builds")
1488 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001489
James E. Blairb536ecc2016-08-31 10:11:42 -07001490 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001491 """Assert that the completed builds are as described.
1492
1493 The list of completed builds is examined and must match
1494 exactly the list of builds described by the input.
1495
1496 :arg list history: A list of dictionaries. Each item in the
1497 list must match the corresponding build in the build
1498 history, and each element of the dictionary must match the
1499 corresponding attribute of the build.
1500
James E. Blairb536ecc2016-08-31 10:11:42 -07001501 :arg bool ordered: If true, the history must match the order
1502 supplied, if false, the builds are permitted to have
1503 arrived in any order.
1504
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001505 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001506 def matches(history_item, item):
1507 for k, v in item.items():
1508 if getattr(history_item, k) != v:
1509 return False
1510 return True
James E. Blair3158e282016-08-19 09:34:11 -07001511 try:
1512 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001513 if ordered:
1514 for i, d in enumerate(history):
1515 if not matches(self.history[i], d):
1516 raise Exception(
1517 "Element %i in history does not match" % (i,))
1518 else:
1519 unseen = self.history[:]
1520 for i, d in enumerate(history):
1521 found = False
1522 for unseen_item in unseen:
1523 if matches(unseen_item, d):
1524 found = True
1525 unseen.remove(unseen_item)
1526 break
1527 if not found:
1528 raise Exception("No match found for element %i "
1529 "in history" % (i,))
1530 if unseen:
1531 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001532 except Exception:
1533 for build in self.history:
1534 self.log.error("Completed build: %s" % build)
1535 else:
1536 self.log.error("No completed builds")
1537 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001538
James E. Blair59fdbac2015-12-07 17:08:06 -08001539 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001540 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1541
1542 def updateConfigLayout(self, path):
1543 root = os.path.join(self.test_root, "config")
1544 os.makedirs(root)
1545 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1546 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001547- tenant:
1548 name: openstack
1549 source:
1550 gerrit:
1551 config-repos:
1552 - %s
1553 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001554 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001555 self.config.set('zuul', 'tenant_config',
1556 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001557
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001558 def addCommitToRepo(self, project, message, files,
1559 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001560 path = os.path.join(self.upstream_root, project)
1561 repo = git.Repo(path)
1562 repo.head.reference = branch
1563 zuul.merger.merger.reset_repo_to_head(repo)
1564 for fn, content in files.items():
1565 fn = os.path.join(path, fn)
1566 with open(fn, 'w') as f:
1567 f.write(content)
1568 repo.index.add([fn])
1569 commit = repo.index.commit(message)
1570 repo.heads[branch].commit = commit
1571 repo.head.reference = branch
1572 repo.git.clean('-x', '-f', '-d')
1573 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001574 if tag:
1575 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001576
James E. Blair7fc8daa2016-08-08 15:37:15 -07001577 def addEvent(self, connection, event):
1578 """Inject a Fake (Gerrit) event.
1579
1580 This method accepts a JSON-encoded event and simulates Zuul
1581 having received it from Gerrit. It could (and should)
1582 eventually apply to any connection type, but is currently only
1583 used with Gerrit connections. The name of the connection is
1584 used to look up the corresponding server, and the event is
1585 simulated as having been received by all Zuul connections
1586 attached to that server. So if two Gerrit connections in Zuul
1587 are connected to the same Gerrit server, and you invoke this
1588 method specifying the name of one of them, the event will be
1589 received by both.
1590
1591 .. note::
1592
1593 "self.fake_gerrit.addEvent" calls should be migrated to
1594 this method.
1595
1596 :arg str connection: The name of the connection corresponding
1597 to the gerrit server.
1598 :arg str event: The JSON-encoded event.
1599
1600 """
1601 specified_conn = self.connections.connections[connection]
1602 for conn in self.connections.connections.values():
1603 if (isinstance(conn, specified_conn.__class__) and
1604 specified_conn.server == conn.server):
1605 conn.addEvent(event)
1606
James E. Blair3f876d52016-07-22 13:07:14 -07001607
1608class AnsibleZuulTestCase(ZuulTestCase):
1609 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001610 run_ansible = True