blob: 2c3f7bb11d757de705f2d9011b1853ab36cd020e [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:
James E. Blair2a535672017-04-27 12:03:15 -0700704 hostname = change.gerrit.canonical_hostname
705 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800706 try:
707 repo = git.Repo(path)
708 except NoSuchPathError as e:
709 self.log.debug('%s' % e)
710 return False
711 ref = self.parameters['ZUUL_REF']
712 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
713 commit_message = '%s-1' % change.subject
714 self.log.debug("Checking if build %s has changes; commit_message "
715 "%s; repo_messages %s" % (self, commit_message,
716 repo_messages))
717 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700718 self.log.debug(" messages do not match")
719 return False
720 self.log.debug(" OK")
721 return True
722
Clark Boylanb640e052014-04-03 16:41:46 -0700723
Paul Belanger174a8272017-03-14 13:20:10 -0400724class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
725 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700726
Paul Belanger174a8272017-03-14 13:20:10 -0400727 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700728 they will report that they have started but then pause until
729 released before reporting completion. This attribute may be
730 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400731 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700732 be explicitly released.
733
734 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800735 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700736 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800737 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400738 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700739 self.hold_jobs_in_build = False
740 self.lock = threading.Lock()
741 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700742 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700743 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700744 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800745
James E. Blaira5dba232016-08-08 15:53:24 -0700746 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400747 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700748
749 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700750 :arg Change change: The :py:class:`~tests.base.FakeChange`
751 instance which should cause the job to fail. This job
752 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700753
754 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700755 l = self.fail_tests.get(name, [])
756 l.append(change)
757 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800758
James E. Blair962220f2016-08-03 11:22:38 -0700759 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700760 """Release a held build.
761
762 :arg str regex: A regular expression which, if supplied, will
763 cause only builds with matching names to be released. If
764 not supplied, all builds will be released.
765
766 """
James E. Blair962220f2016-08-03 11:22:38 -0700767 builds = self.running_builds[:]
768 self.log.debug("Releasing build %s (%s)" % (regex,
769 len(self.running_builds)))
770 for build in builds:
771 if not regex or re.match(regex, build.name):
772 self.log.debug("Releasing build %s" %
773 (build.parameters['ZUUL_UUID']))
774 build.release()
775 else:
776 self.log.debug("Not releasing build %s" %
777 (build.parameters['ZUUL_UUID']))
778 self.log.debug("Done releasing builds %s (%s)" %
779 (regex, len(self.running_builds)))
780
Paul Belanger174a8272017-03-14 13:20:10 -0400781 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700782 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700783 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700784 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700785 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800786 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500787 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800788 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100789 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
790 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700791
792 def stopJob(self, job):
793 self.log.debug("handle stop")
794 parameters = json.loads(job.arguments)
795 uuid = parameters['uuid']
796 for build in self.running_builds:
797 if build.unique == uuid:
798 build.aborted = True
799 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400800 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700801
James E. Blaira002b032017-04-18 10:35:48 -0700802 def stop(self):
803 for build in self.running_builds:
804 build.release()
805 super(RecordingExecutorServer, self).stop()
806
Joshua Hesketh50c21782016-10-13 21:34:14 +1100807
Paul Belanger174a8272017-03-14 13:20:10 -0400808class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700809 def doMergeChanges(self, items):
810 # Get a merger in order to update the repos involved in this job.
811 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
812 if not commit: # merge conflict
813 self.recordResult('MERGER_FAILURE')
814 return commit
815
816 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400817 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400818 self.executor_server.lock.acquire()
819 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700820 BuildHistory(name=build.name, result=result, changes=build.changes,
821 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800822 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700823 pipeline=build.parameters['ZUUL_PIPELINE'])
824 )
Paul Belanger174a8272017-03-14 13:20:10 -0400825 self.executor_server.running_builds.remove(build)
826 del self.executor_server.job_builds[self.job.unique]
827 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700828
829 def runPlaybooks(self, args):
830 build = self.executor_server.job_builds[self.job.unique]
831 build.jobdir = self.jobdir
832
833 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
834 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800835 return result
836
Monty Taylore6562aa2017-02-20 07:37:39 -0500837 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400838 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800839
Paul Belanger174a8272017-03-14 13:20:10 -0400840 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600841 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500842 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800843 else:
844 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700845 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800846
James E. Blairad8dca02017-02-21 11:48:32 -0500847 def getHostList(self, args):
848 self.log.debug("hostlist")
849 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400850 for host in hosts:
851 host['host_vars']['ansible_connection'] = 'local'
852
853 hosts.append(dict(
854 name='localhost',
855 host_vars=dict(ansible_connection='local'),
856 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500857 return hosts
858
James E. Blairf5dbd002015-12-23 15:26:17 -0800859
Clark Boylanb640e052014-04-03 16:41:46 -0700860class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700861 """A Gearman server for use in tests.
862
863 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
864 added to the queue but will not be distributed to workers
865 until released. This attribute may be changed at any time and
866 will take effect for subsequently enqueued jobs, but
867 previously held jobs will still need to be explicitly
868 released.
869
870 """
871
Clark Boylanb640e052014-04-03 16:41:46 -0700872 def __init__(self):
873 self.hold_jobs_in_queue = False
874 super(FakeGearmanServer, self).__init__(0)
875
876 def getJobForConnection(self, connection, peek=False):
877 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
878 for job in queue:
879 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400880 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700881 job.waiting = self.hold_jobs_in_queue
882 else:
883 job.waiting = False
884 if job.waiting:
885 continue
886 if job.name in connection.functions:
887 if not peek:
888 queue.remove(job)
889 connection.related_jobs[job.handle] = job
890 job.worker_connection = connection
891 job.running = True
892 return job
893 return None
894
895 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700896 """Release a held job.
897
898 :arg str regex: A regular expression which, if supplied, will
899 cause only jobs with matching names to be released. If
900 not supplied, all jobs will be released.
901 """
Clark Boylanb640e052014-04-03 16:41:46 -0700902 released = False
903 qlen = (len(self.high_queue) + len(self.normal_queue) +
904 len(self.low_queue))
905 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
906 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400907 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700908 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500909 parameters = json.loads(job.arguments)
910 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700911 self.log.debug("releasing queued job %s" %
912 job.unique)
913 job.waiting = False
914 released = True
915 else:
916 self.log.debug("not releasing queued job %s" %
917 job.unique)
918 if released:
919 self.wakeConnections()
920 qlen = (len(self.high_queue) + len(self.normal_queue) +
921 len(self.low_queue))
922 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
923
924
925class FakeSMTP(object):
926 log = logging.getLogger('zuul.FakeSMTP')
927
928 def __init__(self, messages, server, port):
929 self.server = server
930 self.port = port
931 self.messages = messages
932
933 def sendmail(self, from_email, to_email, msg):
934 self.log.info("Sending email from %s, to %s, with msg %s" % (
935 from_email, to_email, msg))
936
937 headers = msg.split('\n\n', 1)[0]
938 body = msg.split('\n\n', 1)[1]
939
940 self.messages.append(dict(
941 from_email=from_email,
942 to_email=to_email,
943 msg=msg,
944 headers=headers,
945 body=body,
946 ))
947
948 return True
949
950 def quit(self):
951 return True
952
953
James E. Blairdce6cea2016-12-20 16:45:32 -0800954class FakeNodepool(object):
955 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800956 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800957
958 log = logging.getLogger("zuul.test.FakeNodepool")
959
960 def __init__(self, host, port, chroot):
961 self.client = kazoo.client.KazooClient(
962 hosts='%s:%s%s' % (host, port, chroot))
963 self.client.start()
964 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800965 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800966 self.thread = threading.Thread(target=self.run)
967 self.thread.daemon = True
968 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800969 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800970
971 def stop(self):
972 self._running = False
973 self.thread.join()
974 self.client.stop()
975 self.client.close()
976
977 def run(self):
978 while self._running:
979 self._run()
980 time.sleep(0.1)
981
982 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800983 if self.paused:
984 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800985 for req in self.getNodeRequests():
986 self.fulfillRequest(req)
987
988 def getNodeRequests(self):
989 try:
990 reqids = self.client.get_children(self.REQUEST_ROOT)
991 except kazoo.exceptions.NoNodeError:
992 return []
993 reqs = []
994 for oid in sorted(reqids):
995 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800996 try:
997 data, stat = self.client.get(path)
998 data = json.loads(data)
999 data['_oid'] = oid
1000 reqs.append(data)
1001 except kazoo.exceptions.NoNodeError:
1002 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001003 return reqs
1004
James E. Blaire18d4602017-01-05 11:17:28 -08001005 def getNodes(self):
1006 try:
1007 nodeids = self.client.get_children(self.NODE_ROOT)
1008 except kazoo.exceptions.NoNodeError:
1009 return []
1010 nodes = []
1011 for oid in sorted(nodeids):
1012 path = self.NODE_ROOT + '/' + oid
1013 data, stat = self.client.get(path)
1014 data = json.loads(data)
1015 data['_oid'] = oid
1016 try:
1017 lockfiles = self.client.get_children(path + '/lock')
1018 except kazoo.exceptions.NoNodeError:
1019 lockfiles = []
1020 if lockfiles:
1021 data['_lock'] = True
1022 else:
1023 data['_lock'] = False
1024 nodes.append(data)
1025 return nodes
1026
James E. Blaira38c28e2017-01-04 10:33:20 -08001027 def makeNode(self, request_id, node_type):
1028 now = time.time()
1029 path = '/nodepool/nodes/'
1030 data = dict(type=node_type,
1031 provider='test-provider',
1032 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001033 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001034 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001035 public_ipv4='127.0.0.1',
1036 private_ipv4=None,
1037 public_ipv6=None,
1038 allocated_to=request_id,
1039 state='ready',
1040 state_time=now,
1041 created_time=now,
1042 updated_time=now,
1043 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001044 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001045 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001046 data = json.dumps(data)
1047 path = self.client.create(path, data,
1048 makepath=True,
1049 sequence=True)
1050 nodeid = path.split("/")[-1]
1051 return nodeid
1052
James E. Blair6ab79e02017-01-06 10:10:17 -08001053 def addFailRequest(self, request):
1054 self.fail_requests.add(request['_oid'])
1055
James E. Blairdce6cea2016-12-20 16:45:32 -08001056 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001057 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001058 return
1059 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001060 oid = request['_oid']
1061 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001062
James E. Blair6ab79e02017-01-06 10:10:17 -08001063 if oid in self.fail_requests:
1064 request['state'] = 'failed'
1065 else:
1066 request['state'] = 'fulfilled'
1067 nodes = []
1068 for node in request['node_types']:
1069 nodeid = self.makeNode(oid, node)
1070 nodes.append(nodeid)
1071 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001072
James E. Blaira38c28e2017-01-04 10:33:20 -08001073 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001074 path = self.REQUEST_ROOT + '/' + oid
1075 data = json.dumps(request)
1076 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1077 self.client.set(path, data)
1078
1079
James E. Blair498059b2016-12-20 13:50:13 -08001080class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001081 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001082 super(ChrootedKazooFixture, self).__init__()
1083
1084 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1085 if ':' in zk_host:
1086 host, port = zk_host.split(':')
1087 else:
1088 host = zk_host
1089 port = None
1090
1091 self.zookeeper_host = host
1092
1093 if not port:
1094 self.zookeeper_port = 2181
1095 else:
1096 self.zookeeper_port = int(port)
1097
Clark Boylan621ec9a2017-04-07 17:41:33 -07001098 self.test_id = test_id
1099
James E. Blair498059b2016-12-20 13:50:13 -08001100 def _setUp(self):
1101 # Make sure the test chroot paths do not conflict
1102 random_bits = ''.join(random.choice(string.ascii_lowercase +
1103 string.ascii_uppercase)
1104 for x in range(8))
1105
Clark Boylan621ec9a2017-04-07 17:41:33 -07001106 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001107 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1108
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001109 self.addCleanup(self._cleanup)
1110
James E. Blair498059b2016-12-20 13:50:13 -08001111 # Ensure the chroot path exists and clean up any pre-existing znodes.
1112 _tmp_client = kazoo.client.KazooClient(
1113 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1114 _tmp_client.start()
1115
1116 if _tmp_client.exists(self.zookeeper_chroot):
1117 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1118
1119 _tmp_client.ensure_path(self.zookeeper_chroot)
1120 _tmp_client.stop()
1121 _tmp_client.close()
1122
James E. Blair498059b2016-12-20 13:50:13 -08001123 def _cleanup(self):
1124 '''Remove the chroot path.'''
1125 # Need a non-chroot'ed client to remove the chroot path
1126 _tmp_client = kazoo.client.KazooClient(
1127 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1128 _tmp_client.start()
1129 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1130 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001131 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001132
1133
Joshua Heskethd78b4482015-09-14 16:56:34 -06001134class MySQLSchemaFixture(fixtures.Fixture):
1135 def setUp(self):
1136 super(MySQLSchemaFixture, self).setUp()
1137
1138 random_bits = ''.join(random.choice(string.ascii_lowercase +
1139 string.ascii_uppercase)
1140 for x in range(8))
1141 self.name = '%s_%s' % (random_bits, os.getpid())
1142 self.passwd = uuid.uuid4().hex
1143 db = pymysql.connect(host="localhost",
1144 user="openstack_citest",
1145 passwd="openstack_citest",
1146 db="openstack_citest")
1147 cur = db.cursor()
1148 cur.execute("create database %s" % self.name)
1149 cur.execute(
1150 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1151 (self.name, self.name, self.passwd))
1152 cur.execute("flush privileges")
1153
1154 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1155 self.passwd,
1156 self.name)
1157 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1158 self.addCleanup(self.cleanup)
1159
1160 def cleanup(self):
1161 db = pymysql.connect(host="localhost",
1162 user="openstack_citest",
1163 passwd="openstack_citest",
1164 db="openstack_citest")
1165 cur = db.cursor()
1166 cur.execute("drop database %s" % self.name)
1167 cur.execute("drop user '%s'@'localhost'" % self.name)
1168 cur.execute("flush privileges")
1169
1170
Maru Newby3fe5f852015-01-13 04:22:14 +00001171class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001172 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001173 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001174
James E. Blair1c236df2017-02-01 14:07:24 -08001175 def attachLogs(self, *args):
1176 def reader():
1177 self._log_stream.seek(0)
1178 while True:
1179 x = self._log_stream.read(4096)
1180 if not x:
1181 break
1182 yield x.encode('utf8')
1183 content = testtools.content.content_from_reader(
1184 reader,
1185 testtools.content_type.UTF8_TEXT,
1186 False)
1187 self.addDetail('logging', content)
1188
Clark Boylanb640e052014-04-03 16:41:46 -07001189 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001190 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001191 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1192 try:
1193 test_timeout = int(test_timeout)
1194 except ValueError:
1195 # If timeout value is invalid do not set a timeout.
1196 test_timeout = 0
1197 if test_timeout > 0:
1198 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1199
1200 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1201 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1202 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1203 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1204 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1205 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1206 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1207 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1208 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1209 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001210 self._log_stream = StringIO()
1211 self.addOnException(self.attachLogs)
1212 else:
1213 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001214
James E. Blair1c236df2017-02-01 14:07:24 -08001215 handler = logging.StreamHandler(self._log_stream)
1216 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1217 '%(levelname)-8s %(message)s')
1218 handler.setFormatter(formatter)
1219
1220 logger = logging.getLogger()
1221 logger.setLevel(logging.DEBUG)
1222 logger.addHandler(handler)
1223
Clark Boylan3410d532017-04-25 12:35:29 -07001224 # Make sure we don't carry old handlers around in process state
1225 # which slows down test runs
1226 self.addCleanup(logger.removeHandler, handler)
1227 self.addCleanup(handler.close)
1228 self.addCleanup(handler.flush)
1229
James E. Blair1c236df2017-02-01 14:07:24 -08001230 # NOTE(notmorgan): Extract logging overrides for specific
1231 # libraries from the OS_LOG_DEFAULTS env and create loggers
1232 # for each. This is used to limit the output during test runs
1233 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001234 log_defaults_from_env = os.environ.get(
1235 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001236 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001237
James E. Blairdce6cea2016-12-20 16:45:32 -08001238 if log_defaults_from_env:
1239 for default in log_defaults_from_env.split(','):
1240 try:
1241 name, level_str = default.split('=', 1)
1242 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001243 logger = logging.getLogger(name)
1244 logger.setLevel(level)
1245 logger.addHandler(handler)
1246 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001247 except ValueError:
1248 # NOTE(notmorgan): Invalid format of the log default,
1249 # skip and don't try and apply a logger for the
1250 # specified module
1251 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001252
Maru Newby3fe5f852015-01-13 04:22:14 +00001253
1254class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001255 """A test case with a functioning Zuul.
1256
1257 The following class variables are used during test setup and can
1258 be overidden by subclasses but are effectively read-only once a
1259 test method starts running:
1260
1261 :cvar str config_file: This points to the main zuul config file
1262 within the fixtures directory. Subclasses may override this
1263 to obtain a different behavior.
1264
1265 :cvar str tenant_config_file: This is the tenant config file
1266 (which specifies from what git repos the configuration should
1267 be loaded). It defaults to the value specified in
1268 `config_file` but can be overidden by subclasses to obtain a
1269 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001270 configuration. See also the :py:func:`simple_layout`
1271 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001272
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001273 :cvar bool create_project_keys: Indicates whether Zuul should
1274 auto-generate keys for each project, or whether the test
1275 infrastructure should insert dummy keys to save time during
1276 startup. Defaults to False.
1277
James E. Blaire7b99a02016-08-05 14:27:34 -07001278 The following are instance variables that are useful within test
1279 methods:
1280
1281 :ivar FakeGerritConnection fake_<connection>:
1282 A :py:class:`~tests.base.FakeGerritConnection` will be
1283 instantiated for each connection present in the config file
1284 and stored here. For instance, `fake_gerrit` will hold the
1285 FakeGerritConnection object for a connection named `gerrit`.
1286
1287 :ivar FakeGearmanServer gearman_server: An instance of
1288 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1289 server that all of the Zuul components in this test use to
1290 communicate with each other.
1291
Paul Belanger174a8272017-03-14 13:20:10 -04001292 :ivar RecordingExecutorServer executor_server: An instance of
1293 :py:class:`~tests.base.RecordingExecutorServer` which is the
1294 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001295
1296 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1297 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001298 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001299 list upon completion.
1300
1301 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1302 objects representing completed builds. They are appended to
1303 the list in the order they complete.
1304
1305 """
1306
James E. Blair83005782015-12-11 14:46:03 -08001307 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001308 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001309 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001310
1311 def _startMerger(self):
1312 self.merge_server = zuul.merger.server.MergeServer(self.config,
1313 self.connections)
1314 self.merge_server.start()
1315
Maru Newby3fe5f852015-01-13 04:22:14 +00001316 def setUp(self):
1317 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001318
1319 self.setupZK()
1320
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001321 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001322 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001323 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1324 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001325 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001326 tmp_root = tempfile.mkdtemp(
1327 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001328 self.test_root = os.path.join(tmp_root, "zuul-test")
1329 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001330 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001331 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001332 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001333
1334 if os.path.exists(self.test_root):
1335 shutil.rmtree(self.test_root)
1336 os.makedirs(self.test_root)
1337 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001338 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001339
1340 # Make per test copy of Configuration.
1341 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001342 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001343 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001344 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001345 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001346 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001347 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001348
Clark Boylanb640e052014-04-03 16:41:46 -07001349 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001350 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1351 # see: https://github.com/jsocol/pystatsd/issues/61
1352 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001353 os.environ['STATSD_PORT'] = str(self.statsd.port)
1354 self.statsd.start()
1355 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001356 reload_module(statsd)
1357 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001358
1359 self.gearman_server = FakeGearmanServer()
1360
1361 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001362 self.log.info("Gearman server on port %s" %
1363 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001364
James E. Blaire511d2f2016-12-08 15:22:26 -08001365 gerritsource.GerritSource.replication_timeout = 1.5
1366 gerritsource.GerritSource.replication_retry_interval = 0.5
1367 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001368
Joshua Hesketh352264b2015-08-11 23:42:08 +10001369 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001370
Jan Hruban7083edd2015-08-21 14:00:54 +02001371 self.webapp = zuul.webapp.WebApp(
1372 self.sched, port=0, listen_address='127.0.0.1')
1373
Jan Hruban6b71aff2015-10-22 16:58:08 +02001374 self.event_queues = [
1375 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001376 self.sched.trigger_event_queue,
1377 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001378 ]
1379
James E. Blairfef78942016-03-11 16:28:56 -08001380 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001381 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001382
Clark Boylanb640e052014-04-03 16:41:46 -07001383 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001384 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001385 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001386 return FakeURLOpener(self.upstream_root, *args, **kw)
1387
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001388 old_urlopen = urllib.request.urlopen
1389 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001390
James E. Blair3f876d52016-07-22 13:07:14 -07001391 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001392
Paul Belanger174a8272017-03-14 13:20:10 -04001393 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001394 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001395 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001396 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001397 _test_root=self.test_root,
1398 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001399 self.executor_server.start()
1400 self.history = self.executor_server.build_history
1401 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001402
Paul Belanger174a8272017-03-14 13:20:10 -04001403 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001404 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001405 self.merge_client = zuul.merger.client.MergeClient(
1406 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001407 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001408 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001409 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001410
James E. Blair0d5a36e2017-02-21 10:53:44 -05001411 self.fake_nodepool = FakeNodepool(
1412 self.zk_chroot_fixture.zookeeper_host,
1413 self.zk_chroot_fixture.zookeeper_port,
1414 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001415
Paul Belanger174a8272017-03-14 13:20:10 -04001416 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001417 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001418 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001419 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001420
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001421 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001422
1423 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001424 self.webapp.start()
1425 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001426 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001427 # Cleanups are run in reverse order
1428 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001429 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001430 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001431
James E. Blairb9c0d772017-03-03 14:34:49 -08001432 self.sched.reconfigure(self.config)
1433 self.sched.resume()
1434
James E. Blairfef78942016-03-11 16:28:56 -08001435 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001436 # Set up gerrit related fakes
1437 # Set a changes database so multiple FakeGerrit's can report back to
1438 # a virtual canonical database given by the configured hostname
1439 self.gerrit_changes_dbs = {}
1440
1441 def getGerritConnection(driver, name, config):
1442 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1443 con = FakeGerritConnection(driver, name, config,
1444 changes_db=db,
1445 upstream_root=self.upstream_root)
1446 self.event_queues.append(con.event_queue)
1447 setattr(self, 'fake_' + name, con)
1448 return con
1449
1450 self.useFixture(fixtures.MonkeyPatch(
1451 'zuul.driver.gerrit.GerritDriver.getConnection',
1452 getGerritConnection))
1453
1454 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001455 # TODO(jhesketh): This should come from lib.connections for better
1456 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001457 # Register connections from the config
1458 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001459
Joshua Hesketh352264b2015-08-11 23:42:08 +10001460 def FakeSMTPFactory(*args, **kw):
1461 args = [self.smtp_messages] + list(args)
1462 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001463
Joshua Hesketh352264b2015-08-11 23:42:08 +10001464 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001465
James E. Blaire511d2f2016-12-08 15:22:26 -08001466 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001467 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001468 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001469
James E. Blair83005782015-12-11 14:46:03 -08001470 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001471 # This creates the per-test configuration object. It can be
1472 # overriden by subclasses, but should not need to be since it
1473 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001474 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001475 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001476
1477 if not self.setupSimpleLayout():
1478 if hasattr(self, 'tenant_config_file'):
1479 self.config.set('zuul', 'tenant_config',
1480 self.tenant_config_file)
1481 git_path = os.path.join(
1482 os.path.dirname(
1483 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1484 'git')
1485 if os.path.exists(git_path):
1486 for reponame in os.listdir(git_path):
1487 project = reponame.replace('_', '/')
1488 self.copyDirToRepo(project,
1489 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001490 self.setupAllProjectKeys()
1491
James E. Blair06cc3922017-04-19 10:08:10 -07001492 def setupSimpleLayout(self):
1493 # If the test method has been decorated with a simple_layout,
1494 # use that instead of the class tenant_config_file. Set up a
1495 # single config-project with the specified layout, and
1496 # initialize repos for all of the 'project' entries which
1497 # appear in the layout.
1498 test_name = self.id().split('.')[-1]
1499 test = getattr(self, test_name)
1500 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001501 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001502 else:
1503 return False
1504
James E. Blairb70e55a2017-04-19 12:57:02 -07001505 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001506 path = os.path.join(FIXTURE_DIR, path)
1507 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001508 data = f.read()
1509 layout = yaml.safe_load(data)
1510 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001511 untrusted_projects = []
1512 for item in layout:
1513 if 'project' in item:
1514 name = item['project']['name']
1515 untrusted_projects.append(name)
1516 self.init_repo(name)
1517 self.addCommitToRepo(name, 'initial commit',
1518 files={'README': ''},
1519 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001520 if 'job' in item:
1521 jobname = item['job']['name']
1522 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001523
1524 root = os.path.join(self.test_root, "config")
1525 if not os.path.exists(root):
1526 os.makedirs(root)
1527 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1528 config = [{'tenant':
1529 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001530 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001531 {'config-projects': ['common-config'],
1532 'untrusted-projects': untrusted_projects}}}}]
1533 f.write(yaml.dump(config))
1534 f.close()
1535 self.config.set('zuul', 'tenant_config',
1536 os.path.join(FIXTURE_DIR, f.name))
1537
1538 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001539 self.addCommitToRepo('common-config', 'add content from fixture',
1540 files, branch='master', tag='init')
1541
1542 return True
1543
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001544 def setupAllProjectKeys(self):
1545 if self.create_project_keys:
1546 return
1547
1548 path = self.config.get('zuul', 'tenant_config')
1549 with open(os.path.join(FIXTURE_DIR, path)) as f:
1550 tenant_config = yaml.safe_load(f.read())
1551 for tenant in tenant_config:
1552 sources = tenant['tenant']['source']
1553 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001554 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001555 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001556 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001557 self.setupProjectKeys(source, project)
1558
1559 def setupProjectKeys(self, source, project):
1560 # Make sure we set up an RSA key for the project so that we
1561 # don't spend time generating one:
1562
1563 key_root = os.path.join(self.state_root, 'keys')
1564 if not os.path.isdir(key_root):
1565 os.mkdir(key_root, 0o700)
1566 private_key_file = os.path.join(key_root, source, project + '.pem')
1567 private_key_dir = os.path.dirname(private_key_file)
1568 self.log.debug("Installing test keys for project %s at %s" % (
1569 project, private_key_file))
1570 if not os.path.isdir(private_key_dir):
1571 os.makedirs(private_key_dir)
1572 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1573 with open(private_key_file, 'w') as o:
1574 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001575
James E. Blair498059b2016-12-20 13:50:13 -08001576 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001577 self.zk_chroot_fixture = self.useFixture(
1578 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001579 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001580 self.zk_chroot_fixture.zookeeper_host,
1581 self.zk_chroot_fixture.zookeeper_port,
1582 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001583
James E. Blair96c6bf82016-01-15 16:20:40 -08001584 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001585 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001586
1587 files = {}
1588 for (dirpath, dirnames, filenames) in os.walk(source_path):
1589 for filename in filenames:
1590 test_tree_filepath = os.path.join(dirpath, filename)
1591 common_path = os.path.commonprefix([test_tree_filepath,
1592 source_path])
1593 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1594 with open(test_tree_filepath, 'r') as f:
1595 content = f.read()
1596 files[relative_filepath] = content
1597 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001598 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001599
James E. Blaire18d4602017-01-05 11:17:28 -08001600 def assertNodepoolState(self):
1601 # Make sure that there are no pending requests
1602
1603 requests = self.fake_nodepool.getNodeRequests()
1604 self.assertEqual(len(requests), 0)
1605
1606 nodes = self.fake_nodepool.getNodes()
1607 for node in nodes:
1608 self.assertFalse(node['_lock'], "Node %s is locked" %
1609 (node['_oid'],))
1610
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001611 def assertNoGeneratedKeys(self):
1612 # Make sure that Zuul did not generate any project keys
1613 # (unless it was supposed to).
1614
1615 if self.create_project_keys:
1616 return
1617
1618 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1619 test_key = i.read()
1620
1621 key_root = os.path.join(self.state_root, 'keys')
1622 for root, dirname, files in os.walk(key_root):
1623 for fn in files:
1624 with open(os.path.join(root, fn)) as f:
1625 self.assertEqual(test_key, f.read())
1626
Clark Boylanb640e052014-04-03 16:41:46 -07001627 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001628 self.log.debug("Assert final state")
1629 # Make sure no jobs are running
1630 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001631 # Make sure that git.Repo objects have been garbage collected.
1632 repos = []
1633 gc.collect()
1634 for obj in gc.get_objects():
1635 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001636 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001637 repos.append(obj)
1638 self.assertEqual(len(repos), 0)
1639 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001640 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001641 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001642 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001643 for tenant in self.sched.abide.tenants.values():
1644 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001645 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001646 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001647
1648 def shutdown(self):
1649 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001650 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001651 self.merge_server.stop()
1652 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001653 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001654 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001655 self.sched.stop()
1656 self.sched.join()
1657 self.statsd.stop()
1658 self.statsd.join()
1659 self.webapp.stop()
1660 self.webapp.join()
1661 self.rpc.stop()
1662 self.rpc.join()
1663 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001664 self.fake_nodepool.stop()
1665 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001666 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001667 # we whitelist watchdog threads as they have relatively long delays
1668 # before noticing they should exit, but they should exit on their own.
1669 threads = [t for t in threading.enumerate()
1670 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001671 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001672 log_str = ""
1673 for thread_id, stack_frame in sys._current_frames().items():
1674 log_str += "Thread: %s\n" % thread_id
1675 log_str += "".join(traceback.format_stack(stack_frame))
1676 self.log.debug(log_str)
1677 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001678
James E. Blaira002b032017-04-18 10:35:48 -07001679 def assertCleanShutdown(self):
1680 pass
1681
James E. Blairc4ba97a2017-04-19 16:26:24 -07001682 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001683 parts = project.split('/')
1684 path = os.path.join(self.upstream_root, *parts[:-1])
1685 if not os.path.exists(path):
1686 os.makedirs(path)
1687 path = os.path.join(self.upstream_root, project)
1688 repo = git.Repo.init(path)
1689
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001690 with repo.config_writer() as config_writer:
1691 config_writer.set_value('user', 'email', 'user@example.com')
1692 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001693
Clark Boylanb640e052014-04-03 16:41:46 -07001694 repo.index.commit('initial commit')
1695 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001696 if tag:
1697 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001698
James E. Blair97d902e2014-08-21 13:25:56 -07001699 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001700 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001701 repo.git.clean('-x', '-f', '-d')
1702
James E. Blair97d902e2014-08-21 13:25:56 -07001703 def create_branch(self, project, branch):
1704 path = os.path.join(self.upstream_root, project)
1705 repo = git.Repo.init(path)
1706 fn = os.path.join(path, 'README')
1707
1708 branch_head = repo.create_head(branch)
1709 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001710 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001711 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001712 f.close()
1713 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001714 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001715
James E. Blair97d902e2014-08-21 13:25:56 -07001716 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001717 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001718 repo.git.clean('-x', '-f', '-d')
1719
Sachi King9f16d522016-03-16 12:20:45 +11001720 def create_commit(self, project):
1721 path = os.path.join(self.upstream_root, project)
1722 repo = git.Repo(path)
1723 repo.head.reference = repo.heads['master']
1724 file_name = os.path.join(path, 'README')
1725 with open(file_name, 'a') as f:
1726 f.write('creating fake commit\n')
1727 repo.index.add([file_name])
1728 commit = repo.index.commit('Creating a fake commit')
1729 return commit.hexsha
1730
James E. Blairf4a5f022017-04-18 14:01:10 -07001731 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001732 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001733 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001734 while len(self.builds):
1735 self.release(self.builds[0])
1736 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001737 i += 1
1738 if count is not None and i >= count:
1739 break
James E. Blairb8c16472015-05-05 14:55:26 -07001740
Clark Boylanb640e052014-04-03 16:41:46 -07001741 def release(self, job):
1742 if isinstance(job, FakeBuild):
1743 job.release()
1744 else:
1745 job.waiting = False
1746 self.log.debug("Queued job %s released" % job.unique)
1747 self.gearman_server.wakeConnections()
1748
1749 def getParameter(self, job, name):
1750 if isinstance(job, FakeBuild):
1751 return job.parameters[name]
1752 else:
1753 parameters = json.loads(job.arguments)
1754 return parameters[name]
1755
Clark Boylanb640e052014-04-03 16:41:46 -07001756 def haveAllBuildsReported(self):
1757 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001758 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001759 return False
1760 # Find out if every build that the worker has completed has been
1761 # reported back to Zuul. If it hasn't then that means a Gearman
1762 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001763 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001764 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001765 if not zbuild:
1766 # It has already been reported
1767 continue
1768 # It hasn't been reported yet.
1769 return False
1770 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001771 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001772 if connection.state == 'GRAB_WAIT':
1773 return False
1774 return True
1775
1776 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001777 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001778 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001779 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001780 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001781 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001782 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001783 for j in conn.related_jobs.values():
1784 if j.unique == build.uuid:
1785 client_job = j
1786 break
1787 if not client_job:
1788 self.log.debug("%s is not known to the gearman client" %
1789 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001790 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001791 if not client_job.handle:
1792 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001793 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001794 server_job = self.gearman_server.jobs.get(client_job.handle)
1795 if not server_job:
1796 self.log.debug("%s is not known to the gearman server" %
1797 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001798 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001799 if not hasattr(server_job, 'waiting'):
1800 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001801 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001802 if server_job.waiting:
1803 continue
James E. Blair17302972016-08-10 16:11:42 -07001804 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001805 self.log.debug("%s has not reported start" % build)
1806 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001807 worker_build = self.executor_server.job_builds.get(
1808 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001809 if worker_build:
1810 if worker_build.isWaiting():
1811 continue
1812 else:
1813 self.log.debug("%s is running" % worker_build)
1814 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001815 else:
James E. Blair962220f2016-08-03 11:22:38 -07001816 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001817 return False
James E. Blaira002b032017-04-18 10:35:48 -07001818 for (build_uuid, job_worker) in \
1819 self.executor_server.job_workers.items():
1820 if build_uuid not in seen_builds:
1821 self.log.debug("%s is not finalized" % build_uuid)
1822 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001823 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001824
James E. Blairdce6cea2016-12-20 16:45:32 -08001825 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001826 if self.fake_nodepool.paused:
1827 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001828 if self.sched.nodepool.requests:
1829 return False
1830 return True
1831
Jan Hruban6b71aff2015-10-22 16:58:08 +02001832 def eventQueuesEmpty(self):
1833 for queue in self.event_queues:
1834 yield queue.empty()
1835
1836 def eventQueuesJoin(self):
1837 for queue in self.event_queues:
1838 queue.join()
1839
Clark Boylanb640e052014-04-03 16:41:46 -07001840 def waitUntilSettled(self):
1841 self.log.debug("Waiting until settled...")
1842 start = time.time()
1843 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001844 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001845 self.log.error("Timeout waiting for Zuul to settle")
1846 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001847 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001848 self.log.error(" %s: %s" % (queue, queue.empty()))
1849 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001850 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001851 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001852 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001853 self.log.error("All requests completed: %s" %
1854 (self.areAllNodeRequestsComplete(),))
1855 self.log.error("Merge client jobs: %s" %
1856 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001857 raise Exception("Timeout waiting for Zuul to settle")
1858 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001859
Paul Belanger174a8272017-03-14 13:20:10 -04001860 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001861 # have all build states propogated to zuul?
1862 if self.haveAllBuildsReported():
1863 # Join ensures that the queue is empty _and_ events have been
1864 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001865 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001866 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001867 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001868 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001869 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001870 self.areAllNodeRequestsComplete() and
1871 all(self.eventQueuesEmpty())):
1872 # The queue empty check is placed at the end to
1873 # ensure that if a component adds an event between
1874 # when locked the run handler and checked that the
1875 # components were stable, we don't erroneously
1876 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001877 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001878 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001879 self.log.debug("...settled.")
1880 return
1881 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001882 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001883 self.sched.wake_event.wait(0.1)
1884
1885 def countJobResults(self, jobs, result):
1886 jobs = filter(lambda x: x.result == result, jobs)
1887 return len(jobs)
1888
James E. Blair96c6bf82016-01-15 16:20:40 -08001889 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001890 for job in self.history:
1891 if (job.name == name and
1892 (project is None or
1893 job.parameters['ZUUL_PROJECT'] == project)):
1894 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001895 raise Exception("Unable to find job %s in history" % name)
1896
1897 def assertEmptyQueues(self):
1898 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001899 for tenant in self.sched.abide.tenants.values():
1900 for pipeline in tenant.layout.pipelines.values():
1901 for queue in pipeline.queues:
1902 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001903 print('pipeline %s queue %s contents %s' % (
1904 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001905 self.assertEqual(len(queue.queue), 0,
1906 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001907
1908 def assertReportedStat(self, key, value=None, kind=None):
1909 start = time.time()
1910 while time.time() < (start + 5):
1911 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001912 k, v = stat.split(':')
1913 if key == k:
1914 if value is None and kind is None:
1915 return
1916 elif value:
1917 if value == v:
1918 return
1919 elif kind:
1920 if v.endswith('|' + kind):
1921 return
1922 time.sleep(0.1)
1923
Clark Boylanb640e052014-04-03 16:41:46 -07001924 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001925
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001926 def assertBuilds(self, builds):
1927 """Assert that the running builds are as described.
1928
1929 The list of running builds is examined and must match exactly
1930 the list of builds described by the input.
1931
1932 :arg list builds: A list of dictionaries. Each item in the
1933 list must match the corresponding build in the build
1934 history, and each element of the dictionary must match the
1935 corresponding attribute of the build.
1936
1937 """
James E. Blair3158e282016-08-19 09:34:11 -07001938 try:
1939 self.assertEqual(len(self.builds), len(builds))
1940 for i, d in enumerate(builds):
1941 for k, v in d.items():
1942 self.assertEqual(
1943 getattr(self.builds[i], k), v,
1944 "Element %i in builds does not match" % (i,))
1945 except Exception:
1946 for build in self.builds:
1947 self.log.error("Running build: %s" % build)
1948 else:
1949 self.log.error("No running builds")
1950 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001951
James E. Blairb536ecc2016-08-31 10:11:42 -07001952 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001953 """Assert that the completed builds are as described.
1954
1955 The list of completed builds is examined and must match
1956 exactly the list of builds described by the input.
1957
1958 :arg list history: A list of dictionaries. Each item in the
1959 list must match the corresponding build in the build
1960 history, and each element of the dictionary must match the
1961 corresponding attribute of the build.
1962
James E. Blairb536ecc2016-08-31 10:11:42 -07001963 :arg bool ordered: If true, the history must match the order
1964 supplied, if false, the builds are permitted to have
1965 arrived in any order.
1966
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001967 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001968 def matches(history_item, item):
1969 for k, v in item.items():
1970 if getattr(history_item, k) != v:
1971 return False
1972 return True
James E. Blair3158e282016-08-19 09:34:11 -07001973 try:
1974 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001975 if ordered:
1976 for i, d in enumerate(history):
1977 if not matches(self.history[i], d):
1978 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001979 "Element %i in history does not match %s" %
1980 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001981 else:
1982 unseen = self.history[:]
1983 for i, d in enumerate(history):
1984 found = False
1985 for unseen_item in unseen:
1986 if matches(unseen_item, d):
1987 found = True
1988 unseen.remove(unseen_item)
1989 break
1990 if not found:
1991 raise Exception("No match found for element %i "
1992 "in history" % (i,))
1993 if unseen:
1994 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001995 except Exception:
1996 for build in self.history:
1997 self.log.error("Completed build: %s" % build)
1998 else:
1999 self.log.error("No completed builds")
2000 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002001
James E. Blair6ac368c2016-12-22 18:07:20 -08002002 def printHistory(self):
2003 """Log the build history.
2004
2005 This can be useful during tests to summarize what jobs have
2006 completed.
2007
2008 """
2009 self.log.debug("Build history:")
2010 for build in self.history:
2011 self.log.debug(build)
2012
James E. Blair59fdbac2015-12-07 17:08:06 -08002013 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002014 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2015
James E. Blair9ea70072017-04-19 16:05:30 -07002016 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002017 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002018 if not os.path.exists(root):
2019 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002020 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2021 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002022- tenant:
2023 name: openstack
2024 source:
2025 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002026 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002027 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002028 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002029 - org/project
2030 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002031 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002032 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002033 self.config.set('zuul', 'tenant_config',
2034 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002035 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002036
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002037 def addCommitToRepo(self, project, message, files,
2038 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002039 path = os.path.join(self.upstream_root, project)
2040 repo = git.Repo(path)
2041 repo.head.reference = branch
2042 zuul.merger.merger.reset_repo_to_head(repo)
2043 for fn, content in files.items():
2044 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002045 try:
2046 os.makedirs(os.path.dirname(fn))
2047 except OSError:
2048 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002049 with open(fn, 'w') as f:
2050 f.write(content)
2051 repo.index.add([fn])
2052 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002053 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002054 repo.heads[branch].commit = commit
2055 repo.head.reference = branch
2056 repo.git.clean('-x', '-f', '-d')
2057 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002058 if tag:
2059 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002060 return before
2061
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002062 def commitConfigUpdate(self, project_name, source_name):
2063 """Commit an update to zuul.yaml
2064
2065 This overwrites the zuul.yaml in the specificed project with
2066 the contents specified.
2067
2068 :arg str project_name: The name of the project containing
2069 zuul.yaml (e.g., common-config)
2070
2071 :arg str source_name: The path to the file (underneath the
2072 test fixture directory) whose contents should be used to
2073 replace zuul.yaml.
2074 """
2075
2076 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002077 files = {}
2078 with open(source_path, 'r') as f:
2079 data = f.read()
2080 layout = yaml.safe_load(data)
2081 files['zuul.yaml'] = data
2082 for item in layout:
2083 if 'job' in item:
2084 jobname = item['job']['name']
2085 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002086 before = self.addCommitToRepo(
2087 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002088 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002089 return before
2090
James E. Blair7fc8daa2016-08-08 15:37:15 -07002091 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002092
James E. Blair7fc8daa2016-08-08 15:37:15 -07002093 """Inject a Fake (Gerrit) event.
2094
2095 This method accepts a JSON-encoded event and simulates Zuul
2096 having received it from Gerrit. It could (and should)
2097 eventually apply to any connection type, but is currently only
2098 used with Gerrit connections. The name of the connection is
2099 used to look up the corresponding server, and the event is
2100 simulated as having been received by all Zuul connections
2101 attached to that server. So if two Gerrit connections in Zuul
2102 are connected to the same Gerrit server, and you invoke this
2103 method specifying the name of one of them, the event will be
2104 received by both.
2105
2106 .. note::
2107
2108 "self.fake_gerrit.addEvent" calls should be migrated to
2109 this method.
2110
2111 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002112 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002113 :arg str event: The JSON-encoded event.
2114
2115 """
2116 specified_conn = self.connections.connections[connection]
2117 for conn in self.connections.connections.values():
2118 if (isinstance(conn, specified_conn.__class__) and
2119 specified_conn.server == conn.server):
2120 conn.addEvent(event)
2121
James E. Blair3f876d52016-07-22 13:07:14 -07002122
2123class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002124 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002125 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002126
Joshua Heskethd78b4482015-09-14 16:56:34 -06002127
2128class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002129 def setup_config(self):
2130 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002131 for section_name in self.config.sections():
2132 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2133 section_name, re.I)
2134 if not con_match:
2135 continue
2136
2137 if self.config.get(section_name, 'driver') == 'sql':
2138 f = MySQLSchemaFixture()
2139 self.useFixture(f)
2140 if (self.config.get(section_name, 'dburi') ==
2141 '$MYSQL_FIXTURE_DBURI$'):
2142 self.config.set(section_name, 'dburi', f.dburi)