blob: 9983103dee3746ca351298a41e7ca82eabe6b54d [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 Hruban6b71aff2015-10-22 16:58:08 +02001370 self.event_queues = [
1371 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001372 self.sched.trigger_event_queue,
1373 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001374 ]
1375
James E. Blairfef78942016-03-11 16:28:56 -08001376 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001377 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001378
Clark Boylanb640e052014-04-03 16:41:46 -07001379 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001380 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001381 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001382 return FakeURLOpener(self.upstream_root, *args, **kw)
1383
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001384 old_urlopen = urllib.request.urlopen
1385 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001386
James E. Blair3f876d52016-07-22 13:07:14 -07001387 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001388
Paul Belanger174a8272017-03-14 13:20:10 -04001389 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001390 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001391 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001392 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001393 _test_root=self.test_root,
1394 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001395 self.executor_server.start()
1396 self.history = self.executor_server.build_history
1397 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001398
Paul Belanger174a8272017-03-14 13:20:10 -04001399 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001400 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001401 self.merge_client = zuul.merger.client.MergeClient(
1402 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001403 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001404 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001405 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001406
James E. Blair0d5a36e2017-02-21 10:53:44 -05001407 self.fake_nodepool = FakeNodepool(
1408 self.zk_chroot_fixture.zookeeper_host,
1409 self.zk_chroot_fixture.zookeeper_port,
1410 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001411
Paul Belanger174a8272017-03-14 13:20:10 -04001412 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001413 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001414 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001415 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001416
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001417 self.webapp = zuul.webapp.WebApp(
1418 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001419 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001420
1421 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001422 self.webapp.start()
1423 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001424 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001425 # Cleanups are run in reverse order
1426 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001427 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001428 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001429
James E. Blairb9c0d772017-03-03 14:34:49 -08001430 self.sched.reconfigure(self.config)
1431 self.sched.resume()
1432
James E. Blairfef78942016-03-11 16:28:56 -08001433 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001434 # Set up gerrit related fakes
1435 # Set a changes database so multiple FakeGerrit's can report back to
1436 # a virtual canonical database given by the configured hostname
1437 self.gerrit_changes_dbs = {}
1438
1439 def getGerritConnection(driver, name, config):
1440 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1441 con = FakeGerritConnection(driver, name, config,
1442 changes_db=db,
1443 upstream_root=self.upstream_root)
1444 self.event_queues.append(con.event_queue)
1445 setattr(self, 'fake_' + name, con)
1446 return con
1447
1448 self.useFixture(fixtures.MonkeyPatch(
1449 'zuul.driver.gerrit.GerritDriver.getConnection',
1450 getGerritConnection))
1451
1452 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001453 # TODO(jhesketh): This should come from lib.connections for better
1454 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001455 # Register connections from the config
1456 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001457
Joshua Hesketh352264b2015-08-11 23:42:08 +10001458 def FakeSMTPFactory(*args, **kw):
1459 args = [self.smtp_messages] + list(args)
1460 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001461
Joshua Hesketh352264b2015-08-11 23:42:08 +10001462 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001463
James E. Blaire511d2f2016-12-08 15:22:26 -08001464 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001465 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001466 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001467
James E. Blair83005782015-12-11 14:46:03 -08001468 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001469 # This creates the per-test configuration object. It can be
1470 # overriden by subclasses, but should not need to be since it
1471 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001472 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001473 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001474
1475 if not self.setupSimpleLayout():
1476 if hasattr(self, 'tenant_config_file'):
1477 self.config.set('zuul', 'tenant_config',
1478 self.tenant_config_file)
1479 git_path = os.path.join(
1480 os.path.dirname(
1481 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1482 'git')
1483 if os.path.exists(git_path):
1484 for reponame in os.listdir(git_path):
1485 project = reponame.replace('_', '/')
1486 self.copyDirToRepo(project,
1487 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001488 self.setupAllProjectKeys()
1489
James E. Blair06cc3922017-04-19 10:08:10 -07001490 def setupSimpleLayout(self):
1491 # If the test method has been decorated with a simple_layout,
1492 # use that instead of the class tenant_config_file. Set up a
1493 # single config-project with the specified layout, and
1494 # initialize repos for all of the 'project' entries which
1495 # appear in the layout.
1496 test_name = self.id().split('.')[-1]
1497 test = getattr(self, test_name)
1498 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001499 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001500 else:
1501 return False
1502
James E. Blairb70e55a2017-04-19 12:57:02 -07001503 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001504 path = os.path.join(FIXTURE_DIR, path)
1505 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001506 data = f.read()
1507 layout = yaml.safe_load(data)
1508 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001509 untrusted_projects = []
1510 for item in layout:
1511 if 'project' in item:
1512 name = item['project']['name']
1513 untrusted_projects.append(name)
1514 self.init_repo(name)
1515 self.addCommitToRepo(name, 'initial commit',
1516 files={'README': ''},
1517 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001518 if 'job' in item:
1519 jobname = item['job']['name']
1520 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001521
1522 root = os.path.join(self.test_root, "config")
1523 if not os.path.exists(root):
1524 os.makedirs(root)
1525 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1526 config = [{'tenant':
1527 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001528 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001529 {'config-projects': ['common-config'],
1530 'untrusted-projects': untrusted_projects}}}}]
1531 f.write(yaml.dump(config))
1532 f.close()
1533 self.config.set('zuul', 'tenant_config',
1534 os.path.join(FIXTURE_DIR, f.name))
1535
1536 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001537 self.addCommitToRepo('common-config', 'add content from fixture',
1538 files, branch='master', tag='init')
1539
1540 return True
1541
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001542 def setupAllProjectKeys(self):
1543 if self.create_project_keys:
1544 return
1545
1546 path = self.config.get('zuul', 'tenant_config')
1547 with open(os.path.join(FIXTURE_DIR, path)) as f:
1548 tenant_config = yaml.safe_load(f.read())
1549 for tenant in tenant_config:
1550 sources = tenant['tenant']['source']
1551 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001552 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001553 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001554 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001555 self.setupProjectKeys(source, project)
1556
1557 def setupProjectKeys(self, source, project):
1558 # Make sure we set up an RSA key for the project so that we
1559 # don't spend time generating one:
1560
1561 key_root = os.path.join(self.state_root, 'keys')
1562 if not os.path.isdir(key_root):
1563 os.mkdir(key_root, 0o700)
1564 private_key_file = os.path.join(key_root, source, project + '.pem')
1565 private_key_dir = os.path.dirname(private_key_file)
1566 self.log.debug("Installing test keys for project %s at %s" % (
1567 project, private_key_file))
1568 if not os.path.isdir(private_key_dir):
1569 os.makedirs(private_key_dir)
1570 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1571 with open(private_key_file, 'w') as o:
1572 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001573
James E. Blair498059b2016-12-20 13:50:13 -08001574 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001575 self.zk_chroot_fixture = self.useFixture(
1576 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001577 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001578 self.zk_chroot_fixture.zookeeper_host,
1579 self.zk_chroot_fixture.zookeeper_port,
1580 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001581
James E. Blair96c6bf82016-01-15 16:20:40 -08001582 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001583 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001584
1585 files = {}
1586 for (dirpath, dirnames, filenames) in os.walk(source_path):
1587 for filename in filenames:
1588 test_tree_filepath = os.path.join(dirpath, filename)
1589 common_path = os.path.commonprefix([test_tree_filepath,
1590 source_path])
1591 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1592 with open(test_tree_filepath, 'r') as f:
1593 content = f.read()
1594 files[relative_filepath] = content
1595 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001596 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001597
James E. Blaire18d4602017-01-05 11:17:28 -08001598 def assertNodepoolState(self):
1599 # Make sure that there are no pending requests
1600
1601 requests = self.fake_nodepool.getNodeRequests()
1602 self.assertEqual(len(requests), 0)
1603
1604 nodes = self.fake_nodepool.getNodes()
1605 for node in nodes:
1606 self.assertFalse(node['_lock'], "Node %s is locked" %
1607 (node['_oid'],))
1608
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001609 def assertNoGeneratedKeys(self):
1610 # Make sure that Zuul did not generate any project keys
1611 # (unless it was supposed to).
1612
1613 if self.create_project_keys:
1614 return
1615
1616 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1617 test_key = i.read()
1618
1619 key_root = os.path.join(self.state_root, 'keys')
1620 for root, dirname, files in os.walk(key_root):
1621 for fn in files:
1622 with open(os.path.join(root, fn)) as f:
1623 self.assertEqual(test_key, f.read())
1624
Clark Boylanb640e052014-04-03 16:41:46 -07001625 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001626 self.log.debug("Assert final state")
1627 # Make sure no jobs are running
1628 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001629 # Make sure that git.Repo objects have been garbage collected.
1630 repos = []
1631 gc.collect()
1632 for obj in gc.get_objects():
1633 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001634 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001635 repos.append(obj)
1636 self.assertEqual(len(repos), 0)
1637 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001638 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001639 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001640 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001641 for tenant in self.sched.abide.tenants.values():
1642 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001643 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001644 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001645
1646 def shutdown(self):
1647 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001648 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001649 self.merge_server.stop()
1650 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001651 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001652 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001653 self.sched.stop()
1654 self.sched.join()
1655 self.statsd.stop()
1656 self.statsd.join()
1657 self.webapp.stop()
1658 self.webapp.join()
1659 self.rpc.stop()
1660 self.rpc.join()
1661 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001662 self.fake_nodepool.stop()
1663 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001664 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001665 # we whitelist watchdog threads as they have relatively long delays
1666 # before noticing they should exit, but they should exit on their own.
1667 threads = [t for t in threading.enumerate()
1668 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001669 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001670 log_str = ""
1671 for thread_id, stack_frame in sys._current_frames().items():
1672 log_str += "Thread: %s\n" % thread_id
1673 log_str += "".join(traceback.format_stack(stack_frame))
1674 self.log.debug(log_str)
1675 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001676
James E. Blaira002b032017-04-18 10:35:48 -07001677 def assertCleanShutdown(self):
1678 pass
1679
James E. Blairc4ba97a2017-04-19 16:26:24 -07001680 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001681 parts = project.split('/')
1682 path = os.path.join(self.upstream_root, *parts[:-1])
1683 if not os.path.exists(path):
1684 os.makedirs(path)
1685 path = os.path.join(self.upstream_root, project)
1686 repo = git.Repo.init(path)
1687
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001688 with repo.config_writer() as config_writer:
1689 config_writer.set_value('user', 'email', 'user@example.com')
1690 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001691
Clark Boylanb640e052014-04-03 16:41:46 -07001692 repo.index.commit('initial commit')
1693 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001694 if tag:
1695 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001696
James E. Blair97d902e2014-08-21 13:25:56 -07001697 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001698 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001699 repo.git.clean('-x', '-f', '-d')
1700
James E. Blair97d902e2014-08-21 13:25:56 -07001701 def create_branch(self, project, branch):
1702 path = os.path.join(self.upstream_root, project)
1703 repo = git.Repo.init(path)
1704 fn = os.path.join(path, 'README')
1705
1706 branch_head = repo.create_head(branch)
1707 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001708 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001709 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001710 f.close()
1711 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001712 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001713
James E. Blair97d902e2014-08-21 13:25:56 -07001714 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001715 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001716 repo.git.clean('-x', '-f', '-d')
1717
Sachi King9f16d522016-03-16 12:20:45 +11001718 def create_commit(self, project):
1719 path = os.path.join(self.upstream_root, project)
1720 repo = git.Repo(path)
1721 repo.head.reference = repo.heads['master']
1722 file_name = os.path.join(path, 'README')
1723 with open(file_name, 'a') as f:
1724 f.write('creating fake commit\n')
1725 repo.index.add([file_name])
1726 commit = repo.index.commit('Creating a fake commit')
1727 return commit.hexsha
1728
James E. Blairf4a5f022017-04-18 14:01:10 -07001729 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001730 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001731 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001732 while len(self.builds):
1733 self.release(self.builds[0])
1734 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001735 i += 1
1736 if count is not None and i >= count:
1737 break
James E. Blairb8c16472015-05-05 14:55:26 -07001738
Clark Boylanb640e052014-04-03 16:41:46 -07001739 def release(self, job):
1740 if isinstance(job, FakeBuild):
1741 job.release()
1742 else:
1743 job.waiting = False
1744 self.log.debug("Queued job %s released" % job.unique)
1745 self.gearman_server.wakeConnections()
1746
1747 def getParameter(self, job, name):
1748 if isinstance(job, FakeBuild):
1749 return job.parameters[name]
1750 else:
1751 parameters = json.loads(job.arguments)
1752 return parameters[name]
1753
Clark Boylanb640e052014-04-03 16:41:46 -07001754 def haveAllBuildsReported(self):
1755 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001756 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001757 return False
1758 # Find out if every build that the worker has completed has been
1759 # reported back to Zuul. If it hasn't then that means a Gearman
1760 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001761 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001762 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001763 if not zbuild:
1764 # It has already been reported
1765 continue
1766 # It hasn't been reported yet.
1767 return False
1768 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001769 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001770 if connection.state == 'GRAB_WAIT':
1771 return False
1772 return True
1773
1774 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001775 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001776 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001777 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001778 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001779 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001780 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001781 for j in conn.related_jobs.values():
1782 if j.unique == build.uuid:
1783 client_job = j
1784 break
1785 if not client_job:
1786 self.log.debug("%s is not known to the gearman client" %
1787 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001788 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001789 if not client_job.handle:
1790 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001791 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001792 server_job = self.gearman_server.jobs.get(client_job.handle)
1793 if not server_job:
1794 self.log.debug("%s is not known to the gearman server" %
1795 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001796 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001797 if not hasattr(server_job, 'waiting'):
1798 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001799 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001800 if server_job.waiting:
1801 continue
James E. Blair17302972016-08-10 16:11:42 -07001802 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001803 self.log.debug("%s has not reported start" % build)
1804 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001805 worker_build = self.executor_server.job_builds.get(
1806 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001807 if worker_build:
1808 if worker_build.isWaiting():
1809 continue
1810 else:
1811 self.log.debug("%s is running" % worker_build)
1812 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001813 else:
James E. Blair962220f2016-08-03 11:22:38 -07001814 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001815 return False
James E. Blaira002b032017-04-18 10:35:48 -07001816 for (build_uuid, job_worker) in \
1817 self.executor_server.job_workers.items():
1818 if build_uuid not in seen_builds:
1819 self.log.debug("%s is not finalized" % build_uuid)
1820 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001821 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001822
James E. Blairdce6cea2016-12-20 16:45:32 -08001823 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001824 if self.fake_nodepool.paused:
1825 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001826 if self.sched.nodepool.requests:
1827 return False
1828 return True
1829
Jan Hruban6b71aff2015-10-22 16:58:08 +02001830 def eventQueuesEmpty(self):
1831 for queue in self.event_queues:
1832 yield queue.empty()
1833
1834 def eventQueuesJoin(self):
1835 for queue in self.event_queues:
1836 queue.join()
1837
Clark Boylanb640e052014-04-03 16:41:46 -07001838 def waitUntilSettled(self):
1839 self.log.debug("Waiting until settled...")
1840 start = time.time()
1841 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001842 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001843 self.log.error("Timeout waiting for Zuul to settle")
1844 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001845 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001846 self.log.error(" %s: %s" % (queue, queue.empty()))
1847 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001848 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001849 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001850 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001851 self.log.error("All requests completed: %s" %
1852 (self.areAllNodeRequestsComplete(),))
1853 self.log.error("Merge client jobs: %s" %
1854 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001855 raise Exception("Timeout waiting for Zuul to settle")
1856 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001857
Paul Belanger174a8272017-03-14 13:20:10 -04001858 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001859 # have all build states propogated to zuul?
1860 if self.haveAllBuildsReported():
1861 # Join ensures that the queue is empty _and_ events have been
1862 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001863 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001864 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001865 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001866 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001867 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001868 self.areAllNodeRequestsComplete() and
1869 all(self.eventQueuesEmpty())):
1870 # The queue empty check is placed at the end to
1871 # ensure that if a component adds an event between
1872 # when locked the run handler and checked that the
1873 # components were stable, we don't erroneously
1874 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001875 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001876 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001877 self.log.debug("...settled.")
1878 return
1879 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001880 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001881 self.sched.wake_event.wait(0.1)
1882
1883 def countJobResults(self, jobs, result):
1884 jobs = filter(lambda x: x.result == result, jobs)
1885 return len(jobs)
1886
James E. Blair96c6bf82016-01-15 16:20:40 -08001887 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001888 for job in self.history:
1889 if (job.name == name and
1890 (project is None or
1891 job.parameters['ZUUL_PROJECT'] == project)):
1892 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001893 raise Exception("Unable to find job %s in history" % name)
1894
1895 def assertEmptyQueues(self):
1896 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001897 for tenant in self.sched.abide.tenants.values():
1898 for pipeline in tenant.layout.pipelines.values():
1899 for queue in pipeline.queues:
1900 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001901 print('pipeline %s queue %s contents %s' % (
1902 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001903 self.assertEqual(len(queue.queue), 0,
1904 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001905
1906 def assertReportedStat(self, key, value=None, kind=None):
1907 start = time.time()
1908 while time.time() < (start + 5):
1909 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001910 k, v = stat.split(':')
1911 if key == k:
1912 if value is None and kind is None:
1913 return
1914 elif value:
1915 if value == v:
1916 return
1917 elif kind:
1918 if v.endswith('|' + kind):
1919 return
1920 time.sleep(0.1)
1921
Clark Boylanb640e052014-04-03 16:41:46 -07001922 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001923
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001924 def assertBuilds(self, builds):
1925 """Assert that the running builds are as described.
1926
1927 The list of running builds is examined and must match exactly
1928 the list of builds described by the input.
1929
1930 :arg list builds: A list of dictionaries. Each item in the
1931 list must match the corresponding build in the build
1932 history, and each element of the dictionary must match the
1933 corresponding attribute of the build.
1934
1935 """
James E. Blair3158e282016-08-19 09:34:11 -07001936 try:
1937 self.assertEqual(len(self.builds), len(builds))
1938 for i, d in enumerate(builds):
1939 for k, v in d.items():
1940 self.assertEqual(
1941 getattr(self.builds[i], k), v,
1942 "Element %i in builds does not match" % (i,))
1943 except Exception:
1944 for build in self.builds:
1945 self.log.error("Running build: %s" % build)
1946 else:
1947 self.log.error("No running builds")
1948 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001949
James E. Blairb536ecc2016-08-31 10:11:42 -07001950 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001951 """Assert that the completed builds are as described.
1952
1953 The list of completed builds is examined and must match
1954 exactly the list of builds described by the input.
1955
1956 :arg list history: A list of dictionaries. Each item in the
1957 list must match the corresponding build in the build
1958 history, and each element of the dictionary must match the
1959 corresponding attribute of the build.
1960
James E. Blairb536ecc2016-08-31 10:11:42 -07001961 :arg bool ordered: If true, the history must match the order
1962 supplied, if false, the builds are permitted to have
1963 arrived in any order.
1964
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001965 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001966 def matches(history_item, item):
1967 for k, v in item.items():
1968 if getattr(history_item, k) != v:
1969 return False
1970 return True
James E. Blair3158e282016-08-19 09:34:11 -07001971 try:
1972 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001973 if ordered:
1974 for i, d in enumerate(history):
1975 if not matches(self.history[i], d):
1976 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001977 "Element %i in history does not match %s" %
1978 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001979 else:
1980 unseen = self.history[:]
1981 for i, d in enumerate(history):
1982 found = False
1983 for unseen_item in unseen:
1984 if matches(unseen_item, d):
1985 found = True
1986 unseen.remove(unseen_item)
1987 break
1988 if not found:
1989 raise Exception("No match found for element %i "
1990 "in history" % (i,))
1991 if unseen:
1992 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001993 except Exception:
1994 for build in self.history:
1995 self.log.error("Completed build: %s" % build)
1996 else:
1997 self.log.error("No completed builds")
1998 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001999
James E. Blair6ac368c2016-12-22 18:07:20 -08002000 def printHistory(self):
2001 """Log the build history.
2002
2003 This can be useful during tests to summarize what jobs have
2004 completed.
2005
2006 """
2007 self.log.debug("Build history:")
2008 for build in self.history:
2009 self.log.debug(build)
2010
James E. Blair59fdbac2015-12-07 17:08:06 -08002011 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002012 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2013
James E. Blair9ea70072017-04-19 16:05:30 -07002014 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002015 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002016 if not os.path.exists(root):
2017 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002018 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2019 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002020- tenant:
2021 name: openstack
2022 source:
2023 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002024 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002025 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002026 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002027 - org/project
2028 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002029 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002030 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002031 self.config.set('zuul', 'tenant_config',
2032 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002033 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002034
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002035 def addCommitToRepo(self, project, message, files,
2036 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002037 path = os.path.join(self.upstream_root, project)
2038 repo = git.Repo(path)
2039 repo.head.reference = branch
2040 zuul.merger.merger.reset_repo_to_head(repo)
2041 for fn, content in files.items():
2042 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002043 try:
2044 os.makedirs(os.path.dirname(fn))
2045 except OSError:
2046 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002047 with open(fn, 'w') as f:
2048 f.write(content)
2049 repo.index.add([fn])
2050 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002051 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002052 repo.heads[branch].commit = commit
2053 repo.head.reference = branch
2054 repo.git.clean('-x', '-f', '-d')
2055 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002056 if tag:
2057 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002058 return before
2059
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002060 def commitConfigUpdate(self, project_name, source_name):
2061 """Commit an update to zuul.yaml
2062
2063 This overwrites the zuul.yaml in the specificed project with
2064 the contents specified.
2065
2066 :arg str project_name: The name of the project containing
2067 zuul.yaml (e.g., common-config)
2068
2069 :arg str source_name: The path to the file (underneath the
2070 test fixture directory) whose contents should be used to
2071 replace zuul.yaml.
2072 """
2073
2074 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002075 files = {}
2076 with open(source_path, 'r') as f:
2077 data = f.read()
2078 layout = yaml.safe_load(data)
2079 files['zuul.yaml'] = data
2080 for item in layout:
2081 if 'job' in item:
2082 jobname = item['job']['name']
2083 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002084 before = self.addCommitToRepo(
2085 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002086 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002087 return before
2088
James E. Blair7fc8daa2016-08-08 15:37:15 -07002089 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002090
James E. Blair7fc8daa2016-08-08 15:37:15 -07002091 """Inject a Fake (Gerrit) event.
2092
2093 This method accepts a JSON-encoded event and simulates Zuul
2094 having received it from Gerrit. It could (and should)
2095 eventually apply to any connection type, but is currently only
2096 used with Gerrit connections. The name of the connection is
2097 used to look up the corresponding server, and the event is
2098 simulated as having been received by all Zuul connections
2099 attached to that server. So if two Gerrit connections in Zuul
2100 are connected to the same Gerrit server, and you invoke this
2101 method specifying the name of one of them, the event will be
2102 received by both.
2103
2104 .. note::
2105
2106 "self.fake_gerrit.addEvent" calls should be migrated to
2107 this method.
2108
2109 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002110 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002111 :arg str event: The JSON-encoded event.
2112
2113 """
2114 specified_conn = self.connections.connections[connection]
2115 for conn in self.connections.connections.values():
2116 if (isinstance(conn, specified_conn.__class__) and
2117 specified_conn.server == conn.server):
2118 conn.addEvent(event)
2119
James E. Blair3f876d52016-07-22 13:07:14 -07002120
2121class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002122 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002123 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002124
Joshua Heskethd78b4482015-09-14 16:56:34 -06002125
2126class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002127 def setup_config(self):
2128 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002129 for section_name in self.config.sections():
2130 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2131 section_name, re.I)
2132 if not con_match:
2133 continue
2134
2135 if self.config.get(section_name, 'driver') == 'sql':
2136 f = MySQLSchemaFixture()
2137 self.useFixture(f)
2138 if (self.config.get(section_name, 'dburi') ==
2139 '$MYSQL_FIXTURE_DBURI$'):
2140 self.config.set(section_name, 'dburi', f.dburi)