blob: 6d3df8bbe9fbf0afc12278f944f67c2ad5a9958f [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
Clark Boylan3410d532017-04-25 12:35:29 -07001223 # Make sure we don't carry old handlers around in process state
1224 # which slows down test runs
1225 self.addCleanup(logger.removeHandler, handler)
1226 self.addCleanup(handler.close)
1227 self.addCleanup(handler.flush)
1228
James E. Blair1c236df2017-02-01 14:07:24 -08001229 # NOTE(notmorgan): Extract logging overrides for specific
1230 # libraries from the OS_LOG_DEFAULTS env and create loggers
1231 # for each. This is used to limit the output during test runs
1232 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001233 log_defaults_from_env = os.environ.get(
1234 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001235 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001236
James E. Blairdce6cea2016-12-20 16:45:32 -08001237 if log_defaults_from_env:
1238 for default in log_defaults_from_env.split(','):
1239 try:
1240 name, level_str = default.split('=', 1)
1241 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001242 logger = logging.getLogger(name)
1243 logger.setLevel(level)
1244 logger.addHandler(handler)
1245 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001246 except ValueError:
1247 # NOTE(notmorgan): Invalid format of the log default,
1248 # skip and don't try and apply a logger for the
1249 # specified module
1250 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001251
Maru Newby3fe5f852015-01-13 04:22:14 +00001252
1253class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001254 """A test case with a functioning Zuul.
1255
1256 The following class variables are used during test setup and can
1257 be overidden by subclasses but are effectively read-only once a
1258 test method starts running:
1259
1260 :cvar str config_file: This points to the main zuul config file
1261 within the fixtures directory. Subclasses may override this
1262 to obtain a different behavior.
1263
1264 :cvar str tenant_config_file: This is the tenant config file
1265 (which specifies from what git repos the configuration should
1266 be loaded). It defaults to the value specified in
1267 `config_file` but can be overidden by subclasses to obtain a
1268 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001269 configuration. See also the :py:func:`simple_layout`
1270 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001271
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001272 :cvar bool create_project_keys: Indicates whether Zuul should
1273 auto-generate keys for each project, or whether the test
1274 infrastructure should insert dummy keys to save time during
1275 startup. Defaults to False.
1276
James E. Blaire7b99a02016-08-05 14:27:34 -07001277 The following are instance variables that are useful within test
1278 methods:
1279
1280 :ivar FakeGerritConnection fake_<connection>:
1281 A :py:class:`~tests.base.FakeGerritConnection` will be
1282 instantiated for each connection present in the config file
1283 and stored here. For instance, `fake_gerrit` will hold the
1284 FakeGerritConnection object for a connection named `gerrit`.
1285
1286 :ivar FakeGearmanServer gearman_server: An instance of
1287 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1288 server that all of the Zuul components in this test use to
1289 communicate with each other.
1290
Paul Belanger174a8272017-03-14 13:20:10 -04001291 :ivar RecordingExecutorServer executor_server: An instance of
1292 :py:class:`~tests.base.RecordingExecutorServer` which is the
1293 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001294
1295 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1296 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001297 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001298 list upon completion.
1299
1300 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1301 objects representing completed builds. They are appended to
1302 the list in the order they complete.
1303
1304 """
1305
James E. Blair83005782015-12-11 14:46:03 -08001306 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001307 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001308 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001309
1310 def _startMerger(self):
1311 self.merge_server = zuul.merger.server.MergeServer(self.config,
1312 self.connections)
1313 self.merge_server.start()
1314
Maru Newby3fe5f852015-01-13 04:22:14 +00001315 def setUp(self):
1316 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001317
1318 self.setupZK()
1319
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001320 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001321 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001322 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1323 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001324 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001325 tmp_root = tempfile.mkdtemp(
1326 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001327 self.test_root = os.path.join(tmp_root, "zuul-test")
1328 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001329 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001330 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001331 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001332
1333 if os.path.exists(self.test_root):
1334 shutil.rmtree(self.test_root)
1335 os.makedirs(self.test_root)
1336 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001337 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001338
1339 # Make per test copy of Configuration.
1340 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001341 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001342 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001343 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001344 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001345 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001346 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001347
Clark Boylanb640e052014-04-03 16:41:46 -07001348 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001349 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1350 # see: https://github.com/jsocol/pystatsd/issues/61
1351 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001352 os.environ['STATSD_PORT'] = str(self.statsd.port)
1353 self.statsd.start()
1354 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001355 reload_module(statsd)
1356 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001357
1358 self.gearman_server = FakeGearmanServer()
1359
1360 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001361 self.log.info("Gearman server on port %s" %
1362 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001363
James E. Blaire511d2f2016-12-08 15:22:26 -08001364 gerritsource.GerritSource.replication_timeout = 1.5
1365 gerritsource.GerritSource.replication_retry_interval = 0.5
1366 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001367
Joshua Hesketh352264b2015-08-11 23:42:08 +10001368 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001369
Jan Hruban7083edd2015-08-21 14:00:54 +02001370 self.webapp = zuul.webapp.WebApp(
1371 self.sched, port=0, listen_address='127.0.0.1')
1372
Jan Hruban6b71aff2015-10-22 16:58:08 +02001373 self.event_queues = [
1374 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001375 self.sched.trigger_event_queue,
1376 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001377 ]
1378
James E. Blairfef78942016-03-11 16:28:56 -08001379 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001380 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001381
Clark Boylanb640e052014-04-03 16:41:46 -07001382 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001383 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001384 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001385 return FakeURLOpener(self.upstream_root, *args, **kw)
1386
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001387 old_urlopen = urllib.request.urlopen
1388 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001389
James E. Blair3f876d52016-07-22 13:07:14 -07001390 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001391
Paul Belanger174a8272017-03-14 13:20:10 -04001392 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001393 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001394 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001395 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001396 _test_root=self.test_root,
1397 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001398 self.executor_server.start()
1399 self.history = self.executor_server.build_history
1400 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001401
Paul Belanger174a8272017-03-14 13:20:10 -04001402 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001403 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001404 self.merge_client = zuul.merger.client.MergeClient(
1405 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001406 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001407 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001408 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001409
James E. Blair0d5a36e2017-02-21 10:53:44 -05001410 self.fake_nodepool = FakeNodepool(
1411 self.zk_chroot_fixture.zookeeper_host,
1412 self.zk_chroot_fixture.zookeeper_port,
1413 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001414
Paul Belanger174a8272017-03-14 13:20:10 -04001415 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001416 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001417 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001418 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001419
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001420 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001421
1422 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001423 self.webapp.start()
1424 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001425 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001426 # Cleanups are run in reverse order
1427 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001428 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001429 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001430
James E. Blairb9c0d772017-03-03 14:34:49 -08001431 self.sched.reconfigure(self.config)
1432 self.sched.resume()
1433
James E. Blairfef78942016-03-11 16:28:56 -08001434 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001435 # Set up gerrit related fakes
1436 # Set a changes database so multiple FakeGerrit's can report back to
1437 # a virtual canonical database given by the configured hostname
1438 self.gerrit_changes_dbs = {}
1439
1440 def getGerritConnection(driver, name, config):
1441 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1442 con = FakeGerritConnection(driver, name, config,
1443 changes_db=db,
1444 upstream_root=self.upstream_root)
1445 self.event_queues.append(con.event_queue)
1446 setattr(self, 'fake_' + name, con)
1447 return con
1448
1449 self.useFixture(fixtures.MonkeyPatch(
1450 'zuul.driver.gerrit.GerritDriver.getConnection',
1451 getGerritConnection))
1452
1453 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001454 # TODO(jhesketh): This should come from lib.connections for better
1455 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001456 # Register connections from the config
1457 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001458
Joshua Hesketh352264b2015-08-11 23:42:08 +10001459 def FakeSMTPFactory(*args, **kw):
1460 args = [self.smtp_messages] + list(args)
1461 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001462
Joshua Hesketh352264b2015-08-11 23:42:08 +10001463 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001464
James E. Blaire511d2f2016-12-08 15:22:26 -08001465 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001466 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001467 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001468
James E. Blair83005782015-12-11 14:46:03 -08001469 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001470 # This creates the per-test configuration object. It can be
1471 # overriden by subclasses, but should not need to be since it
1472 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001473 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001474 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001475
1476 if not self.setupSimpleLayout():
1477 if hasattr(self, 'tenant_config_file'):
1478 self.config.set('zuul', 'tenant_config',
1479 self.tenant_config_file)
1480 git_path = os.path.join(
1481 os.path.dirname(
1482 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1483 'git')
1484 if os.path.exists(git_path):
1485 for reponame in os.listdir(git_path):
1486 project = reponame.replace('_', '/')
1487 self.copyDirToRepo(project,
1488 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001489 self.setupAllProjectKeys()
1490
James E. Blair06cc3922017-04-19 10:08:10 -07001491 def setupSimpleLayout(self):
1492 # If the test method has been decorated with a simple_layout,
1493 # use that instead of the class tenant_config_file. Set up a
1494 # single config-project with the specified layout, and
1495 # initialize repos for all of the 'project' entries which
1496 # appear in the layout.
1497 test_name = self.id().split('.')[-1]
1498 test = getattr(self, test_name)
1499 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001500 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001501 else:
1502 return False
1503
James E. Blairb70e55a2017-04-19 12:57:02 -07001504 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001505 path = os.path.join(FIXTURE_DIR, path)
1506 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001507 data = f.read()
1508 layout = yaml.safe_load(data)
1509 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001510 untrusted_projects = []
1511 for item in layout:
1512 if 'project' in item:
1513 name = item['project']['name']
1514 untrusted_projects.append(name)
1515 self.init_repo(name)
1516 self.addCommitToRepo(name, 'initial commit',
1517 files={'README': ''},
1518 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001519 if 'job' in item:
1520 jobname = item['job']['name']
1521 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001522
1523 root = os.path.join(self.test_root, "config")
1524 if not os.path.exists(root):
1525 os.makedirs(root)
1526 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1527 config = [{'tenant':
1528 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001529 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001530 {'config-projects': ['common-config'],
1531 'untrusted-projects': untrusted_projects}}}}]
1532 f.write(yaml.dump(config))
1533 f.close()
1534 self.config.set('zuul', 'tenant_config',
1535 os.path.join(FIXTURE_DIR, f.name))
1536
1537 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001538 self.addCommitToRepo('common-config', 'add content from fixture',
1539 files, branch='master', tag='init')
1540
1541 return True
1542
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001543 def setupAllProjectKeys(self):
1544 if self.create_project_keys:
1545 return
1546
1547 path = self.config.get('zuul', 'tenant_config')
1548 with open(os.path.join(FIXTURE_DIR, path)) as f:
1549 tenant_config = yaml.safe_load(f.read())
1550 for tenant in tenant_config:
1551 sources = tenant['tenant']['source']
1552 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001553 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001554 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001555 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001556 self.setupProjectKeys(source, project)
1557
1558 def setupProjectKeys(self, source, project):
1559 # Make sure we set up an RSA key for the project so that we
1560 # don't spend time generating one:
1561
1562 key_root = os.path.join(self.state_root, 'keys')
1563 if not os.path.isdir(key_root):
1564 os.mkdir(key_root, 0o700)
1565 private_key_file = os.path.join(key_root, source, project + '.pem')
1566 private_key_dir = os.path.dirname(private_key_file)
1567 self.log.debug("Installing test keys for project %s at %s" % (
1568 project, private_key_file))
1569 if not os.path.isdir(private_key_dir):
1570 os.makedirs(private_key_dir)
1571 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1572 with open(private_key_file, 'w') as o:
1573 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001574
James E. Blair498059b2016-12-20 13:50:13 -08001575 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001576 self.zk_chroot_fixture = self.useFixture(
1577 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001578 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001579 self.zk_chroot_fixture.zookeeper_host,
1580 self.zk_chroot_fixture.zookeeper_port,
1581 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001582
James E. Blair96c6bf82016-01-15 16:20:40 -08001583 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001584 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001585
1586 files = {}
1587 for (dirpath, dirnames, filenames) in os.walk(source_path):
1588 for filename in filenames:
1589 test_tree_filepath = os.path.join(dirpath, filename)
1590 common_path = os.path.commonprefix([test_tree_filepath,
1591 source_path])
1592 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1593 with open(test_tree_filepath, 'r') as f:
1594 content = f.read()
1595 files[relative_filepath] = content
1596 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001597 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001598
James E. Blaire18d4602017-01-05 11:17:28 -08001599 def assertNodepoolState(self):
1600 # Make sure that there are no pending requests
1601
1602 requests = self.fake_nodepool.getNodeRequests()
1603 self.assertEqual(len(requests), 0)
1604
1605 nodes = self.fake_nodepool.getNodes()
1606 for node in nodes:
1607 self.assertFalse(node['_lock'], "Node %s is locked" %
1608 (node['_oid'],))
1609
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001610 def assertNoGeneratedKeys(self):
1611 # Make sure that Zuul did not generate any project keys
1612 # (unless it was supposed to).
1613
1614 if self.create_project_keys:
1615 return
1616
1617 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1618 test_key = i.read()
1619
1620 key_root = os.path.join(self.state_root, 'keys')
1621 for root, dirname, files in os.walk(key_root):
1622 for fn in files:
1623 with open(os.path.join(root, fn)) as f:
1624 self.assertEqual(test_key, f.read())
1625
Clark Boylanb640e052014-04-03 16:41:46 -07001626 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001627 self.log.debug("Assert final state")
1628 # Make sure no jobs are running
1629 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001630 # Make sure that git.Repo objects have been garbage collected.
1631 repos = []
1632 gc.collect()
1633 for obj in gc.get_objects():
1634 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001635 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001636 repos.append(obj)
1637 self.assertEqual(len(repos), 0)
1638 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001639 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001640 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001641 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001642 for tenant in self.sched.abide.tenants.values():
1643 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001644 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001645 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001646
1647 def shutdown(self):
1648 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001649 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001650 self.merge_server.stop()
1651 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001652 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001653 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001654 self.sched.stop()
1655 self.sched.join()
1656 self.statsd.stop()
1657 self.statsd.join()
1658 self.webapp.stop()
1659 self.webapp.join()
1660 self.rpc.stop()
1661 self.rpc.join()
1662 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001663 self.fake_nodepool.stop()
1664 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001665 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001666 # we whitelist watchdog threads as they have relatively long delays
1667 # before noticing they should exit, but they should exit on their own.
1668 threads = [t for t in threading.enumerate()
1669 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001670 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001671 log_str = ""
1672 for thread_id, stack_frame in sys._current_frames().items():
1673 log_str += "Thread: %s\n" % thread_id
1674 log_str += "".join(traceback.format_stack(stack_frame))
1675 self.log.debug(log_str)
1676 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001677
James E. Blaira002b032017-04-18 10:35:48 -07001678 def assertCleanShutdown(self):
1679 pass
1680
James E. Blairc4ba97a2017-04-19 16:26:24 -07001681 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001682 parts = project.split('/')
1683 path = os.path.join(self.upstream_root, *parts[:-1])
1684 if not os.path.exists(path):
1685 os.makedirs(path)
1686 path = os.path.join(self.upstream_root, project)
1687 repo = git.Repo.init(path)
1688
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001689 with repo.config_writer() as config_writer:
1690 config_writer.set_value('user', 'email', 'user@example.com')
1691 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001692
Clark Boylanb640e052014-04-03 16:41:46 -07001693 repo.index.commit('initial commit')
1694 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001695 if tag:
1696 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001697
James E. Blair97d902e2014-08-21 13:25:56 -07001698 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001699 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001700 repo.git.clean('-x', '-f', '-d')
1701
James E. Blair97d902e2014-08-21 13:25:56 -07001702 def create_branch(self, project, branch):
1703 path = os.path.join(self.upstream_root, project)
1704 repo = git.Repo.init(path)
1705 fn = os.path.join(path, 'README')
1706
1707 branch_head = repo.create_head(branch)
1708 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001709 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001710 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001711 f.close()
1712 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001713 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001714
James E. Blair97d902e2014-08-21 13:25:56 -07001715 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001716 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001717 repo.git.clean('-x', '-f', '-d')
1718
Sachi King9f16d522016-03-16 12:20:45 +11001719 def create_commit(self, project):
1720 path = os.path.join(self.upstream_root, project)
1721 repo = git.Repo(path)
1722 repo.head.reference = repo.heads['master']
1723 file_name = os.path.join(path, 'README')
1724 with open(file_name, 'a') as f:
1725 f.write('creating fake commit\n')
1726 repo.index.add([file_name])
1727 commit = repo.index.commit('Creating a fake commit')
1728 return commit.hexsha
1729
James E. Blairf4a5f022017-04-18 14:01:10 -07001730 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001731 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001732 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001733 while len(self.builds):
1734 self.release(self.builds[0])
1735 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001736 i += 1
1737 if count is not None and i >= count:
1738 break
James E. Blairb8c16472015-05-05 14:55:26 -07001739
Clark Boylanb640e052014-04-03 16:41:46 -07001740 def release(self, job):
1741 if isinstance(job, FakeBuild):
1742 job.release()
1743 else:
1744 job.waiting = False
1745 self.log.debug("Queued job %s released" % job.unique)
1746 self.gearman_server.wakeConnections()
1747
1748 def getParameter(self, job, name):
1749 if isinstance(job, FakeBuild):
1750 return job.parameters[name]
1751 else:
1752 parameters = json.loads(job.arguments)
1753 return parameters[name]
1754
Clark Boylanb640e052014-04-03 16:41:46 -07001755 def haveAllBuildsReported(self):
1756 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001757 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001758 return False
1759 # Find out if every build that the worker has completed has been
1760 # reported back to Zuul. If it hasn't then that means a Gearman
1761 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001762 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001763 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001764 if not zbuild:
1765 # It has already been reported
1766 continue
1767 # It hasn't been reported yet.
1768 return False
1769 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001770 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001771 if connection.state == 'GRAB_WAIT':
1772 return False
1773 return True
1774
1775 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001776 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001777 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001778 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001779 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001780 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001781 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001782 for j in conn.related_jobs.values():
1783 if j.unique == build.uuid:
1784 client_job = j
1785 break
1786 if not client_job:
1787 self.log.debug("%s is not known to the gearman client" %
1788 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001789 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001790 if not client_job.handle:
1791 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001792 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001793 server_job = self.gearman_server.jobs.get(client_job.handle)
1794 if not server_job:
1795 self.log.debug("%s is not known to the gearman server" %
1796 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001797 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001798 if not hasattr(server_job, 'waiting'):
1799 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001800 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001801 if server_job.waiting:
1802 continue
James E. Blair17302972016-08-10 16:11:42 -07001803 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001804 self.log.debug("%s has not reported start" % build)
1805 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001806 worker_build = self.executor_server.job_builds.get(
1807 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001808 if worker_build:
1809 if worker_build.isWaiting():
1810 continue
1811 else:
1812 self.log.debug("%s is running" % worker_build)
1813 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001814 else:
James E. Blair962220f2016-08-03 11:22:38 -07001815 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001816 return False
James E. Blaira002b032017-04-18 10:35:48 -07001817 for (build_uuid, job_worker) in \
1818 self.executor_server.job_workers.items():
1819 if build_uuid not in seen_builds:
1820 self.log.debug("%s is not finalized" % build_uuid)
1821 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001822 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001823
James E. Blairdce6cea2016-12-20 16:45:32 -08001824 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001825 if self.fake_nodepool.paused:
1826 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001827 if self.sched.nodepool.requests:
1828 return False
1829 return True
1830
Jan Hruban6b71aff2015-10-22 16:58:08 +02001831 def eventQueuesEmpty(self):
1832 for queue in self.event_queues:
1833 yield queue.empty()
1834
1835 def eventQueuesJoin(self):
1836 for queue in self.event_queues:
1837 queue.join()
1838
Clark Boylanb640e052014-04-03 16:41:46 -07001839 def waitUntilSettled(self):
1840 self.log.debug("Waiting until settled...")
1841 start = time.time()
1842 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001843 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001844 self.log.error("Timeout waiting for Zuul to settle")
1845 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001846 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001847 self.log.error(" %s: %s" % (queue, queue.empty()))
1848 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001849 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001850 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001851 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001852 self.log.error("All requests completed: %s" %
1853 (self.areAllNodeRequestsComplete(),))
1854 self.log.error("Merge client jobs: %s" %
1855 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001856 raise Exception("Timeout waiting for Zuul to settle")
1857 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001858
Paul Belanger174a8272017-03-14 13:20:10 -04001859 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001860 # have all build states propogated to zuul?
1861 if self.haveAllBuildsReported():
1862 # Join ensures that the queue is empty _and_ events have been
1863 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001864 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001865 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001866 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001867 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001868 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001869 self.areAllNodeRequestsComplete() and
1870 all(self.eventQueuesEmpty())):
1871 # The queue empty check is placed at the end to
1872 # ensure that if a component adds an event between
1873 # when locked the run handler and checked that the
1874 # components were stable, we don't erroneously
1875 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001876 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001877 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001878 self.log.debug("...settled.")
1879 return
1880 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001881 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001882 self.sched.wake_event.wait(0.1)
1883
1884 def countJobResults(self, jobs, result):
1885 jobs = filter(lambda x: x.result == result, jobs)
1886 return len(jobs)
1887
James E. Blair96c6bf82016-01-15 16:20:40 -08001888 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001889 for job in self.history:
1890 if (job.name == name and
1891 (project is None or
1892 job.parameters['ZUUL_PROJECT'] == project)):
1893 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001894 raise Exception("Unable to find job %s in history" % name)
1895
1896 def assertEmptyQueues(self):
1897 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001898 for tenant in self.sched.abide.tenants.values():
1899 for pipeline in tenant.layout.pipelines.values():
1900 for queue in pipeline.queues:
1901 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001902 print('pipeline %s queue %s contents %s' % (
1903 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001904 self.assertEqual(len(queue.queue), 0,
1905 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001906
1907 def assertReportedStat(self, key, value=None, kind=None):
1908 start = time.time()
1909 while time.time() < (start + 5):
1910 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001911 k, v = stat.split(':')
1912 if key == k:
1913 if value is None and kind is None:
1914 return
1915 elif value:
1916 if value == v:
1917 return
1918 elif kind:
1919 if v.endswith('|' + kind):
1920 return
1921 time.sleep(0.1)
1922
Clark Boylanb640e052014-04-03 16:41:46 -07001923 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001924
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001925 def assertBuilds(self, builds):
1926 """Assert that the running builds are as described.
1927
1928 The list of running builds is examined and must match exactly
1929 the list of builds described by the input.
1930
1931 :arg list builds: A list of dictionaries. Each item in the
1932 list must match the corresponding build in the build
1933 history, and each element of the dictionary must match the
1934 corresponding attribute of the build.
1935
1936 """
James E. Blair3158e282016-08-19 09:34:11 -07001937 try:
1938 self.assertEqual(len(self.builds), len(builds))
1939 for i, d in enumerate(builds):
1940 for k, v in d.items():
1941 self.assertEqual(
1942 getattr(self.builds[i], k), v,
1943 "Element %i in builds does not match" % (i,))
1944 except Exception:
1945 for build in self.builds:
1946 self.log.error("Running build: %s" % build)
1947 else:
1948 self.log.error("No running builds")
1949 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001950
James E. Blairb536ecc2016-08-31 10:11:42 -07001951 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001952 """Assert that the completed builds are as described.
1953
1954 The list of completed builds is examined and must match
1955 exactly the list of builds described by the input.
1956
1957 :arg list history: A list of dictionaries. Each item in the
1958 list must match the corresponding build in the build
1959 history, and each element of the dictionary must match the
1960 corresponding attribute of the build.
1961
James E. Blairb536ecc2016-08-31 10:11:42 -07001962 :arg bool ordered: If true, the history must match the order
1963 supplied, if false, the builds are permitted to have
1964 arrived in any order.
1965
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001966 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001967 def matches(history_item, item):
1968 for k, v in item.items():
1969 if getattr(history_item, k) != v:
1970 return False
1971 return True
James E. Blair3158e282016-08-19 09:34:11 -07001972 try:
1973 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001974 if ordered:
1975 for i, d in enumerate(history):
1976 if not matches(self.history[i], d):
1977 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001978 "Element %i in history does not match %s" %
1979 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001980 else:
1981 unseen = self.history[:]
1982 for i, d in enumerate(history):
1983 found = False
1984 for unseen_item in unseen:
1985 if matches(unseen_item, d):
1986 found = True
1987 unseen.remove(unseen_item)
1988 break
1989 if not found:
1990 raise Exception("No match found for element %i "
1991 "in history" % (i,))
1992 if unseen:
1993 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001994 except Exception:
1995 for build in self.history:
1996 self.log.error("Completed build: %s" % build)
1997 else:
1998 self.log.error("No completed builds")
1999 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002000
James E. Blair6ac368c2016-12-22 18:07:20 -08002001 def printHistory(self):
2002 """Log the build history.
2003
2004 This can be useful during tests to summarize what jobs have
2005 completed.
2006
2007 """
2008 self.log.debug("Build history:")
2009 for build in self.history:
2010 self.log.debug(build)
2011
James E. Blair59fdbac2015-12-07 17:08:06 -08002012 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002013 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2014
James E. Blair9ea70072017-04-19 16:05:30 -07002015 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002016 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002017 if not os.path.exists(root):
2018 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002019 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2020 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002021- tenant:
2022 name: openstack
2023 source:
2024 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002025 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002026 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002027 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002028 - org/project
2029 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002030 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002031 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002032 self.config.set('zuul', 'tenant_config',
2033 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002034 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002035
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002036 def addCommitToRepo(self, project, message, files,
2037 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002038 path = os.path.join(self.upstream_root, project)
2039 repo = git.Repo(path)
2040 repo.head.reference = branch
2041 zuul.merger.merger.reset_repo_to_head(repo)
2042 for fn, content in files.items():
2043 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002044 try:
2045 os.makedirs(os.path.dirname(fn))
2046 except OSError:
2047 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002048 with open(fn, 'w') as f:
2049 f.write(content)
2050 repo.index.add([fn])
2051 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002052 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002053 repo.heads[branch].commit = commit
2054 repo.head.reference = branch
2055 repo.git.clean('-x', '-f', '-d')
2056 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002057 if tag:
2058 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002059 return before
2060
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002061 def commitConfigUpdate(self, project_name, source_name):
2062 """Commit an update to zuul.yaml
2063
2064 This overwrites the zuul.yaml in the specificed project with
2065 the contents specified.
2066
2067 :arg str project_name: The name of the project containing
2068 zuul.yaml (e.g., common-config)
2069
2070 :arg str source_name: The path to the file (underneath the
2071 test fixture directory) whose contents should be used to
2072 replace zuul.yaml.
2073 """
2074
2075 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002076 files = {}
2077 with open(source_path, 'r') as f:
2078 data = f.read()
2079 layout = yaml.safe_load(data)
2080 files['zuul.yaml'] = data
2081 for item in layout:
2082 if 'job' in item:
2083 jobname = item['job']['name']
2084 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002085 before = self.addCommitToRepo(
2086 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002087 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002088 return before
2089
James E. Blair7fc8daa2016-08-08 15:37:15 -07002090 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002091
James E. Blair7fc8daa2016-08-08 15:37:15 -07002092 """Inject a Fake (Gerrit) event.
2093
2094 This method accepts a JSON-encoded event and simulates Zuul
2095 having received it from Gerrit. It could (and should)
2096 eventually apply to any connection type, but is currently only
2097 used with Gerrit connections. The name of the connection is
2098 used to look up the corresponding server, and the event is
2099 simulated as having been received by all Zuul connections
2100 attached to that server. So if two Gerrit connections in Zuul
2101 are connected to the same Gerrit server, and you invoke this
2102 method specifying the name of one of them, the event will be
2103 received by both.
2104
2105 .. note::
2106
2107 "self.fake_gerrit.addEvent" calls should be migrated to
2108 this method.
2109
2110 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002111 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002112 :arg str event: The JSON-encoded event.
2113
2114 """
2115 specified_conn = self.connections.connections[connection]
2116 for conn in self.connections.connections.values():
2117 if (isinstance(conn, specified_conn.__class__) and
2118 specified_conn.server == conn.server):
2119 conn.addEvent(event)
2120
James E. Blair3f876d52016-07-22 13:07:14 -07002121
2122class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002123 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002124 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002125
Joshua Heskethd78b4482015-09-14 16:56:34 -06002126
2127class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002128 def setup_config(self):
2129 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002130 for section_name in self.config.sections():
2131 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2132 section_name, re.I)
2133 if not con_match:
2134 continue
2135
2136 if self.config.get(section_name, 'driver') == 'sql':
2137 f = MySQLSchemaFixture()
2138 self.useFixture(f)
2139 if (self.config.get(section_name, 'dburi') ==
2140 '$MYSQL_FIXTURE_DBURI$'):
2141 self.config.set(section_name, 'dburi', f.dburi)