blob: 5f6022a2a0fd793dd9826e6c1c4d1574d0fbfd4a [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.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
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 Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
Clark Boylan8208c192017-04-24 18:08:08 -070041import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070042import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import uuid
44
Clark Boylanb640e052014-04-03 16:41:46 -070045
46import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import statsd
53import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080054import testtools.content
55import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080056from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000057import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070058
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.scheduler
62import zuul.webapp
63import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040064import zuul.executor.server
65import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080066import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070067import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070068import zuul.merger.merger
69import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080071import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070072
73FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
74 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080075
76KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070077
Clark Boylanb640e052014-04-03 16:41:46 -070078
79def repack_repo(path):
80 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
81 output = subprocess.Popen(cmd, close_fds=True,
82 stdout=subprocess.PIPE,
83 stderr=subprocess.PIPE)
84 out = output.communicate()
85 if output.returncode:
86 raise Exception("git repack returned %d" % output.returncode)
87 return out
88
89
90def random_sha1():
91 return hashlib.sha1(str(random.random())).hexdigest()
92
93
James E. Blaira190f3b2015-01-05 14:56:54 -080094def iterate_timeout(max_seconds, purpose):
95 start = time.time()
96 count = 0
97 while (time.time() < start + max_seconds):
98 count += 1
99 yield count
100 time.sleep(0)
101 raise Exception("Timeout waiting for %s" % purpose)
102
103
Jesse Keating436a5452017-04-20 11:48:41 -0700104def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700105 """Specify a layout file for use by a test method.
106
107 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700108 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700109
110 Some tests require only a very simple configuration. For those,
111 establishing a complete config directory hierachy is too much
112 work. In those cases, you can add a simple zuul.yaml file to the
113 test fixtures directory (in fixtures/layouts/foo.yaml) and use
114 this decorator to indicate the test method should use that rather
115 than the tenant config file specified by the test class.
116
117 The decorator will cause that layout file to be added to a
118 config-project called "common-config" and each "project" instance
119 referenced in the layout file will have a git repo automatically
120 initialized.
121 """
122
123 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700124 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700125 return test
126 return decorator
127
128
Clark Boylanb640e052014-04-03 16:41:46 -0700129class ChangeReference(git.Reference):
130 _common_path_default = "refs/changes"
131 _points_to_commits_only = True
132
133
134class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700135 categories = {'approved': ('Approved', -1, 1),
136 'code-review': ('Code-Review', -2, 2),
137 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700138
139 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700140 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700141 self.gerrit = gerrit
142 self.reported = 0
143 self.queried = 0
144 self.patchsets = []
145 self.number = number
146 self.project = project
147 self.branch = branch
148 self.subject = subject
149 self.latest_patchset = 0
150 self.depends_on_change = None
151 self.needed_by_changes = []
152 self.fail_merge = False
153 self.messages = []
154 self.data = {
155 'branch': branch,
156 'comments': [],
157 'commitMessage': subject,
158 'createdOn': time.time(),
159 'id': 'I' + random_sha1(),
160 'lastUpdated': time.time(),
161 'number': str(number),
162 'open': status == 'NEW',
163 'owner': {'email': 'user@example.com',
164 'name': 'User Name',
165 'username': 'username'},
166 'patchSets': self.patchsets,
167 'project': project,
168 'status': status,
169 'subject': subject,
170 'submitRecords': [],
171 'url': 'https://hostname/%s' % number}
172
173 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700174 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700175 self.data['submitRecords'] = self.getSubmitRecords()
176 self.open = status == 'NEW'
177
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700178 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700179 path = os.path.join(self.upstream_root, self.project)
180 repo = git.Repo(path)
181 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
182 self.latest_patchset),
183 'refs/tags/init')
184 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700185 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.git.clean('-x', '-f', '-d')
187
188 path = os.path.join(self.upstream_root, self.project)
189 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 for fn, content in files.items():
191 fn = os.path.join(path, fn)
192 with open(fn, 'w') as f:
193 f.write(content)
194 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700195 else:
196 for fni in range(100):
197 fn = os.path.join(path, str(fni))
198 f = open(fn, 'w')
199 for ci in range(4096):
200 f.write(random.choice(string.printable))
201 f.close()
202 repo.index.add([fn])
203
204 r = repo.index.commit(msg)
205 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700206 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700207 repo.git.clean('-x', '-f', '-d')
208 repo.heads['master'].checkout()
209 return r
210
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700211 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700212 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700214 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 data = ("test %s %s %s\n" %
216 (self.branch, self.number, self.latest_patchset))
217 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700218 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700219 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700220 ps_files = [{'file': '/COMMIT_MSG',
221 'type': 'ADDED'},
222 {'file': 'README',
223 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700225 ps_files.append({'file': f, 'type': 'ADDED'})
226 d = {'approvals': [],
227 'createdOn': time.time(),
228 'files': ps_files,
229 'number': str(self.latest_patchset),
230 'ref': 'refs/changes/1/%s/%s' % (self.number,
231 self.latest_patchset),
232 'revision': c.hexsha,
233 'uploader': {'email': 'user@example.com',
234 'name': 'User name',
235 'username': 'user'}}
236 self.data['currentPatchSet'] = d
237 self.patchsets.append(d)
238 self.data['submitRecords'] = self.getSubmitRecords()
239
240 def getPatchsetCreatedEvent(self, patchset):
241 event = {"type": "patchset-created",
242 "change": {"project": self.project,
243 "branch": self.branch,
244 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
245 "number": str(self.number),
246 "subject": self.subject,
247 "owner": {"name": "User Name"},
248 "url": "https://hostname/3"},
249 "patchSet": self.patchsets[patchset - 1],
250 "uploader": {"name": "User Name"}}
251 return event
252
253 def getChangeRestoredEvent(self):
254 event = {"type": "change-restored",
255 "change": {"project": self.project,
256 "branch": self.branch,
257 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
258 "number": str(self.number),
259 "subject": self.subject,
260 "owner": {"name": "User Name"},
261 "url": "https://hostname/3"},
262 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100263 "patchSet": self.patchsets[-1],
264 "reason": ""}
265 return event
266
267 def getChangeAbandonedEvent(self):
268 event = {"type": "change-abandoned",
269 "change": {"project": self.project,
270 "branch": self.branch,
271 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
272 "number": str(self.number),
273 "subject": self.subject,
274 "owner": {"name": "User Name"},
275 "url": "https://hostname/3"},
276 "abandoner": {"name": "User Name"},
277 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700278 "reason": ""}
279 return event
280
281 def getChangeCommentEvent(self, patchset):
282 event = {"type": "comment-added",
283 "change": {"project": self.project,
284 "branch": self.branch,
285 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
286 "number": str(self.number),
287 "subject": self.subject,
288 "owner": {"name": "User Name"},
289 "url": "https://hostname/3"},
290 "patchSet": self.patchsets[patchset - 1],
291 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700292 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700293 "description": "Code-Review",
294 "value": "0"}],
295 "comment": "This is a comment"}
296 return event
297
James E. Blairc2a5ed72017-02-20 14:12:01 -0500298 def getChangeMergedEvent(self):
299 event = {"submitter": {"name": "Jenkins",
300 "username": "jenkins"},
301 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
302 "patchSet": self.patchsets[-1],
303 "change": self.data,
304 "type": "change-merged",
305 "eventCreatedOn": 1487613810}
306 return event
307
James E. Blair8cce42e2016-10-18 08:18:36 -0700308 def getRefUpdatedEvent(self):
309 path = os.path.join(self.upstream_root, self.project)
310 repo = git.Repo(path)
311 oldrev = repo.heads[self.branch].commit.hexsha
312
313 event = {
314 "type": "ref-updated",
315 "submitter": {
316 "name": "User Name",
317 },
318 "refUpdate": {
319 "oldRev": oldrev,
320 "newRev": self.patchsets[-1]['revision'],
321 "refName": self.branch,
322 "project": self.project,
323 }
324 }
325 return event
326
Joshua Hesketh642824b2014-07-01 17:54:59 +1000327 def addApproval(self, category, value, username='reviewer_john',
328 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700329 if not granted_on:
330 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000331 approval = {
332 'description': self.categories[category][0],
333 'type': category,
334 'value': str(value),
335 'by': {
336 'username': username,
337 'email': username + '@example.com',
338 },
339 'grantedOn': int(granted_on)
340 }
Clark Boylanb640e052014-04-03 16:41:46 -0700341 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
342 if x['by']['username'] == username and x['type'] == category:
343 del self.patchsets[-1]['approvals'][i]
344 self.patchsets[-1]['approvals'].append(approval)
345 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000346 'author': {'email': 'author@example.com',
347 'name': 'Patchset Author',
348 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700349 'change': {'branch': self.branch,
350 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
351 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000352 'owner': {'email': 'owner@example.com',
353 'name': 'Change Owner',
354 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700355 'project': self.project,
356 'subject': self.subject,
357 'topic': 'master',
358 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000359 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'patchSet': self.patchsets[-1],
361 'type': 'comment-added'}
362 self.data['submitRecords'] = self.getSubmitRecords()
363 return json.loads(json.dumps(event))
364
365 def getSubmitRecords(self):
366 status = {}
367 for cat in self.categories.keys():
368 status[cat] = 0
369
370 for a in self.patchsets[-1]['approvals']:
371 cur = status[a['type']]
372 cat_min, cat_max = self.categories[a['type']][1:]
373 new = int(a['value'])
374 if new == cat_min:
375 cur = new
376 elif abs(new) > abs(cur):
377 cur = new
378 status[a['type']] = cur
379
380 labels = []
381 ok = True
382 for typ, cat in self.categories.items():
383 cur = status[typ]
384 cat_min, cat_max = cat[1:]
385 if cur == cat_min:
386 value = 'REJECT'
387 ok = False
388 elif cur == cat_max:
389 value = 'OK'
390 else:
391 value = 'NEED'
392 ok = False
393 labels.append({'label': cat[0], 'status': value})
394 if ok:
395 return [{'status': 'OK'}]
396 return [{'status': 'NOT_READY',
397 'labels': labels}]
398
399 def setDependsOn(self, other, patchset):
400 self.depends_on_change = other
401 d = {'id': other.data['id'],
402 'number': other.data['number'],
403 'ref': other.patchsets[patchset - 1]['ref']
404 }
405 self.data['dependsOn'] = [d]
406
407 other.needed_by_changes.append(self)
408 needed = other.data.get('neededBy', [])
409 d = {'id': self.data['id'],
410 'number': self.data['number'],
411 'ref': self.patchsets[patchset - 1]['ref'],
412 'revision': self.patchsets[patchset - 1]['revision']
413 }
414 needed.append(d)
415 other.data['neededBy'] = needed
416
417 def query(self):
418 self.queried += 1
419 d = self.data.get('dependsOn')
420 if d:
421 d = d[0]
422 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
423 d['isCurrentPatchSet'] = True
424 else:
425 d['isCurrentPatchSet'] = False
426 return json.loads(json.dumps(self.data))
427
428 def setMerged(self):
429 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000430 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700431 return
432 if self.fail_merge:
433 return
434 self.data['status'] = 'MERGED'
435 self.open = False
436
437 path = os.path.join(self.upstream_root, self.project)
438 repo = git.Repo(path)
439 repo.heads[self.branch].commit = \
440 repo.commit(self.patchsets[-1]['revision'])
441
442 def setReported(self):
443 self.reported += 1
444
445
James E. Blaire511d2f2016-12-08 15:22:26 -0800446class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700447 """A Fake Gerrit connection for use in tests.
448
449 This subclasses
450 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
451 ability for tests to add changes to the fake Gerrit it represents.
452 """
453
Joshua Hesketh352264b2015-08-11 23:42:08 +1000454 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700455
James E. Blaire511d2f2016-12-08 15:22:26 -0800456 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700457 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000459 connection_config)
460
James E. Blair7fc8daa2016-08-08 15:37:15 -0700461 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700462 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
463 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000464 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700465 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200466 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700467
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700468 def addFakeChange(self, project, branch, subject, status='NEW',
469 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700470 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700471 self.change_number += 1
472 c = FakeChange(self, self.change_number, project, branch, subject,
473 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700474 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700475 self.changes[self.change_number] = c
476 return c
477
Clark Boylanb640e052014-04-03 16:41:46 -0700478 def review(self, project, changeid, message, action):
479 number, ps = changeid.split(',')
480 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000481
482 # Add the approval back onto the change (ie simulate what gerrit would
483 # do).
484 # Usually when zuul leaves a review it'll create a feedback loop where
485 # zuul's review enters another gerrit event (which is then picked up by
486 # zuul). However, we can't mimic this behaviour (by adding this
487 # approval event into the queue) as it stops jobs from checking what
488 # happens before this event is triggered. If a job needs to see what
489 # happens they can add their own verified event into the queue.
490 # Nevertheless, we can update change with the new review in gerrit.
491
James E. Blair8b5408c2016-08-08 15:37:46 -0700492 for cat in action.keys():
493 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000494 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000495
James E. Blair8b5408c2016-08-08 15:37:46 -0700496 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497 if 'label' in action:
498 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000499 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000502
Clark Boylanb640e052014-04-03 16:41:46 -0700503 if 'submit' in action:
504 change.setMerged()
505 if message:
506 change.setReported()
507
508 def query(self, number):
509 change = self.changes.get(int(number))
510 if change:
511 return change.query()
512 return {}
513
James E. Blairc494d542014-08-06 09:23:52 -0700514 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700515 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700516 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800517 if query.startswith('change:'):
518 # Query a specific changeid
519 changeid = query[len('change:'):]
520 l = [change.query() for change in self.changes.values()
521 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700522 elif query.startswith('message:'):
523 # Query the content of a commit message
524 msg = query[len('message:'):].strip()
525 l = [change.query() for change in self.changes.values()
526 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800527 else:
528 # Query all open changes
529 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700530 return l
James E. Blairc494d542014-08-06 09:23:52 -0700531
Joshua Hesketh352264b2015-08-11 23:42:08 +1000532 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700533 pass
534
Joshua Hesketh352264b2015-08-11 23:42:08 +1000535 def getGitUrl(self, project):
536 return os.path.join(self.upstream_root, project.name)
537
Clark Boylanb640e052014-04-03 16:41:46 -0700538
539class BuildHistory(object):
540 def __init__(self, **kw):
541 self.__dict__.update(kw)
542
543 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700544 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
545 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700546
547
548class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200549 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700551 self.url = url
552
553 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700554 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700555 path = res.path
556 project = '/'.join(path.split('/')[2:-2])
557 ret = '001e# service=git-upload-pack\n'
558 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
559 'multi_ack thin-pack side-band side-band-64k ofs-delta '
560 'shallow no-progress include-tag multi_ack_detailed no-done\n')
561 path = os.path.join(self.upstream_root, project)
562 repo = git.Repo(path)
563 for ref in repo.refs:
564 r = ref.object.hexsha + ' ' + ref.path + '\n'
565 ret += '%04x%s' % (len(r) + 4, r)
566 ret += '0000'
567 return ret
568
569
Clark Boylanb640e052014-04-03 16:41:46 -0700570class FakeStatsd(threading.Thread):
571 def __init__(self):
572 threading.Thread.__init__(self)
573 self.daemon = True
574 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
575 self.sock.bind(('', 0))
576 self.port = self.sock.getsockname()[1]
577 self.wake_read, self.wake_write = os.pipe()
578 self.stats = []
579
580 def run(self):
581 while True:
582 poll = select.poll()
583 poll.register(self.sock, select.POLLIN)
584 poll.register(self.wake_read, select.POLLIN)
585 ret = poll.poll()
586 for (fd, event) in ret:
587 if fd == self.sock.fileno():
588 data = self.sock.recvfrom(1024)
589 if not data:
590 return
591 self.stats.append(data[0])
592 if fd == self.wake_read:
593 return
594
595 def stop(self):
596 os.write(self.wake_write, '1\n')
597
598
James E. Blaire1767bc2016-08-02 10:00:27 -0700599class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700600 log = logging.getLogger("zuul.test")
601
Paul Belanger174a8272017-03-14 13:20:10 -0400602 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700603 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400604 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700605 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700606 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700607 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700608 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700609 # TODOv3(jeblair): self.node is really "the image of the node
610 # assigned". We should rename it (self.node_image?) if we
611 # keep using it like this, or we may end up exposing more of
612 # the complexity around multi-node jobs here
613 # (self.nodes[0].image?)
614 self.node = None
615 if len(self.parameters.get('nodes')) == 1:
616 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700617 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100618 self.pipeline = self.parameters['ZUUL_PIPELINE']
619 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700620 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700621 self.wait_condition = threading.Condition()
622 self.waiting = False
623 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500624 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700625 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700626 self.changes = None
627 if 'ZUUL_CHANGE_IDS' in self.parameters:
628 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700629
James E. Blair3158e282016-08-19 09:34:11 -0700630 def __repr__(self):
631 waiting = ''
632 if self.waiting:
633 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100634 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
635 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700636
Clark Boylanb640e052014-04-03 16:41:46 -0700637 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700638 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700639 self.wait_condition.acquire()
640 self.wait_condition.notify()
641 self.waiting = False
642 self.log.debug("Build %s released" % self.unique)
643 self.wait_condition.release()
644
645 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700646 """Return whether this build is being held.
647
648 :returns: Whether the build is being held.
649 :rtype: bool
650 """
651
Clark Boylanb640e052014-04-03 16:41:46 -0700652 self.wait_condition.acquire()
653 if self.waiting:
654 ret = True
655 else:
656 ret = False
657 self.wait_condition.release()
658 return ret
659
660 def _wait(self):
661 self.wait_condition.acquire()
662 self.waiting = True
663 self.log.debug("Build %s waiting" % self.unique)
664 self.wait_condition.wait()
665 self.wait_condition.release()
666
667 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700668 self.log.debug('Running build %s' % self.unique)
669
Paul Belanger174a8272017-03-14 13:20:10 -0400670 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700671 self.log.debug('Holding build %s' % self.unique)
672 self._wait()
673 self.log.debug("Build %s continuing" % self.unique)
674
James E. Blair412fba82017-01-26 15:00:50 -0800675 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700676 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800677 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700678 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800679 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500680 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800681 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700682
James E. Blaire1767bc2016-08-02 10:00:27 -0700683 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700684
James E. Blaira5dba232016-08-08 15:53:24 -0700685 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400686 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700687 for change in changes:
688 if self.hasChanges(change):
689 return True
690 return False
691
James E. Blaire7b99a02016-08-05 14:27:34 -0700692 def hasChanges(self, *changes):
693 """Return whether this build has certain changes in its git repos.
694
695 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700696 are expected to be present (in order) in the git repository of
697 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700698
699 :returns: Whether the build has the indicated changes.
700 :rtype: bool
701
702 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800703 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500704 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800705 try:
706 repo = git.Repo(path)
707 except NoSuchPathError as e:
708 self.log.debug('%s' % e)
709 return False
710 ref = self.parameters['ZUUL_REF']
711 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
712 commit_message = '%s-1' % change.subject
713 self.log.debug("Checking if build %s has changes; commit_message "
714 "%s; repo_messages %s" % (self, commit_message,
715 repo_messages))
716 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700717 self.log.debug(" messages do not match")
718 return False
719 self.log.debug(" OK")
720 return True
721
Clark Boylanb640e052014-04-03 16:41:46 -0700722
Paul Belanger174a8272017-03-14 13:20:10 -0400723class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
724 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700725
Paul Belanger174a8272017-03-14 13:20:10 -0400726 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700727 they will report that they have started but then pause until
728 released before reporting completion. This attribute may be
729 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400730 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700731 be explicitly released.
732
733 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800734 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700735 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800736 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400737 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700738 self.hold_jobs_in_build = False
739 self.lock = threading.Lock()
740 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700741 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700742 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700743 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800744
James E. Blaira5dba232016-08-08 15:53:24 -0700745 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400746 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700747
748 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700749 :arg Change change: The :py:class:`~tests.base.FakeChange`
750 instance which should cause the job to fail. This job
751 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700752
753 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700754 l = self.fail_tests.get(name, [])
755 l.append(change)
756 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800757
James E. Blair962220f2016-08-03 11:22:38 -0700758 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700759 """Release a held build.
760
761 :arg str regex: A regular expression which, if supplied, will
762 cause only builds with matching names to be released. If
763 not supplied, all builds will be released.
764
765 """
James E. Blair962220f2016-08-03 11:22:38 -0700766 builds = self.running_builds[:]
767 self.log.debug("Releasing build %s (%s)" % (regex,
768 len(self.running_builds)))
769 for build in builds:
770 if not regex or re.match(regex, build.name):
771 self.log.debug("Releasing build %s" %
772 (build.parameters['ZUUL_UUID']))
773 build.release()
774 else:
775 self.log.debug("Not releasing build %s" %
776 (build.parameters['ZUUL_UUID']))
777 self.log.debug("Done releasing builds %s (%s)" %
778 (regex, len(self.running_builds)))
779
Paul Belanger174a8272017-03-14 13:20:10 -0400780 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700781 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700782 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700783 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700784 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800785 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500786 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800787 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100788 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
789 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700790
791 def stopJob(self, job):
792 self.log.debug("handle stop")
793 parameters = json.loads(job.arguments)
794 uuid = parameters['uuid']
795 for build in self.running_builds:
796 if build.unique == uuid:
797 build.aborted = True
798 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400799 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700800
James E. Blaira002b032017-04-18 10:35:48 -0700801 def stop(self):
802 for build in self.running_builds:
803 build.release()
804 super(RecordingExecutorServer, self).stop()
805
Joshua Hesketh50c21782016-10-13 21:34:14 +1100806
Paul Belanger174a8272017-03-14 13:20:10 -0400807class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700808 def doMergeChanges(self, items):
809 # Get a merger in order to update the repos involved in this job.
810 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
811 if not commit: # merge conflict
812 self.recordResult('MERGER_FAILURE')
813 return commit
814
815 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400816 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400817 self.executor_server.lock.acquire()
818 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700819 BuildHistory(name=build.name, result=result, changes=build.changes,
820 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800821 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700822 pipeline=build.parameters['ZUUL_PIPELINE'])
823 )
Paul Belanger174a8272017-03-14 13:20:10 -0400824 self.executor_server.running_builds.remove(build)
825 del self.executor_server.job_builds[self.job.unique]
826 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700827
828 def runPlaybooks(self, args):
829 build = self.executor_server.job_builds[self.job.unique]
830 build.jobdir = self.jobdir
831
832 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
833 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800834 return result
835
Monty Taylore6562aa2017-02-20 07:37:39 -0500836 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400837 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800838
Paul Belanger174a8272017-03-14 13:20:10 -0400839 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600840 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500841 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800842 else:
843 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700844 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800845
James E. Blairad8dca02017-02-21 11:48:32 -0500846 def getHostList(self, args):
847 self.log.debug("hostlist")
848 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400849 for host in hosts:
850 host['host_vars']['ansible_connection'] = 'local'
851
852 hosts.append(dict(
853 name='localhost',
854 host_vars=dict(ansible_connection='local'),
855 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500856 return hosts
857
James E. Blairf5dbd002015-12-23 15:26:17 -0800858
Clark Boylanb640e052014-04-03 16:41:46 -0700859class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700860 """A Gearman server for use in tests.
861
862 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
863 added to the queue but will not be distributed to workers
864 until released. This attribute may be changed at any time and
865 will take effect for subsequently enqueued jobs, but
866 previously held jobs will still need to be explicitly
867 released.
868
869 """
870
Clark Boylanb640e052014-04-03 16:41:46 -0700871 def __init__(self):
872 self.hold_jobs_in_queue = False
873 super(FakeGearmanServer, self).__init__(0)
874
875 def getJobForConnection(self, connection, peek=False):
876 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
877 for job in queue:
878 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400879 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700880 job.waiting = self.hold_jobs_in_queue
881 else:
882 job.waiting = False
883 if job.waiting:
884 continue
885 if job.name in connection.functions:
886 if not peek:
887 queue.remove(job)
888 connection.related_jobs[job.handle] = job
889 job.worker_connection = connection
890 job.running = True
891 return job
892 return None
893
894 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700895 """Release a held job.
896
897 :arg str regex: A regular expression which, if supplied, will
898 cause only jobs with matching names to be released. If
899 not supplied, all jobs will be released.
900 """
Clark Boylanb640e052014-04-03 16:41:46 -0700901 released = False
902 qlen = (len(self.high_queue) + len(self.normal_queue) +
903 len(self.low_queue))
904 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
905 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400906 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700907 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500908 parameters = json.loads(job.arguments)
909 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700910 self.log.debug("releasing queued job %s" %
911 job.unique)
912 job.waiting = False
913 released = True
914 else:
915 self.log.debug("not releasing queued job %s" %
916 job.unique)
917 if released:
918 self.wakeConnections()
919 qlen = (len(self.high_queue) + len(self.normal_queue) +
920 len(self.low_queue))
921 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
922
923
924class FakeSMTP(object):
925 log = logging.getLogger('zuul.FakeSMTP')
926
927 def __init__(self, messages, server, port):
928 self.server = server
929 self.port = port
930 self.messages = messages
931
932 def sendmail(self, from_email, to_email, msg):
933 self.log.info("Sending email from %s, to %s, with msg %s" % (
934 from_email, to_email, msg))
935
936 headers = msg.split('\n\n', 1)[0]
937 body = msg.split('\n\n', 1)[1]
938
939 self.messages.append(dict(
940 from_email=from_email,
941 to_email=to_email,
942 msg=msg,
943 headers=headers,
944 body=body,
945 ))
946
947 return True
948
949 def quit(self):
950 return True
951
952
James E. Blairdce6cea2016-12-20 16:45:32 -0800953class FakeNodepool(object):
954 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800955 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800956
957 log = logging.getLogger("zuul.test.FakeNodepool")
958
959 def __init__(self, host, port, chroot):
960 self.client = kazoo.client.KazooClient(
961 hosts='%s:%s%s' % (host, port, chroot))
962 self.client.start()
963 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800964 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800965 self.thread = threading.Thread(target=self.run)
966 self.thread.daemon = True
967 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800968 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800969
970 def stop(self):
971 self._running = False
972 self.thread.join()
973 self.client.stop()
974 self.client.close()
975
976 def run(self):
977 while self._running:
978 self._run()
979 time.sleep(0.1)
980
981 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800982 if self.paused:
983 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800984 for req in self.getNodeRequests():
985 self.fulfillRequest(req)
986
987 def getNodeRequests(self):
988 try:
989 reqids = self.client.get_children(self.REQUEST_ROOT)
990 except kazoo.exceptions.NoNodeError:
991 return []
992 reqs = []
993 for oid in sorted(reqids):
994 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800995 try:
996 data, stat = self.client.get(path)
997 data = json.loads(data)
998 data['_oid'] = oid
999 reqs.append(data)
1000 except kazoo.exceptions.NoNodeError:
1001 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001002 return reqs
1003
James E. Blaire18d4602017-01-05 11:17:28 -08001004 def getNodes(self):
1005 try:
1006 nodeids = self.client.get_children(self.NODE_ROOT)
1007 except kazoo.exceptions.NoNodeError:
1008 return []
1009 nodes = []
1010 for oid in sorted(nodeids):
1011 path = self.NODE_ROOT + '/' + oid
1012 data, stat = self.client.get(path)
1013 data = json.loads(data)
1014 data['_oid'] = oid
1015 try:
1016 lockfiles = self.client.get_children(path + '/lock')
1017 except kazoo.exceptions.NoNodeError:
1018 lockfiles = []
1019 if lockfiles:
1020 data['_lock'] = True
1021 else:
1022 data['_lock'] = False
1023 nodes.append(data)
1024 return nodes
1025
James E. Blaira38c28e2017-01-04 10:33:20 -08001026 def makeNode(self, request_id, node_type):
1027 now = time.time()
1028 path = '/nodepool/nodes/'
1029 data = dict(type=node_type,
1030 provider='test-provider',
1031 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001032 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001033 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001034 public_ipv4='127.0.0.1',
1035 private_ipv4=None,
1036 public_ipv6=None,
1037 allocated_to=request_id,
1038 state='ready',
1039 state_time=now,
1040 created_time=now,
1041 updated_time=now,
1042 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001043 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001044 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001045 data = json.dumps(data)
1046 path = self.client.create(path, data,
1047 makepath=True,
1048 sequence=True)
1049 nodeid = path.split("/")[-1]
1050 return nodeid
1051
James E. Blair6ab79e02017-01-06 10:10:17 -08001052 def addFailRequest(self, request):
1053 self.fail_requests.add(request['_oid'])
1054
James E. Blairdce6cea2016-12-20 16:45:32 -08001055 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001056 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001057 return
1058 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001059 oid = request['_oid']
1060 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001061
James E. Blair6ab79e02017-01-06 10:10:17 -08001062 if oid in self.fail_requests:
1063 request['state'] = 'failed'
1064 else:
1065 request['state'] = 'fulfilled'
1066 nodes = []
1067 for node in request['node_types']:
1068 nodeid = self.makeNode(oid, node)
1069 nodes.append(nodeid)
1070 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001071
James E. Blaira38c28e2017-01-04 10:33:20 -08001072 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001073 path = self.REQUEST_ROOT + '/' + oid
1074 data = json.dumps(request)
1075 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1076 self.client.set(path, data)
1077
1078
James E. Blair498059b2016-12-20 13:50:13 -08001079class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001080 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001081 super(ChrootedKazooFixture, self).__init__()
1082
1083 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1084 if ':' in zk_host:
1085 host, port = zk_host.split(':')
1086 else:
1087 host = zk_host
1088 port = None
1089
1090 self.zookeeper_host = host
1091
1092 if not port:
1093 self.zookeeper_port = 2181
1094 else:
1095 self.zookeeper_port = int(port)
1096
Clark Boylan621ec9a2017-04-07 17:41:33 -07001097 self.test_id = test_id
1098
James E. Blair498059b2016-12-20 13:50:13 -08001099 def _setUp(self):
1100 # Make sure the test chroot paths do not conflict
1101 random_bits = ''.join(random.choice(string.ascii_lowercase +
1102 string.ascii_uppercase)
1103 for x in range(8))
1104
Clark Boylan621ec9a2017-04-07 17:41:33 -07001105 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001106 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1107
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001108 self.addCleanup(self._cleanup)
1109
James E. Blair498059b2016-12-20 13:50:13 -08001110 # Ensure the chroot path exists and clean up any pre-existing znodes.
1111 _tmp_client = kazoo.client.KazooClient(
1112 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1113 _tmp_client.start()
1114
1115 if _tmp_client.exists(self.zookeeper_chroot):
1116 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1117
1118 _tmp_client.ensure_path(self.zookeeper_chroot)
1119 _tmp_client.stop()
1120 _tmp_client.close()
1121
James E. Blair498059b2016-12-20 13:50:13 -08001122 def _cleanup(self):
1123 '''Remove the chroot path.'''
1124 # Need a non-chroot'ed client to remove the chroot path
1125 _tmp_client = kazoo.client.KazooClient(
1126 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1127 _tmp_client.start()
1128 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1129 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001130 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001131
1132
Joshua Heskethd78b4482015-09-14 16:56:34 -06001133class MySQLSchemaFixture(fixtures.Fixture):
1134 def setUp(self):
1135 super(MySQLSchemaFixture, self).setUp()
1136
1137 random_bits = ''.join(random.choice(string.ascii_lowercase +
1138 string.ascii_uppercase)
1139 for x in range(8))
1140 self.name = '%s_%s' % (random_bits, os.getpid())
1141 self.passwd = uuid.uuid4().hex
1142 db = pymysql.connect(host="localhost",
1143 user="openstack_citest",
1144 passwd="openstack_citest",
1145 db="openstack_citest")
1146 cur = db.cursor()
1147 cur.execute("create database %s" % self.name)
1148 cur.execute(
1149 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1150 (self.name, self.name, self.passwd))
1151 cur.execute("flush privileges")
1152
1153 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1154 self.passwd,
1155 self.name)
1156 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1157 self.addCleanup(self.cleanup)
1158
1159 def cleanup(self):
1160 db = pymysql.connect(host="localhost",
1161 user="openstack_citest",
1162 passwd="openstack_citest",
1163 db="openstack_citest")
1164 cur = db.cursor()
1165 cur.execute("drop database %s" % self.name)
1166 cur.execute("drop user '%s'@'localhost'" % self.name)
1167 cur.execute("flush privileges")
1168
1169
Maru Newby3fe5f852015-01-13 04:22:14 +00001170class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001171 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001172 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001173
James E. Blair1c236df2017-02-01 14:07:24 -08001174 def attachLogs(self, *args):
1175 def reader():
1176 self._log_stream.seek(0)
1177 while True:
1178 x = self._log_stream.read(4096)
1179 if not x:
1180 break
1181 yield x.encode('utf8')
1182 content = testtools.content.content_from_reader(
1183 reader,
1184 testtools.content_type.UTF8_TEXT,
1185 False)
1186 self.addDetail('logging', content)
1187
Clark Boylanb640e052014-04-03 16:41:46 -07001188 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001189 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001190 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1191 try:
1192 test_timeout = int(test_timeout)
1193 except ValueError:
1194 # If timeout value is invalid do not set a timeout.
1195 test_timeout = 0
1196 if test_timeout > 0:
1197 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1198
1199 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1200 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1201 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1202 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1203 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1204 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1205 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1206 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1207 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1208 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001209 self._log_stream = StringIO()
1210 self.addOnException(self.attachLogs)
1211 else:
1212 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001213
James E. Blair1c236df2017-02-01 14:07:24 -08001214 handler = logging.StreamHandler(self._log_stream)
1215 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1216 '%(levelname)-8s %(message)s')
1217 handler.setFormatter(formatter)
1218
1219 logger = logging.getLogger()
1220 logger.setLevel(logging.DEBUG)
1221 logger.addHandler(handler)
1222
1223 # NOTE(notmorgan): Extract logging overrides for specific
1224 # libraries from the OS_LOG_DEFAULTS env and create loggers
1225 # for each. This is used to limit the output during test runs
1226 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001227 log_defaults_from_env = os.environ.get(
1228 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001229 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001230
James E. Blairdce6cea2016-12-20 16:45:32 -08001231 if log_defaults_from_env:
1232 for default in log_defaults_from_env.split(','):
1233 try:
1234 name, level_str = default.split('=', 1)
1235 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001236 logger = logging.getLogger(name)
1237 logger.setLevel(level)
1238 logger.addHandler(handler)
1239 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001240 except ValueError:
1241 # NOTE(notmorgan): Invalid format of the log default,
1242 # skip and don't try and apply a logger for the
1243 # specified module
1244 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001245
Maru Newby3fe5f852015-01-13 04:22:14 +00001246
1247class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001248 """A test case with a functioning Zuul.
1249
1250 The following class variables are used during test setup and can
1251 be overidden by subclasses but are effectively read-only once a
1252 test method starts running:
1253
1254 :cvar str config_file: This points to the main zuul config file
1255 within the fixtures directory. Subclasses may override this
1256 to obtain a different behavior.
1257
1258 :cvar str tenant_config_file: This is the tenant config file
1259 (which specifies from what git repos the configuration should
1260 be loaded). It defaults to the value specified in
1261 `config_file` but can be overidden by subclasses to obtain a
1262 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001263 configuration. See also the :py:func:`simple_layout`
1264 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001265
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001266 :cvar bool create_project_keys: Indicates whether Zuul should
1267 auto-generate keys for each project, or whether the test
1268 infrastructure should insert dummy keys to save time during
1269 startup. Defaults to False.
1270
James E. Blaire7b99a02016-08-05 14:27:34 -07001271 The following are instance variables that are useful within test
1272 methods:
1273
1274 :ivar FakeGerritConnection fake_<connection>:
1275 A :py:class:`~tests.base.FakeGerritConnection` will be
1276 instantiated for each connection present in the config file
1277 and stored here. For instance, `fake_gerrit` will hold the
1278 FakeGerritConnection object for a connection named `gerrit`.
1279
1280 :ivar FakeGearmanServer gearman_server: An instance of
1281 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1282 server that all of the Zuul components in this test use to
1283 communicate with each other.
1284
Paul Belanger174a8272017-03-14 13:20:10 -04001285 :ivar RecordingExecutorServer executor_server: An instance of
1286 :py:class:`~tests.base.RecordingExecutorServer` which is the
1287 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001288
1289 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1290 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001291 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001292 list upon completion.
1293
1294 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1295 objects representing completed builds. They are appended to
1296 the list in the order they complete.
1297
1298 """
1299
James E. Blair83005782015-12-11 14:46:03 -08001300 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001301 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001302 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001303
1304 def _startMerger(self):
1305 self.merge_server = zuul.merger.server.MergeServer(self.config,
1306 self.connections)
1307 self.merge_server.start()
1308
Maru Newby3fe5f852015-01-13 04:22:14 +00001309 def setUp(self):
1310 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001311
1312 self.setupZK()
1313
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001314 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001315 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001316 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1317 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001318 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001319 tmp_root = tempfile.mkdtemp(
1320 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001321 self.test_root = os.path.join(tmp_root, "zuul-test")
1322 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001323 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001324 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001325 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001326
1327 if os.path.exists(self.test_root):
1328 shutil.rmtree(self.test_root)
1329 os.makedirs(self.test_root)
1330 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001331 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001332
1333 # Make per test copy of Configuration.
1334 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001335 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001336 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001337 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001338 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001339 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001340 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001341
Clark Boylanb640e052014-04-03 16:41:46 -07001342 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001343 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1344 # see: https://github.com/jsocol/pystatsd/issues/61
1345 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001346 os.environ['STATSD_PORT'] = str(self.statsd.port)
1347 self.statsd.start()
1348 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001349 reload_module(statsd)
1350 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001351
1352 self.gearman_server = FakeGearmanServer()
1353
1354 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001355 self.log.info("Gearman server on port %s" %
1356 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001357
James E. Blaire511d2f2016-12-08 15:22:26 -08001358 gerritsource.GerritSource.replication_timeout = 1.5
1359 gerritsource.GerritSource.replication_retry_interval = 0.5
1360 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001361
Joshua Hesketh352264b2015-08-11 23:42:08 +10001362 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001363
Jan Hruban6b71aff2015-10-22 16:58:08 +02001364 self.event_queues = [
1365 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001366 self.sched.trigger_event_queue,
1367 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001368 ]
1369
James E. Blairfef78942016-03-11 16:28:56 -08001370 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001371 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001372
Clark Boylanb640e052014-04-03 16:41:46 -07001373 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001374 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001375 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001376 return FakeURLOpener(self.upstream_root, *args, **kw)
1377
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001378 old_urlopen = urllib.request.urlopen
1379 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001380
James E. Blair3f876d52016-07-22 13:07:14 -07001381 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001382
Paul Belanger174a8272017-03-14 13:20:10 -04001383 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001384 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001385 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001386 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001387 _test_root=self.test_root,
1388 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001389 self.executor_server.start()
1390 self.history = self.executor_server.build_history
1391 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001392
Paul Belanger174a8272017-03-14 13:20:10 -04001393 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001394 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001395 self.merge_client = zuul.merger.client.MergeClient(
1396 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001397 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001398 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001399 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001400
James E. Blair0d5a36e2017-02-21 10:53:44 -05001401 self.fake_nodepool = FakeNodepool(
1402 self.zk_chroot_fixture.zookeeper_host,
1403 self.zk_chroot_fixture.zookeeper_port,
1404 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001405
Paul Belanger174a8272017-03-14 13:20:10 -04001406 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001407 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001408 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001409 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001410
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001411 self.webapp = zuul.webapp.WebApp(
1412 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001413 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001414
1415 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001416 self.webapp.start()
1417 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001418 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001419 # Cleanups are run in reverse order
1420 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001421 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001422 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001423
James E. Blairb9c0d772017-03-03 14:34:49 -08001424 self.sched.reconfigure(self.config)
1425 self.sched.resume()
1426
James E. Blairfef78942016-03-11 16:28:56 -08001427 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001428 # Set up gerrit related fakes
1429 # Set a changes database so multiple FakeGerrit's can report back to
1430 # a virtual canonical database given by the configured hostname
1431 self.gerrit_changes_dbs = {}
1432
1433 def getGerritConnection(driver, name, config):
1434 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1435 con = FakeGerritConnection(driver, name, config,
1436 changes_db=db,
1437 upstream_root=self.upstream_root)
1438 self.event_queues.append(con.event_queue)
1439 setattr(self, 'fake_' + name, con)
1440 return con
1441
1442 self.useFixture(fixtures.MonkeyPatch(
1443 'zuul.driver.gerrit.GerritDriver.getConnection',
1444 getGerritConnection))
1445
1446 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001447 # TODO(jhesketh): This should come from lib.connections for better
1448 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001449 # Register connections from the config
1450 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001451
Joshua Hesketh352264b2015-08-11 23:42:08 +10001452 def FakeSMTPFactory(*args, **kw):
1453 args = [self.smtp_messages] + list(args)
1454 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001455
Joshua Hesketh352264b2015-08-11 23:42:08 +10001456 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001457
James E. Blaire511d2f2016-12-08 15:22:26 -08001458 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001459 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001460 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001461
James E. Blair83005782015-12-11 14:46:03 -08001462 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001463 # This creates the per-test configuration object. It can be
1464 # overriden by subclasses, but should not need to be since it
1465 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001466 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001467 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001468
1469 if not self.setupSimpleLayout():
1470 if hasattr(self, 'tenant_config_file'):
1471 self.config.set('zuul', 'tenant_config',
1472 self.tenant_config_file)
1473 git_path = os.path.join(
1474 os.path.dirname(
1475 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1476 'git')
1477 if os.path.exists(git_path):
1478 for reponame in os.listdir(git_path):
1479 project = reponame.replace('_', '/')
1480 self.copyDirToRepo(project,
1481 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001482 self.setupAllProjectKeys()
1483
James E. Blair06cc3922017-04-19 10:08:10 -07001484 def setupSimpleLayout(self):
1485 # If the test method has been decorated with a simple_layout,
1486 # use that instead of the class tenant_config_file. Set up a
1487 # single config-project with the specified layout, and
1488 # initialize repos for all of the 'project' entries which
1489 # appear in the layout.
1490 test_name = self.id().split('.')[-1]
1491 test = getattr(self, test_name)
1492 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001493 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001494 else:
1495 return False
1496
James E. Blairb70e55a2017-04-19 12:57:02 -07001497 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001498 path = os.path.join(FIXTURE_DIR, path)
1499 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001500 data = f.read()
1501 layout = yaml.safe_load(data)
1502 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001503 untrusted_projects = []
1504 for item in layout:
1505 if 'project' in item:
1506 name = item['project']['name']
1507 untrusted_projects.append(name)
1508 self.init_repo(name)
1509 self.addCommitToRepo(name, 'initial commit',
1510 files={'README': ''},
1511 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001512 if 'job' in item:
1513 jobname = item['job']['name']
1514 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001515
1516 root = os.path.join(self.test_root, "config")
1517 if not os.path.exists(root):
1518 os.makedirs(root)
1519 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1520 config = [{'tenant':
1521 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001522 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001523 {'config-projects': ['common-config'],
1524 'untrusted-projects': untrusted_projects}}}}]
1525 f.write(yaml.dump(config))
1526 f.close()
1527 self.config.set('zuul', 'tenant_config',
1528 os.path.join(FIXTURE_DIR, f.name))
1529
1530 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001531 self.addCommitToRepo('common-config', 'add content from fixture',
1532 files, branch='master', tag='init')
1533
1534 return True
1535
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001536 def setupAllProjectKeys(self):
1537 if self.create_project_keys:
1538 return
1539
1540 path = self.config.get('zuul', 'tenant_config')
1541 with open(os.path.join(FIXTURE_DIR, path)) as f:
1542 tenant_config = yaml.safe_load(f.read())
1543 for tenant in tenant_config:
1544 sources = tenant['tenant']['source']
1545 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001546 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001547 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001548 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001549 self.setupProjectKeys(source, project)
1550
1551 def setupProjectKeys(self, source, project):
1552 # Make sure we set up an RSA key for the project so that we
1553 # don't spend time generating one:
1554
1555 key_root = os.path.join(self.state_root, 'keys')
1556 if not os.path.isdir(key_root):
1557 os.mkdir(key_root, 0o700)
1558 private_key_file = os.path.join(key_root, source, project + '.pem')
1559 private_key_dir = os.path.dirname(private_key_file)
1560 self.log.debug("Installing test keys for project %s at %s" % (
1561 project, private_key_file))
1562 if not os.path.isdir(private_key_dir):
1563 os.makedirs(private_key_dir)
1564 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1565 with open(private_key_file, 'w') as o:
1566 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001567
James E. Blair498059b2016-12-20 13:50:13 -08001568 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001569 self.zk_chroot_fixture = self.useFixture(
1570 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001571 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001572 self.zk_chroot_fixture.zookeeper_host,
1573 self.zk_chroot_fixture.zookeeper_port,
1574 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001575
James E. Blair96c6bf82016-01-15 16:20:40 -08001576 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001577 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001578
1579 files = {}
1580 for (dirpath, dirnames, filenames) in os.walk(source_path):
1581 for filename in filenames:
1582 test_tree_filepath = os.path.join(dirpath, filename)
1583 common_path = os.path.commonprefix([test_tree_filepath,
1584 source_path])
1585 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1586 with open(test_tree_filepath, 'r') as f:
1587 content = f.read()
1588 files[relative_filepath] = content
1589 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001590 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001591
James E. Blaire18d4602017-01-05 11:17:28 -08001592 def assertNodepoolState(self):
1593 # Make sure that there are no pending requests
1594
1595 requests = self.fake_nodepool.getNodeRequests()
1596 self.assertEqual(len(requests), 0)
1597
1598 nodes = self.fake_nodepool.getNodes()
1599 for node in nodes:
1600 self.assertFalse(node['_lock'], "Node %s is locked" %
1601 (node['_oid'],))
1602
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001603 def assertNoGeneratedKeys(self):
1604 # Make sure that Zuul did not generate any project keys
1605 # (unless it was supposed to).
1606
1607 if self.create_project_keys:
1608 return
1609
1610 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1611 test_key = i.read()
1612
1613 key_root = os.path.join(self.state_root, 'keys')
1614 for root, dirname, files in os.walk(key_root):
1615 for fn in files:
1616 with open(os.path.join(root, fn)) as f:
1617 self.assertEqual(test_key, f.read())
1618
Clark Boylanb640e052014-04-03 16:41:46 -07001619 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001620 self.log.debug("Assert final state")
1621 # Make sure no jobs are running
1622 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001623 # Make sure that git.Repo objects have been garbage collected.
1624 repos = []
1625 gc.collect()
1626 for obj in gc.get_objects():
1627 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001628 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001629 repos.append(obj)
1630 self.assertEqual(len(repos), 0)
1631 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001632 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001633 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001634 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001635 for tenant in self.sched.abide.tenants.values():
1636 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001637 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001638 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001639
1640 def shutdown(self):
1641 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001642 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001643 self.merge_server.stop()
1644 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001645 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001646 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001647 self.sched.stop()
1648 self.sched.join()
1649 self.statsd.stop()
1650 self.statsd.join()
1651 self.webapp.stop()
1652 self.webapp.join()
1653 self.rpc.stop()
1654 self.rpc.join()
1655 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001656 self.fake_nodepool.stop()
1657 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001658 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001659 # we whitelist watchdog threads as they have relatively long delays
1660 # before noticing they should exit, but they should exit on their own.
1661 threads = [t for t in threading.enumerate()
1662 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001663 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001664 log_str = ""
1665 for thread_id, stack_frame in sys._current_frames().items():
1666 log_str += "Thread: %s\n" % thread_id
1667 log_str += "".join(traceback.format_stack(stack_frame))
1668 self.log.debug(log_str)
1669 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001670
James E. Blaira002b032017-04-18 10:35:48 -07001671 def assertCleanShutdown(self):
1672 pass
1673
James E. Blairc4ba97a2017-04-19 16:26:24 -07001674 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001675 parts = project.split('/')
1676 path = os.path.join(self.upstream_root, *parts[:-1])
1677 if not os.path.exists(path):
1678 os.makedirs(path)
1679 path = os.path.join(self.upstream_root, project)
1680 repo = git.Repo.init(path)
1681
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001682 with repo.config_writer() as config_writer:
1683 config_writer.set_value('user', 'email', 'user@example.com')
1684 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001685
Clark Boylanb640e052014-04-03 16:41:46 -07001686 repo.index.commit('initial commit')
1687 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001688 if tag:
1689 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001690
James E. Blair97d902e2014-08-21 13:25:56 -07001691 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001692 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001693 repo.git.clean('-x', '-f', '-d')
1694
James E. Blair97d902e2014-08-21 13:25:56 -07001695 def create_branch(self, project, branch):
1696 path = os.path.join(self.upstream_root, project)
1697 repo = git.Repo.init(path)
1698 fn = os.path.join(path, 'README')
1699
1700 branch_head = repo.create_head(branch)
1701 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001702 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001703 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001704 f.close()
1705 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001706 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001707
James E. Blair97d902e2014-08-21 13:25:56 -07001708 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001709 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001710 repo.git.clean('-x', '-f', '-d')
1711
Sachi King9f16d522016-03-16 12:20:45 +11001712 def create_commit(self, project):
1713 path = os.path.join(self.upstream_root, project)
1714 repo = git.Repo(path)
1715 repo.head.reference = repo.heads['master']
1716 file_name = os.path.join(path, 'README')
1717 with open(file_name, 'a') as f:
1718 f.write('creating fake commit\n')
1719 repo.index.add([file_name])
1720 commit = repo.index.commit('Creating a fake commit')
1721 return commit.hexsha
1722
James E. Blairf4a5f022017-04-18 14:01:10 -07001723 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001724 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001725 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001726 while len(self.builds):
1727 self.release(self.builds[0])
1728 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001729 i += 1
1730 if count is not None and i >= count:
1731 break
James E. Blairb8c16472015-05-05 14:55:26 -07001732
Clark Boylanb640e052014-04-03 16:41:46 -07001733 def release(self, job):
1734 if isinstance(job, FakeBuild):
1735 job.release()
1736 else:
1737 job.waiting = False
1738 self.log.debug("Queued job %s released" % job.unique)
1739 self.gearman_server.wakeConnections()
1740
1741 def getParameter(self, job, name):
1742 if isinstance(job, FakeBuild):
1743 return job.parameters[name]
1744 else:
1745 parameters = json.loads(job.arguments)
1746 return parameters[name]
1747
Clark Boylanb640e052014-04-03 16:41:46 -07001748 def haveAllBuildsReported(self):
1749 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001750 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001751 return False
1752 # Find out if every build that the worker has completed has been
1753 # reported back to Zuul. If it hasn't then that means a Gearman
1754 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001755 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001756 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001757 if not zbuild:
1758 # It has already been reported
1759 continue
1760 # It hasn't been reported yet.
1761 return False
1762 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001763 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001764 if connection.state == 'GRAB_WAIT':
1765 return False
1766 return True
1767
1768 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001769 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001770 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001771 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001772 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001773 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001774 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001775 for j in conn.related_jobs.values():
1776 if j.unique == build.uuid:
1777 client_job = j
1778 break
1779 if not client_job:
1780 self.log.debug("%s is not known to the gearman client" %
1781 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001782 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001783 if not client_job.handle:
1784 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001785 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001786 server_job = self.gearman_server.jobs.get(client_job.handle)
1787 if not server_job:
1788 self.log.debug("%s is not known to the gearman server" %
1789 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001790 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001791 if not hasattr(server_job, 'waiting'):
1792 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001793 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001794 if server_job.waiting:
1795 continue
James E. Blair17302972016-08-10 16:11:42 -07001796 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001797 self.log.debug("%s has not reported start" % build)
1798 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001799 worker_build = self.executor_server.job_builds.get(
1800 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001801 if worker_build:
1802 if worker_build.isWaiting():
1803 continue
1804 else:
1805 self.log.debug("%s is running" % worker_build)
1806 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001807 else:
James E. Blair962220f2016-08-03 11:22:38 -07001808 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001809 return False
James E. Blaira002b032017-04-18 10:35:48 -07001810 for (build_uuid, job_worker) in \
1811 self.executor_server.job_workers.items():
1812 if build_uuid not in seen_builds:
1813 self.log.debug("%s is not finalized" % build_uuid)
1814 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001815 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001816
James E. Blairdce6cea2016-12-20 16:45:32 -08001817 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001818 if self.fake_nodepool.paused:
1819 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001820 if self.sched.nodepool.requests:
1821 return False
1822 return True
1823
Jan Hruban6b71aff2015-10-22 16:58:08 +02001824 def eventQueuesEmpty(self):
1825 for queue in self.event_queues:
1826 yield queue.empty()
1827
1828 def eventQueuesJoin(self):
1829 for queue in self.event_queues:
1830 queue.join()
1831
Clark Boylanb640e052014-04-03 16:41:46 -07001832 def waitUntilSettled(self):
1833 self.log.debug("Waiting until settled...")
1834 start = time.time()
1835 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001836 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001837 self.log.error("Timeout waiting for Zuul to settle")
1838 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001839 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001840 self.log.error(" %s: %s" % (queue, queue.empty()))
1841 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001842 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001843 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001844 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001845 self.log.error("All requests completed: %s" %
1846 (self.areAllNodeRequestsComplete(),))
1847 self.log.error("Merge client jobs: %s" %
1848 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001849 raise Exception("Timeout waiting for Zuul to settle")
1850 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001851
Paul Belanger174a8272017-03-14 13:20:10 -04001852 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001853 # have all build states propogated to zuul?
1854 if self.haveAllBuildsReported():
1855 # Join ensures that the queue is empty _and_ events have been
1856 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001857 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001858 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001859 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001860 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001861 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001862 self.areAllNodeRequestsComplete() and
1863 all(self.eventQueuesEmpty())):
1864 # The queue empty check is placed at the end to
1865 # ensure that if a component adds an event between
1866 # when locked the run handler and checked that the
1867 # components were stable, we don't erroneously
1868 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001869 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001870 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001871 self.log.debug("...settled.")
1872 return
1873 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001874 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001875 self.sched.wake_event.wait(0.1)
1876
1877 def countJobResults(self, jobs, result):
1878 jobs = filter(lambda x: x.result == result, jobs)
1879 return len(jobs)
1880
James E. Blair96c6bf82016-01-15 16:20:40 -08001881 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001882 for job in self.history:
1883 if (job.name == name and
1884 (project is None or
1885 job.parameters['ZUUL_PROJECT'] == project)):
1886 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001887 raise Exception("Unable to find job %s in history" % name)
1888
1889 def assertEmptyQueues(self):
1890 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001891 for tenant in self.sched.abide.tenants.values():
1892 for pipeline in tenant.layout.pipelines.values():
1893 for queue in pipeline.queues:
1894 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001895 print('pipeline %s queue %s contents %s' % (
1896 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001897 self.assertEqual(len(queue.queue), 0,
1898 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001899
1900 def assertReportedStat(self, key, value=None, kind=None):
1901 start = time.time()
1902 while time.time() < (start + 5):
1903 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001904 k, v = stat.split(':')
1905 if key == k:
1906 if value is None and kind is None:
1907 return
1908 elif value:
1909 if value == v:
1910 return
1911 elif kind:
1912 if v.endswith('|' + kind):
1913 return
1914 time.sleep(0.1)
1915
Clark Boylanb640e052014-04-03 16:41:46 -07001916 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001917
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001918 def assertBuilds(self, builds):
1919 """Assert that the running builds are as described.
1920
1921 The list of running builds is examined and must match exactly
1922 the list of builds described by the input.
1923
1924 :arg list builds: A list of dictionaries. Each item in the
1925 list must match the corresponding build in the build
1926 history, and each element of the dictionary must match the
1927 corresponding attribute of the build.
1928
1929 """
James E. Blair3158e282016-08-19 09:34:11 -07001930 try:
1931 self.assertEqual(len(self.builds), len(builds))
1932 for i, d in enumerate(builds):
1933 for k, v in d.items():
1934 self.assertEqual(
1935 getattr(self.builds[i], k), v,
1936 "Element %i in builds does not match" % (i,))
1937 except Exception:
1938 for build in self.builds:
1939 self.log.error("Running build: %s" % build)
1940 else:
1941 self.log.error("No running builds")
1942 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001943
James E. Blairb536ecc2016-08-31 10:11:42 -07001944 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001945 """Assert that the completed builds are as described.
1946
1947 The list of completed builds is examined and must match
1948 exactly the list of builds described by the input.
1949
1950 :arg list history: A list of dictionaries. Each item in the
1951 list must match the corresponding build in the build
1952 history, and each element of the dictionary must match the
1953 corresponding attribute of the build.
1954
James E. Blairb536ecc2016-08-31 10:11:42 -07001955 :arg bool ordered: If true, the history must match the order
1956 supplied, if false, the builds are permitted to have
1957 arrived in any order.
1958
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001959 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001960 def matches(history_item, item):
1961 for k, v in item.items():
1962 if getattr(history_item, k) != v:
1963 return False
1964 return True
James E. Blair3158e282016-08-19 09:34:11 -07001965 try:
1966 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001967 if ordered:
1968 for i, d in enumerate(history):
1969 if not matches(self.history[i], d):
1970 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001971 "Element %i in history does not match %s" %
1972 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001973 else:
1974 unseen = self.history[:]
1975 for i, d in enumerate(history):
1976 found = False
1977 for unseen_item in unseen:
1978 if matches(unseen_item, d):
1979 found = True
1980 unseen.remove(unseen_item)
1981 break
1982 if not found:
1983 raise Exception("No match found for element %i "
1984 "in history" % (i,))
1985 if unseen:
1986 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001987 except Exception:
1988 for build in self.history:
1989 self.log.error("Completed build: %s" % build)
1990 else:
1991 self.log.error("No completed builds")
1992 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001993
James E. Blair6ac368c2016-12-22 18:07:20 -08001994 def printHistory(self):
1995 """Log the build history.
1996
1997 This can be useful during tests to summarize what jobs have
1998 completed.
1999
2000 """
2001 self.log.debug("Build history:")
2002 for build in self.history:
2003 self.log.debug(build)
2004
James E. Blair59fdbac2015-12-07 17:08:06 -08002005 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002006 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2007
James E. Blair9ea70072017-04-19 16:05:30 -07002008 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002009 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002010 if not os.path.exists(root):
2011 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002012 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2013 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002014- tenant:
2015 name: openstack
2016 source:
2017 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002018 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002019 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002020 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002021 - org/project
2022 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002023 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002024 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002025 self.config.set('zuul', 'tenant_config',
2026 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002027 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002028
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002029 def addCommitToRepo(self, project, message, files,
2030 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002031 path = os.path.join(self.upstream_root, project)
2032 repo = git.Repo(path)
2033 repo.head.reference = branch
2034 zuul.merger.merger.reset_repo_to_head(repo)
2035 for fn, content in files.items():
2036 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002037 try:
2038 os.makedirs(os.path.dirname(fn))
2039 except OSError:
2040 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002041 with open(fn, 'w') as f:
2042 f.write(content)
2043 repo.index.add([fn])
2044 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002045 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002046 repo.heads[branch].commit = commit
2047 repo.head.reference = branch
2048 repo.git.clean('-x', '-f', '-d')
2049 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002050 if tag:
2051 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002052 return before
2053
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002054 def commitConfigUpdate(self, project_name, source_name):
2055 """Commit an update to zuul.yaml
2056
2057 This overwrites the zuul.yaml in the specificed project with
2058 the contents specified.
2059
2060 :arg str project_name: The name of the project containing
2061 zuul.yaml (e.g., common-config)
2062
2063 :arg str source_name: The path to the file (underneath the
2064 test fixture directory) whose contents should be used to
2065 replace zuul.yaml.
2066 """
2067
2068 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002069 files = {}
2070 with open(source_path, 'r') as f:
2071 data = f.read()
2072 layout = yaml.safe_load(data)
2073 files['zuul.yaml'] = data
2074 for item in layout:
2075 if 'job' in item:
2076 jobname = item['job']['name']
2077 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002078 before = self.addCommitToRepo(
2079 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002080 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002081 return before
2082
James E. Blair7fc8daa2016-08-08 15:37:15 -07002083 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002084
James E. Blair7fc8daa2016-08-08 15:37:15 -07002085 """Inject a Fake (Gerrit) event.
2086
2087 This method accepts a JSON-encoded event and simulates Zuul
2088 having received it from Gerrit. It could (and should)
2089 eventually apply to any connection type, but is currently only
2090 used with Gerrit connections. The name of the connection is
2091 used to look up the corresponding server, and the event is
2092 simulated as having been received by all Zuul connections
2093 attached to that server. So if two Gerrit connections in Zuul
2094 are connected to the same Gerrit server, and you invoke this
2095 method specifying the name of one of them, the event will be
2096 received by both.
2097
2098 .. note::
2099
2100 "self.fake_gerrit.addEvent" calls should be migrated to
2101 this method.
2102
2103 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002104 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002105 :arg str event: The JSON-encoded event.
2106
2107 """
2108 specified_conn = self.connections.connections[connection]
2109 for conn in self.connections.connections.values():
2110 if (isinstance(conn, specified_conn.__class__) and
2111 specified_conn.server == conn.server):
2112 conn.addEvent(event)
2113
James E. Blair3f876d52016-07-22 13:07:14 -07002114
2115class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002116 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002117 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002118
Joshua Heskethd78b4482015-09-14 16:56:34 -06002119
2120class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002121 def setup_config(self):
2122 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002123 for section_name in self.config.sections():
2124 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2125 section_name, re.I)
2126 if not con_match:
2127 continue
2128
2129 if self.config.get(section_name, 'driver') == 'sql':
2130 f = MySQLSchemaFixture()
2131 self.useFixture(f)
2132 if (self.config.get(section_name, 'dburi') ==
2133 '$MYSQL_FIXTURE_DBURI$'):
2134 self.config.set(section_name, 'dburi', f.dburi)