blob: c01e9c390d100e9ad164167973761f3c36e841a6 [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
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
22import json
23import logging
24import os
Christian Berendt12d4d722014-06-07 21:03:45 +020025from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070026from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070027import random
28import re
29import select
30import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030031from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070032try:
33 from cStringIO import StringIO
34except Exception:
35 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070036import socket
37import string
38import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080039import sys
James E. Blairf84026c2015-12-08 16:11:46 -080040import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070041import threading
Clark Boylan8208c192017-04-24 18:08:08 -070042import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070043import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060044import uuid
45
Clark Boylanb640e052014-04-03 16:41:46 -070046
47import git
48import gear
49import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080050import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080051import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060052import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070053import statsd
54import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080055import testtools.content
56import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080057from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000058import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070059
James E. Blaire511d2f2016-12-08 15:22:26 -080060import zuul.driver.gerrit.gerritsource as gerritsource
61import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070062import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.scheduler
64import zuul.webapp
65import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040066import zuul.executor.server
67import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080068import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070069import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070070import zuul.merger.merger
71import zuul.merger.server
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
Joshua Hesketh352264b2015-08-11 23:42:08 +1000534 def getGitUrl(self, project):
535 return os.path.join(self.upstream_root, project.name)
536
Clark Boylanb640e052014-04-03 16:41:46 -0700537
Gregory Haynes4fc12542015-04-22 20:38:06 -0700538class GithubChangeReference(git.Reference):
539 _common_path_default = "refs/pull"
540 _points_to_commits_only = True
541
542
543class FakeGithubPullRequest(object):
544
545 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800546 subject, upstream_root, files=[], number_of_commits=1,
547 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548 """Creates a new PR with several commits.
549 Sends an event about opened PR."""
550 self.github = github
551 self.source = github
552 self.number = number
553 self.project = project
554 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100555 self.subject = subject
556 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700557 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100558 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700559 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100560 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100561 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800562 self.reviews = []
563 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self.updated_at = None
565 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100566 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100567 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700568 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
572
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
577
Jan Hruban570d01c2016-03-10 21:51:32 +0100578 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100580 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581 self._updateTimeStamp()
582
583 def getPullRequestOpenedEvent(self):
584 return self._getPullRequestEvent('opened')
585
586 def getPullRequestSynchronizeEvent(self):
587 return self._getPullRequestEvent('synchronize')
588
589 def getPullRequestReopenedEvent(self):
590 return self._getPullRequestEvent('reopened')
591
592 def getPullRequestClosedEvent(self):
593 return self._getPullRequestEvent('closed')
594
595 def addComment(self, message):
596 self.comments.append(message)
597 self._updateTimeStamp()
598
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200599 def getCommentAddedEvent(self, text):
600 name = 'issue_comment'
601 data = {
602 'action': 'created',
603 'issue': {
604 'number': self.number
605 },
606 'comment': {
607 'body': text
608 },
609 'repository': {
610 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100611 },
612 'sender': {
613 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200614 }
615 }
616 return (name, data)
617
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800618 def getReviewAddedEvent(self, review):
619 name = 'pull_request_review'
620 data = {
621 'action': 'submitted',
622 'pull_request': {
623 'number': self.number,
624 'title': self.subject,
625 'updated_at': self.updated_at,
626 'base': {
627 'ref': self.branch,
628 'repo': {
629 'full_name': self.project
630 }
631 },
632 'head': {
633 'sha': self.head_sha
634 }
635 },
636 'review': {
637 'state': review
638 },
639 'repository': {
640 'full_name': self.project
641 },
642 'sender': {
643 'login': 'ghuser'
644 }
645 }
646 return (name, data)
647
Jan Hruban16ad31f2015-11-07 14:39:07 +0100648 def addLabel(self, name):
649 if name not in self.labels:
650 self.labels.append(name)
651 self._updateTimeStamp()
652 return self._getLabelEvent(name)
653
654 def removeLabel(self, name):
655 if name in self.labels:
656 self.labels.remove(name)
657 self._updateTimeStamp()
658 return self._getUnlabelEvent(name)
659
660 def _getLabelEvent(self, label):
661 name = 'pull_request'
662 data = {
663 'action': 'labeled',
664 'pull_request': {
665 'number': self.number,
666 'updated_at': self.updated_at,
667 'base': {
668 'ref': self.branch,
669 'repo': {
670 'full_name': self.project
671 }
672 },
673 'head': {
674 'sha': self.head_sha
675 }
676 },
677 'label': {
678 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100679 },
680 'sender': {
681 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100682 }
683 }
684 return (name, data)
685
686 def _getUnlabelEvent(self, label):
687 name = 'pull_request'
688 data = {
689 'action': 'unlabeled',
690 'pull_request': {
691 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100692 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100693 'updated_at': self.updated_at,
694 'base': {
695 'ref': self.branch,
696 'repo': {
697 'full_name': self.project
698 }
699 },
700 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800701 'sha': self.head_sha,
702 'repo': {
703 'full_name': self.project
704 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100705 }
706 },
707 'label': {
708 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100709 },
710 'sender': {
711 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100712 }
713 }
714 return (name, data)
715
Gregory Haynes4fc12542015-04-22 20:38:06 -0700716 def _getRepo(self):
717 repo_path = os.path.join(self.upstream_root, self.project)
718 return git.Repo(repo_path)
719
720 def _createPRRef(self):
721 repo = self._getRepo()
722 GithubChangeReference.create(
723 repo, self._getPRReference(), 'refs/tags/init')
724
Jan Hruban570d01c2016-03-10 21:51:32 +0100725 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700726 repo = self._getRepo()
727 ref = repo.references[self._getPRReference()]
728 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100729 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700730 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100731 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700732 repo.head.reference = ref
733 zuul.merger.merger.reset_repo_to_head(repo)
734 repo.git.clean('-x', '-f', '-d')
735
Jan Hruban570d01c2016-03-10 21:51:32 +0100736 if files:
737 fn = files[0]
738 self.files = files
739 else:
740 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
741 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100742 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700743 fn = os.path.join(repo.working_dir, fn)
744 f = open(fn, 'w')
745 with open(fn, 'w') as f:
746 f.write("test %s %s\n" %
747 (self.branch, self.number))
748 repo.index.add([fn])
749
750 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800751 # Create an empty set of statuses for the given sha,
752 # each sha on a PR may have a status set on it
753 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700754 repo.head.reference = 'master'
755 zuul.merger.merger.reset_repo_to_head(repo)
756 repo.git.clean('-x', '-f', '-d')
757 repo.heads['master'].checkout()
758
759 def _updateTimeStamp(self):
760 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
761
762 def getPRHeadSha(self):
763 repo = self._getRepo()
764 return repo.references[self._getPRReference()].commit.hexsha
765
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800766 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800767 # Since we're bypassing github API, which would require a user, we
768 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800769 # insert the status at the top of the list, to simulate that it
770 # is the most recent set status
771 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100772 'state': state,
773 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800774 'description': description,
775 'context': context,
776 'creator': {
777 'login': user
778 }
779 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100780
Jesse Keatingae4cd272017-01-30 17:10:44 -0800781 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800782 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
783 # convert the timestamp to a str format that would be returned
784 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800785
Adam Gandelmand81dd762017-02-09 15:15:49 -0800786 if granted_on:
787 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
788 submitted_at = time.strftime(
789 gh_time_format, granted_on.timetuple())
790 else:
791 # github timestamps only down to the second, so we need to make
792 # sure reviews that tests add appear to be added over a period of
793 # time in the past and not all at once.
794 if not self.reviews:
795 # the first review happens 10 mins ago
796 offset = 600
797 else:
798 # subsequent reviews happen 1 minute closer to now
799 offset = 600 - (len(self.reviews) * 60)
800
801 granted_on = datetime.datetime.utcfromtimestamp(
802 time.time() - offset)
803 submitted_at = time.strftime(
804 gh_time_format, granted_on.timetuple())
805
Jesse Keatingae4cd272017-01-30 17:10:44 -0800806 self.reviews.append({
807 'state': state,
808 'user': {
809 'login': user,
810 'email': user + "@derp.com",
811 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800812 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800813 })
814
Gregory Haynes4fc12542015-04-22 20:38:06 -0700815 def _getPRReference(self):
816 return '%s/head' % self.number
817
818 def _getPullRequestEvent(self, action):
819 name = 'pull_request'
820 data = {
821 'action': action,
822 'number': self.number,
823 'pull_request': {
824 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100825 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700826 'updated_at': self.updated_at,
827 'base': {
828 'ref': self.branch,
829 'repo': {
830 'full_name': self.project
831 }
832 },
833 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800834 'sha': self.head_sha,
835 'repo': {
836 'full_name': self.project
837 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700838 }
Jan Hruban3b415922016-02-03 13:10:22 +0100839 },
840 'sender': {
841 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700842 }
843 }
844 return (name, data)
845
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800846 def getCommitStatusEvent(self, context, state='success', user='zuul'):
847 name = 'status'
848 data = {
849 'state': state,
850 'sha': self.head_sha,
851 'description': 'Test results for %s: %s' % (self.head_sha, state),
852 'target_url': 'http://zuul/%s' % self.head_sha,
853 'branches': [],
854 'context': context,
855 'sender': {
856 'login': user
857 }
858 }
859 return (name, data)
860
Gregory Haynes4fc12542015-04-22 20:38:06 -0700861
862class FakeGithubConnection(githubconnection.GithubConnection):
863 log = logging.getLogger("zuul.test.FakeGithubConnection")
864
865 def __init__(self, driver, connection_name, connection_config,
866 upstream_root=None):
867 super(FakeGithubConnection, self).__init__(driver, connection_name,
868 connection_config)
869 self.connection_name = connection_name
870 self.pr_number = 0
871 self.pull_requests = []
872 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100873 self.merge_failure = False
874 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700875
Jan Hruban570d01c2016-03-10 21:51:32 +0100876 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 self.pr_number += 1
878 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100879 self, self.pr_number, project, branch, subject, self.upstream_root,
880 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700881 self.pull_requests.append(pull_request)
882 return pull_request
883
Wayne1a78c612015-06-11 17:14:13 -0700884 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
885 if not old_rev:
886 old_rev = '00000000000000000000000000000000'
887 if not new_rev:
888 new_rev = random_sha1()
889 name = 'push'
890 data = {
891 'ref': ref,
892 'before': old_rev,
893 'after': new_rev,
894 'repository': {
895 'full_name': project
896 }
897 }
898 return (name, data)
899
Gregory Haynes4fc12542015-04-22 20:38:06 -0700900 def emitEvent(self, event):
901 """Emulates sending the GitHub webhook event to the connection."""
902 port = self.webapp.server.socket.getsockname()[1]
903 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700904 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700905 headers = {'X-Github-Event': name}
906 req = urllib.request.Request(
907 'http://localhost:%s/connection/%s/payload'
908 % (port, self.connection_name),
909 data=payload, headers=headers)
910 urllib.request.urlopen(req)
911
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200912 def getPull(self, project, number):
913 pr = self.pull_requests[number - 1]
914 data = {
915 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100916 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200917 'updated_at': pr.updated_at,
918 'base': {
919 'repo': {
920 'full_name': pr.project
921 },
922 'ref': pr.branch,
923 },
Jan Hruban37615e52015-11-19 14:30:49 +0100924 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700925 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200926 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800927 'sha': pr.head_sha,
928 'repo': {
929 'full_name': pr.project
930 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200931 }
932 }
933 return data
934
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800935 def getPullBySha(self, sha):
936 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
937 if len(prs) > 1:
938 raise Exception('Multiple pulls found with head sha: %s' % sha)
939 pr = prs[0]
940 return self.getPull(pr.project, pr.number)
941
Jan Hruban570d01c2016-03-10 21:51:32 +0100942 def getPullFileNames(self, project, number):
943 pr = self.pull_requests[number - 1]
944 return pr.files
945
Jesse Keatingae4cd272017-01-30 17:10:44 -0800946 def _getPullReviews(self, owner, project, number):
947 pr = self.pull_requests[number - 1]
948 return pr.reviews
949
Jan Hruban3b415922016-02-03 13:10:22 +0100950 def getUser(self, login):
951 data = {
952 'username': login,
953 'name': 'Github User',
954 'email': 'github.user@example.com'
955 }
956 return data
957
Jesse Keatingae4cd272017-01-30 17:10:44 -0800958 def getRepoPermission(self, project, login):
959 owner, proj = project.split('/')
960 for pr in self.pull_requests:
961 pr_owner, pr_project = pr.project.split('/')
962 if (pr_owner == owner and proj == pr_project):
963 if login in pr.writers:
964 return 'write'
965 else:
966 return 'read'
967
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 def getGitUrl(self, project):
969 return os.path.join(self.upstream_root, str(project))
970
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200971 def real_getGitUrl(self, project):
972 return super(FakeGithubConnection, self).getGitUrl(project)
973
Gregory Haynes4fc12542015-04-22 20:38:06 -0700974 def getProjectBranches(self, project):
975 """Masks getProjectBranches since we don't have a real github"""
976
977 # just returns master for now
978 return ['master']
979
Jan Hrubane252a732017-01-03 15:03:09 +0100980 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700981 pull_request = self.pull_requests[pr_number - 1]
982 pull_request.addComment(message)
983
Jan Hruban3b415922016-02-03 13:10:22 +0100984 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100985 pull_request = self.pull_requests[pr_number - 1]
986 if self.merge_failure:
987 raise Exception('Pull request was not merged')
988 if self.merge_not_allowed_count > 0:
989 self.merge_not_allowed_count -= 1
990 raise MergeFailure('Merge was not successful due to mergeability'
991 ' conflict')
992 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100993 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100994
Jesse Keatingd96e5882017-01-19 13:55:50 -0800995 def getCommitStatuses(self, project, sha):
996 owner, proj = project.split('/')
997 for pr in self.pull_requests:
998 pr_owner, pr_project = pr.project.split('/')
999 if (pr_owner == owner and pr_project == proj and
1000 pr.head_sha == sha):
1001 return pr.statuses[sha]
1002
Jan Hrubane252a732017-01-03 15:03:09 +01001003 def setCommitStatus(self, project, sha, state,
1004 url='', description='', context=''):
1005 owner, proj = project.split('/')
1006 for pr in self.pull_requests:
1007 pr_owner, pr_project = pr.project.split('/')
1008 if (pr_owner == owner and pr_project == proj and
1009 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001010 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001011
Jan Hruban16ad31f2015-11-07 14:39:07 +01001012 def labelPull(self, project, pr_number, label):
1013 pull_request = self.pull_requests[pr_number - 1]
1014 pull_request.addLabel(label)
1015
1016 def unlabelPull(self, project, pr_number, label):
1017 pull_request = self.pull_requests[pr_number - 1]
1018 pull_request.removeLabel(label)
1019
Gregory Haynes4fc12542015-04-22 20:38:06 -07001020
Clark Boylanb640e052014-04-03 16:41:46 -07001021class BuildHistory(object):
1022 def __init__(self, **kw):
1023 self.__dict__.update(kw)
1024
1025 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001026 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1027 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001028
1029
1030class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +02001031 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -07001032 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -07001033 self.url = url
1034
1035 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001036 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -07001037 path = res.path
1038 project = '/'.join(path.split('/')[2:-2])
1039 ret = '001e# service=git-upload-pack\n'
1040 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
1041 'multi_ack thin-pack side-band side-band-64k ofs-delta '
1042 'shallow no-progress include-tag multi_ack_detailed no-done\n')
1043 path = os.path.join(self.upstream_root, project)
1044 repo = git.Repo(path)
1045 for ref in repo.refs:
1046 r = ref.object.hexsha + ' ' + ref.path + '\n'
1047 ret += '%04x%s' % (len(r) + 4, r)
1048 ret += '0000'
1049 return ret
1050
1051
Clark Boylanb640e052014-04-03 16:41:46 -07001052class FakeStatsd(threading.Thread):
1053 def __init__(self):
1054 threading.Thread.__init__(self)
1055 self.daemon = True
1056 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1057 self.sock.bind(('', 0))
1058 self.port = self.sock.getsockname()[1]
1059 self.wake_read, self.wake_write = os.pipe()
1060 self.stats = []
1061
1062 def run(self):
1063 while True:
1064 poll = select.poll()
1065 poll.register(self.sock, select.POLLIN)
1066 poll.register(self.wake_read, select.POLLIN)
1067 ret = poll.poll()
1068 for (fd, event) in ret:
1069 if fd == self.sock.fileno():
1070 data = self.sock.recvfrom(1024)
1071 if not data:
1072 return
1073 self.stats.append(data[0])
1074 if fd == self.wake_read:
1075 return
1076
1077 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001078 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001079
1080
James E. Blaire1767bc2016-08-02 10:00:27 -07001081class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001082 log = logging.getLogger("zuul.test")
1083
Paul Belanger174a8272017-03-14 13:20:10 -04001084 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001085 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001086 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001087 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001088 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001089 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001090 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001091 # TODOv3(jeblair): self.node is really "the image of the node
1092 # assigned". We should rename it (self.node_image?) if we
1093 # keep using it like this, or we may end up exposing more of
1094 # the complexity around multi-node jobs here
1095 # (self.nodes[0].image?)
1096 self.node = None
1097 if len(self.parameters.get('nodes')) == 1:
1098 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001099 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001100 self.pipeline = self.parameters['ZUUL_PIPELINE']
1101 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001102 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001103 self.wait_condition = threading.Condition()
1104 self.waiting = False
1105 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001106 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001107 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001108 self.changes = None
1109 if 'ZUUL_CHANGE_IDS' in self.parameters:
1110 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001111
James E. Blair3158e282016-08-19 09:34:11 -07001112 def __repr__(self):
1113 waiting = ''
1114 if self.waiting:
1115 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001116 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1117 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001118
Clark Boylanb640e052014-04-03 16:41:46 -07001119 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001120 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001121 self.wait_condition.acquire()
1122 self.wait_condition.notify()
1123 self.waiting = False
1124 self.log.debug("Build %s released" % self.unique)
1125 self.wait_condition.release()
1126
1127 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001128 """Return whether this build is being held.
1129
1130 :returns: Whether the build is being held.
1131 :rtype: bool
1132 """
1133
Clark Boylanb640e052014-04-03 16:41:46 -07001134 self.wait_condition.acquire()
1135 if self.waiting:
1136 ret = True
1137 else:
1138 ret = False
1139 self.wait_condition.release()
1140 return ret
1141
1142 def _wait(self):
1143 self.wait_condition.acquire()
1144 self.waiting = True
1145 self.log.debug("Build %s waiting" % self.unique)
1146 self.wait_condition.wait()
1147 self.wait_condition.release()
1148
1149 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001150 self.log.debug('Running build %s' % self.unique)
1151
Paul Belanger174a8272017-03-14 13:20:10 -04001152 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.log.debug('Holding build %s' % self.unique)
1154 self._wait()
1155 self.log.debug("Build %s continuing" % self.unique)
1156
James E. Blair412fba82017-01-26 15:00:50 -08001157 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001158 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001159 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001160 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001161 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001162 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001163 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001164
James E. Blaire1767bc2016-08-02 10:00:27 -07001165 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001166
James E. Blaira5dba232016-08-08 15:53:24 -07001167 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001168 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001169 for change in changes:
1170 if self.hasChanges(change):
1171 return True
1172 return False
1173
James E. Blaire7b99a02016-08-05 14:27:34 -07001174 def hasChanges(self, *changes):
1175 """Return whether this build has certain changes in its git repos.
1176
1177 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001178 are expected to be present (in order) in the git repository of
1179 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001180
1181 :returns: Whether the build has the indicated changes.
1182 :rtype: bool
1183
1184 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001185 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001186 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001187 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001188 try:
1189 repo = git.Repo(path)
1190 except NoSuchPathError as e:
1191 self.log.debug('%s' % e)
1192 return False
1193 ref = self.parameters['ZUUL_REF']
1194 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1195 commit_message = '%s-1' % change.subject
1196 self.log.debug("Checking if build %s has changes; commit_message "
1197 "%s; repo_messages %s" % (self, commit_message,
1198 repo_messages))
1199 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001200 self.log.debug(" messages do not match")
1201 return False
1202 self.log.debug(" OK")
1203 return True
1204
Clark Boylanb640e052014-04-03 16:41:46 -07001205
Paul Belanger174a8272017-03-14 13:20:10 -04001206class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1207 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001208
Paul Belanger174a8272017-03-14 13:20:10 -04001209 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001210 they will report that they have started but then pause until
1211 released before reporting completion. This attribute may be
1212 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001213 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001214 be explicitly released.
1215
1216 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001217 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001218 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001219 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001220 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001221 self.hold_jobs_in_build = False
1222 self.lock = threading.Lock()
1223 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001224 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001225 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001226 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001227
James E. Blaira5dba232016-08-08 15:53:24 -07001228 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001229 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001230
1231 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001232 :arg Change change: The :py:class:`~tests.base.FakeChange`
1233 instance which should cause the job to fail. This job
1234 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001235
1236 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001237 l = self.fail_tests.get(name, [])
1238 l.append(change)
1239 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001240
James E. Blair962220f2016-08-03 11:22:38 -07001241 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001242 """Release a held build.
1243
1244 :arg str regex: A regular expression which, if supplied, will
1245 cause only builds with matching names to be released. If
1246 not supplied, all builds will be released.
1247
1248 """
James E. Blair962220f2016-08-03 11:22:38 -07001249 builds = self.running_builds[:]
1250 self.log.debug("Releasing build %s (%s)" % (regex,
1251 len(self.running_builds)))
1252 for build in builds:
1253 if not regex or re.match(regex, build.name):
1254 self.log.debug("Releasing build %s" %
1255 (build.parameters['ZUUL_UUID']))
1256 build.release()
1257 else:
1258 self.log.debug("Not releasing build %s" %
1259 (build.parameters['ZUUL_UUID']))
1260 self.log.debug("Done releasing builds %s (%s)" %
1261 (regex, len(self.running_builds)))
1262
Paul Belanger174a8272017-03-14 13:20:10 -04001263 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001264 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001265 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001266 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001267 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001268 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001269 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001270 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001271 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1272 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001273
1274 def stopJob(self, job):
1275 self.log.debug("handle stop")
1276 parameters = json.loads(job.arguments)
1277 uuid = parameters['uuid']
1278 for build in self.running_builds:
1279 if build.unique == uuid:
1280 build.aborted = True
1281 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001282 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001283
James E. Blaira002b032017-04-18 10:35:48 -07001284 def stop(self):
1285 for build in self.running_builds:
1286 build.release()
1287 super(RecordingExecutorServer, self).stop()
1288
Joshua Hesketh50c21782016-10-13 21:34:14 +11001289
Paul Belanger174a8272017-03-14 13:20:10 -04001290class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001291 def doMergeChanges(self, items):
1292 # Get a merger in order to update the repos involved in this job.
1293 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1294 if not commit: # merge conflict
1295 self.recordResult('MERGER_FAILURE')
1296 return commit
1297
1298 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001299 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001300 self.executor_server.lock.acquire()
1301 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001302 BuildHistory(name=build.name, result=result, changes=build.changes,
1303 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001304 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001305 pipeline=build.parameters['ZUUL_PIPELINE'])
1306 )
Paul Belanger174a8272017-03-14 13:20:10 -04001307 self.executor_server.running_builds.remove(build)
1308 del self.executor_server.job_builds[self.job.unique]
1309 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001310
1311 def runPlaybooks(self, args):
1312 build = self.executor_server.job_builds[self.job.unique]
1313 build.jobdir = self.jobdir
1314
1315 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1316 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001317 return result
1318
Monty Taylore6562aa2017-02-20 07:37:39 -05001319 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001320 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001321
Paul Belanger174a8272017-03-14 13:20:10 -04001322 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001323 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001324 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001325 else:
1326 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001327 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001328
James E. Blairad8dca02017-02-21 11:48:32 -05001329 def getHostList(self, args):
1330 self.log.debug("hostlist")
1331 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001332 for host in hosts:
1333 host['host_vars']['ansible_connection'] = 'local'
1334
1335 hosts.append(dict(
1336 name='localhost',
1337 host_vars=dict(ansible_connection='local'),
1338 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001339 return hosts
1340
James E. Blairf5dbd002015-12-23 15:26:17 -08001341
Clark Boylanb640e052014-04-03 16:41:46 -07001342class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001343 """A Gearman server for use in tests.
1344
1345 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1346 added to the queue but will not be distributed to workers
1347 until released. This attribute may be changed at any time and
1348 will take effect for subsequently enqueued jobs, but
1349 previously held jobs will still need to be explicitly
1350 released.
1351
1352 """
1353
Clark Boylanb640e052014-04-03 16:41:46 -07001354 def __init__(self):
1355 self.hold_jobs_in_queue = False
1356 super(FakeGearmanServer, self).__init__(0)
1357
1358 def getJobForConnection(self, connection, peek=False):
1359 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1360 for job in queue:
1361 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001362 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001363 job.waiting = self.hold_jobs_in_queue
1364 else:
1365 job.waiting = False
1366 if job.waiting:
1367 continue
1368 if job.name in connection.functions:
1369 if not peek:
1370 queue.remove(job)
1371 connection.related_jobs[job.handle] = job
1372 job.worker_connection = connection
1373 job.running = True
1374 return job
1375 return None
1376
1377 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001378 """Release a held job.
1379
1380 :arg str regex: A regular expression which, if supplied, will
1381 cause only jobs with matching names to be released. If
1382 not supplied, all jobs will be released.
1383 """
Clark Boylanb640e052014-04-03 16:41:46 -07001384 released = False
1385 qlen = (len(self.high_queue) + len(self.normal_queue) +
1386 len(self.low_queue))
1387 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1388 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001389 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001390 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001391 parameters = json.loads(job.arguments)
1392 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001393 self.log.debug("releasing queued job %s" %
1394 job.unique)
1395 job.waiting = False
1396 released = True
1397 else:
1398 self.log.debug("not releasing queued job %s" %
1399 job.unique)
1400 if released:
1401 self.wakeConnections()
1402 qlen = (len(self.high_queue) + len(self.normal_queue) +
1403 len(self.low_queue))
1404 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1405
1406
1407class FakeSMTP(object):
1408 log = logging.getLogger('zuul.FakeSMTP')
1409
1410 def __init__(self, messages, server, port):
1411 self.server = server
1412 self.port = port
1413 self.messages = messages
1414
1415 def sendmail(self, from_email, to_email, msg):
1416 self.log.info("Sending email from %s, to %s, with msg %s" % (
1417 from_email, to_email, msg))
1418
1419 headers = msg.split('\n\n', 1)[0]
1420 body = msg.split('\n\n', 1)[1]
1421
1422 self.messages.append(dict(
1423 from_email=from_email,
1424 to_email=to_email,
1425 msg=msg,
1426 headers=headers,
1427 body=body,
1428 ))
1429
1430 return True
1431
1432 def quit(self):
1433 return True
1434
1435
James E. Blairdce6cea2016-12-20 16:45:32 -08001436class FakeNodepool(object):
1437 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001438 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001439
1440 log = logging.getLogger("zuul.test.FakeNodepool")
1441
1442 def __init__(self, host, port, chroot):
1443 self.client = kazoo.client.KazooClient(
1444 hosts='%s:%s%s' % (host, port, chroot))
1445 self.client.start()
1446 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001447 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001448 self.thread = threading.Thread(target=self.run)
1449 self.thread.daemon = True
1450 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001451 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001452
1453 def stop(self):
1454 self._running = False
1455 self.thread.join()
1456 self.client.stop()
1457 self.client.close()
1458
1459 def run(self):
1460 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001461 try:
1462 self._run()
1463 except Exception:
1464 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001465 time.sleep(0.1)
1466
1467 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001468 if self.paused:
1469 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001470 for req in self.getNodeRequests():
1471 self.fulfillRequest(req)
1472
1473 def getNodeRequests(self):
1474 try:
1475 reqids = self.client.get_children(self.REQUEST_ROOT)
1476 except kazoo.exceptions.NoNodeError:
1477 return []
1478 reqs = []
1479 for oid in sorted(reqids):
1480 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001481 try:
1482 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001483 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001484 data['_oid'] = oid
1485 reqs.append(data)
1486 except kazoo.exceptions.NoNodeError:
1487 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001488 return reqs
1489
James E. Blaire18d4602017-01-05 11:17:28 -08001490 def getNodes(self):
1491 try:
1492 nodeids = self.client.get_children(self.NODE_ROOT)
1493 except kazoo.exceptions.NoNodeError:
1494 return []
1495 nodes = []
1496 for oid in sorted(nodeids):
1497 path = self.NODE_ROOT + '/' + oid
1498 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001499 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001500 data['_oid'] = oid
1501 try:
1502 lockfiles = self.client.get_children(path + '/lock')
1503 except kazoo.exceptions.NoNodeError:
1504 lockfiles = []
1505 if lockfiles:
1506 data['_lock'] = True
1507 else:
1508 data['_lock'] = False
1509 nodes.append(data)
1510 return nodes
1511
James E. Blaira38c28e2017-01-04 10:33:20 -08001512 def makeNode(self, request_id, node_type):
1513 now = time.time()
1514 path = '/nodepool/nodes/'
1515 data = dict(type=node_type,
1516 provider='test-provider',
1517 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001518 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001519 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001520 public_ipv4='127.0.0.1',
1521 private_ipv4=None,
1522 public_ipv6=None,
1523 allocated_to=request_id,
1524 state='ready',
1525 state_time=now,
1526 created_time=now,
1527 updated_time=now,
1528 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001529 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001530 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001531 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001532 path = self.client.create(path, data,
1533 makepath=True,
1534 sequence=True)
1535 nodeid = path.split("/")[-1]
1536 return nodeid
1537
James E. Blair6ab79e02017-01-06 10:10:17 -08001538 def addFailRequest(self, request):
1539 self.fail_requests.add(request['_oid'])
1540
James E. Blairdce6cea2016-12-20 16:45:32 -08001541 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001542 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001543 return
1544 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001545 oid = request['_oid']
1546 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001547
James E. Blair6ab79e02017-01-06 10:10:17 -08001548 if oid in self.fail_requests:
1549 request['state'] = 'failed'
1550 else:
1551 request['state'] = 'fulfilled'
1552 nodes = []
1553 for node in request['node_types']:
1554 nodeid = self.makeNode(oid, node)
1555 nodes.append(nodeid)
1556 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001557
James E. Blaira38c28e2017-01-04 10:33:20 -08001558 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001559 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001560 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001561 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001562 try:
1563 self.client.set(path, data)
1564 except kazoo.exceptions.NoNodeError:
1565 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001566
1567
James E. Blair498059b2016-12-20 13:50:13 -08001568class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001569 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001570 super(ChrootedKazooFixture, self).__init__()
1571
1572 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1573 if ':' in zk_host:
1574 host, port = zk_host.split(':')
1575 else:
1576 host = zk_host
1577 port = None
1578
1579 self.zookeeper_host = host
1580
1581 if not port:
1582 self.zookeeper_port = 2181
1583 else:
1584 self.zookeeper_port = int(port)
1585
Clark Boylan621ec9a2017-04-07 17:41:33 -07001586 self.test_id = test_id
1587
James E. Blair498059b2016-12-20 13:50:13 -08001588 def _setUp(self):
1589 # Make sure the test chroot paths do not conflict
1590 random_bits = ''.join(random.choice(string.ascii_lowercase +
1591 string.ascii_uppercase)
1592 for x in range(8))
1593
Clark Boylan621ec9a2017-04-07 17:41:33 -07001594 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001595 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1596
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001597 self.addCleanup(self._cleanup)
1598
James E. Blair498059b2016-12-20 13:50:13 -08001599 # Ensure the chroot path exists and clean up any pre-existing znodes.
1600 _tmp_client = kazoo.client.KazooClient(
1601 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1602 _tmp_client.start()
1603
1604 if _tmp_client.exists(self.zookeeper_chroot):
1605 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1606
1607 _tmp_client.ensure_path(self.zookeeper_chroot)
1608 _tmp_client.stop()
1609 _tmp_client.close()
1610
James E. Blair498059b2016-12-20 13:50:13 -08001611 def _cleanup(self):
1612 '''Remove the chroot path.'''
1613 # Need a non-chroot'ed client to remove the chroot path
1614 _tmp_client = kazoo.client.KazooClient(
1615 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1616 _tmp_client.start()
1617 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1618 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001619 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001620
1621
Joshua Heskethd78b4482015-09-14 16:56:34 -06001622class MySQLSchemaFixture(fixtures.Fixture):
1623 def setUp(self):
1624 super(MySQLSchemaFixture, self).setUp()
1625
1626 random_bits = ''.join(random.choice(string.ascii_lowercase +
1627 string.ascii_uppercase)
1628 for x in range(8))
1629 self.name = '%s_%s' % (random_bits, os.getpid())
1630 self.passwd = uuid.uuid4().hex
1631 db = pymysql.connect(host="localhost",
1632 user="openstack_citest",
1633 passwd="openstack_citest",
1634 db="openstack_citest")
1635 cur = db.cursor()
1636 cur.execute("create database %s" % self.name)
1637 cur.execute(
1638 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1639 (self.name, self.name, self.passwd))
1640 cur.execute("flush privileges")
1641
1642 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1643 self.passwd,
1644 self.name)
1645 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1646 self.addCleanup(self.cleanup)
1647
1648 def cleanup(self):
1649 db = pymysql.connect(host="localhost",
1650 user="openstack_citest",
1651 passwd="openstack_citest",
1652 db="openstack_citest")
1653 cur = db.cursor()
1654 cur.execute("drop database %s" % self.name)
1655 cur.execute("drop user '%s'@'localhost'" % self.name)
1656 cur.execute("flush privileges")
1657
1658
Maru Newby3fe5f852015-01-13 04:22:14 +00001659class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001660 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001661 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001662
James E. Blair1c236df2017-02-01 14:07:24 -08001663 def attachLogs(self, *args):
1664 def reader():
1665 self._log_stream.seek(0)
1666 while True:
1667 x = self._log_stream.read(4096)
1668 if not x:
1669 break
1670 yield x.encode('utf8')
1671 content = testtools.content.content_from_reader(
1672 reader,
1673 testtools.content_type.UTF8_TEXT,
1674 False)
1675 self.addDetail('logging', content)
1676
Clark Boylanb640e052014-04-03 16:41:46 -07001677 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001678 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001679 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1680 try:
1681 test_timeout = int(test_timeout)
1682 except ValueError:
1683 # If timeout value is invalid do not set a timeout.
1684 test_timeout = 0
1685 if test_timeout > 0:
1686 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1687
1688 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1689 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1690 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1691 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1692 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1693 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1694 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1695 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1696 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1697 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001698 self._log_stream = StringIO()
1699 self.addOnException(self.attachLogs)
1700 else:
1701 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001702
James E. Blair1c236df2017-02-01 14:07:24 -08001703 handler = logging.StreamHandler(self._log_stream)
1704 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1705 '%(levelname)-8s %(message)s')
1706 handler.setFormatter(formatter)
1707
1708 logger = logging.getLogger()
1709 logger.setLevel(logging.DEBUG)
1710 logger.addHandler(handler)
1711
Clark Boylan3410d532017-04-25 12:35:29 -07001712 # Make sure we don't carry old handlers around in process state
1713 # which slows down test runs
1714 self.addCleanup(logger.removeHandler, handler)
1715 self.addCleanup(handler.close)
1716 self.addCleanup(handler.flush)
1717
James E. Blair1c236df2017-02-01 14:07:24 -08001718 # NOTE(notmorgan): Extract logging overrides for specific
1719 # libraries from the OS_LOG_DEFAULTS env and create loggers
1720 # for each. This is used to limit the output during test runs
1721 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001722 log_defaults_from_env = os.environ.get(
1723 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001724 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001725
James E. Blairdce6cea2016-12-20 16:45:32 -08001726 if log_defaults_from_env:
1727 for default in log_defaults_from_env.split(','):
1728 try:
1729 name, level_str = default.split('=', 1)
1730 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001731 logger = logging.getLogger(name)
1732 logger.setLevel(level)
1733 logger.addHandler(handler)
1734 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001735 except ValueError:
1736 # NOTE(notmorgan): Invalid format of the log default,
1737 # skip and don't try and apply a logger for the
1738 # specified module
1739 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001740
Maru Newby3fe5f852015-01-13 04:22:14 +00001741
1742class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001743 """A test case with a functioning Zuul.
1744
1745 The following class variables are used during test setup and can
1746 be overidden by subclasses but are effectively read-only once a
1747 test method starts running:
1748
1749 :cvar str config_file: This points to the main zuul config file
1750 within the fixtures directory. Subclasses may override this
1751 to obtain a different behavior.
1752
1753 :cvar str tenant_config_file: This is the tenant config file
1754 (which specifies from what git repos the configuration should
1755 be loaded). It defaults to the value specified in
1756 `config_file` but can be overidden by subclasses to obtain a
1757 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001758 configuration. See also the :py:func:`simple_layout`
1759 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001760
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001761 :cvar bool create_project_keys: Indicates whether Zuul should
1762 auto-generate keys for each project, or whether the test
1763 infrastructure should insert dummy keys to save time during
1764 startup. Defaults to False.
1765
James E. Blaire7b99a02016-08-05 14:27:34 -07001766 The following are instance variables that are useful within test
1767 methods:
1768
1769 :ivar FakeGerritConnection fake_<connection>:
1770 A :py:class:`~tests.base.FakeGerritConnection` will be
1771 instantiated for each connection present in the config file
1772 and stored here. For instance, `fake_gerrit` will hold the
1773 FakeGerritConnection object for a connection named `gerrit`.
1774
1775 :ivar FakeGearmanServer gearman_server: An instance of
1776 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1777 server that all of the Zuul components in this test use to
1778 communicate with each other.
1779
Paul Belanger174a8272017-03-14 13:20:10 -04001780 :ivar RecordingExecutorServer executor_server: An instance of
1781 :py:class:`~tests.base.RecordingExecutorServer` which is the
1782 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001783
1784 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1785 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001786 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001787 list upon completion.
1788
1789 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1790 objects representing completed builds. They are appended to
1791 the list in the order they complete.
1792
1793 """
1794
James E. Blair83005782015-12-11 14:46:03 -08001795 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001796 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001797 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001798
1799 def _startMerger(self):
1800 self.merge_server = zuul.merger.server.MergeServer(self.config,
1801 self.connections)
1802 self.merge_server.start()
1803
Maru Newby3fe5f852015-01-13 04:22:14 +00001804 def setUp(self):
1805 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001806
1807 self.setupZK()
1808
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001809 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001810 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001811 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1812 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001813 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001814 tmp_root = tempfile.mkdtemp(
1815 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001816 self.test_root = os.path.join(tmp_root, "zuul-test")
1817 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001818 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001819 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001820 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001821
1822 if os.path.exists(self.test_root):
1823 shutil.rmtree(self.test_root)
1824 os.makedirs(self.test_root)
1825 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001826 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001827
1828 # Make per test copy of Configuration.
1829 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001830 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001831 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001832 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001833 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001834 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001835 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001836
Clark Boylanb640e052014-04-03 16:41:46 -07001837 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001838 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1839 # see: https://github.com/jsocol/pystatsd/issues/61
1840 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001841 os.environ['STATSD_PORT'] = str(self.statsd.port)
1842 self.statsd.start()
1843 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001844 reload_module(statsd)
1845 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001846
1847 self.gearman_server = FakeGearmanServer()
1848
1849 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001850 self.log.info("Gearman server on port %s" %
1851 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001852
James E. Blaire511d2f2016-12-08 15:22:26 -08001853 gerritsource.GerritSource.replication_timeout = 1.5
1854 gerritsource.GerritSource.replication_retry_interval = 0.5
1855 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001856
Joshua Hesketh352264b2015-08-11 23:42:08 +10001857 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001858
Jan Hruban7083edd2015-08-21 14:00:54 +02001859 self.webapp = zuul.webapp.WebApp(
1860 self.sched, port=0, listen_address='127.0.0.1')
1861
Jan Hruban6b71aff2015-10-22 16:58:08 +02001862 self.event_queues = [
1863 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001864 self.sched.trigger_event_queue,
1865 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001866 ]
1867
James E. Blairfef78942016-03-11 16:28:56 -08001868 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001869 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001870
Clark Boylanb640e052014-04-03 16:41:46 -07001871 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001872 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001873 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001874 return FakeURLOpener(self.upstream_root, *args, **kw)
1875
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001876 old_urlopen = urllib.request.urlopen
1877 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001878
James E. Blair3f876d52016-07-22 13:07:14 -07001879 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001880
Paul Belanger174a8272017-03-14 13:20:10 -04001881 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001882 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001883 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001884 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001885 _test_root=self.test_root,
1886 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001887 self.executor_server.start()
1888 self.history = self.executor_server.build_history
1889 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001890
Paul Belanger174a8272017-03-14 13:20:10 -04001891 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001892 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001893 self.merge_client = zuul.merger.client.MergeClient(
1894 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001895 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001896 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001897 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001898
James E. Blair0d5a36e2017-02-21 10:53:44 -05001899 self.fake_nodepool = FakeNodepool(
1900 self.zk_chroot_fixture.zookeeper_host,
1901 self.zk_chroot_fixture.zookeeper_port,
1902 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001903
Paul Belanger174a8272017-03-14 13:20:10 -04001904 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001905 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001906 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001907 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001908
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001909 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001910
1911 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001912 self.webapp.start()
1913 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001914 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001915 # Cleanups are run in reverse order
1916 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001917 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001918 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001919
James E. Blairb9c0d772017-03-03 14:34:49 -08001920 self.sched.reconfigure(self.config)
1921 self.sched.resume()
1922
James E. Blairfef78942016-03-11 16:28:56 -08001923 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001924 # Set up gerrit related fakes
1925 # Set a changes database so multiple FakeGerrit's can report back to
1926 # a virtual canonical database given by the configured hostname
1927 self.gerrit_changes_dbs = {}
1928
1929 def getGerritConnection(driver, name, config):
1930 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1931 con = FakeGerritConnection(driver, name, config,
1932 changes_db=db,
1933 upstream_root=self.upstream_root)
1934 self.event_queues.append(con.event_queue)
1935 setattr(self, 'fake_' + name, con)
1936 return con
1937
1938 self.useFixture(fixtures.MonkeyPatch(
1939 'zuul.driver.gerrit.GerritDriver.getConnection',
1940 getGerritConnection))
1941
Gregory Haynes4fc12542015-04-22 20:38:06 -07001942 def getGithubConnection(driver, name, config):
1943 con = FakeGithubConnection(driver, name, config,
1944 upstream_root=self.upstream_root)
1945 setattr(self, 'fake_' + name, con)
1946 return con
1947
1948 self.useFixture(fixtures.MonkeyPatch(
1949 'zuul.driver.github.GithubDriver.getConnection',
1950 getGithubConnection))
1951
James E. Blaire511d2f2016-12-08 15:22:26 -08001952 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001953 # TODO(jhesketh): This should come from lib.connections for better
1954 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001955 # Register connections from the config
1956 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001957
Joshua Hesketh352264b2015-08-11 23:42:08 +10001958 def FakeSMTPFactory(*args, **kw):
1959 args = [self.smtp_messages] + list(args)
1960 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001961
Joshua Hesketh352264b2015-08-11 23:42:08 +10001962 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001963
James E. Blaire511d2f2016-12-08 15:22:26 -08001964 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001965 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001966 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001967
James E. Blair83005782015-12-11 14:46:03 -08001968 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001969 # This creates the per-test configuration object. It can be
1970 # overriden by subclasses, but should not need to be since it
1971 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001972 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001973 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001974
1975 if not self.setupSimpleLayout():
1976 if hasattr(self, 'tenant_config_file'):
1977 self.config.set('zuul', 'tenant_config',
1978 self.tenant_config_file)
1979 git_path = os.path.join(
1980 os.path.dirname(
1981 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1982 'git')
1983 if os.path.exists(git_path):
1984 for reponame in os.listdir(git_path):
1985 project = reponame.replace('_', '/')
1986 self.copyDirToRepo(project,
1987 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001988 self.setupAllProjectKeys()
1989
James E. Blair06cc3922017-04-19 10:08:10 -07001990 def setupSimpleLayout(self):
1991 # If the test method has been decorated with a simple_layout,
1992 # use that instead of the class tenant_config_file. Set up a
1993 # single config-project with the specified layout, and
1994 # initialize repos for all of the 'project' entries which
1995 # appear in the layout.
1996 test_name = self.id().split('.')[-1]
1997 test = getattr(self, test_name)
1998 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001999 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002000 else:
2001 return False
2002
James E. Blairb70e55a2017-04-19 12:57:02 -07002003 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002004 path = os.path.join(FIXTURE_DIR, path)
2005 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002006 data = f.read()
2007 layout = yaml.safe_load(data)
2008 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002009 untrusted_projects = []
2010 for item in layout:
2011 if 'project' in item:
2012 name = item['project']['name']
2013 untrusted_projects.append(name)
2014 self.init_repo(name)
2015 self.addCommitToRepo(name, 'initial commit',
2016 files={'README': ''},
2017 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002018 if 'job' in item:
2019 jobname = item['job']['name']
2020 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002021
2022 root = os.path.join(self.test_root, "config")
2023 if not os.path.exists(root):
2024 os.makedirs(root)
2025 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2026 config = [{'tenant':
2027 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002028 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002029 {'config-projects': ['common-config'],
2030 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002031 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002032 f.close()
2033 self.config.set('zuul', 'tenant_config',
2034 os.path.join(FIXTURE_DIR, f.name))
2035
2036 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002037 self.addCommitToRepo('common-config', 'add content from fixture',
2038 files, branch='master', tag='init')
2039
2040 return True
2041
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002042 def setupAllProjectKeys(self):
2043 if self.create_project_keys:
2044 return
2045
2046 path = self.config.get('zuul', 'tenant_config')
2047 with open(os.path.join(FIXTURE_DIR, path)) as f:
2048 tenant_config = yaml.safe_load(f.read())
2049 for tenant in tenant_config:
2050 sources = tenant['tenant']['source']
2051 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002052 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002053 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002054 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002055 self.setupProjectKeys(source, project)
2056
2057 def setupProjectKeys(self, source, project):
2058 # Make sure we set up an RSA key for the project so that we
2059 # don't spend time generating one:
2060
2061 key_root = os.path.join(self.state_root, 'keys')
2062 if not os.path.isdir(key_root):
2063 os.mkdir(key_root, 0o700)
2064 private_key_file = os.path.join(key_root, source, project + '.pem')
2065 private_key_dir = os.path.dirname(private_key_file)
2066 self.log.debug("Installing test keys for project %s at %s" % (
2067 project, private_key_file))
2068 if not os.path.isdir(private_key_dir):
2069 os.makedirs(private_key_dir)
2070 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2071 with open(private_key_file, 'w') as o:
2072 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002073
James E. Blair498059b2016-12-20 13:50:13 -08002074 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002075 self.zk_chroot_fixture = self.useFixture(
2076 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002077 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002078 self.zk_chroot_fixture.zookeeper_host,
2079 self.zk_chroot_fixture.zookeeper_port,
2080 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002081
James E. Blair96c6bf82016-01-15 16:20:40 -08002082 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002083 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002084
2085 files = {}
2086 for (dirpath, dirnames, filenames) in os.walk(source_path):
2087 for filename in filenames:
2088 test_tree_filepath = os.path.join(dirpath, filename)
2089 common_path = os.path.commonprefix([test_tree_filepath,
2090 source_path])
2091 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2092 with open(test_tree_filepath, 'r') as f:
2093 content = f.read()
2094 files[relative_filepath] = content
2095 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002096 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002097
James E. Blaire18d4602017-01-05 11:17:28 -08002098 def assertNodepoolState(self):
2099 # Make sure that there are no pending requests
2100
2101 requests = self.fake_nodepool.getNodeRequests()
2102 self.assertEqual(len(requests), 0)
2103
2104 nodes = self.fake_nodepool.getNodes()
2105 for node in nodes:
2106 self.assertFalse(node['_lock'], "Node %s is locked" %
2107 (node['_oid'],))
2108
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002109 def assertNoGeneratedKeys(self):
2110 # Make sure that Zuul did not generate any project keys
2111 # (unless it was supposed to).
2112
2113 if self.create_project_keys:
2114 return
2115
2116 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2117 test_key = i.read()
2118
2119 key_root = os.path.join(self.state_root, 'keys')
2120 for root, dirname, files in os.walk(key_root):
2121 for fn in files:
2122 with open(os.path.join(root, fn)) as f:
2123 self.assertEqual(test_key, f.read())
2124
Clark Boylanb640e052014-04-03 16:41:46 -07002125 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002126 self.log.debug("Assert final state")
2127 # Make sure no jobs are running
2128 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002129 # Make sure that git.Repo objects have been garbage collected.
2130 repos = []
2131 gc.collect()
2132 for obj in gc.get_objects():
2133 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08002134 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07002135 repos.append(obj)
2136 self.assertEqual(len(repos), 0)
2137 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002138 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002139 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002140 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002141 for tenant in self.sched.abide.tenants.values():
2142 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002143 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002144 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002145
2146 def shutdown(self):
2147 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002148 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07002149 self.merge_server.stop()
2150 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002151 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002152 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002153 self.sched.stop()
2154 self.sched.join()
2155 self.statsd.stop()
2156 self.statsd.join()
2157 self.webapp.stop()
2158 self.webapp.join()
2159 self.rpc.stop()
2160 self.rpc.join()
2161 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002162 self.fake_nodepool.stop()
2163 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002164 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002165 # we whitelist watchdog threads as they have relatively long delays
2166 # before noticing they should exit, but they should exit on their own.
2167 threads = [t for t in threading.enumerate()
2168 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002169 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002170 log_str = ""
2171 for thread_id, stack_frame in sys._current_frames().items():
2172 log_str += "Thread: %s\n" % thread_id
2173 log_str += "".join(traceback.format_stack(stack_frame))
2174 self.log.debug(log_str)
2175 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002176
James E. Blaira002b032017-04-18 10:35:48 -07002177 def assertCleanShutdown(self):
2178 pass
2179
James E. Blairc4ba97a2017-04-19 16:26:24 -07002180 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002181 parts = project.split('/')
2182 path = os.path.join(self.upstream_root, *parts[:-1])
2183 if not os.path.exists(path):
2184 os.makedirs(path)
2185 path = os.path.join(self.upstream_root, project)
2186 repo = git.Repo.init(path)
2187
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002188 with repo.config_writer() as config_writer:
2189 config_writer.set_value('user', 'email', 'user@example.com')
2190 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002191
Clark Boylanb640e052014-04-03 16:41:46 -07002192 repo.index.commit('initial commit')
2193 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002194 if tag:
2195 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002196
James E. Blair97d902e2014-08-21 13:25:56 -07002197 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002198 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002199 repo.git.clean('-x', '-f', '-d')
2200
James E. Blair97d902e2014-08-21 13:25:56 -07002201 def create_branch(self, project, branch):
2202 path = os.path.join(self.upstream_root, project)
2203 repo = git.Repo.init(path)
2204 fn = os.path.join(path, 'README')
2205
2206 branch_head = repo.create_head(branch)
2207 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002208 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002209 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002210 f.close()
2211 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002212 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002213
James E. Blair97d902e2014-08-21 13:25:56 -07002214 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002215 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002216 repo.git.clean('-x', '-f', '-d')
2217
Sachi King9f16d522016-03-16 12:20:45 +11002218 def create_commit(self, project):
2219 path = os.path.join(self.upstream_root, project)
2220 repo = git.Repo(path)
2221 repo.head.reference = repo.heads['master']
2222 file_name = os.path.join(path, 'README')
2223 with open(file_name, 'a') as f:
2224 f.write('creating fake commit\n')
2225 repo.index.add([file_name])
2226 commit = repo.index.commit('Creating a fake commit')
2227 return commit.hexsha
2228
James E. Blairf4a5f022017-04-18 14:01:10 -07002229 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002230 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002231 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002232 while len(self.builds):
2233 self.release(self.builds[0])
2234 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002235 i += 1
2236 if count is not None and i >= count:
2237 break
James E. Blairb8c16472015-05-05 14:55:26 -07002238
Clark Boylanb640e052014-04-03 16:41:46 -07002239 def release(self, job):
2240 if isinstance(job, FakeBuild):
2241 job.release()
2242 else:
2243 job.waiting = False
2244 self.log.debug("Queued job %s released" % job.unique)
2245 self.gearman_server.wakeConnections()
2246
2247 def getParameter(self, job, name):
2248 if isinstance(job, FakeBuild):
2249 return job.parameters[name]
2250 else:
2251 parameters = json.loads(job.arguments)
2252 return parameters[name]
2253
Clark Boylanb640e052014-04-03 16:41:46 -07002254 def haveAllBuildsReported(self):
2255 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002256 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002257 return False
2258 # Find out if every build that the worker has completed has been
2259 # reported back to Zuul. If it hasn't then that means a Gearman
2260 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002261 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002262 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002263 if not zbuild:
2264 # It has already been reported
2265 continue
2266 # It hasn't been reported yet.
2267 return False
2268 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002269 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002270 if connection.state == 'GRAB_WAIT':
2271 return False
2272 return True
2273
2274 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002275 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002276 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002277 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002278 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002279 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002280 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002281 for j in conn.related_jobs.values():
2282 if j.unique == build.uuid:
2283 client_job = j
2284 break
2285 if not client_job:
2286 self.log.debug("%s is not known to the gearman client" %
2287 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002288 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002289 if not client_job.handle:
2290 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002291 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002292 server_job = self.gearman_server.jobs.get(client_job.handle)
2293 if not server_job:
2294 self.log.debug("%s is not known to the gearman server" %
2295 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002296 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002297 if not hasattr(server_job, 'waiting'):
2298 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002299 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002300 if server_job.waiting:
2301 continue
James E. Blair17302972016-08-10 16:11:42 -07002302 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002303 self.log.debug("%s has not reported start" % build)
2304 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002305 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002306 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002307 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002308 if worker_build:
2309 if worker_build.isWaiting():
2310 continue
2311 else:
2312 self.log.debug("%s is running" % worker_build)
2313 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002314 else:
James E. Blair962220f2016-08-03 11:22:38 -07002315 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002316 return False
James E. Blaira002b032017-04-18 10:35:48 -07002317 for (build_uuid, job_worker) in \
2318 self.executor_server.job_workers.items():
2319 if build_uuid not in seen_builds:
2320 self.log.debug("%s is not finalized" % build_uuid)
2321 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002322 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002323
James E. Blairdce6cea2016-12-20 16:45:32 -08002324 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002325 if self.fake_nodepool.paused:
2326 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002327 if self.sched.nodepool.requests:
2328 return False
2329 return True
2330
Jan Hruban6b71aff2015-10-22 16:58:08 +02002331 def eventQueuesEmpty(self):
2332 for queue in self.event_queues:
2333 yield queue.empty()
2334
2335 def eventQueuesJoin(self):
2336 for queue in self.event_queues:
2337 queue.join()
2338
Clark Boylanb640e052014-04-03 16:41:46 -07002339 def waitUntilSettled(self):
2340 self.log.debug("Waiting until settled...")
2341 start = time.time()
2342 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002343 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002344 self.log.error("Timeout waiting for Zuul to settle")
2345 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002346 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002347 self.log.error(" %s: %s" % (queue, queue.empty()))
2348 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002349 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002350 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002351 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002352 self.log.error("All requests completed: %s" %
2353 (self.areAllNodeRequestsComplete(),))
2354 self.log.error("Merge client jobs: %s" %
2355 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002356 raise Exception("Timeout waiting for Zuul to settle")
2357 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002358
Paul Belanger174a8272017-03-14 13:20:10 -04002359 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002360 # have all build states propogated to zuul?
2361 if self.haveAllBuildsReported():
2362 # Join ensures that the queue is empty _and_ events have been
2363 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002364 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002365 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002366 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002367 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002368 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002369 self.areAllNodeRequestsComplete() and
2370 all(self.eventQueuesEmpty())):
2371 # The queue empty check is placed at the end to
2372 # ensure that if a component adds an event between
2373 # when locked the run handler and checked that the
2374 # components were stable, we don't erroneously
2375 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002376 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002377 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002378 self.log.debug("...settled.")
2379 return
2380 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002381 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002382 self.sched.wake_event.wait(0.1)
2383
2384 def countJobResults(self, jobs, result):
2385 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002386 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002387
James E. Blair96c6bf82016-01-15 16:20:40 -08002388 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002389 for job in self.history:
2390 if (job.name == name and
2391 (project is None or
2392 job.parameters['ZUUL_PROJECT'] == project)):
2393 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002394 raise Exception("Unable to find job %s in history" % name)
2395
2396 def assertEmptyQueues(self):
2397 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002398 for tenant in self.sched.abide.tenants.values():
2399 for pipeline in tenant.layout.pipelines.values():
2400 for queue in pipeline.queues:
2401 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002402 print('pipeline %s queue %s contents %s' % (
2403 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002404 self.assertEqual(len(queue.queue), 0,
2405 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002406
2407 def assertReportedStat(self, key, value=None, kind=None):
2408 start = time.time()
2409 while time.time() < (start + 5):
2410 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002411 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002412 if key == k:
2413 if value is None and kind is None:
2414 return
2415 elif value:
2416 if value == v:
2417 return
2418 elif kind:
2419 if v.endswith('|' + kind):
2420 return
2421 time.sleep(0.1)
2422
Clark Boylanb640e052014-04-03 16:41:46 -07002423 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002424
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002425 def assertBuilds(self, builds):
2426 """Assert that the running builds are as described.
2427
2428 The list of running builds is examined and must match exactly
2429 the list of builds described by the input.
2430
2431 :arg list builds: A list of dictionaries. Each item in the
2432 list must match the corresponding build in the build
2433 history, and each element of the dictionary must match the
2434 corresponding attribute of the build.
2435
2436 """
James E. Blair3158e282016-08-19 09:34:11 -07002437 try:
2438 self.assertEqual(len(self.builds), len(builds))
2439 for i, d in enumerate(builds):
2440 for k, v in d.items():
2441 self.assertEqual(
2442 getattr(self.builds[i], k), v,
2443 "Element %i in builds does not match" % (i,))
2444 except Exception:
2445 for build in self.builds:
2446 self.log.error("Running build: %s" % build)
2447 else:
2448 self.log.error("No running builds")
2449 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002450
James E. Blairb536ecc2016-08-31 10:11:42 -07002451 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002452 """Assert that the completed builds are as described.
2453
2454 The list of completed builds is examined and must match
2455 exactly the list of builds described by the input.
2456
2457 :arg list history: A list of dictionaries. Each item in the
2458 list must match the corresponding build in the build
2459 history, and each element of the dictionary must match the
2460 corresponding attribute of the build.
2461
James E. Blairb536ecc2016-08-31 10:11:42 -07002462 :arg bool ordered: If true, the history must match the order
2463 supplied, if false, the builds are permitted to have
2464 arrived in any order.
2465
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002466 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002467 def matches(history_item, item):
2468 for k, v in item.items():
2469 if getattr(history_item, k) != v:
2470 return False
2471 return True
James E. Blair3158e282016-08-19 09:34:11 -07002472 try:
2473 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002474 if ordered:
2475 for i, d in enumerate(history):
2476 if not matches(self.history[i], d):
2477 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002478 "Element %i in history does not match %s" %
2479 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002480 else:
2481 unseen = self.history[:]
2482 for i, d in enumerate(history):
2483 found = False
2484 for unseen_item in unseen:
2485 if matches(unseen_item, d):
2486 found = True
2487 unseen.remove(unseen_item)
2488 break
2489 if not found:
2490 raise Exception("No match found for element %i "
2491 "in history" % (i,))
2492 if unseen:
2493 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002494 except Exception:
2495 for build in self.history:
2496 self.log.error("Completed build: %s" % build)
2497 else:
2498 self.log.error("No completed builds")
2499 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002500
James E. Blair6ac368c2016-12-22 18:07:20 -08002501 def printHistory(self):
2502 """Log the build history.
2503
2504 This can be useful during tests to summarize what jobs have
2505 completed.
2506
2507 """
2508 self.log.debug("Build history:")
2509 for build in self.history:
2510 self.log.debug(build)
2511
James E. Blair59fdbac2015-12-07 17:08:06 -08002512 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002513 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2514
James E. Blair9ea70072017-04-19 16:05:30 -07002515 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002516 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002517 if not os.path.exists(root):
2518 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002519 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2520 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002521- tenant:
2522 name: openstack
2523 source:
2524 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002525 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002526 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002527 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002528 - org/project
2529 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002530 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002531 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002532 self.config.set('zuul', 'tenant_config',
2533 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002534 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002535
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002536 def addCommitToRepo(self, project, message, files,
2537 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002538 path = os.path.join(self.upstream_root, project)
2539 repo = git.Repo(path)
2540 repo.head.reference = branch
2541 zuul.merger.merger.reset_repo_to_head(repo)
2542 for fn, content in files.items():
2543 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002544 try:
2545 os.makedirs(os.path.dirname(fn))
2546 except OSError:
2547 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002548 with open(fn, 'w') as f:
2549 f.write(content)
2550 repo.index.add([fn])
2551 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002552 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002553 repo.heads[branch].commit = commit
2554 repo.head.reference = branch
2555 repo.git.clean('-x', '-f', '-d')
2556 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002557 if tag:
2558 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002559 return before
2560
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002561 def commitConfigUpdate(self, project_name, source_name):
2562 """Commit an update to zuul.yaml
2563
2564 This overwrites the zuul.yaml in the specificed project with
2565 the contents specified.
2566
2567 :arg str project_name: The name of the project containing
2568 zuul.yaml (e.g., common-config)
2569
2570 :arg str source_name: The path to the file (underneath the
2571 test fixture directory) whose contents should be used to
2572 replace zuul.yaml.
2573 """
2574
2575 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002576 files = {}
2577 with open(source_path, 'r') as f:
2578 data = f.read()
2579 layout = yaml.safe_load(data)
2580 files['zuul.yaml'] = data
2581 for item in layout:
2582 if 'job' in item:
2583 jobname = item['job']['name']
2584 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002585 before = self.addCommitToRepo(
2586 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002587 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002588 return before
2589
James E. Blair7fc8daa2016-08-08 15:37:15 -07002590 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002591
James E. Blair7fc8daa2016-08-08 15:37:15 -07002592 """Inject a Fake (Gerrit) event.
2593
2594 This method accepts a JSON-encoded event and simulates Zuul
2595 having received it from Gerrit. It could (and should)
2596 eventually apply to any connection type, but is currently only
2597 used with Gerrit connections. The name of the connection is
2598 used to look up the corresponding server, and the event is
2599 simulated as having been received by all Zuul connections
2600 attached to that server. So if two Gerrit connections in Zuul
2601 are connected to the same Gerrit server, and you invoke this
2602 method specifying the name of one of them, the event will be
2603 received by both.
2604
2605 .. note::
2606
2607 "self.fake_gerrit.addEvent" calls should be migrated to
2608 this method.
2609
2610 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002611 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002612 :arg str event: The JSON-encoded event.
2613
2614 """
2615 specified_conn = self.connections.connections[connection]
2616 for conn in self.connections.connections.values():
2617 if (isinstance(conn, specified_conn.__class__) and
2618 specified_conn.server == conn.server):
2619 conn.addEvent(event)
2620
James E. Blair3f876d52016-07-22 13:07:14 -07002621
2622class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002623 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002624 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002625
Joshua Heskethd78b4482015-09-14 16:56:34 -06002626
2627class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002628 def setup_config(self):
2629 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002630 for section_name in self.config.sections():
2631 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2632 section_name, re.I)
2633 if not con_match:
2634 continue
2635
2636 if self.config.get(section_name, 'driver') == 'sql':
2637 f = MySQLSchemaFixture()
2638 self.useFixture(f)
2639 if (self.config.get(section_name, 'dburi') ==
2640 '$MYSQL_FIXTURE_DBURI$'):
2641 self.config.set(section_name, 'dburi', f.dburi)