blob: c3b8a9b72ca4ee8df16bfeb4c2a4e76869d45b71 [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
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
63import zuul.webapp
64import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040065import zuul.executor.server
66import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080067import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070068import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070069import zuul.merger.merger
70import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020071import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070072import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080073import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010074from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070075
76FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
77 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080078
79KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070080
Clark Boylanb640e052014-04-03 16:41:46 -070081
82def repack_repo(path):
83 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
84 output = subprocess.Popen(cmd, close_fds=True,
85 stdout=subprocess.PIPE,
86 stderr=subprocess.PIPE)
87 out = output.communicate()
88 if output.returncode:
89 raise Exception("git repack returned %d" % output.returncode)
90 return out
91
92
93def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040094 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070095
96
James E. Blaira190f3b2015-01-05 14:56:54 -080097def iterate_timeout(max_seconds, purpose):
98 start = time.time()
99 count = 0
100 while (time.time() < start + max_seconds):
101 count += 1
102 yield count
103 time.sleep(0)
104 raise Exception("Timeout waiting for %s" % purpose)
105
106
Jesse Keating436a5452017-04-20 11:48:41 -0700107def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700108 """Specify a layout file for use by a test method.
109
110 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700111 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700112
113 Some tests require only a very simple configuration. For those,
114 establishing a complete config directory hierachy is too much
115 work. In those cases, you can add a simple zuul.yaml file to the
116 test fixtures directory (in fixtures/layouts/foo.yaml) and use
117 this decorator to indicate the test method should use that rather
118 than the tenant config file specified by the test class.
119
120 The decorator will cause that layout file to be added to a
121 config-project called "common-config" and each "project" instance
122 referenced in the layout file will have a git repo automatically
123 initialized.
124 """
125
126 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700127 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700128 return test
129 return decorator
130
131
Gregory Haynes4fc12542015-04-22 20:38:06 -0700132class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700133 _common_path_default = "refs/changes"
134 _points_to_commits_only = True
135
136
Gregory Haynes4fc12542015-04-22 20:38:06 -0700137class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700138 categories = {'approved': ('Approved', -1, 1),
139 'code-review': ('Code-Review', -2, 2),
140 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700141
142 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700143 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700145 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.reported = 0
147 self.queried = 0
148 self.patchsets = []
149 self.number = number
150 self.project = project
151 self.branch = branch
152 self.subject = subject
153 self.latest_patchset = 0
154 self.depends_on_change = None
155 self.needed_by_changes = []
156 self.fail_merge = False
157 self.messages = []
158 self.data = {
159 'branch': branch,
160 'comments': [],
161 'commitMessage': subject,
162 'createdOn': time.time(),
163 'id': 'I' + random_sha1(),
164 'lastUpdated': time.time(),
165 'number': str(number),
166 'open': status == 'NEW',
167 'owner': {'email': 'user@example.com',
168 'name': 'User Name',
169 'username': 'username'},
170 'patchSets': self.patchsets,
171 'project': project,
172 'status': status,
173 'subject': subject,
174 'submitRecords': [],
175 'url': 'https://hostname/%s' % number}
176
177 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700178 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 self.data['submitRecords'] = self.getSubmitRecords()
180 self.open = status == 'NEW'
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 path = os.path.join(self.upstream_root, self.project)
184 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700185 ref = GerritChangeReference.create(
186 repo, '1/%s/%s' % (self.number, self.latest_patchset),
187 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700189 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.git.clean('-x', '-f', '-d')
191
192 path = os.path.join(self.upstream_root, self.project)
193 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for fn, content in files.items():
195 fn = os.path.join(path, fn)
196 with open(fn, 'w') as f:
197 f.write(content)
198 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700199 else:
200 for fni in range(100):
201 fn = os.path.join(path, str(fni))
202 f = open(fn, 'w')
203 for ci in range(4096):
204 f.write(random.choice(string.printable))
205 f.close()
206 repo.index.add([fn])
207
208 r = repo.index.commit(msg)
209 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700210 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700211 repo.git.clean('-x', '-f', '-d')
212 repo.heads['master'].checkout()
213 return r
214
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700216 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700218 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700219 data = ("test %s %s %s\n" %
220 (self.branch, self.number, self.latest_patchset))
221 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700222 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700223 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700224 ps_files = [{'file': '/COMMIT_MSG',
225 'type': 'ADDED'},
226 {'file': 'README',
227 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700228 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700229 ps_files.append({'file': f, 'type': 'ADDED'})
230 d = {'approvals': [],
231 'createdOn': time.time(),
232 'files': ps_files,
233 'number': str(self.latest_patchset),
234 'ref': 'refs/changes/1/%s/%s' % (self.number,
235 self.latest_patchset),
236 'revision': c.hexsha,
237 'uploader': {'email': 'user@example.com',
238 'name': 'User name',
239 'username': 'user'}}
240 self.data['currentPatchSet'] = d
241 self.patchsets.append(d)
242 self.data['submitRecords'] = self.getSubmitRecords()
243
244 def getPatchsetCreatedEvent(self, patchset):
245 event = {"type": "patchset-created",
246 "change": {"project": self.project,
247 "branch": self.branch,
248 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
249 "number": str(self.number),
250 "subject": self.subject,
251 "owner": {"name": "User Name"},
252 "url": "https://hostname/3"},
253 "patchSet": self.patchsets[patchset - 1],
254 "uploader": {"name": "User Name"}}
255 return event
256
257 def getChangeRestoredEvent(self):
258 event = {"type": "change-restored",
259 "change": {"project": self.project,
260 "branch": self.branch,
261 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
262 "number": str(self.number),
263 "subject": self.subject,
264 "owner": {"name": "User Name"},
265 "url": "https://hostname/3"},
266 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100267 "patchSet": self.patchsets[-1],
268 "reason": ""}
269 return event
270
271 def getChangeAbandonedEvent(self):
272 event = {"type": "change-abandoned",
273 "change": {"project": self.project,
274 "branch": self.branch,
275 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
276 "number": str(self.number),
277 "subject": self.subject,
278 "owner": {"name": "User Name"},
279 "url": "https://hostname/3"},
280 "abandoner": {"name": "User Name"},
281 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700282 "reason": ""}
283 return event
284
285 def getChangeCommentEvent(self, patchset):
286 event = {"type": "comment-added",
287 "change": {"project": self.project,
288 "branch": self.branch,
289 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
290 "number": str(self.number),
291 "subject": self.subject,
292 "owner": {"name": "User Name"},
293 "url": "https://hostname/3"},
294 "patchSet": self.patchsets[patchset - 1],
295 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700296 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700297 "description": "Code-Review",
298 "value": "0"}],
299 "comment": "This is a comment"}
300 return event
301
James E. Blairc2a5ed72017-02-20 14:12:01 -0500302 def getChangeMergedEvent(self):
303 event = {"submitter": {"name": "Jenkins",
304 "username": "jenkins"},
305 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
306 "patchSet": self.patchsets[-1],
307 "change": self.data,
308 "type": "change-merged",
309 "eventCreatedOn": 1487613810}
310 return event
311
James E. Blair8cce42e2016-10-18 08:18:36 -0700312 def getRefUpdatedEvent(self):
313 path = os.path.join(self.upstream_root, self.project)
314 repo = git.Repo(path)
315 oldrev = repo.heads[self.branch].commit.hexsha
316
317 event = {
318 "type": "ref-updated",
319 "submitter": {
320 "name": "User Name",
321 },
322 "refUpdate": {
323 "oldRev": oldrev,
324 "newRev": self.patchsets[-1]['revision'],
325 "refName": self.branch,
326 "project": self.project,
327 }
328 }
329 return event
330
Joshua Hesketh642824b2014-07-01 17:54:59 +1000331 def addApproval(self, category, value, username='reviewer_john',
332 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700333 if not granted_on:
334 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000335 approval = {
336 'description': self.categories[category][0],
337 'type': category,
338 'value': str(value),
339 'by': {
340 'username': username,
341 'email': username + '@example.com',
342 },
343 'grantedOn': int(granted_on)
344 }
Clark Boylanb640e052014-04-03 16:41:46 -0700345 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
346 if x['by']['username'] == username and x['type'] == category:
347 del self.patchsets[-1]['approvals'][i]
348 self.patchsets[-1]['approvals'].append(approval)
349 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000350 'author': {'email': 'author@example.com',
351 'name': 'Patchset Author',
352 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700353 'change': {'branch': self.branch,
354 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
355 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000356 'owner': {'email': 'owner@example.com',
357 'name': 'Change Owner',
358 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700359 'project': self.project,
360 'subject': self.subject,
361 'topic': 'master',
362 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000363 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700364 'patchSet': self.patchsets[-1],
365 'type': 'comment-added'}
366 self.data['submitRecords'] = self.getSubmitRecords()
367 return json.loads(json.dumps(event))
368
369 def getSubmitRecords(self):
370 status = {}
371 for cat in self.categories.keys():
372 status[cat] = 0
373
374 for a in self.patchsets[-1]['approvals']:
375 cur = status[a['type']]
376 cat_min, cat_max = self.categories[a['type']][1:]
377 new = int(a['value'])
378 if new == cat_min:
379 cur = new
380 elif abs(new) > abs(cur):
381 cur = new
382 status[a['type']] = cur
383
384 labels = []
385 ok = True
386 for typ, cat in self.categories.items():
387 cur = status[typ]
388 cat_min, cat_max = cat[1:]
389 if cur == cat_min:
390 value = 'REJECT'
391 ok = False
392 elif cur == cat_max:
393 value = 'OK'
394 else:
395 value = 'NEED'
396 ok = False
397 labels.append({'label': cat[0], 'status': value})
398 if ok:
399 return [{'status': 'OK'}]
400 return [{'status': 'NOT_READY',
401 'labels': labels}]
402
403 def setDependsOn(self, other, patchset):
404 self.depends_on_change = other
405 d = {'id': other.data['id'],
406 'number': other.data['number'],
407 'ref': other.patchsets[patchset - 1]['ref']
408 }
409 self.data['dependsOn'] = [d]
410
411 other.needed_by_changes.append(self)
412 needed = other.data.get('neededBy', [])
413 d = {'id': self.data['id'],
414 'number': self.data['number'],
415 'ref': self.patchsets[patchset - 1]['ref'],
416 'revision': self.patchsets[patchset - 1]['revision']
417 }
418 needed.append(d)
419 other.data['neededBy'] = needed
420
421 def query(self):
422 self.queried += 1
423 d = self.data.get('dependsOn')
424 if d:
425 d = d[0]
426 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
427 d['isCurrentPatchSet'] = True
428 else:
429 d['isCurrentPatchSet'] = False
430 return json.loads(json.dumps(self.data))
431
432 def setMerged(self):
433 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000434 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700435 return
436 if self.fail_merge:
437 return
438 self.data['status'] = 'MERGED'
439 self.open = False
440
441 path = os.path.join(self.upstream_root, self.project)
442 repo = git.Repo(path)
443 repo.heads[self.branch].commit = \
444 repo.commit(self.patchsets[-1]['revision'])
445
446 def setReported(self):
447 self.reported += 1
448
449
James E. Blaire511d2f2016-12-08 15:22:26 -0800450class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700451 """A Fake Gerrit connection for use in tests.
452
453 This subclasses
454 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
455 ability for tests to add changes to the fake Gerrit it represents.
456 """
457
Joshua Hesketh352264b2015-08-11 23:42:08 +1000458 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700459
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700461 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800462 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000463 connection_config)
464
James E. Blair7fc8daa2016-08-08 15:37:15 -0700465 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700466 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
467 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000468 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700469 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200470 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700471
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700472 def addFakeChange(self, project, branch, subject, status='NEW',
473 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700474 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700475 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700476 c = FakeGerritChange(self, self.change_number, project, branch,
477 subject, upstream_root=self.upstream_root,
478 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700479 self.changes[self.change_number] = c
480 return c
481
Clark Boylanb640e052014-04-03 16:41:46 -0700482 def review(self, project, changeid, message, action):
483 number, ps = changeid.split(',')
484 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000485
486 # Add the approval back onto the change (ie simulate what gerrit would
487 # do).
488 # Usually when zuul leaves a review it'll create a feedback loop where
489 # zuul's review enters another gerrit event (which is then picked up by
490 # zuul). However, we can't mimic this behaviour (by adding this
491 # approval event into the queue) as it stops jobs from checking what
492 # happens before this event is triggered. If a job needs to see what
493 # happens they can add their own verified event into the queue.
494 # Nevertheless, we can update change with the new review in gerrit.
495
James E. Blair8b5408c2016-08-08 15:37:46 -0700496 for cat in action.keys():
497 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000498 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000501
Clark Boylanb640e052014-04-03 16:41:46 -0700502 if 'submit' in action:
503 change.setMerged()
504 if message:
505 change.setReported()
506
507 def query(self, number):
508 change = self.changes.get(int(number))
509 if change:
510 return change.query()
511 return {}
512
James E. Blairc494d542014-08-06 09:23:52 -0700513 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700514 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700515 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800516 if query.startswith('change:'):
517 # Query a specific changeid
518 changeid = query[len('change:'):]
519 l = [change.query() for change in self.changes.values()
520 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700521 elif query.startswith('message:'):
522 # Query the content of a commit message
523 msg = query[len('message:'):].strip()
524 l = [change.query() for change in self.changes.values()
525 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800526 else:
527 # Query all open changes
528 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700529 return l
James E. Blairc494d542014-08-06 09:23:52 -0700530
Joshua Hesketh352264b2015-08-11 23:42:08 +1000531 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700532 pass
533
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200534 def _uploadPack(self, project):
535 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
536 'multi_ack thin-pack side-band side-band-64k ofs-delta '
537 'shallow no-progress include-tag multi_ack_detailed no-done\n')
538 path = os.path.join(self.upstream_root, project.name)
539 repo = git.Repo(path)
540 for ref in repo.refs:
541 r = ref.object.hexsha + ' ' + ref.path + '\n'
542 ret += '%04x%s' % (len(r) + 4, r)
543 ret += '0000'
544 return ret
545
Joshua Hesketh352264b2015-08-11 23:42:08 +1000546 def getGitUrl(self, project):
547 return os.path.join(self.upstream_root, project.name)
548
Clark Boylanb640e052014-04-03 16:41:46 -0700549
Gregory Haynes4fc12542015-04-22 20:38:06 -0700550class GithubChangeReference(git.Reference):
551 _common_path_default = "refs/pull"
552 _points_to_commits_only = True
553
554
555class FakeGithubPullRequest(object):
556
557 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100558 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700559 """Creates a new PR with several commits.
560 Sends an event about opened PR."""
561 self.github = github
562 self.source = github
563 self.number = number
564 self.project = project
565 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100566 self.subject = subject
567 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700568 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100569 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700570 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100571 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100572 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700573 self.updated_at = None
574 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100575 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100576 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700577 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100578 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 self._updateTimeStamp()
580
Jan Hruban570d01c2016-03-10 21:51:32 +0100581 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100583 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 self._updateTimeStamp()
585
Jan Hruban570d01c2016-03-10 21:51:32 +0100586 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700587 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 self._updateTimeStamp()
590
591 def getPullRequestOpenedEvent(self):
592 return self._getPullRequestEvent('opened')
593
594 def getPullRequestSynchronizeEvent(self):
595 return self._getPullRequestEvent('synchronize')
596
597 def getPullRequestReopenedEvent(self):
598 return self._getPullRequestEvent('reopened')
599
600 def getPullRequestClosedEvent(self):
601 return self._getPullRequestEvent('closed')
602
603 def addComment(self, message):
604 self.comments.append(message)
605 self._updateTimeStamp()
606
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200607 def getCommentAddedEvent(self, text):
608 name = 'issue_comment'
609 data = {
610 'action': 'created',
611 'issue': {
612 'number': self.number
613 },
614 'comment': {
615 'body': text
616 },
617 'repository': {
618 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100619 },
620 'sender': {
621 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200622 }
623 }
624 return (name, data)
625
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800626 def getReviewAddedEvent(self, review):
627 name = 'pull_request_review'
628 data = {
629 'action': 'submitted',
630 'pull_request': {
631 'number': self.number,
632 'title': self.subject,
633 'updated_at': self.updated_at,
634 'base': {
635 'ref': self.branch,
636 'repo': {
637 'full_name': self.project
638 }
639 },
640 'head': {
641 'sha': self.head_sha
642 }
643 },
644 'review': {
645 'state': review
646 },
647 'repository': {
648 'full_name': self.project
649 },
650 'sender': {
651 'login': 'ghuser'
652 }
653 }
654 return (name, data)
655
Jan Hruban16ad31f2015-11-07 14:39:07 +0100656 def addLabel(self, name):
657 if name not in self.labels:
658 self.labels.append(name)
659 self._updateTimeStamp()
660 return self._getLabelEvent(name)
661
662 def removeLabel(self, name):
663 if name in self.labels:
664 self.labels.remove(name)
665 self._updateTimeStamp()
666 return self._getUnlabelEvent(name)
667
668 def _getLabelEvent(self, label):
669 name = 'pull_request'
670 data = {
671 'action': 'labeled',
672 'pull_request': {
673 'number': self.number,
674 'updated_at': self.updated_at,
675 'base': {
676 'ref': self.branch,
677 'repo': {
678 'full_name': self.project
679 }
680 },
681 'head': {
682 'sha': self.head_sha
683 }
684 },
685 'label': {
686 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100687 },
688 'sender': {
689 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100690 }
691 }
692 return (name, data)
693
694 def _getUnlabelEvent(self, label):
695 name = 'pull_request'
696 data = {
697 'action': 'unlabeled',
698 'pull_request': {
699 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100700 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100701 'updated_at': self.updated_at,
702 'base': {
703 'ref': self.branch,
704 'repo': {
705 'full_name': self.project
706 }
707 },
708 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800709 'sha': self.head_sha,
710 'repo': {
711 'full_name': self.project
712 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100713 }
714 },
715 'label': {
716 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100717 },
718 'sender': {
719 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100720 }
721 }
722 return (name, data)
723
Gregory Haynes4fc12542015-04-22 20:38:06 -0700724 def _getRepo(self):
725 repo_path = os.path.join(self.upstream_root, self.project)
726 return git.Repo(repo_path)
727
728 def _createPRRef(self):
729 repo = self._getRepo()
730 GithubChangeReference.create(
731 repo, self._getPRReference(), 'refs/tags/init')
732
Jan Hruban570d01c2016-03-10 21:51:32 +0100733 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700734 repo = self._getRepo()
735 ref = repo.references[self._getPRReference()]
736 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100737 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700738 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100739 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700740 repo.head.reference = ref
741 zuul.merger.merger.reset_repo_to_head(repo)
742 repo.git.clean('-x', '-f', '-d')
743
Jan Hruban570d01c2016-03-10 21:51:32 +0100744 if files:
745 fn = files[0]
746 self.files = files
747 else:
748 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
749 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100750 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700751 fn = os.path.join(repo.working_dir, fn)
752 f = open(fn, 'w')
753 with open(fn, 'w') as f:
754 f.write("test %s %s\n" %
755 (self.branch, self.number))
756 repo.index.add([fn])
757
758 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800759 # Create an empty set of statuses for the given sha,
760 # each sha on a PR may have a status set on it
761 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700762 repo.head.reference = 'master'
763 zuul.merger.merger.reset_repo_to_head(repo)
764 repo.git.clean('-x', '-f', '-d')
765 repo.heads['master'].checkout()
766
767 def _updateTimeStamp(self):
768 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
769
770 def getPRHeadSha(self):
771 repo = self._getRepo()
772 return repo.references[self._getPRReference()].commit.hexsha
773
Jesse Keatingd96e5882017-01-19 13:55:50 -0800774 def setStatus(self, sha, state, url, description, context):
775 # Since we're bypassing github API, which would require a user, we
776 # hard set the user as 'zuul' here.
777 user = 'zuul'
778 # insert the status at the top of the list, to simulate that it
779 # is the most recent set status
780 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100781 'state': state,
782 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800783 'description': description,
784 'context': context,
785 'creator': {
786 'login': user
787 }
788 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100789
Gregory Haynes4fc12542015-04-22 20:38:06 -0700790 def _getPRReference(self):
791 return '%s/head' % self.number
792
793 def _getPullRequestEvent(self, action):
794 name = 'pull_request'
795 data = {
796 'action': action,
797 'number': self.number,
798 'pull_request': {
799 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100800 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700801 'updated_at': self.updated_at,
802 'base': {
803 'ref': self.branch,
804 'repo': {
805 'full_name': self.project
806 }
807 },
808 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800809 'sha': self.head_sha,
810 'repo': {
811 'full_name': self.project
812 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700813 }
Jan Hruban3b415922016-02-03 13:10:22 +0100814 },
815 'sender': {
816 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700817 }
818 }
819 return (name, data)
820
821
822class FakeGithubConnection(githubconnection.GithubConnection):
823 log = logging.getLogger("zuul.test.FakeGithubConnection")
824
825 def __init__(self, driver, connection_name, connection_config,
826 upstream_root=None):
827 super(FakeGithubConnection, self).__init__(driver, connection_name,
828 connection_config)
829 self.connection_name = connection_name
830 self.pr_number = 0
831 self.pull_requests = []
832 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100833 self.merge_failure = False
834 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700835
Jan Hruban570d01c2016-03-10 21:51:32 +0100836 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700837 self.pr_number += 1
838 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100839 self, self.pr_number, project, branch, subject, self.upstream_root,
840 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700841 self.pull_requests.append(pull_request)
842 return pull_request
843
Wayne1a78c612015-06-11 17:14:13 -0700844 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
845 if not old_rev:
846 old_rev = '00000000000000000000000000000000'
847 if not new_rev:
848 new_rev = random_sha1()
849 name = 'push'
850 data = {
851 'ref': ref,
852 'before': old_rev,
853 'after': new_rev,
854 'repository': {
855 'full_name': project
856 }
857 }
858 return (name, data)
859
Gregory Haynes4fc12542015-04-22 20:38:06 -0700860 def emitEvent(self, event):
861 """Emulates sending the GitHub webhook event to the connection."""
862 port = self.webapp.server.socket.getsockname()[1]
863 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700864 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700865 headers = {'X-Github-Event': name}
866 req = urllib.request.Request(
867 'http://localhost:%s/connection/%s/payload'
868 % (port, self.connection_name),
869 data=payload, headers=headers)
870 urllib.request.urlopen(req)
871
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200872 def getPull(self, project, number):
873 pr = self.pull_requests[number - 1]
874 data = {
875 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100876 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200877 'updated_at': pr.updated_at,
878 'base': {
879 'repo': {
880 'full_name': pr.project
881 },
882 'ref': pr.branch,
883 },
Jan Hruban37615e52015-11-19 14:30:49 +0100884 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200885 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800886 'sha': pr.head_sha,
887 'repo': {
888 'full_name': pr.project
889 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200890 }
891 }
892 return data
893
Jan Hruban570d01c2016-03-10 21:51:32 +0100894 def getPullFileNames(self, project, number):
895 pr = self.pull_requests[number - 1]
896 return pr.files
897
Jan Hruban3b415922016-02-03 13:10:22 +0100898 def getUser(self, login):
899 data = {
900 'username': login,
901 'name': 'Github User',
902 'email': 'github.user@example.com'
903 }
904 return data
905
Gregory Haynes4fc12542015-04-22 20:38:06 -0700906 def getGitUrl(self, project):
907 return os.path.join(self.upstream_root, str(project))
908
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200909 def real_getGitUrl(self, project):
910 return super(FakeGithubConnection, self).getGitUrl(project)
911
Gregory Haynes4fc12542015-04-22 20:38:06 -0700912 def getProjectBranches(self, project):
913 """Masks getProjectBranches since we don't have a real github"""
914
915 # just returns master for now
916 return ['master']
917
Jan Hrubane252a732017-01-03 15:03:09 +0100918 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700919 pull_request = self.pull_requests[pr_number - 1]
920 pull_request.addComment(message)
921
Jan Hruban3b415922016-02-03 13:10:22 +0100922 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100923 pull_request = self.pull_requests[pr_number - 1]
924 if self.merge_failure:
925 raise Exception('Pull request was not merged')
926 if self.merge_not_allowed_count > 0:
927 self.merge_not_allowed_count -= 1
928 raise MergeFailure('Merge was not successful due to mergeability'
929 ' conflict')
930 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100931 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100932
Jesse Keatingd96e5882017-01-19 13:55:50 -0800933 def getCommitStatuses(self, project, sha):
934 owner, proj = project.split('/')
935 for pr in self.pull_requests:
936 pr_owner, pr_project = pr.project.split('/')
937 if (pr_owner == owner and pr_project == proj and
938 pr.head_sha == sha):
939 return pr.statuses[sha]
940
Jan Hrubane252a732017-01-03 15:03:09 +0100941 def setCommitStatus(self, project, sha, state,
942 url='', description='', context=''):
943 owner, proj = project.split('/')
944 for pr in self.pull_requests:
945 pr_owner, pr_project = pr.project.split('/')
946 if (pr_owner == owner and pr_project == proj and
947 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800948 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +0100949
Jan Hruban16ad31f2015-11-07 14:39:07 +0100950 def labelPull(self, project, pr_number, label):
951 pull_request = self.pull_requests[pr_number - 1]
952 pull_request.addLabel(label)
953
954 def unlabelPull(self, project, pr_number, label):
955 pull_request = self.pull_requests[pr_number - 1]
956 pull_request.removeLabel(label)
957
Gregory Haynes4fc12542015-04-22 20:38:06 -0700958
Clark Boylanb640e052014-04-03 16:41:46 -0700959class BuildHistory(object):
960 def __init__(self, **kw):
961 self.__dict__.update(kw)
962
963 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700964 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
965 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700966
967
Clark Boylanb640e052014-04-03 16:41:46 -0700968class FakeStatsd(threading.Thread):
969 def __init__(self):
970 threading.Thread.__init__(self)
971 self.daemon = True
972 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
973 self.sock.bind(('', 0))
974 self.port = self.sock.getsockname()[1]
975 self.wake_read, self.wake_write = os.pipe()
976 self.stats = []
977
978 def run(self):
979 while True:
980 poll = select.poll()
981 poll.register(self.sock, select.POLLIN)
982 poll.register(self.wake_read, select.POLLIN)
983 ret = poll.poll()
984 for (fd, event) in ret:
985 if fd == self.sock.fileno():
986 data = self.sock.recvfrom(1024)
987 if not data:
988 return
989 self.stats.append(data[0])
990 if fd == self.wake_read:
991 return
992
993 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -0700994 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -0700995
996
James E. Blaire1767bc2016-08-02 10:00:27 -0700997class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700998 log = logging.getLogger("zuul.test")
999
Paul Belanger174a8272017-03-14 13:20:10 -04001000 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001001 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001002 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001003 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001004 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001005 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001006 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001007 # TODOv3(jeblair): self.node is really "the image of the node
1008 # assigned". We should rename it (self.node_image?) if we
1009 # keep using it like this, or we may end up exposing more of
1010 # the complexity around multi-node jobs here
1011 # (self.nodes[0].image?)
1012 self.node = None
1013 if len(self.parameters.get('nodes')) == 1:
1014 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001015 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001016 self.pipeline = self.parameters['ZUUL_PIPELINE']
1017 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001018 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001019 self.wait_condition = threading.Condition()
1020 self.waiting = False
1021 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001022 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001023 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001024 self.changes = None
1025 if 'ZUUL_CHANGE_IDS' in self.parameters:
1026 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001027
James E. Blair3158e282016-08-19 09:34:11 -07001028 def __repr__(self):
1029 waiting = ''
1030 if self.waiting:
1031 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001032 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1033 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001034
Clark Boylanb640e052014-04-03 16:41:46 -07001035 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001036 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001037 self.wait_condition.acquire()
1038 self.wait_condition.notify()
1039 self.waiting = False
1040 self.log.debug("Build %s released" % self.unique)
1041 self.wait_condition.release()
1042
1043 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001044 """Return whether this build is being held.
1045
1046 :returns: Whether the build is being held.
1047 :rtype: bool
1048 """
1049
Clark Boylanb640e052014-04-03 16:41:46 -07001050 self.wait_condition.acquire()
1051 if self.waiting:
1052 ret = True
1053 else:
1054 ret = False
1055 self.wait_condition.release()
1056 return ret
1057
1058 def _wait(self):
1059 self.wait_condition.acquire()
1060 self.waiting = True
1061 self.log.debug("Build %s waiting" % self.unique)
1062 self.wait_condition.wait()
1063 self.wait_condition.release()
1064
1065 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001066 self.log.debug('Running build %s' % self.unique)
1067
Paul Belanger174a8272017-03-14 13:20:10 -04001068 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001069 self.log.debug('Holding build %s' % self.unique)
1070 self._wait()
1071 self.log.debug("Build %s continuing" % self.unique)
1072
James E. Blair412fba82017-01-26 15:00:50 -08001073 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001074 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001075 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001076 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001077 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001078 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001079 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001080
James E. Blaire1767bc2016-08-02 10:00:27 -07001081 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001082
James E. Blaira5dba232016-08-08 15:53:24 -07001083 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001084 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001085 for change in changes:
1086 if self.hasChanges(change):
1087 return True
1088 return False
1089
James E. Blaire7b99a02016-08-05 14:27:34 -07001090 def hasChanges(self, *changes):
1091 """Return whether this build has certain changes in its git repos.
1092
1093 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001094 are expected to be present (in order) in the git repository of
1095 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001096
1097 :returns: Whether the build has the indicated changes.
1098 :rtype: bool
1099
1100 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001101 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001102 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001103 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001104 try:
1105 repo = git.Repo(path)
1106 except NoSuchPathError as e:
1107 self.log.debug('%s' % e)
1108 return False
1109 ref = self.parameters['ZUUL_REF']
1110 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1111 commit_message = '%s-1' % change.subject
1112 self.log.debug("Checking if build %s has changes; commit_message "
1113 "%s; repo_messages %s" % (self, commit_message,
1114 repo_messages))
1115 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001116 self.log.debug(" messages do not match")
1117 return False
1118 self.log.debug(" OK")
1119 return True
1120
Clark Boylanb640e052014-04-03 16:41:46 -07001121
Paul Belanger174a8272017-03-14 13:20:10 -04001122class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1123 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001124
Paul Belanger174a8272017-03-14 13:20:10 -04001125 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001126 they will report that they have started but then pause until
1127 released before reporting completion. This attribute may be
1128 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001129 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001130 be explicitly released.
1131
1132 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001133 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001134 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001135 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001136 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001137 self.hold_jobs_in_build = False
1138 self.lock = threading.Lock()
1139 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001140 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001141 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001142 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001143
James E. Blaira5dba232016-08-08 15:53:24 -07001144 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001145 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001146
1147 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001148 :arg Change change: The :py:class:`~tests.base.FakeChange`
1149 instance which should cause the job to fail. This job
1150 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001151
1152 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001153 l = self.fail_tests.get(name, [])
1154 l.append(change)
1155 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001156
James E. Blair962220f2016-08-03 11:22:38 -07001157 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001158 """Release a held build.
1159
1160 :arg str regex: A regular expression which, if supplied, will
1161 cause only builds with matching names to be released. If
1162 not supplied, all builds will be released.
1163
1164 """
James E. Blair962220f2016-08-03 11:22:38 -07001165 builds = self.running_builds[:]
1166 self.log.debug("Releasing build %s (%s)" % (regex,
1167 len(self.running_builds)))
1168 for build in builds:
1169 if not regex or re.match(regex, build.name):
1170 self.log.debug("Releasing build %s" %
1171 (build.parameters['ZUUL_UUID']))
1172 build.release()
1173 else:
1174 self.log.debug("Not releasing build %s" %
1175 (build.parameters['ZUUL_UUID']))
1176 self.log.debug("Done releasing builds %s (%s)" %
1177 (regex, len(self.running_builds)))
1178
Paul Belanger174a8272017-03-14 13:20:10 -04001179 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001180 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001181 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001182 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001183 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001184 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001185 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001186 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001187 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1188 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001189
1190 def stopJob(self, job):
1191 self.log.debug("handle stop")
1192 parameters = json.loads(job.arguments)
1193 uuid = parameters['uuid']
1194 for build in self.running_builds:
1195 if build.unique == uuid:
1196 build.aborted = True
1197 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001198 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001199
James E. Blaira002b032017-04-18 10:35:48 -07001200 def stop(self):
1201 for build in self.running_builds:
1202 build.release()
1203 super(RecordingExecutorServer, self).stop()
1204
Joshua Hesketh50c21782016-10-13 21:34:14 +11001205
Paul Belanger174a8272017-03-14 13:20:10 -04001206class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001207 def doMergeChanges(self, items):
1208 # Get a merger in order to update the repos involved in this job.
1209 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1210 if not commit: # merge conflict
1211 self.recordResult('MERGER_FAILURE')
1212 return commit
1213
1214 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001215 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001216 self.executor_server.lock.acquire()
1217 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001218 BuildHistory(name=build.name, result=result, changes=build.changes,
1219 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001220 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001221 pipeline=build.parameters['ZUUL_PIPELINE'])
1222 )
Paul Belanger174a8272017-03-14 13:20:10 -04001223 self.executor_server.running_builds.remove(build)
1224 del self.executor_server.job_builds[self.job.unique]
1225 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001226
1227 def runPlaybooks(self, args):
1228 build = self.executor_server.job_builds[self.job.unique]
1229 build.jobdir = self.jobdir
1230
1231 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1232 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001233 return result
1234
Monty Taylore6562aa2017-02-20 07:37:39 -05001235 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001236 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001237
Paul Belanger174a8272017-03-14 13:20:10 -04001238 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001239 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001240 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001241 else:
1242 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001243 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001244
James E. Blairad8dca02017-02-21 11:48:32 -05001245 def getHostList(self, args):
1246 self.log.debug("hostlist")
1247 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001248 for host in hosts:
1249 host['host_vars']['ansible_connection'] = 'local'
1250
1251 hosts.append(dict(
1252 name='localhost',
1253 host_vars=dict(ansible_connection='local'),
1254 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001255 return hosts
1256
James E. Blairf5dbd002015-12-23 15:26:17 -08001257
Clark Boylanb640e052014-04-03 16:41:46 -07001258class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001259 """A Gearman server for use in tests.
1260
1261 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1262 added to the queue but will not be distributed to workers
1263 until released. This attribute may be changed at any time and
1264 will take effect for subsequently enqueued jobs, but
1265 previously held jobs will still need to be explicitly
1266 released.
1267
1268 """
1269
Clark Boylanb640e052014-04-03 16:41:46 -07001270 def __init__(self):
1271 self.hold_jobs_in_queue = False
1272 super(FakeGearmanServer, self).__init__(0)
1273
1274 def getJobForConnection(self, connection, peek=False):
1275 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1276 for job in queue:
1277 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001278 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001279 job.waiting = self.hold_jobs_in_queue
1280 else:
1281 job.waiting = False
1282 if job.waiting:
1283 continue
1284 if job.name in connection.functions:
1285 if not peek:
1286 queue.remove(job)
1287 connection.related_jobs[job.handle] = job
1288 job.worker_connection = connection
1289 job.running = True
1290 return job
1291 return None
1292
1293 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001294 """Release a held job.
1295
1296 :arg str regex: A regular expression which, if supplied, will
1297 cause only jobs with matching names to be released. If
1298 not supplied, all jobs will be released.
1299 """
Clark Boylanb640e052014-04-03 16:41:46 -07001300 released = False
1301 qlen = (len(self.high_queue) + len(self.normal_queue) +
1302 len(self.low_queue))
1303 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1304 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001305 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001306 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001307 parameters = json.loads(job.arguments)
1308 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001309 self.log.debug("releasing queued job %s" %
1310 job.unique)
1311 job.waiting = False
1312 released = True
1313 else:
1314 self.log.debug("not releasing queued job %s" %
1315 job.unique)
1316 if released:
1317 self.wakeConnections()
1318 qlen = (len(self.high_queue) + len(self.normal_queue) +
1319 len(self.low_queue))
1320 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1321
1322
1323class FakeSMTP(object):
1324 log = logging.getLogger('zuul.FakeSMTP')
1325
1326 def __init__(self, messages, server, port):
1327 self.server = server
1328 self.port = port
1329 self.messages = messages
1330
1331 def sendmail(self, from_email, to_email, msg):
1332 self.log.info("Sending email from %s, to %s, with msg %s" % (
1333 from_email, to_email, msg))
1334
1335 headers = msg.split('\n\n', 1)[0]
1336 body = msg.split('\n\n', 1)[1]
1337
1338 self.messages.append(dict(
1339 from_email=from_email,
1340 to_email=to_email,
1341 msg=msg,
1342 headers=headers,
1343 body=body,
1344 ))
1345
1346 return True
1347
1348 def quit(self):
1349 return True
1350
1351
James E. Blairdce6cea2016-12-20 16:45:32 -08001352class FakeNodepool(object):
1353 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001354 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001355
1356 log = logging.getLogger("zuul.test.FakeNodepool")
1357
1358 def __init__(self, host, port, chroot):
1359 self.client = kazoo.client.KazooClient(
1360 hosts='%s:%s%s' % (host, port, chroot))
1361 self.client.start()
1362 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001363 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001364 self.thread = threading.Thread(target=self.run)
1365 self.thread.daemon = True
1366 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001367 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001368
1369 def stop(self):
1370 self._running = False
1371 self.thread.join()
1372 self.client.stop()
1373 self.client.close()
1374
1375 def run(self):
1376 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001377 try:
1378 self._run()
1379 except Exception:
1380 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001381 time.sleep(0.1)
1382
1383 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001384 if self.paused:
1385 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001386 for req in self.getNodeRequests():
1387 self.fulfillRequest(req)
1388
1389 def getNodeRequests(self):
1390 try:
1391 reqids = self.client.get_children(self.REQUEST_ROOT)
1392 except kazoo.exceptions.NoNodeError:
1393 return []
1394 reqs = []
1395 for oid in sorted(reqids):
1396 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001397 try:
1398 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001399 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001400 data['_oid'] = oid
1401 reqs.append(data)
1402 except kazoo.exceptions.NoNodeError:
1403 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001404 return reqs
1405
James E. Blaire18d4602017-01-05 11:17:28 -08001406 def getNodes(self):
1407 try:
1408 nodeids = self.client.get_children(self.NODE_ROOT)
1409 except kazoo.exceptions.NoNodeError:
1410 return []
1411 nodes = []
1412 for oid in sorted(nodeids):
1413 path = self.NODE_ROOT + '/' + oid
1414 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001415 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001416 data['_oid'] = oid
1417 try:
1418 lockfiles = self.client.get_children(path + '/lock')
1419 except kazoo.exceptions.NoNodeError:
1420 lockfiles = []
1421 if lockfiles:
1422 data['_lock'] = True
1423 else:
1424 data['_lock'] = False
1425 nodes.append(data)
1426 return nodes
1427
James E. Blaira38c28e2017-01-04 10:33:20 -08001428 def makeNode(self, request_id, node_type):
1429 now = time.time()
1430 path = '/nodepool/nodes/'
1431 data = dict(type=node_type,
1432 provider='test-provider',
1433 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001434 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001435 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001436 public_ipv4='127.0.0.1',
1437 private_ipv4=None,
1438 public_ipv6=None,
1439 allocated_to=request_id,
1440 state='ready',
1441 state_time=now,
1442 created_time=now,
1443 updated_time=now,
1444 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001445 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001446 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001447 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001448 path = self.client.create(path, data,
1449 makepath=True,
1450 sequence=True)
1451 nodeid = path.split("/")[-1]
1452 return nodeid
1453
James E. Blair6ab79e02017-01-06 10:10:17 -08001454 def addFailRequest(self, request):
1455 self.fail_requests.add(request['_oid'])
1456
James E. Blairdce6cea2016-12-20 16:45:32 -08001457 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001458 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001459 return
1460 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001461 oid = request['_oid']
1462 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001463
James E. Blair6ab79e02017-01-06 10:10:17 -08001464 if oid in self.fail_requests:
1465 request['state'] = 'failed'
1466 else:
1467 request['state'] = 'fulfilled'
1468 nodes = []
1469 for node in request['node_types']:
1470 nodeid = self.makeNode(oid, node)
1471 nodes.append(nodeid)
1472 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001473
James E. Blaira38c28e2017-01-04 10:33:20 -08001474 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001475 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001476 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001477 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001478 try:
1479 self.client.set(path, data)
1480 except kazoo.exceptions.NoNodeError:
1481 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001482
1483
James E. Blair498059b2016-12-20 13:50:13 -08001484class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001485 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001486 super(ChrootedKazooFixture, self).__init__()
1487
1488 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1489 if ':' in zk_host:
1490 host, port = zk_host.split(':')
1491 else:
1492 host = zk_host
1493 port = None
1494
1495 self.zookeeper_host = host
1496
1497 if not port:
1498 self.zookeeper_port = 2181
1499 else:
1500 self.zookeeper_port = int(port)
1501
Clark Boylan621ec9a2017-04-07 17:41:33 -07001502 self.test_id = test_id
1503
James E. Blair498059b2016-12-20 13:50:13 -08001504 def _setUp(self):
1505 # Make sure the test chroot paths do not conflict
1506 random_bits = ''.join(random.choice(string.ascii_lowercase +
1507 string.ascii_uppercase)
1508 for x in range(8))
1509
Clark Boylan621ec9a2017-04-07 17:41:33 -07001510 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001511 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1512
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001513 self.addCleanup(self._cleanup)
1514
James E. Blair498059b2016-12-20 13:50:13 -08001515 # Ensure the chroot path exists and clean up any pre-existing znodes.
1516 _tmp_client = kazoo.client.KazooClient(
1517 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1518 _tmp_client.start()
1519
1520 if _tmp_client.exists(self.zookeeper_chroot):
1521 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1522
1523 _tmp_client.ensure_path(self.zookeeper_chroot)
1524 _tmp_client.stop()
1525 _tmp_client.close()
1526
James E. Blair498059b2016-12-20 13:50:13 -08001527 def _cleanup(self):
1528 '''Remove the chroot path.'''
1529 # Need a non-chroot'ed client to remove the chroot path
1530 _tmp_client = kazoo.client.KazooClient(
1531 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1532 _tmp_client.start()
1533 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1534 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001535 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001536
1537
Joshua Heskethd78b4482015-09-14 16:56:34 -06001538class MySQLSchemaFixture(fixtures.Fixture):
1539 def setUp(self):
1540 super(MySQLSchemaFixture, self).setUp()
1541
1542 random_bits = ''.join(random.choice(string.ascii_lowercase +
1543 string.ascii_uppercase)
1544 for x in range(8))
1545 self.name = '%s_%s' % (random_bits, os.getpid())
1546 self.passwd = uuid.uuid4().hex
1547 db = pymysql.connect(host="localhost",
1548 user="openstack_citest",
1549 passwd="openstack_citest",
1550 db="openstack_citest")
1551 cur = db.cursor()
1552 cur.execute("create database %s" % self.name)
1553 cur.execute(
1554 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1555 (self.name, self.name, self.passwd))
1556 cur.execute("flush privileges")
1557
1558 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1559 self.passwd,
1560 self.name)
1561 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1562 self.addCleanup(self.cleanup)
1563
1564 def cleanup(self):
1565 db = pymysql.connect(host="localhost",
1566 user="openstack_citest",
1567 passwd="openstack_citest",
1568 db="openstack_citest")
1569 cur = db.cursor()
1570 cur.execute("drop database %s" % self.name)
1571 cur.execute("drop user '%s'@'localhost'" % self.name)
1572 cur.execute("flush privileges")
1573
1574
Maru Newby3fe5f852015-01-13 04:22:14 +00001575class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001576 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001577 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001578
James E. Blair1c236df2017-02-01 14:07:24 -08001579 def attachLogs(self, *args):
1580 def reader():
1581 self._log_stream.seek(0)
1582 while True:
1583 x = self._log_stream.read(4096)
1584 if not x:
1585 break
1586 yield x.encode('utf8')
1587 content = testtools.content.content_from_reader(
1588 reader,
1589 testtools.content_type.UTF8_TEXT,
1590 False)
1591 self.addDetail('logging', content)
1592
Clark Boylanb640e052014-04-03 16:41:46 -07001593 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001594 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001595 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1596 try:
1597 test_timeout = int(test_timeout)
1598 except ValueError:
1599 # If timeout value is invalid do not set a timeout.
1600 test_timeout = 0
1601 if test_timeout > 0:
1602 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1603
1604 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1605 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1606 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1607 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1608 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1609 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1610 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1611 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1612 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1613 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001614 self._log_stream = StringIO()
1615 self.addOnException(self.attachLogs)
1616 else:
1617 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001618
James E. Blair73b41772017-05-22 13:22:55 -07001619 # NOTE(jeblair): this is temporary extra debugging to try to
1620 # track down a possible leak.
1621 orig_git_repo_init = git.Repo.__init__
1622
1623 def git_repo_init(myself, *args, **kw):
1624 orig_git_repo_init(myself, *args, **kw)
1625 self.log.debug("Created git repo 0x%x %s" %
1626 (id(myself), repr(myself)))
1627
1628 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1629 git_repo_init))
1630
James E. Blair1c236df2017-02-01 14:07:24 -08001631 handler = logging.StreamHandler(self._log_stream)
1632 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1633 '%(levelname)-8s %(message)s')
1634 handler.setFormatter(formatter)
1635
1636 logger = logging.getLogger()
1637 logger.setLevel(logging.DEBUG)
1638 logger.addHandler(handler)
1639
Clark Boylan3410d532017-04-25 12:35:29 -07001640 # Make sure we don't carry old handlers around in process state
1641 # which slows down test runs
1642 self.addCleanup(logger.removeHandler, handler)
1643 self.addCleanup(handler.close)
1644 self.addCleanup(handler.flush)
1645
James E. Blair1c236df2017-02-01 14:07:24 -08001646 # NOTE(notmorgan): Extract logging overrides for specific
1647 # libraries from the OS_LOG_DEFAULTS env and create loggers
1648 # for each. This is used to limit the output during test runs
1649 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001650 log_defaults_from_env = os.environ.get(
1651 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001652 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001653
James E. Blairdce6cea2016-12-20 16:45:32 -08001654 if log_defaults_from_env:
1655 for default in log_defaults_from_env.split(','):
1656 try:
1657 name, level_str = default.split('=', 1)
1658 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001659 logger = logging.getLogger(name)
1660 logger.setLevel(level)
1661 logger.addHandler(handler)
1662 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001663 except ValueError:
1664 # NOTE(notmorgan): Invalid format of the log default,
1665 # skip and don't try and apply a logger for the
1666 # specified module
1667 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001668
Maru Newby3fe5f852015-01-13 04:22:14 +00001669
1670class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001671 """A test case with a functioning Zuul.
1672
1673 The following class variables are used during test setup and can
1674 be overidden by subclasses but are effectively read-only once a
1675 test method starts running:
1676
1677 :cvar str config_file: This points to the main zuul config file
1678 within the fixtures directory. Subclasses may override this
1679 to obtain a different behavior.
1680
1681 :cvar str tenant_config_file: This is the tenant config file
1682 (which specifies from what git repos the configuration should
1683 be loaded). It defaults to the value specified in
1684 `config_file` but can be overidden by subclasses to obtain a
1685 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001686 configuration. See also the :py:func:`simple_layout`
1687 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001688
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001689 :cvar bool create_project_keys: Indicates whether Zuul should
1690 auto-generate keys for each project, or whether the test
1691 infrastructure should insert dummy keys to save time during
1692 startup. Defaults to False.
1693
James E. Blaire7b99a02016-08-05 14:27:34 -07001694 The following are instance variables that are useful within test
1695 methods:
1696
1697 :ivar FakeGerritConnection fake_<connection>:
1698 A :py:class:`~tests.base.FakeGerritConnection` will be
1699 instantiated for each connection present in the config file
1700 and stored here. For instance, `fake_gerrit` will hold the
1701 FakeGerritConnection object for a connection named `gerrit`.
1702
1703 :ivar FakeGearmanServer gearman_server: An instance of
1704 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1705 server that all of the Zuul components in this test use to
1706 communicate with each other.
1707
Paul Belanger174a8272017-03-14 13:20:10 -04001708 :ivar RecordingExecutorServer executor_server: An instance of
1709 :py:class:`~tests.base.RecordingExecutorServer` which is the
1710 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001711
1712 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1713 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001714 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001715 list upon completion.
1716
1717 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1718 objects representing completed builds. They are appended to
1719 the list in the order they complete.
1720
1721 """
1722
James E. Blair83005782015-12-11 14:46:03 -08001723 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001724 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001725 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001726
1727 def _startMerger(self):
1728 self.merge_server = zuul.merger.server.MergeServer(self.config,
1729 self.connections)
1730 self.merge_server.start()
1731
Maru Newby3fe5f852015-01-13 04:22:14 +00001732 def setUp(self):
1733 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001734
1735 self.setupZK()
1736
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001737 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001738 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001739 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1740 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001741 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001742 tmp_root = tempfile.mkdtemp(
1743 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001744 self.test_root = os.path.join(tmp_root, "zuul-test")
1745 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001746 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001747 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001748 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001749
1750 if os.path.exists(self.test_root):
1751 shutil.rmtree(self.test_root)
1752 os.makedirs(self.test_root)
1753 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001754 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001755
1756 # Make per test copy of Configuration.
1757 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001758 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001759 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001760 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001761 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001762 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001763 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001764
Clark Boylanb640e052014-04-03 16:41:46 -07001765 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001766 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1767 # see: https://github.com/jsocol/pystatsd/issues/61
1768 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001769 os.environ['STATSD_PORT'] = str(self.statsd.port)
1770 self.statsd.start()
1771 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001772 reload_module(statsd)
1773 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001774
1775 self.gearman_server = FakeGearmanServer()
1776
1777 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001778 self.log.info("Gearman server on port %s" %
1779 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001780
James E. Blaire511d2f2016-12-08 15:22:26 -08001781 gerritsource.GerritSource.replication_timeout = 1.5
1782 gerritsource.GerritSource.replication_retry_interval = 0.5
1783 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001784
Joshua Hesketh352264b2015-08-11 23:42:08 +10001785 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001786
Jan Hruban7083edd2015-08-21 14:00:54 +02001787 self.webapp = zuul.webapp.WebApp(
1788 self.sched, port=0, listen_address='127.0.0.1')
1789
Jan Hruban6b71aff2015-10-22 16:58:08 +02001790 self.event_queues = [
1791 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001792 self.sched.trigger_event_queue,
1793 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001794 ]
1795
James E. Blairfef78942016-03-11 16:28:56 -08001796 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001797 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001798
Paul Belanger174a8272017-03-14 13:20:10 -04001799 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001800 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001801 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001802 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001803 _test_root=self.test_root,
1804 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001805 self.executor_server.start()
1806 self.history = self.executor_server.build_history
1807 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001808
Paul Belanger174a8272017-03-14 13:20:10 -04001809 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001810 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001811 self.merge_client = zuul.merger.client.MergeClient(
1812 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001813 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001814 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001815 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001816
James E. Blair0d5a36e2017-02-21 10:53:44 -05001817 self.fake_nodepool = FakeNodepool(
1818 self.zk_chroot_fixture.zookeeper_host,
1819 self.zk_chroot_fixture.zookeeper_port,
1820 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001821
Paul Belanger174a8272017-03-14 13:20:10 -04001822 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001823 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001824 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001825 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001826
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001827 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001828
1829 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001830 self.webapp.start()
1831 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001832 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001833 # Cleanups are run in reverse order
1834 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001835 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001836 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001837
James E. Blairb9c0d772017-03-03 14:34:49 -08001838 self.sched.reconfigure(self.config)
1839 self.sched.resume()
1840
James E. Blairfef78942016-03-11 16:28:56 -08001841 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001842 # Set up gerrit related fakes
1843 # Set a changes database so multiple FakeGerrit's can report back to
1844 # a virtual canonical database given by the configured hostname
1845 self.gerrit_changes_dbs = {}
1846
1847 def getGerritConnection(driver, name, config):
1848 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1849 con = FakeGerritConnection(driver, name, config,
1850 changes_db=db,
1851 upstream_root=self.upstream_root)
1852 self.event_queues.append(con.event_queue)
1853 setattr(self, 'fake_' + name, con)
1854 return con
1855
1856 self.useFixture(fixtures.MonkeyPatch(
1857 'zuul.driver.gerrit.GerritDriver.getConnection',
1858 getGerritConnection))
1859
Gregory Haynes4fc12542015-04-22 20:38:06 -07001860 def getGithubConnection(driver, name, config):
1861 con = FakeGithubConnection(driver, name, config,
1862 upstream_root=self.upstream_root)
1863 setattr(self, 'fake_' + name, con)
1864 return con
1865
1866 self.useFixture(fixtures.MonkeyPatch(
1867 'zuul.driver.github.GithubDriver.getConnection',
1868 getGithubConnection))
1869
James E. Blaire511d2f2016-12-08 15:22:26 -08001870 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001871 # TODO(jhesketh): This should come from lib.connections for better
1872 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001873 # Register connections from the config
1874 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001875
Joshua Hesketh352264b2015-08-11 23:42:08 +10001876 def FakeSMTPFactory(*args, **kw):
1877 args = [self.smtp_messages] + list(args)
1878 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001879
Joshua Hesketh352264b2015-08-11 23:42:08 +10001880 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001881
James E. Blaire511d2f2016-12-08 15:22:26 -08001882 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001883 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001884 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001885
James E. Blair83005782015-12-11 14:46:03 -08001886 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001887 # This creates the per-test configuration object. It can be
1888 # overriden by subclasses, but should not need to be since it
1889 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001890 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001891 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001892
1893 if not self.setupSimpleLayout():
1894 if hasattr(self, 'tenant_config_file'):
1895 self.config.set('zuul', 'tenant_config',
1896 self.tenant_config_file)
1897 git_path = os.path.join(
1898 os.path.dirname(
1899 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1900 'git')
1901 if os.path.exists(git_path):
1902 for reponame in os.listdir(git_path):
1903 project = reponame.replace('_', '/')
1904 self.copyDirToRepo(project,
1905 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001906 self.setupAllProjectKeys()
1907
James E. Blair06cc3922017-04-19 10:08:10 -07001908 def setupSimpleLayout(self):
1909 # If the test method has been decorated with a simple_layout,
1910 # use that instead of the class tenant_config_file. Set up a
1911 # single config-project with the specified layout, and
1912 # initialize repos for all of the 'project' entries which
1913 # appear in the layout.
1914 test_name = self.id().split('.')[-1]
1915 test = getattr(self, test_name)
1916 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001917 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001918 else:
1919 return False
1920
James E. Blairb70e55a2017-04-19 12:57:02 -07001921 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001922 path = os.path.join(FIXTURE_DIR, path)
1923 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001924 data = f.read()
1925 layout = yaml.safe_load(data)
1926 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001927 untrusted_projects = []
1928 for item in layout:
1929 if 'project' in item:
1930 name = item['project']['name']
1931 untrusted_projects.append(name)
1932 self.init_repo(name)
1933 self.addCommitToRepo(name, 'initial commit',
1934 files={'README': ''},
1935 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001936 if 'job' in item:
1937 jobname = item['job']['name']
1938 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001939
1940 root = os.path.join(self.test_root, "config")
1941 if not os.path.exists(root):
1942 os.makedirs(root)
1943 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1944 config = [{'tenant':
1945 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001946 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001947 {'config-projects': ['common-config'],
1948 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07001949 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07001950 f.close()
1951 self.config.set('zuul', 'tenant_config',
1952 os.path.join(FIXTURE_DIR, f.name))
1953
1954 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001955 self.addCommitToRepo('common-config', 'add content from fixture',
1956 files, branch='master', tag='init')
1957
1958 return True
1959
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001960 def setupAllProjectKeys(self):
1961 if self.create_project_keys:
1962 return
1963
1964 path = self.config.get('zuul', 'tenant_config')
1965 with open(os.path.join(FIXTURE_DIR, path)) as f:
1966 tenant_config = yaml.safe_load(f.read())
1967 for tenant in tenant_config:
1968 sources = tenant['tenant']['source']
1969 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001970 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001971 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001972 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001973 self.setupProjectKeys(source, project)
1974
1975 def setupProjectKeys(self, source, project):
1976 # Make sure we set up an RSA key for the project so that we
1977 # don't spend time generating one:
1978
1979 key_root = os.path.join(self.state_root, 'keys')
1980 if not os.path.isdir(key_root):
1981 os.mkdir(key_root, 0o700)
1982 private_key_file = os.path.join(key_root, source, project + '.pem')
1983 private_key_dir = os.path.dirname(private_key_file)
1984 self.log.debug("Installing test keys for project %s at %s" % (
1985 project, private_key_file))
1986 if not os.path.isdir(private_key_dir):
1987 os.makedirs(private_key_dir)
1988 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1989 with open(private_key_file, 'w') as o:
1990 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001991
James E. Blair498059b2016-12-20 13:50:13 -08001992 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001993 self.zk_chroot_fixture = self.useFixture(
1994 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001995 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001996 self.zk_chroot_fixture.zookeeper_host,
1997 self.zk_chroot_fixture.zookeeper_port,
1998 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001999
James E. Blair96c6bf82016-01-15 16:20:40 -08002000 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002001 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002002
2003 files = {}
2004 for (dirpath, dirnames, filenames) in os.walk(source_path):
2005 for filename in filenames:
2006 test_tree_filepath = os.path.join(dirpath, filename)
2007 common_path = os.path.commonprefix([test_tree_filepath,
2008 source_path])
2009 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2010 with open(test_tree_filepath, 'r') as f:
2011 content = f.read()
2012 files[relative_filepath] = content
2013 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002014 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002015
James E. Blaire18d4602017-01-05 11:17:28 -08002016 def assertNodepoolState(self):
2017 # Make sure that there are no pending requests
2018
2019 requests = self.fake_nodepool.getNodeRequests()
2020 self.assertEqual(len(requests), 0)
2021
2022 nodes = self.fake_nodepool.getNodes()
2023 for node in nodes:
2024 self.assertFalse(node['_lock'], "Node %s is locked" %
2025 (node['_oid'],))
2026
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002027 def assertNoGeneratedKeys(self):
2028 # Make sure that Zuul did not generate any project keys
2029 # (unless it was supposed to).
2030
2031 if self.create_project_keys:
2032 return
2033
2034 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2035 test_key = i.read()
2036
2037 key_root = os.path.join(self.state_root, 'keys')
2038 for root, dirname, files in os.walk(key_root):
2039 for fn in files:
2040 with open(os.path.join(root, fn)) as f:
2041 self.assertEqual(test_key, f.read())
2042
Clark Boylanb640e052014-04-03 16:41:46 -07002043 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002044 self.log.debug("Assert final state")
2045 # Make sure no jobs are running
2046 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002047 # Make sure that git.Repo objects have been garbage collected.
2048 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002049 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002050 gc.collect()
2051 for obj in gc.get_objects():
2052 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002053 self.log.debug("Leaked git repo object: 0x%x %s" %
2054 (id(obj), repr(obj)))
2055 for ref in gc.get_referrers(obj):
2056 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002057 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002058 if repos:
2059 for obj in gc.garbage:
2060 self.log.debug(" Garbage %s" % (repr(obj)))
2061 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002062 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002063 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002064 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002065 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002066 for tenant in self.sched.abide.tenants.values():
2067 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002068 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002069 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002070
2071 def shutdown(self):
2072 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002073 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002074 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002075 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002076 self.sched.stop()
2077 self.sched.join()
2078 self.statsd.stop()
2079 self.statsd.join()
2080 self.webapp.stop()
2081 self.webapp.join()
2082 self.rpc.stop()
2083 self.rpc.join()
2084 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002085 self.fake_nodepool.stop()
2086 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002087 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002088 # we whitelist watchdog threads as they have relatively long delays
2089 # before noticing they should exit, but they should exit on their own.
2090 threads = [t for t in threading.enumerate()
2091 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002092 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002093 log_str = ""
2094 for thread_id, stack_frame in sys._current_frames().items():
2095 log_str += "Thread: %s\n" % thread_id
2096 log_str += "".join(traceback.format_stack(stack_frame))
2097 self.log.debug(log_str)
2098 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002099
James E. Blaira002b032017-04-18 10:35:48 -07002100 def assertCleanShutdown(self):
2101 pass
2102
James E. Blairc4ba97a2017-04-19 16:26:24 -07002103 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002104 parts = project.split('/')
2105 path = os.path.join(self.upstream_root, *parts[:-1])
2106 if not os.path.exists(path):
2107 os.makedirs(path)
2108 path = os.path.join(self.upstream_root, project)
2109 repo = git.Repo.init(path)
2110
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002111 with repo.config_writer() as config_writer:
2112 config_writer.set_value('user', 'email', 'user@example.com')
2113 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002114
Clark Boylanb640e052014-04-03 16:41:46 -07002115 repo.index.commit('initial commit')
2116 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002117 if tag:
2118 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002119
James E. Blair97d902e2014-08-21 13:25:56 -07002120 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002121 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002122 repo.git.clean('-x', '-f', '-d')
2123
James E. Blair97d902e2014-08-21 13:25:56 -07002124 def create_branch(self, project, branch):
2125 path = os.path.join(self.upstream_root, project)
2126 repo = git.Repo.init(path)
2127 fn = os.path.join(path, 'README')
2128
2129 branch_head = repo.create_head(branch)
2130 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002131 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002132 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002133 f.close()
2134 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002135 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002136
James E. Blair97d902e2014-08-21 13:25:56 -07002137 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002138 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002139 repo.git.clean('-x', '-f', '-d')
2140
Sachi King9f16d522016-03-16 12:20:45 +11002141 def create_commit(self, project):
2142 path = os.path.join(self.upstream_root, project)
2143 repo = git.Repo(path)
2144 repo.head.reference = repo.heads['master']
2145 file_name = os.path.join(path, 'README')
2146 with open(file_name, 'a') as f:
2147 f.write('creating fake commit\n')
2148 repo.index.add([file_name])
2149 commit = repo.index.commit('Creating a fake commit')
2150 return commit.hexsha
2151
James E. Blairf4a5f022017-04-18 14:01:10 -07002152 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002153 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002154 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002155 while len(self.builds):
2156 self.release(self.builds[0])
2157 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002158 i += 1
2159 if count is not None and i >= count:
2160 break
James E. Blairb8c16472015-05-05 14:55:26 -07002161
Clark Boylanb640e052014-04-03 16:41:46 -07002162 def release(self, job):
2163 if isinstance(job, FakeBuild):
2164 job.release()
2165 else:
2166 job.waiting = False
2167 self.log.debug("Queued job %s released" % job.unique)
2168 self.gearman_server.wakeConnections()
2169
2170 def getParameter(self, job, name):
2171 if isinstance(job, FakeBuild):
2172 return job.parameters[name]
2173 else:
2174 parameters = json.loads(job.arguments)
2175 return parameters[name]
2176
Clark Boylanb640e052014-04-03 16:41:46 -07002177 def haveAllBuildsReported(self):
2178 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002179 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002180 return False
2181 # Find out if every build that the worker has completed has been
2182 # reported back to Zuul. If it hasn't then that means a Gearman
2183 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002184 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002185 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002186 if not zbuild:
2187 # It has already been reported
2188 continue
2189 # It hasn't been reported yet.
2190 return False
2191 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002192 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002193 if connection.state == 'GRAB_WAIT':
2194 return False
2195 return True
2196
2197 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002198 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002199 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002200 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002201 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002202 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002203 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002204 for j in conn.related_jobs.values():
2205 if j.unique == build.uuid:
2206 client_job = j
2207 break
2208 if not client_job:
2209 self.log.debug("%s is not known to the gearman client" %
2210 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002211 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002212 if not client_job.handle:
2213 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002214 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002215 server_job = self.gearman_server.jobs.get(client_job.handle)
2216 if not server_job:
2217 self.log.debug("%s is not known to the gearman server" %
2218 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002219 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002220 if not hasattr(server_job, 'waiting'):
2221 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002222 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002223 if server_job.waiting:
2224 continue
James E. Blair17302972016-08-10 16:11:42 -07002225 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002226 self.log.debug("%s has not reported start" % build)
2227 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002228 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002229 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002230 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002231 if worker_build:
2232 if worker_build.isWaiting():
2233 continue
2234 else:
2235 self.log.debug("%s is running" % worker_build)
2236 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002237 else:
James E. Blair962220f2016-08-03 11:22:38 -07002238 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002239 return False
James E. Blaira002b032017-04-18 10:35:48 -07002240 for (build_uuid, job_worker) in \
2241 self.executor_server.job_workers.items():
2242 if build_uuid not in seen_builds:
2243 self.log.debug("%s is not finalized" % build_uuid)
2244 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002245 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002246
James E. Blairdce6cea2016-12-20 16:45:32 -08002247 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002248 if self.fake_nodepool.paused:
2249 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002250 if self.sched.nodepool.requests:
2251 return False
2252 return True
2253
Jan Hruban6b71aff2015-10-22 16:58:08 +02002254 def eventQueuesEmpty(self):
2255 for queue in self.event_queues:
2256 yield queue.empty()
2257
2258 def eventQueuesJoin(self):
2259 for queue in self.event_queues:
2260 queue.join()
2261
Clark Boylanb640e052014-04-03 16:41:46 -07002262 def waitUntilSettled(self):
2263 self.log.debug("Waiting until settled...")
2264 start = time.time()
2265 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002266 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002267 self.log.error("Timeout waiting for Zuul to settle")
2268 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002269 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002270 self.log.error(" %s: %s" % (queue, queue.empty()))
2271 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002272 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002273 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002274 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002275 self.log.error("All requests completed: %s" %
2276 (self.areAllNodeRequestsComplete(),))
2277 self.log.error("Merge client jobs: %s" %
2278 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002279 raise Exception("Timeout waiting for Zuul to settle")
2280 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002281
Paul Belanger174a8272017-03-14 13:20:10 -04002282 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002283 # have all build states propogated to zuul?
2284 if self.haveAllBuildsReported():
2285 # Join ensures that the queue is empty _and_ events have been
2286 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002287 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002288 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002289 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002290 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002291 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002292 self.areAllNodeRequestsComplete() and
2293 all(self.eventQueuesEmpty())):
2294 # The queue empty check is placed at the end to
2295 # ensure that if a component adds an event between
2296 # when locked the run handler and checked that the
2297 # components were stable, we don't erroneously
2298 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002299 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002300 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002301 self.log.debug("...settled.")
2302 return
2303 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002304 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002305 self.sched.wake_event.wait(0.1)
2306
2307 def countJobResults(self, jobs, result):
2308 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002309 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002310
James E. Blair96c6bf82016-01-15 16:20:40 -08002311 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002312 for job in self.history:
2313 if (job.name == name and
2314 (project is None or
2315 job.parameters['ZUUL_PROJECT'] == project)):
2316 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002317 raise Exception("Unable to find job %s in history" % name)
2318
2319 def assertEmptyQueues(self):
2320 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002321 for tenant in self.sched.abide.tenants.values():
2322 for pipeline in tenant.layout.pipelines.values():
2323 for queue in pipeline.queues:
2324 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002325 print('pipeline %s queue %s contents %s' % (
2326 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002327 self.assertEqual(len(queue.queue), 0,
2328 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002329
2330 def assertReportedStat(self, key, value=None, kind=None):
2331 start = time.time()
2332 while time.time() < (start + 5):
2333 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002334 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002335 if key == k:
2336 if value is None and kind is None:
2337 return
2338 elif value:
2339 if value == v:
2340 return
2341 elif kind:
2342 if v.endswith('|' + kind):
2343 return
2344 time.sleep(0.1)
2345
Clark Boylanb640e052014-04-03 16:41:46 -07002346 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002347
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002348 def assertBuilds(self, builds):
2349 """Assert that the running builds are as described.
2350
2351 The list of running builds is examined and must match exactly
2352 the list of builds described by the input.
2353
2354 :arg list builds: A list of dictionaries. Each item in the
2355 list must match the corresponding build in the build
2356 history, and each element of the dictionary must match the
2357 corresponding attribute of the build.
2358
2359 """
James E. Blair3158e282016-08-19 09:34:11 -07002360 try:
2361 self.assertEqual(len(self.builds), len(builds))
2362 for i, d in enumerate(builds):
2363 for k, v in d.items():
2364 self.assertEqual(
2365 getattr(self.builds[i], k), v,
2366 "Element %i in builds does not match" % (i,))
2367 except Exception:
2368 for build in self.builds:
2369 self.log.error("Running build: %s" % build)
2370 else:
2371 self.log.error("No running builds")
2372 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002373
James E. Blairb536ecc2016-08-31 10:11:42 -07002374 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002375 """Assert that the completed builds are as described.
2376
2377 The list of completed builds is examined and must match
2378 exactly the list of builds described by the input.
2379
2380 :arg list history: A list of dictionaries. Each item in the
2381 list must match the corresponding build in the build
2382 history, and each element of the dictionary must match the
2383 corresponding attribute of the build.
2384
James E. Blairb536ecc2016-08-31 10:11:42 -07002385 :arg bool ordered: If true, the history must match the order
2386 supplied, if false, the builds are permitted to have
2387 arrived in any order.
2388
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002389 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002390 def matches(history_item, item):
2391 for k, v in item.items():
2392 if getattr(history_item, k) != v:
2393 return False
2394 return True
James E. Blair3158e282016-08-19 09:34:11 -07002395 try:
2396 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002397 if ordered:
2398 for i, d in enumerate(history):
2399 if not matches(self.history[i], d):
2400 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002401 "Element %i in history does not match %s" %
2402 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002403 else:
2404 unseen = self.history[:]
2405 for i, d in enumerate(history):
2406 found = False
2407 for unseen_item in unseen:
2408 if matches(unseen_item, d):
2409 found = True
2410 unseen.remove(unseen_item)
2411 break
2412 if not found:
2413 raise Exception("No match found for element %i "
2414 "in history" % (i,))
2415 if unseen:
2416 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002417 except Exception:
2418 for build in self.history:
2419 self.log.error("Completed build: %s" % build)
2420 else:
2421 self.log.error("No completed builds")
2422 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002423
James E. Blair6ac368c2016-12-22 18:07:20 -08002424 def printHistory(self):
2425 """Log the build history.
2426
2427 This can be useful during tests to summarize what jobs have
2428 completed.
2429
2430 """
2431 self.log.debug("Build history:")
2432 for build in self.history:
2433 self.log.debug(build)
2434
James E. Blair59fdbac2015-12-07 17:08:06 -08002435 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002436 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2437
James E. Blair9ea70072017-04-19 16:05:30 -07002438 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002439 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002440 if not os.path.exists(root):
2441 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002442 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2443 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002444- tenant:
2445 name: openstack
2446 source:
2447 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002448 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002449 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002450 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002451 - org/project
2452 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002453 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002454 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002455 self.config.set('zuul', 'tenant_config',
2456 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002457 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002458
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002459 def addCommitToRepo(self, project, message, files,
2460 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002461 path = os.path.join(self.upstream_root, project)
2462 repo = git.Repo(path)
2463 repo.head.reference = branch
2464 zuul.merger.merger.reset_repo_to_head(repo)
2465 for fn, content in files.items():
2466 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002467 try:
2468 os.makedirs(os.path.dirname(fn))
2469 except OSError:
2470 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002471 with open(fn, 'w') as f:
2472 f.write(content)
2473 repo.index.add([fn])
2474 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002475 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002476 repo.heads[branch].commit = commit
2477 repo.head.reference = branch
2478 repo.git.clean('-x', '-f', '-d')
2479 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002480 if tag:
2481 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002482 return before
2483
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002484 def commitConfigUpdate(self, project_name, source_name):
2485 """Commit an update to zuul.yaml
2486
2487 This overwrites the zuul.yaml in the specificed project with
2488 the contents specified.
2489
2490 :arg str project_name: The name of the project containing
2491 zuul.yaml (e.g., common-config)
2492
2493 :arg str source_name: The path to the file (underneath the
2494 test fixture directory) whose contents should be used to
2495 replace zuul.yaml.
2496 """
2497
2498 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002499 files = {}
2500 with open(source_path, 'r') as f:
2501 data = f.read()
2502 layout = yaml.safe_load(data)
2503 files['zuul.yaml'] = data
2504 for item in layout:
2505 if 'job' in item:
2506 jobname = item['job']['name']
2507 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002508 before = self.addCommitToRepo(
2509 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002510 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002511 return before
2512
James E. Blair7fc8daa2016-08-08 15:37:15 -07002513 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002514
James E. Blair7fc8daa2016-08-08 15:37:15 -07002515 """Inject a Fake (Gerrit) event.
2516
2517 This method accepts a JSON-encoded event and simulates Zuul
2518 having received it from Gerrit. It could (and should)
2519 eventually apply to any connection type, but is currently only
2520 used with Gerrit connections. The name of the connection is
2521 used to look up the corresponding server, and the event is
2522 simulated as having been received by all Zuul connections
2523 attached to that server. So if two Gerrit connections in Zuul
2524 are connected to the same Gerrit server, and you invoke this
2525 method specifying the name of one of them, the event will be
2526 received by both.
2527
2528 .. note::
2529
2530 "self.fake_gerrit.addEvent" calls should be migrated to
2531 this method.
2532
2533 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002534 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002535 :arg str event: The JSON-encoded event.
2536
2537 """
2538 specified_conn = self.connections.connections[connection]
2539 for conn in self.connections.connections.values():
2540 if (isinstance(conn, specified_conn.__class__) and
2541 specified_conn.server == conn.server):
2542 conn.addEvent(event)
2543
James E. Blair3f876d52016-07-22 13:07:14 -07002544
2545class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002546 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002547 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002548
Joshua Heskethd78b4482015-09-14 16:56:34 -06002549
2550class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002551 def setup_config(self):
2552 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002553 for section_name in self.config.sections():
2554 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2555 section_name, re.I)
2556 if not con_match:
2557 continue
2558
2559 if self.config.get(section_name, 'driver') == 'sql':
2560 f = MySQLSchemaFixture()
2561 self.useFixture(f)
2562 if (self.config.get(section_name, 'dburi') ==
2563 '$MYSQL_FIXTURE_DBURI$'):
2564 self.config.set(section_name, 'dburi', f.dburi)