blob: b78495deb54d99532e1e0fc28d20a887540d866a [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
Jesse Keating71a47ff2017-06-06 11:36:43 -0700884 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
885 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700886 if not old_rev:
887 old_rev = '00000000000000000000000000000000'
888 if not new_rev:
889 new_rev = random_sha1()
890 name = 'push'
891 data = {
892 'ref': ref,
893 'before': old_rev,
894 'after': new_rev,
895 'repository': {
896 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700897 },
898 'commits': [
899 {
900 'added': added_files,
901 'removed': removed_files,
902 'modified': modified_files
903 }
904 ]
Wayne1a78c612015-06-11 17:14:13 -0700905 }
906 return (name, data)
907
Gregory Haynes4fc12542015-04-22 20:38:06 -0700908 def emitEvent(self, event):
909 """Emulates sending the GitHub webhook event to the connection."""
910 port = self.webapp.server.socket.getsockname()[1]
911 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700912 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700913 headers = {'X-Github-Event': name}
914 req = urllib.request.Request(
915 'http://localhost:%s/connection/%s/payload'
916 % (port, self.connection_name),
917 data=payload, headers=headers)
918 urllib.request.urlopen(req)
919
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200920 def getPull(self, project, number):
921 pr = self.pull_requests[number - 1]
922 data = {
923 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100924 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200925 'updated_at': pr.updated_at,
926 'base': {
927 'repo': {
928 'full_name': pr.project
929 },
930 'ref': pr.branch,
931 },
Jan Hruban37615e52015-11-19 14:30:49 +0100932 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700933 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200934 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800935 'sha': pr.head_sha,
936 'repo': {
937 'full_name': pr.project
938 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200939 }
940 }
941 return data
942
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800943 def getPullBySha(self, sha):
944 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
945 if len(prs) > 1:
946 raise Exception('Multiple pulls found with head sha: %s' % sha)
947 pr = prs[0]
948 return self.getPull(pr.project, pr.number)
949
Jan Hruban570d01c2016-03-10 21:51:32 +0100950 def getPullFileNames(self, project, number):
951 pr = self.pull_requests[number - 1]
952 return pr.files
953
Jesse Keatingae4cd272017-01-30 17:10:44 -0800954 def _getPullReviews(self, owner, project, number):
955 pr = self.pull_requests[number - 1]
956 return pr.reviews
957
Jan Hruban3b415922016-02-03 13:10:22 +0100958 def getUser(self, login):
959 data = {
960 'username': login,
961 'name': 'Github User',
962 'email': 'github.user@example.com'
963 }
964 return data
965
Jesse Keatingae4cd272017-01-30 17:10:44 -0800966 def getRepoPermission(self, project, login):
967 owner, proj = project.split('/')
968 for pr in self.pull_requests:
969 pr_owner, pr_project = pr.project.split('/')
970 if (pr_owner == owner and proj == pr_project):
971 if login in pr.writers:
972 return 'write'
973 else:
974 return 'read'
975
Gregory Haynes4fc12542015-04-22 20:38:06 -0700976 def getGitUrl(self, project):
977 return os.path.join(self.upstream_root, str(project))
978
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200979 def real_getGitUrl(self, project):
980 return super(FakeGithubConnection, self).getGitUrl(project)
981
Gregory Haynes4fc12542015-04-22 20:38:06 -0700982 def getProjectBranches(self, project):
983 """Masks getProjectBranches since we don't have a real github"""
984
985 # just returns master for now
986 return ['master']
987
Jan Hrubane252a732017-01-03 15:03:09 +0100988 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700989 pull_request = self.pull_requests[pr_number - 1]
990 pull_request.addComment(message)
991
Jan Hruban3b415922016-02-03 13:10:22 +0100992 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100993 pull_request = self.pull_requests[pr_number - 1]
994 if self.merge_failure:
995 raise Exception('Pull request was not merged')
996 if self.merge_not_allowed_count > 0:
997 self.merge_not_allowed_count -= 1
998 raise MergeFailure('Merge was not successful due to mergeability'
999 ' conflict')
1000 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001001 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001002
Jesse Keatingd96e5882017-01-19 13:55:50 -08001003 def getCommitStatuses(self, project, sha):
1004 owner, proj = project.split('/')
1005 for pr in self.pull_requests:
1006 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -07001007 # This is somewhat risky, if the same commit exists in multiple
1008 # PRs, we might grab the wrong one that doesn't have a status
1009 # that is expected to be there. Maybe re-work this so that there
1010 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001011 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001012 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001013 return pr.statuses[sha]
1014
Jan Hrubane252a732017-01-03 15:03:09 +01001015 def setCommitStatus(self, project, sha, state,
1016 url='', description='', context=''):
1017 owner, proj = project.split('/')
1018 for pr in self.pull_requests:
1019 pr_owner, pr_project = pr.project.split('/')
1020 if (pr_owner == owner and pr_project == proj and
1021 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001022 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001023
Jan Hruban16ad31f2015-11-07 14:39:07 +01001024 def labelPull(self, project, pr_number, label):
1025 pull_request = self.pull_requests[pr_number - 1]
1026 pull_request.addLabel(label)
1027
1028 def unlabelPull(self, project, pr_number, label):
1029 pull_request = self.pull_requests[pr_number - 1]
1030 pull_request.removeLabel(label)
1031
Gregory Haynes4fc12542015-04-22 20:38:06 -07001032
Clark Boylanb640e052014-04-03 16:41:46 -07001033class BuildHistory(object):
1034 def __init__(self, **kw):
1035 self.__dict__.update(kw)
1036
1037 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001038 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1039 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001040
1041
1042class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +02001043 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -07001044 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -07001045 self.url = url
1046
1047 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001048 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -07001049 path = res.path
1050 project = '/'.join(path.split('/')[2:-2])
1051 ret = '001e# service=git-upload-pack\n'
1052 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
1053 'multi_ack thin-pack side-band side-band-64k ofs-delta '
1054 'shallow no-progress include-tag multi_ack_detailed no-done\n')
1055 path = os.path.join(self.upstream_root, project)
1056 repo = git.Repo(path)
1057 for ref in repo.refs:
1058 r = ref.object.hexsha + ' ' + ref.path + '\n'
1059 ret += '%04x%s' % (len(r) + 4, r)
1060 ret += '0000'
1061 return ret
1062
1063
Clark Boylanb640e052014-04-03 16:41:46 -07001064class FakeStatsd(threading.Thread):
1065 def __init__(self):
1066 threading.Thread.__init__(self)
1067 self.daemon = True
1068 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1069 self.sock.bind(('', 0))
1070 self.port = self.sock.getsockname()[1]
1071 self.wake_read, self.wake_write = os.pipe()
1072 self.stats = []
1073
1074 def run(self):
1075 while True:
1076 poll = select.poll()
1077 poll.register(self.sock, select.POLLIN)
1078 poll.register(self.wake_read, select.POLLIN)
1079 ret = poll.poll()
1080 for (fd, event) in ret:
1081 if fd == self.sock.fileno():
1082 data = self.sock.recvfrom(1024)
1083 if not data:
1084 return
1085 self.stats.append(data[0])
1086 if fd == self.wake_read:
1087 return
1088
1089 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001090 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001091
1092
James E. Blaire1767bc2016-08-02 10:00:27 -07001093class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001094 log = logging.getLogger("zuul.test")
1095
Paul Belanger174a8272017-03-14 13:20:10 -04001096 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001097 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001098 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001099 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001100 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001101 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001102 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001103 # TODOv3(jeblair): self.node is really "the image of the node
1104 # assigned". We should rename it (self.node_image?) if we
1105 # keep using it like this, or we may end up exposing more of
1106 # the complexity around multi-node jobs here
1107 # (self.nodes[0].image?)
1108 self.node = None
1109 if len(self.parameters.get('nodes')) == 1:
1110 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001111 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001112 self.pipeline = self.parameters['ZUUL_PIPELINE']
1113 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001114 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001115 self.wait_condition = threading.Condition()
1116 self.waiting = False
1117 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001118 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001119 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001120 self.changes = None
1121 if 'ZUUL_CHANGE_IDS' in self.parameters:
1122 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001123
James E. Blair3158e282016-08-19 09:34:11 -07001124 def __repr__(self):
1125 waiting = ''
1126 if self.waiting:
1127 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001128 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1129 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001130
Clark Boylanb640e052014-04-03 16:41:46 -07001131 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001132 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001133 self.wait_condition.acquire()
1134 self.wait_condition.notify()
1135 self.waiting = False
1136 self.log.debug("Build %s released" % self.unique)
1137 self.wait_condition.release()
1138
1139 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001140 """Return whether this build is being held.
1141
1142 :returns: Whether the build is being held.
1143 :rtype: bool
1144 """
1145
Clark Boylanb640e052014-04-03 16:41:46 -07001146 self.wait_condition.acquire()
1147 if self.waiting:
1148 ret = True
1149 else:
1150 ret = False
1151 self.wait_condition.release()
1152 return ret
1153
1154 def _wait(self):
1155 self.wait_condition.acquire()
1156 self.waiting = True
1157 self.log.debug("Build %s waiting" % self.unique)
1158 self.wait_condition.wait()
1159 self.wait_condition.release()
1160
1161 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001162 self.log.debug('Running build %s' % self.unique)
1163
Paul Belanger174a8272017-03-14 13:20:10 -04001164 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001165 self.log.debug('Holding build %s' % self.unique)
1166 self._wait()
1167 self.log.debug("Build %s continuing" % self.unique)
1168
James E. Blair412fba82017-01-26 15:00:50 -08001169 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001170 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001171 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001172 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001173 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001174 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001175 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001176
James E. Blaire1767bc2016-08-02 10:00:27 -07001177 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001178
James E. Blaira5dba232016-08-08 15:53:24 -07001179 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001180 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001181 for change in changes:
1182 if self.hasChanges(change):
1183 return True
1184 return False
1185
James E. Blaire7b99a02016-08-05 14:27:34 -07001186 def hasChanges(self, *changes):
1187 """Return whether this build has certain changes in its git repos.
1188
1189 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001190 are expected to be present (in order) in the git repository of
1191 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001192
1193 :returns: Whether the build has the indicated changes.
1194 :rtype: bool
1195
1196 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001197 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001198 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001199 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001200 try:
1201 repo = git.Repo(path)
1202 except NoSuchPathError as e:
1203 self.log.debug('%s' % e)
1204 return False
1205 ref = self.parameters['ZUUL_REF']
1206 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1207 commit_message = '%s-1' % change.subject
1208 self.log.debug("Checking if build %s has changes; commit_message "
1209 "%s; repo_messages %s" % (self, commit_message,
1210 repo_messages))
1211 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001212 self.log.debug(" messages do not match")
1213 return False
1214 self.log.debug(" OK")
1215 return True
1216
James E. Blaird8af5422017-05-24 13:59:40 -07001217 def getWorkspaceRepos(self, projects):
1218 """Return workspace git repo objects for the listed projects
1219
1220 :arg list projects: A list of strings, each the canonical name
1221 of a project.
1222
1223 :returns: A dictionary of {name: repo} for every listed
1224 project.
1225 :rtype: dict
1226
1227 """
1228
1229 repos = {}
1230 for project in projects:
1231 path = os.path.join(self.jobdir.src_root, project)
1232 repo = git.Repo(path)
1233 repos[project] = repo
1234 return repos
1235
Clark Boylanb640e052014-04-03 16:41:46 -07001236
Paul Belanger174a8272017-03-14 13:20:10 -04001237class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1238 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001239
Paul Belanger174a8272017-03-14 13:20:10 -04001240 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001241 they will report that they have started but then pause until
1242 released before reporting completion. This attribute may be
1243 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001244 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001245 be explicitly released.
1246
1247 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001248 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001249 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001250 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001251 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001252 self.hold_jobs_in_build = False
1253 self.lock = threading.Lock()
1254 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001255 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001257 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001258
James E. Blaira5dba232016-08-08 15:53:24 -07001259 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001260 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001261
1262 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001263 :arg Change change: The :py:class:`~tests.base.FakeChange`
1264 instance which should cause the job to fail. This job
1265 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001266
1267 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001268 l = self.fail_tests.get(name, [])
1269 l.append(change)
1270 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001271
James E. Blair962220f2016-08-03 11:22:38 -07001272 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001273 """Release a held build.
1274
1275 :arg str regex: A regular expression which, if supplied, will
1276 cause only builds with matching names to be released. If
1277 not supplied, all builds will be released.
1278
1279 """
James E. Blair962220f2016-08-03 11:22:38 -07001280 builds = self.running_builds[:]
1281 self.log.debug("Releasing build %s (%s)" % (regex,
1282 len(self.running_builds)))
1283 for build in builds:
1284 if not regex or re.match(regex, build.name):
1285 self.log.debug("Releasing build %s" %
1286 (build.parameters['ZUUL_UUID']))
1287 build.release()
1288 else:
1289 self.log.debug("Not releasing build %s" %
1290 (build.parameters['ZUUL_UUID']))
1291 self.log.debug("Done releasing builds %s (%s)" %
1292 (regex, len(self.running_builds)))
1293
Paul Belanger174a8272017-03-14 13:20:10 -04001294 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001295 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001296 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001297 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001298 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001299 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001300 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001301 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001302 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1303 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001304
1305 def stopJob(self, job):
1306 self.log.debug("handle stop")
1307 parameters = json.loads(job.arguments)
1308 uuid = parameters['uuid']
1309 for build in self.running_builds:
1310 if build.unique == uuid:
1311 build.aborted = True
1312 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001313 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001314
James E. Blaira002b032017-04-18 10:35:48 -07001315 def stop(self):
1316 for build in self.running_builds:
1317 build.release()
1318 super(RecordingExecutorServer, self).stop()
1319
Joshua Hesketh50c21782016-10-13 21:34:14 +11001320
Paul Belanger174a8272017-03-14 13:20:10 -04001321class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001322 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001323 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001324 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001325 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001326 if not commit: # merge conflict
1327 self.recordResult('MERGER_FAILURE')
1328 return commit
1329
1330 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001331 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001332 self.executor_server.lock.acquire()
1333 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001334 BuildHistory(name=build.name, result=result, changes=build.changes,
1335 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001336 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001337 pipeline=build.parameters['ZUUL_PIPELINE'])
1338 )
Paul Belanger174a8272017-03-14 13:20:10 -04001339 self.executor_server.running_builds.remove(build)
1340 del self.executor_server.job_builds[self.job.unique]
1341 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001342
1343 def runPlaybooks(self, args):
1344 build = self.executor_server.job_builds[self.job.unique]
1345 build.jobdir = self.jobdir
1346
1347 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1348 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001349 return result
1350
Monty Taylore6562aa2017-02-20 07:37:39 -05001351 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001352 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001353
Paul Belanger174a8272017-03-14 13:20:10 -04001354 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001355 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001356 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001357 else:
1358 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001359 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001360
James E. Blairad8dca02017-02-21 11:48:32 -05001361 def getHostList(self, args):
1362 self.log.debug("hostlist")
1363 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001364 for host in hosts:
1365 host['host_vars']['ansible_connection'] = 'local'
1366
1367 hosts.append(dict(
1368 name='localhost',
1369 host_vars=dict(ansible_connection='local'),
1370 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001371 return hosts
1372
James E. Blairf5dbd002015-12-23 15:26:17 -08001373
Clark Boylanb640e052014-04-03 16:41:46 -07001374class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001375 """A Gearman server for use in tests.
1376
1377 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1378 added to the queue but will not be distributed to workers
1379 until released. This attribute may be changed at any time and
1380 will take effect for subsequently enqueued jobs, but
1381 previously held jobs will still need to be explicitly
1382 released.
1383
1384 """
1385
Clark Boylanb640e052014-04-03 16:41:46 -07001386 def __init__(self):
1387 self.hold_jobs_in_queue = False
1388 super(FakeGearmanServer, self).__init__(0)
1389
1390 def getJobForConnection(self, connection, peek=False):
1391 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1392 for job in queue:
1393 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001394 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001395 job.waiting = self.hold_jobs_in_queue
1396 else:
1397 job.waiting = False
1398 if job.waiting:
1399 continue
1400 if job.name in connection.functions:
1401 if not peek:
1402 queue.remove(job)
1403 connection.related_jobs[job.handle] = job
1404 job.worker_connection = connection
1405 job.running = True
1406 return job
1407 return None
1408
1409 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001410 """Release a held job.
1411
1412 :arg str regex: A regular expression which, if supplied, will
1413 cause only jobs with matching names to be released. If
1414 not supplied, all jobs will be released.
1415 """
Clark Boylanb640e052014-04-03 16:41:46 -07001416 released = False
1417 qlen = (len(self.high_queue) + len(self.normal_queue) +
1418 len(self.low_queue))
1419 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1420 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001421 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001422 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001423 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001424 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001425 self.log.debug("releasing queued job %s" %
1426 job.unique)
1427 job.waiting = False
1428 released = True
1429 else:
1430 self.log.debug("not releasing queued job %s" %
1431 job.unique)
1432 if released:
1433 self.wakeConnections()
1434 qlen = (len(self.high_queue) + len(self.normal_queue) +
1435 len(self.low_queue))
1436 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1437
1438
1439class FakeSMTP(object):
1440 log = logging.getLogger('zuul.FakeSMTP')
1441
1442 def __init__(self, messages, server, port):
1443 self.server = server
1444 self.port = port
1445 self.messages = messages
1446
1447 def sendmail(self, from_email, to_email, msg):
1448 self.log.info("Sending email from %s, to %s, with msg %s" % (
1449 from_email, to_email, msg))
1450
1451 headers = msg.split('\n\n', 1)[0]
1452 body = msg.split('\n\n', 1)[1]
1453
1454 self.messages.append(dict(
1455 from_email=from_email,
1456 to_email=to_email,
1457 msg=msg,
1458 headers=headers,
1459 body=body,
1460 ))
1461
1462 return True
1463
1464 def quit(self):
1465 return True
1466
1467
James E. Blairdce6cea2016-12-20 16:45:32 -08001468class FakeNodepool(object):
1469 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001470 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001471
1472 log = logging.getLogger("zuul.test.FakeNodepool")
1473
1474 def __init__(self, host, port, chroot):
1475 self.client = kazoo.client.KazooClient(
1476 hosts='%s:%s%s' % (host, port, chroot))
1477 self.client.start()
1478 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001479 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001480 self.thread = threading.Thread(target=self.run)
1481 self.thread.daemon = True
1482 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001483 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001484
1485 def stop(self):
1486 self._running = False
1487 self.thread.join()
1488 self.client.stop()
1489 self.client.close()
1490
1491 def run(self):
1492 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001493 try:
1494 self._run()
1495 except Exception:
1496 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001497 time.sleep(0.1)
1498
1499 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001500 if self.paused:
1501 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001502 for req in self.getNodeRequests():
1503 self.fulfillRequest(req)
1504
1505 def getNodeRequests(self):
1506 try:
1507 reqids = self.client.get_children(self.REQUEST_ROOT)
1508 except kazoo.exceptions.NoNodeError:
1509 return []
1510 reqs = []
1511 for oid in sorted(reqids):
1512 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001513 try:
1514 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001515 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001516 data['_oid'] = oid
1517 reqs.append(data)
1518 except kazoo.exceptions.NoNodeError:
1519 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001520 return reqs
1521
James E. Blaire18d4602017-01-05 11:17:28 -08001522 def getNodes(self):
1523 try:
1524 nodeids = self.client.get_children(self.NODE_ROOT)
1525 except kazoo.exceptions.NoNodeError:
1526 return []
1527 nodes = []
1528 for oid in sorted(nodeids):
1529 path = self.NODE_ROOT + '/' + oid
1530 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001531 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001532 data['_oid'] = oid
1533 try:
1534 lockfiles = self.client.get_children(path + '/lock')
1535 except kazoo.exceptions.NoNodeError:
1536 lockfiles = []
1537 if lockfiles:
1538 data['_lock'] = True
1539 else:
1540 data['_lock'] = False
1541 nodes.append(data)
1542 return nodes
1543
James E. Blaira38c28e2017-01-04 10:33:20 -08001544 def makeNode(self, request_id, node_type):
1545 now = time.time()
1546 path = '/nodepool/nodes/'
1547 data = dict(type=node_type,
1548 provider='test-provider',
1549 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001550 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001551 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001552 public_ipv4='127.0.0.1',
1553 private_ipv4=None,
1554 public_ipv6=None,
1555 allocated_to=request_id,
1556 state='ready',
1557 state_time=now,
1558 created_time=now,
1559 updated_time=now,
1560 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001561 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001562 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001563 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001564 path = self.client.create(path, data,
1565 makepath=True,
1566 sequence=True)
1567 nodeid = path.split("/")[-1]
1568 return nodeid
1569
James E. Blair6ab79e02017-01-06 10:10:17 -08001570 def addFailRequest(self, request):
1571 self.fail_requests.add(request['_oid'])
1572
James E. Blairdce6cea2016-12-20 16:45:32 -08001573 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001574 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001575 return
1576 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001577 oid = request['_oid']
1578 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001579
James E. Blair6ab79e02017-01-06 10:10:17 -08001580 if oid in self.fail_requests:
1581 request['state'] = 'failed'
1582 else:
1583 request['state'] = 'fulfilled'
1584 nodes = []
1585 for node in request['node_types']:
1586 nodeid = self.makeNode(oid, node)
1587 nodes.append(nodeid)
1588 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001589
James E. Blaira38c28e2017-01-04 10:33:20 -08001590 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001591 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001592 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001593 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001594 try:
1595 self.client.set(path, data)
1596 except kazoo.exceptions.NoNodeError:
1597 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001598
1599
James E. Blair498059b2016-12-20 13:50:13 -08001600class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001601 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001602 super(ChrootedKazooFixture, self).__init__()
1603
1604 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1605 if ':' in zk_host:
1606 host, port = zk_host.split(':')
1607 else:
1608 host = zk_host
1609 port = None
1610
1611 self.zookeeper_host = host
1612
1613 if not port:
1614 self.zookeeper_port = 2181
1615 else:
1616 self.zookeeper_port = int(port)
1617
Clark Boylan621ec9a2017-04-07 17:41:33 -07001618 self.test_id = test_id
1619
James E. Blair498059b2016-12-20 13:50:13 -08001620 def _setUp(self):
1621 # Make sure the test chroot paths do not conflict
1622 random_bits = ''.join(random.choice(string.ascii_lowercase +
1623 string.ascii_uppercase)
1624 for x in range(8))
1625
Clark Boylan621ec9a2017-04-07 17:41:33 -07001626 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001627 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1628
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001629 self.addCleanup(self._cleanup)
1630
James E. Blair498059b2016-12-20 13:50:13 -08001631 # Ensure the chroot path exists and clean up any pre-existing znodes.
1632 _tmp_client = kazoo.client.KazooClient(
1633 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1634 _tmp_client.start()
1635
1636 if _tmp_client.exists(self.zookeeper_chroot):
1637 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1638
1639 _tmp_client.ensure_path(self.zookeeper_chroot)
1640 _tmp_client.stop()
1641 _tmp_client.close()
1642
James E. Blair498059b2016-12-20 13:50:13 -08001643 def _cleanup(self):
1644 '''Remove the chroot path.'''
1645 # Need a non-chroot'ed client to remove the chroot path
1646 _tmp_client = kazoo.client.KazooClient(
1647 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1648 _tmp_client.start()
1649 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1650 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001651 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001652
1653
Joshua Heskethd78b4482015-09-14 16:56:34 -06001654class MySQLSchemaFixture(fixtures.Fixture):
1655 def setUp(self):
1656 super(MySQLSchemaFixture, self).setUp()
1657
1658 random_bits = ''.join(random.choice(string.ascii_lowercase +
1659 string.ascii_uppercase)
1660 for x in range(8))
1661 self.name = '%s_%s' % (random_bits, os.getpid())
1662 self.passwd = uuid.uuid4().hex
1663 db = pymysql.connect(host="localhost",
1664 user="openstack_citest",
1665 passwd="openstack_citest",
1666 db="openstack_citest")
1667 cur = db.cursor()
1668 cur.execute("create database %s" % self.name)
1669 cur.execute(
1670 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1671 (self.name, self.name, self.passwd))
1672 cur.execute("flush privileges")
1673
1674 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1675 self.passwd,
1676 self.name)
1677 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1678 self.addCleanup(self.cleanup)
1679
1680 def cleanup(self):
1681 db = pymysql.connect(host="localhost",
1682 user="openstack_citest",
1683 passwd="openstack_citest",
1684 db="openstack_citest")
1685 cur = db.cursor()
1686 cur.execute("drop database %s" % self.name)
1687 cur.execute("drop user '%s'@'localhost'" % self.name)
1688 cur.execute("flush privileges")
1689
1690
Maru Newby3fe5f852015-01-13 04:22:14 +00001691class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001692 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001693 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001694
James E. Blair1c236df2017-02-01 14:07:24 -08001695 def attachLogs(self, *args):
1696 def reader():
1697 self._log_stream.seek(0)
1698 while True:
1699 x = self._log_stream.read(4096)
1700 if not x:
1701 break
1702 yield x.encode('utf8')
1703 content = testtools.content.content_from_reader(
1704 reader,
1705 testtools.content_type.UTF8_TEXT,
1706 False)
1707 self.addDetail('logging', content)
1708
Clark Boylanb640e052014-04-03 16:41:46 -07001709 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001710 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001711 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1712 try:
1713 test_timeout = int(test_timeout)
1714 except ValueError:
1715 # If timeout value is invalid do not set a timeout.
1716 test_timeout = 0
1717 if test_timeout > 0:
1718 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1719
1720 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1721 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1722 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1723 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1724 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1725 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1726 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1727 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1728 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1729 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001730 self._log_stream = StringIO()
1731 self.addOnException(self.attachLogs)
1732 else:
1733 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001734
James E. Blair73b41772017-05-22 13:22:55 -07001735 # NOTE(jeblair): this is temporary extra debugging to try to
1736 # track down a possible leak.
1737 orig_git_repo_init = git.Repo.__init__
1738
1739 def git_repo_init(myself, *args, **kw):
1740 orig_git_repo_init(myself, *args, **kw)
1741 self.log.debug("Created git repo 0x%x %s" %
1742 (id(myself), repr(myself)))
1743
1744 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1745 git_repo_init))
1746
James E. Blair1c236df2017-02-01 14:07:24 -08001747 handler = logging.StreamHandler(self._log_stream)
1748 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1749 '%(levelname)-8s %(message)s')
1750 handler.setFormatter(formatter)
1751
1752 logger = logging.getLogger()
1753 logger.setLevel(logging.DEBUG)
1754 logger.addHandler(handler)
1755
Clark Boylan3410d532017-04-25 12:35:29 -07001756 # Make sure we don't carry old handlers around in process state
1757 # which slows down test runs
1758 self.addCleanup(logger.removeHandler, handler)
1759 self.addCleanup(handler.close)
1760 self.addCleanup(handler.flush)
1761
James E. Blair1c236df2017-02-01 14:07:24 -08001762 # NOTE(notmorgan): Extract logging overrides for specific
1763 # libraries from the OS_LOG_DEFAULTS env and create loggers
1764 # for each. This is used to limit the output during test runs
1765 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001766 log_defaults_from_env = os.environ.get(
1767 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001768 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001769
James E. Blairdce6cea2016-12-20 16:45:32 -08001770 if log_defaults_from_env:
1771 for default in log_defaults_from_env.split(','):
1772 try:
1773 name, level_str = default.split('=', 1)
1774 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001775 logger = logging.getLogger(name)
1776 logger.setLevel(level)
1777 logger.addHandler(handler)
1778 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001779 except ValueError:
1780 # NOTE(notmorgan): Invalid format of the log default,
1781 # skip and don't try and apply a logger for the
1782 # specified module
1783 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001784
Maru Newby3fe5f852015-01-13 04:22:14 +00001785
1786class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001787 """A test case with a functioning Zuul.
1788
1789 The following class variables are used during test setup and can
1790 be overidden by subclasses but are effectively read-only once a
1791 test method starts running:
1792
1793 :cvar str config_file: This points to the main zuul config file
1794 within the fixtures directory. Subclasses may override this
1795 to obtain a different behavior.
1796
1797 :cvar str tenant_config_file: This is the tenant config file
1798 (which specifies from what git repos the configuration should
1799 be loaded). It defaults to the value specified in
1800 `config_file` but can be overidden by subclasses to obtain a
1801 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001802 configuration. See also the :py:func:`simple_layout`
1803 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001804
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001805 :cvar bool create_project_keys: Indicates whether Zuul should
1806 auto-generate keys for each project, or whether the test
1807 infrastructure should insert dummy keys to save time during
1808 startup. Defaults to False.
1809
James E. Blaire7b99a02016-08-05 14:27:34 -07001810 The following are instance variables that are useful within test
1811 methods:
1812
1813 :ivar FakeGerritConnection fake_<connection>:
1814 A :py:class:`~tests.base.FakeGerritConnection` will be
1815 instantiated for each connection present in the config file
1816 and stored here. For instance, `fake_gerrit` will hold the
1817 FakeGerritConnection object for a connection named `gerrit`.
1818
1819 :ivar FakeGearmanServer gearman_server: An instance of
1820 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1821 server that all of the Zuul components in this test use to
1822 communicate with each other.
1823
Paul Belanger174a8272017-03-14 13:20:10 -04001824 :ivar RecordingExecutorServer executor_server: An instance of
1825 :py:class:`~tests.base.RecordingExecutorServer` which is the
1826 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001827
1828 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1829 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001830 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001831 list upon completion.
1832
1833 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1834 objects representing completed builds. They are appended to
1835 the list in the order they complete.
1836
1837 """
1838
James E. Blair83005782015-12-11 14:46:03 -08001839 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001840 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001841 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001842
1843 def _startMerger(self):
1844 self.merge_server = zuul.merger.server.MergeServer(self.config,
1845 self.connections)
1846 self.merge_server.start()
1847
Maru Newby3fe5f852015-01-13 04:22:14 +00001848 def setUp(self):
1849 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001850
1851 self.setupZK()
1852
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001853 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001854 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001855 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1856 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001857 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001858 tmp_root = tempfile.mkdtemp(
1859 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001860 self.test_root = os.path.join(tmp_root, "zuul-test")
1861 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001862 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001863 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001864 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001865
1866 if os.path.exists(self.test_root):
1867 shutil.rmtree(self.test_root)
1868 os.makedirs(self.test_root)
1869 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001870 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001871
1872 # Make per test copy of Configuration.
1873 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001874 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1875 if not os.path.exists(self.private_key_file):
1876 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1877 shutil.copy(src_private_key_file, self.private_key_file)
1878 shutil.copy('{}.pub'.format(src_private_key_file),
1879 '{}.pub'.format(self.private_key_file))
1880 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001881 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001882 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001883 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001884 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001885 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001886 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001887 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001888
Clark Boylanb640e052014-04-03 16:41:46 -07001889 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001890 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1891 # see: https://github.com/jsocol/pystatsd/issues/61
1892 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001893 os.environ['STATSD_PORT'] = str(self.statsd.port)
1894 self.statsd.start()
1895 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001896 reload_module(statsd)
1897 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001898
1899 self.gearman_server = FakeGearmanServer()
1900
1901 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001902 self.log.info("Gearman server on port %s" %
1903 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001904
James E. Blaire511d2f2016-12-08 15:22:26 -08001905 gerritsource.GerritSource.replication_timeout = 1.5
1906 gerritsource.GerritSource.replication_retry_interval = 0.5
1907 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001908
Joshua Hesketh352264b2015-08-11 23:42:08 +10001909 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001910
Jan Hruban7083edd2015-08-21 14:00:54 +02001911 self.webapp = zuul.webapp.WebApp(
1912 self.sched, port=0, listen_address='127.0.0.1')
1913
Jan Hruban6b71aff2015-10-22 16:58:08 +02001914 self.event_queues = [
1915 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001916 self.sched.trigger_event_queue,
1917 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001918 ]
1919
James E. Blairfef78942016-03-11 16:28:56 -08001920 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001921 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001922
Clark Boylanb640e052014-04-03 16:41:46 -07001923 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001924 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001925 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001926 return FakeURLOpener(self.upstream_root, *args, **kw)
1927
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001928 old_urlopen = urllib.request.urlopen
1929 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001930
Paul Belanger174a8272017-03-14 13:20:10 -04001931 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001932 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001933 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001934 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001935 _test_root=self.test_root,
1936 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001937 self.executor_server.start()
1938 self.history = self.executor_server.build_history
1939 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001940
Paul Belanger174a8272017-03-14 13:20:10 -04001941 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001942 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001943 self.merge_client = zuul.merger.client.MergeClient(
1944 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001945 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001946 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001947 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001948
James E. Blair0d5a36e2017-02-21 10:53:44 -05001949 self.fake_nodepool = FakeNodepool(
1950 self.zk_chroot_fixture.zookeeper_host,
1951 self.zk_chroot_fixture.zookeeper_port,
1952 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001953
Paul Belanger174a8272017-03-14 13:20:10 -04001954 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001955 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001956 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001957 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001958
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001959 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001960
1961 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001962 self.webapp.start()
1963 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001964 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001965 # Cleanups are run in reverse order
1966 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001967 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001968 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001969
James E. Blairb9c0d772017-03-03 14:34:49 -08001970 self.sched.reconfigure(self.config)
1971 self.sched.resume()
1972
Tobias Henkel7df274b2017-05-26 17:41:11 +02001973 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001974 # Set up gerrit related fakes
1975 # Set a changes database so multiple FakeGerrit's can report back to
1976 # a virtual canonical database given by the configured hostname
1977 self.gerrit_changes_dbs = {}
1978
1979 def getGerritConnection(driver, name, config):
1980 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1981 con = FakeGerritConnection(driver, name, config,
1982 changes_db=db,
1983 upstream_root=self.upstream_root)
1984 self.event_queues.append(con.event_queue)
1985 setattr(self, 'fake_' + name, con)
1986 return con
1987
1988 self.useFixture(fixtures.MonkeyPatch(
1989 'zuul.driver.gerrit.GerritDriver.getConnection',
1990 getGerritConnection))
1991
Gregory Haynes4fc12542015-04-22 20:38:06 -07001992 def getGithubConnection(driver, name, config):
1993 con = FakeGithubConnection(driver, name, config,
1994 upstream_root=self.upstream_root)
1995 setattr(self, 'fake_' + name, con)
1996 return con
1997
1998 self.useFixture(fixtures.MonkeyPatch(
1999 'zuul.driver.github.GithubDriver.getConnection',
2000 getGithubConnection))
2001
James E. Blaire511d2f2016-12-08 15:22:26 -08002002 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002003 # TODO(jhesketh): This should come from lib.connections for better
2004 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002005 # Register connections from the config
2006 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002007
Joshua Hesketh352264b2015-08-11 23:42:08 +10002008 def FakeSMTPFactory(*args, **kw):
2009 args = [self.smtp_messages] + list(args)
2010 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002011
Joshua Hesketh352264b2015-08-11 23:42:08 +10002012 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002013
James E. Blaire511d2f2016-12-08 15:22:26 -08002014 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002015 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002016 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002017
James E. Blair83005782015-12-11 14:46:03 -08002018 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002019 # This creates the per-test configuration object. It can be
2020 # overriden by subclasses, but should not need to be since it
2021 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002022 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002023 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002024
2025 if not self.setupSimpleLayout():
2026 if hasattr(self, 'tenant_config_file'):
2027 self.config.set('zuul', 'tenant_config',
2028 self.tenant_config_file)
2029 git_path = os.path.join(
2030 os.path.dirname(
2031 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2032 'git')
2033 if os.path.exists(git_path):
2034 for reponame in os.listdir(git_path):
2035 project = reponame.replace('_', '/')
2036 self.copyDirToRepo(project,
2037 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002038 self.setupAllProjectKeys()
2039
James E. Blair06cc3922017-04-19 10:08:10 -07002040 def setupSimpleLayout(self):
2041 # If the test method has been decorated with a simple_layout,
2042 # use that instead of the class tenant_config_file. Set up a
2043 # single config-project with the specified layout, and
2044 # initialize repos for all of the 'project' entries which
2045 # appear in the layout.
2046 test_name = self.id().split('.')[-1]
2047 test = getattr(self, test_name)
2048 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002049 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002050 else:
2051 return False
2052
James E. Blairb70e55a2017-04-19 12:57:02 -07002053 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002054 path = os.path.join(FIXTURE_DIR, path)
2055 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002056 data = f.read()
2057 layout = yaml.safe_load(data)
2058 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002059 untrusted_projects = []
2060 for item in layout:
2061 if 'project' in item:
2062 name = item['project']['name']
2063 untrusted_projects.append(name)
2064 self.init_repo(name)
2065 self.addCommitToRepo(name, 'initial commit',
2066 files={'README': ''},
2067 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002068 if 'job' in item:
2069 jobname = item['job']['name']
2070 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002071
2072 root = os.path.join(self.test_root, "config")
2073 if not os.path.exists(root):
2074 os.makedirs(root)
2075 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2076 config = [{'tenant':
2077 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002078 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002079 {'config-projects': ['common-config'],
2080 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002081 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002082 f.close()
2083 self.config.set('zuul', 'tenant_config',
2084 os.path.join(FIXTURE_DIR, f.name))
2085
2086 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002087 self.addCommitToRepo('common-config', 'add content from fixture',
2088 files, branch='master', tag='init')
2089
2090 return True
2091
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002092 def setupAllProjectKeys(self):
2093 if self.create_project_keys:
2094 return
2095
2096 path = self.config.get('zuul', 'tenant_config')
2097 with open(os.path.join(FIXTURE_DIR, path)) as f:
2098 tenant_config = yaml.safe_load(f.read())
2099 for tenant in tenant_config:
2100 sources = tenant['tenant']['source']
2101 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002102 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002103 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002104 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002105 self.setupProjectKeys(source, project)
2106
2107 def setupProjectKeys(self, source, project):
2108 # Make sure we set up an RSA key for the project so that we
2109 # don't spend time generating one:
2110
2111 key_root = os.path.join(self.state_root, 'keys')
2112 if not os.path.isdir(key_root):
2113 os.mkdir(key_root, 0o700)
2114 private_key_file = os.path.join(key_root, source, project + '.pem')
2115 private_key_dir = os.path.dirname(private_key_file)
2116 self.log.debug("Installing test keys for project %s at %s" % (
2117 project, private_key_file))
2118 if not os.path.isdir(private_key_dir):
2119 os.makedirs(private_key_dir)
2120 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2121 with open(private_key_file, 'w') as o:
2122 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002123
James E. Blair498059b2016-12-20 13:50:13 -08002124 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002125 self.zk_chroot_fixture = self.useFixture(
2126 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002127 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002128 self.zk_chroot_fixture.zookeeper_host,
2129 self.zk_chroot_fixture.zookeeper_port,
2130 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002131
James E. Blair96c6bf82016-01-15 16:20:40 -08002132 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002133 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002134
2135 files = {}
2136 for (dirpath, dirnames, filenames) in os.walk(source_path):
2137 for filename in filenames:
2138 test_tree_filepath = os.path.join(dirpath, filename)
2139 common_path = os.path.commonprefix([test_tree_filepath,
2140 source_path])
2141 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2142 with open(test_tree_filepath, 'r') as f:
2143 content = f.read()
2144 files[relative_filepath] = content
2145 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002146 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002147
James E. Blaire18d4602017-01-05 11:17:28 -08002148 def assertNodepoolState(self):
2149 # Make sure that there are no pending requests
2150
2151 requests = self.fake_nodepool.getNodeRequests()
2152 self.assertEqual(len(requests), 0)
2153
2154 nodes = self.fake_nodepool.getNodes()
2155 for node in nodes:
2156 self.assertFalse(node['_lock'], "Node %s is locked" %
2157 (node['_oid'],))
2158
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002159 def assertNoGeneratedKeys(self):
2160 # Make sure that Zuul did not generate any project keys
2161 # (unless it was supposed to).
2162
2163 if self.create_project_keys:
2164 return
2165
2166 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2167 test_key = i.read()
2168
2169 key_root = os.path.join(self.state_root, 'keys')
2170 for root, dirname, files in os.walk(key_root):
2171 for fn in files:
2172 with open(os.path.join(root, fn)) as f:
2173 self.assertEqual(test_key, f.read())
2174
Clark Boylanb640e052014-04-03 16:41:46 -07002175 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002176 self.log.debug("Assert final state")
2177 # Make sure no jobs are running
2178 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002179 # Make sure that git.Repo objects have been garbage collected.
2180 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002181 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002182 gc.collect()
2183 for obj in gc.get_objects():
2184 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002185 self.log.debug("Leaked git repo object: 0x%x %s" %
2186 (id(obj), repr(obj)))
2187 for ref in gc.get_referrers(obj):
2188 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002189 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002190 if repos:
2191 for obj in gc.garbage:
2192 self.log.debug(" Garbage %s" % (repr(obj)))
2193 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002194 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002195 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002196 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002197 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002198 for tenant in self.sched.abide.tenants.values():
2199 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002200 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002201 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002202
2203 def shutdown(self):
2204 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002205 self.executor_server.hold_jobs_in_build = False
2206 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002207 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002208 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002209 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002210 self.sched.stop()
2211 self.sched.join()
2212 self.statsd.stop()
2213 self.statsd.join()
2214 self.webapp.stop()
2215 self.webapp.join()
2216 self.rpc.stop()
2217 self.rpc.join()
2218 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002219 self.fake_nodepool.stop()
2220 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002221 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002222 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002223 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002224 # Further the pydevd threads also need to be whitelisted so debugging
2225 # e.g. in PyCharm is possible without breaking shutdown.
2226 whitelist = ['executor-watchdog',
2227 'pydevd.CommandThread',
2228 'pydevd.Reader',
2229 'pydevd.Writer',
2230 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002231 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002232 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002233 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002234 log_str = ""
2235 for thread_id, stack_frame in sys._current_frames().items():
2236 log_str += "Thread: %s\n" % thread_id
2237 log_str += "".join(traceback.format_stack(stack_frame))
2238 self.log.debug(log_str)
2239 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002240
James E. Blaira002b032017-04-18 10:35:48 -07002241 def assertCleanShutdown(self):
2242 pass
2243
James E. Blairc4ba97a2017-04-19 16:26:24 -07002244 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002245 parts = project.split('/')
2246 path = os.path.join(self.upstream_root, *parts[:-1])
2247 if not os.path.exists(path):
2248 os.makedirs(path)
2249 path = os.path.join(self.upstream_root, project)
2250 repo = git.Repo.init(path)
2251
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002252 with repo.config_writer() as config_writer:
2253 config_writer.set_value('user', 'email', 'user@example.com')
2254 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002255
Clark Boylanb640e052014-04-03 16:41:46 -07002256 repo.index.commit('initial commit')
2257 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002258 if tag:
2259 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002260
James E. Blair97d902e2014-08-21 13:25:56 -07002261 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002262 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002263 repo.git.clean('-x', '-f', '-d')
2264
James E. Blair97d902e2014-08-21 13:25:56 -07002265 def create_branch(self, project, branch):
2266 path = os.path.join(self.upstream_root, project)
2267 repo = git.Repo.init(path)
2268 fn = os.path.join(path, 'README')
2269
2270 branch_head = repo.create_head(branch)
2271 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002272 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002273 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002274 f.close()
2275 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002276 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002277
James E. Blair97d902e2014-08-21 13:25:56 -07002278 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002279 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002280 repo.git.clean('-x', '-f', '-d')
2281
Sachi King9f16d522016-03-16 12:20:45 +11002282 def create_commit(self, project):
2283 path = os.path.join(self.upstream_root, project)
2284 repo = git.Repo(path)
2285 repo.head.reference = repo.heads['master']
2286 file_name = os.path.join(path, 'README')
2287 with open(file_name, 'a') as f:
2288 f.write('creating fake commit\n')
2289 repo.index.add([file_name])
2290 commit = repo.index.commit('Creating a fake commit')
2291 return commit.hexsha
2292
James E. Blairf4a5f022017-04-18 14:01:10 -07002293 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002294 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002295 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002296 while len(self.builds):
2297 self.release(self.builds[0])
2298 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002299 i += 1
2300 if count is not None and i >= count:
2301 break
James E. Blairb8c16472015-05-05 14:55:26 -07002302
Clark Boylanb640e052014-04-03 16:41:46 -07002303 def release(self, job):
2304 if isinstance(job, FakeBuild):
2305 job.release()
2306 else:
2307 job.waiting = False
2308 self.log.debug("Queued job %s released" % job.unique)
2309 self.gearman_server.wakeConnections()
2310
2311 def getParameter(self, job, name):
2312 if isinstance(job, FakeBuild):
2313 return job.parameters[name]
2314 else:
2315 parameters = json.loads(job.arguments)
2316 return parameters[name]
2317
Clark Boylanb640e052014-04-03 16:41:46 -07002318 def haveAllBuildsReported(self):
2319 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002320 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002321 return False
2322 # Find out if every build that the worker has completed has been
2323 # reported back to Zuul. If it hasn't then that means a Gearman
2324 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002325 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002326 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002327 if not zbuild:
2328 # It has already been reported
2329 continue
2330 # It hasn't been reported yet.
2331 return False
2332 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002333 worker = self.executor_server.executor_worker
2334 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002335 if connection.state == 'GRAB_WAIT':
2336 return False
2337 return True
2338
2339 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002340 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002341 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002342 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002343 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002344 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002345 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002346 for j in conn.related_jobs.values():
2347 if j.unique == build.uuid:
2348 client_job = j
2349 break
2350 if not client_job:
2351 self.log.debug("%s is not known to the gearman client" %
2352 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002353 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002354 if not client_job.handle:
2355 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002356 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002357 server_job = self.gearman_server.jobs.get(client_job.handle)
2358 if not server_job:
2359 self.log.debug("%s is not known to the gearman server" %
2360 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002361 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002362 if not hasattr(server_job, 'waiting'):
2363 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002364 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002365 if server_job.waiting:
2366 continue
James E. Blair17302972016-08-10 16:11:42 -07002367 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002368 self.log.debug("%s has not reported start" % build)
2369 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002370 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002371 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002372 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002373 if worker_build:
2374 if worker_build.isWaiting():
2375 continue
2376 else:
2377 self.log.debug("%s is running" % worker_build)
2378 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002379 else:
James E. Blair962220f2016-08-03 11:22:38 -07002380 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002381 return False
James E. Blaira002b032017-04-18 10:35:48 -07002382 for (build_uuid, job_worker) in \
2383 self.executor_server.job_workers.items():
2384 if build_uuid not in seen_builds:
2385 self.log.debug("%s is not finalized" % build_uuid)
2386 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002387 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002388
James E. Blairdce6cea2016-12-20 16:45:32 -08002389 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002390 if self.fake_nodepool.paused:
2391 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002392 if self.sched.nodepool.requests:
2393 return False
2394 return True
2395
Jan Hruban6b71aff2015-10-22 16:58:08 +02002396 def eventQueuesEmpty(self):
2397 for queue in self.event_queues:
2398 yield queue.empty()
2399
2400 def eventQueuesJoin(self):
2401 for queue in self.event_queues:
2402 queue.join()
2403
Clark Boylanb640e052014-04-03 16:41:46 -07002404 def waitUntilSettled(self):
2405 self.log.debug("Waiting until settled...")
2406 start = time.time()
2407 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002408 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002409 self.log.error("Timeout waiting for Zuul to settle")
2410 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002411 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002412 self.log.error(" %s: %s" % (queue, queue.empty()))
2413 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002414 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002415 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002416 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002417 self.log.error("All requests completed: %s" %
2418 (self.areAllNodeRequestsComplete(),))
2419 self.log.error("Merge client jobs: %s" %
2420 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002421 raise Exception("Timeout waiting for Zuul to settle")
2422 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002423
Paul Belanger174a8272017-03-14 13:20:10 -04002424 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002425 # have all build states propogated to zuul?
2426 if self.haveAllBuildsReported():
2427 # Join ensures that the queue is empty _and_ events have been
2428 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002429 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002430 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002431 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002432 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002433 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002434 self.areAllNodeRequestsComplete() and
2435 all(self.eventQueuesEmpty())):
2436 # The queue empty check is placed at the end to
2437 # ensure that if a component adds an event between
2438 # when locked the run handler and checked that the
2439 # components were stable, we don't erroneously
2440 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002441 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002442 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002443 self.log.debug("...settled.")
2444 return
2445 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002446 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002447 self.sched.wake_event.wait(0.1)
2448
2449 def countJobResults(self, jobs, result):
2450 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002451 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002452
James E. Blair96c6bf82016-01-15 16:20:40 -08002453 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002454 for job in self.history:
2455 if (job.name == name and
2456 (project is None or
2457 job.parameters['ZUUL_PROJECT'] == project)):
2458 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002459 raise Exception("Unable to find job %s in history" % name)
2460
2461 def assertEmptyQueues(self):
2462 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002463 for tenant in self.sched.abide.tenants.values():
2464 for pipeline in tenant.layout.pipelines.values():
2465 for queue in pipeline.queues:
2466 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002467 print('pipeline %s queue %s contents %s' % (
2468 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002469 self.assertEqual(len(queue.queue), 0,
2470 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002471
2472 def assertReportedStat(self, key, value=None, kind=None):
2473 start = time.time()
2474 while time.time() < (start + 5):
2475 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002476 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002477 if key == k:
2478 if value is None and kind is None:
2479 return
2480 elif value:
2481 if value == v:
2482 return
2483 elif kind:
2484 if v.endswith('|' + kind):
2485 return
2486 time.sleep(0.1)
2487
Clark Boylanb640e052014-04-03 16:41:46 -07002488 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002489
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002490 def assertBuilds(self, builds):
2491 """Assert that the running builds are as described.
2492
2493 The list of running builds is examined and must match exactly
2494 the list of builds described by the input.
2495
2496 :arg list builds: A list of dictionaries. Each item in the
2497 list must match the corresponding build in the build
2498 history, and each element of the dictionary must match the
2499 corresponding attribute of the build.
2500
2501 """
James E. Blair3158e282016-08-19 09:34:11 -07002502 try:
2503 self.assertEqual(len(self.builds), len(builds))
2504 for i, d in enumerate(builds):
2505 for k, v in d.items():
2506 self.assertEqual(
2507 getattr(self.builds[i], k), v,
2508 "Element %i in builds does not match" % (i,))
2509 except Exception:
2510 for build in self.builds:
2511 self.log.error("Running build: %s" % build)
2512 else:
2513 self.log.error("No running builds")
2514 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002515
James E. Blairb536ecc2016-08-31 10:11:42 -07002516 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002517 """Assert that the completed builds are as described.
2518
2519 The list of completed builds is examined and must match
2520 exactly the list of builds described by the input.
2521
2522 :arg list history: A list of dictionaries. Each item in the
2523 list must match the corresponding build in the build
2524 history, and each element of the dictionary must match the
2525 corresponding attribute of the build.
2526
James E. Blairb536ecc2016-08-31 10:11:42 -07002527 :arg bool ordered: If true, the history must match the order
2528 supplied, if false, the builds are permitted to have
2529 arrived in any order.
2530
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002531 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002532 def matches(history_item, item):
2533 for k, v in item.items():
2534 if getattr(history_item, k) != v:
2535 return False
2536 return True
James E. Blair3158e282016-08-19 09:34:11 -07002537 try:
2538 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002539 if ordered:
2540 for i, d in enumerate(history):
2541 if not matches(self.history[i], d):
2542 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002543 "Element %i in history does not match %s" %
2544 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002545 else:
2546 unseen = self.history[:]
2547 for i, d in enumerate(history):
2548 found = False
2549 for unseen_item in unseen:
2550 if matches(unseen_item, d):
2551 found = True
2552 unseen.remove(unseen_item)
2553 break
2554 if not found:
2555 raise Exception("No match found for element %i "
2556 "in history" % (i,))
2557 if unseen:
2558 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002559 except Exception:
2560 for build in self.history:
2561 self.log.error("Completed build: %s" % build)
2562 else:
2563 self.log.error("No completed builds")
2564 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002565
James E. Blair6ac368c2016-12-22 18:07:20 -08002566 def printHistory(self):
2567 """Log the build history.
2568
2569 This can be useful during tests to summarize what jobs have
2570 completed.
2571
2572 """
2573 self.log.debug("Build history:")
2574 for build in self.history:
2575 self.log.debug(build)
2576
James E. Blair59fdbac2015-12-07 17:08:06 -08002577 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002578 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2579
James E. Blair9ea70072017-04-19 16:05:30 -07002580 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002581 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002582 if not os.path.exists(root):
2583 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002584 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2585 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002586- tenant:
2587 name: openstack
2588 source:
2589 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002590 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002591 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002592 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002593 - org/project
2594 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002595 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002596 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002597 self.config.set('zuul', 'tenant_config',
2598 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002599 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002600
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002601 def addCommitToRepo(self, project, message, files,
2602 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002603 path = os.path.join(self.upstream_root, project)
2604 repo = git.Repo(path)
2605 repo.head.reference = branch
2606 zuul.merger.merger.reset_repo_to_head(repo)
2607 for fn, content in files.items():
2608 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002609 try:
2610 os.makedirs(os.path.dirname(fn))
2611 except OSError:
2612 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002613 with open(fn, 'w') as f:
2614 f.write(content)
2615 repo.index.add([fn])
2616 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002617 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002618 repo.heads[branch].commit = commit
2619 repo.head.reference = branch
2620 repo.git.clean('-x', '-f', '-d')
2621 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002622 if tag:
2623 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002624 return before
2625
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002626 def commitConfigUpdate(self, project_name, source_name):
2627 """Commit an update to zuul.yaml
2628
2629 This overwrites the zuul.yaml in the specificed project with
2630 the contents specified.
2631
2632 :arg str project_name: The name of the project containing
2633 zuul.yaml (e.g., common-config)
2634
2635 :arg str source_name: The path to the file (underneath the
2636 test fixture directory) whose contents should be used to
2637 replace zuul.yaml.
2638 """
2639
2640 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002641 files = {}
2642 with open(source_path, 'r') as f:
2643 data = f.read()
2644 layout = yaml.safe_load(data)
2645 files['zuul.yaml'] = data
2646 for item in layout:
2647 if 'job' in item:
2648 jobname = item['job']['name']
2649 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002650 before = self.addCommitToRepo(
2651 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002652 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002653 return before
2654
James E. Blair7fc8daa2016-08-08 15:37:15 -07002655 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002656
James E. Blair7fc8daa2016-08-08 15:37:15 -07002657 """Inject a Fake (Gerrit) event.
2658
2659 This method accepts a JSON-encoded event and simulates Zuul
2660 having received it from Gerrit. It could (and should)
2661 eventually apply to any connection type, but is currently only
2662 used with Gerrit connections. The name of the connection is
2663 used to look up the corresponding server, and the event is
2664 simulated as having been received by all Zuul connections
2665 attached to that server. So if two Gerrit connections in Zuul
2666 are connected to the same Gerrit server, and you invoke this
2667 method specifying the name of one of them, the event will be
2668 received by both.
2669
2670 .. note::
2671
2672 "self.fake_gerrit.addEvent" calls should be migrated to
2673 this method.
2674
2675 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002676 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002677 :arg str event: The JSON-encoded event.
2678
2679 """
2680 specified_conn = self.connections.connections[connection]
2681 for conn in self.connections.connections.values():
2682 if (isinstance(conn, specified_conn.__class__) and
2683 specified_conn.server == conn.server):
2684 conn.addEvent(event)
2685
James E. Blaird8af5422017-05-24 13:59:40 -07002686 def getUpstreamRepos(self, projects):
2687 """Return upstream git repo objects for the listed projects
2688
2689 :arg list projects: A list of strings, each the canonical name
2690 of a project.
2691
2692 :returns: A dictionary of {name: repo} for every listed
2693 project.
2694 :rtype: dict
2695
2696 """
2697
2698 repos = {}
2699 for project in projects:
2700 # FIXME(jeblair): the upstream root does not yet have a
2701 # hostname component; that needs to be added, and this
2702 # line removed:
2703 tmp_project_name = '/'.join(project.split('/')[1:])
2704 path = os.path.join(self.upstream_root, tmp_project_name)
2705 repo = git.Repo(path)
2706 repos[project] = repo
2707 return repos
2708
James E. Blair3f876d52016-07-22 13:07:14 -07002709
2710class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002711 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002712 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002713
Joshua Heskethd78b4482015-09-14 16:56:34 -06002714
2715class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002716 def setup_config(self):
2717 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002718 for section_name in self.config.sections():
2719 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2720 section_name, re.I)
2721 if not con_match:
2722 continue
2723
2724 if self.config.get(section_name, 'driver') == 'sql':
2725 f = MySQLSchemaFixture()
2726 self.useFixture(f)
2727 if (self.config.get(section_name, 'dburi') ==
2728 '$MYSQL_FIXTURE_DBURI$'):
2729 self.config.set(section_name, 'dburi', f.dburi)