blob: c6f1a4b9ea7afeb874bd424151588a65f253e21f [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
Jesse Keating8c2eb572017-05-30 17:31:45 -0700595 def getPushEvent(self, old_sha, ref='refs/heads/master'):
596 name = 'push'
597 data = {
598 'ref': ref,
599 'before': old_sha,
600 'after': self.head_sha,
601 'repository': {
602 'full_name': self.project
603 },
604 'sender': {
605 'login': 'ghuser'
606 }
607 }
608 return (name, data)
609
Gregory Haynes4fc12542015-04-22 20:38:06 -0700610 def addComment(self, message):
611 self.comments.append(message)
612 self._updateTimeStamp()
613
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200614 def getCommentAddedEvent(self, text):
615 name = 'issue_comment'
616 data = {
617 'action': 'created',
618 'issue': {
619 'number': self.number
620 },
621 'comment': {
622 'body': text
623 },
624 'repository': {
625 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100626 },
627 'sender': {
628 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200629 }
630 }
631 return (name, data)
632
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800633 def getReviewAddedEvent(self, review):
634 name = 'pull_request_review'
635 data = {
636 'action': 'submitted',
637 'pull_request': {
638 'number': self.number,
639 'title': self.subject,
640 'updated_at': self.updated_at,
641 'base': {
642 'ref': self.branch,
643 'repo': {
644 'full_name': self.project
645 }
646 },
647 'head': {
648 'sha': self.head_sha
649 }
650 },
651 'review': {
652 'state': review
653 },
654 'repository': {
655 'full_name': self.project
656 },
657 'sender': {
658 'login': 'ghuser'
659 }
660 }
661 return (name, data)
662
Jan Hruban16ad31f2015-11-07 14:39:07 +0100663 def addLabel(self, name):
664 if name not in self.labels:
665 self.labels.append(name)
666 self._updateTimeStamp()
667 return self._getLabelEvent(name)
668
669 def removeLabel(self, name):
670 if name in self.labels:
671 self.labels.remove(name)
672 self._updateTimeStamp()
673 return self._getUnlabelEvent(name)
674
675 def _getLabelEvent(self, label):
676 name = 'pull_request'
677 data = {
678 'action': 'labeled',
679 'pull_request': {
680 'number': self.number,
681 'updated_at': self.updated_at,
682 'base': {
683 'ref': self.branch,
684 'repo': {
685 'full_name': self.project
686 }
687 },
688 'head': {
689 'sha': self.head_sha
690 }
691 },
692 'label': {
693 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100694 },
695 'sender': {
696 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100697 }
698 }
699 return (name, data)
700
701 def _getUnlabelEvent(self, label):
702 name = 'pull_request'
703 data = {
704 'action': 'unlabeled',
705 'pull_request': {
706 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100707 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100708 'updated_at': self.updated_at,
709 'base': {
710 'ref': self.branch,
711 'repo': {
712 'full_name': self.project
713 }
714 },
715 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800716 'sha': self.head_sha,
717 'repo': {
718 'full_name': self.project
719 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100720 }
721 },
722 'label': {
723 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100724 },
725 'sender': {
726 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100727 }
728 }
729 return (name, data)
730
Gregory Haynes4fc12542015-04-22 20:38:06 -0700731 def _getRepo(self):
732 repo_path = os.path.join(self.upstream_root, self.project)
733 return git.Repo(repo_path)
734
735 def _createPRRef(self):
736 repo = self._getRepo()
737 GithubChangeReference.create(
738 repo, self._getPRReference(), 'refs/tags/init')
739
Jan Hruban570d01c2016-03-10 21:51:32 +0100740 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700741 repo = self._getRepo()
742 ref = repo.references[self._getPRReference()]
743 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100744 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700745 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100746 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700747 repo.head.reference = ref
748 zuul.merger.merger.reset_repo_to_head(repo)
749 repo.git.clean('-x', '-f', '-d')
750
Jan Hruban570d01c2016-03-10 21:51:32 +0100751 if files:
752 fn = files[0]
753 self.files = files
754 else:
755 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
756 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100757 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700758 fn = os.path.join(repo.working_dir, fn)
759 f = open(fn, 'w')
760 with open(fn, 'w') as f:
761 f.write("test %s %s\n" %
762 (self.branch, self.number))
763 repo.index.add([fn])
764
765 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800766 # Create an empty set of statuses for the given sha,
767 # each sha on a PR may have a status set on it
768 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700769 repo.head.reference = 'master'
770 zuul.merger.merger.reset_repo_to_head(repo)
771 repo.git.clean('-x', '-f', '-d')
772 repo.heads['master'].checkout()
773
774 def _updateTimeStamp(self):
775 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
776
777 def getPRHeadSha(self):
778 repo = self._getRepo()
779 return repo.references[self._getPRReference()].commit.hexsha
780
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800781 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800782 # Since we're bypassing github API, which would require a user, we
783 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800784 # insert the status at the top of the list, to simulate that it
785 # is the most recent set status
786 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100787 'state': state,
788 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800789 'description': description,
790 'context': context,
791 'creator': {
792 'login': user
793 }
794 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100795
Jesse Keatingae4cd272017-01-30 17:10:44 -0800796 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800797 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
798 # convert the timestamp to a str format that would be returned
799 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800800
Adam Gandelmand81dd762017-02-09 15:15:49 -0800801 if granted_on:
802 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
803 submitted_at = time.strftime(
804 gh_time_format, granted_on.timetuple())
805 else:
806 # github timestamps only down to the second, so we need to make
807 # sure reviews that tests add appear to be added over a period of
808 # time in the past and not all at once.
809 if not self.reviews:
810 # the first review happens 10 mins ago
811 offset = 600
812 else:
813 # subsequent reviews happen 1 minute closer to now
814 offset = 600 - (len(self.reviews) * 60)
815
816 granted_on = datetime.datetime.utcfromtimestamp(
817 time.time() - offset)
818 submitted_at = time.strftime(
819 gh_time_format, granted_on.timetuple())
820
Jesse Keatingae4cd272017-01-30 17:10:44 -0800821 self.reviews.append({
822 'state': state,
823 'user': {
824 'login': user,
825 'email': user + "@derp.com",
826 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800827 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800828 })
829
Gregory Haynes4fc12542015-04-22 20:38:06 -0700830 def _getPRReference(self):
831 return '%s/head' % self.number
832
833 def _getPullRequestEvent(self, action):
834 name = 'pull_request'
835 data = {
836 'action': action,
837 'number': self.number,
838 'pull_request': {
839 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100840 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700841 'updated_at': self.updated_at,
842 'base': {
843 'ref': self.branch,
844 'repo': {
845 'full_name': self.project
846 }
847 },
848 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800849 'sha': self.head_sha,
850 'repo': {
851 'full_name': self.project
852 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700853 }
Jan Hruban3b415922016-02-03 13:10:22 +0100854 },
855 'sender': {
856 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700857 }
858 }
859 return (name, data)
860
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800861 def getCommitStatusEvent(self, context, state='success', user='zuul'):
862 name = 'status'
863 data = {
864 'state': state,
865 'sha': self.head_sha,
866 'description': 'Test results for %s: %s' % (self.head_sha, state),
867 'target_url': 'http://zuul/%s' % self.head_sha,
868 'branches': [],
869 'context': context,
870 'sender': {
871 'login': user
872 }
873 }
874 return (name, data)
875
Gregory Haynes4fc12542015-04-22 20:38:06 -0700876
877class FakeGithubConnection(githubconnection.GithubConnection):
878 log = logging.getLogger("zuul.test.FakeGithubConnection")
879
880 def __init__(self, driver, connection_name, connection_config,
881 upstream_root=None):
882 super(FakeGithubConnection, self).__init__(driver, connection_name,
883 connection_config)
884 self.connection_name = connection_name
885 self.pr_number = 0
886 self.pull_requests = []
887 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100888 self.merge_failure = False
889 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700890
Jan Hruban570d01c2016-03-10 21:51:32 +0100891 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700892 self.pr_number += 1
893 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100894 self, self.pr_number, project, branch, subject, self.upstream_root,
895 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700896 self.pull_requests.append(pull_request)
897 return pull_request
898
Wayne1a78c612015-06-11 17:14:13 -0700899 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
900 if not old_rev:
901 old_rev = '00000000000000000000000000000000'
902 if not new_rev:
903 new_rev = random_sha1()
904 name = 'push'
905 data = {
906 'ref': ref,
907 'before': old_rev,
908 'after': new_rev,
909 'repository': {
910 'full_name': project
911 }
912 }
913 return (name, data)
914
Gregory Haynes4fc12542015-04-22 20:38:06 -0700915 def emitEvent(self, event):
916 """Emulates sending the GitHub webhook event to the connection."""
917 port = self.webapp.server.socket.getsockname()[1]
918 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700919 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700920 headers = {'X-Github-Event': name}
921 req = urllib.request.Request(
922 'http://localhost:%s/connection/%s/payload'
923 % (port, self.connection_name),
924 data=payload, headers=headers)
925 urllib.request.urlopen(req)
926
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200927 def getPull(self, project, number):
928 pr = self.pull_requests[number - 1]
929 data = {
930 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100931 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200932 'updated_at': pr.updated_at,
933 'base': {
934 'repo': {
935 'full_name': pr.project
936 },
937 'ref': pr.branch,
938 },
Jan Hruban37615e52015-11-19 14:30:49 +0100939 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700940 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200941 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800942 'sha': pr.head_sha,
943 'repo': {
944 'full_name': pr.project
945 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200946 }
947 }
948 return data
949
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800950 def getPullBySha(self, sha):
951 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
952 if len(prs) > 1:
953 raise Exception('Multiple pulls found with head sha: %s' % sha)
954 pr = prs[0]
955 return self.getPull(pr.project, pr.number)
956
Jan Hruban570d01c2016-03-10 21:51:32 +0100957 def getPullFileNames(self, project, number):
958 pr = self.pull_requests[number - 1]
959 return pr.files
960
Jesse Keatingae4cd272017-01-30 17:10:44 -0800961 def _getPullReviews(self, owner, project, number):
962 pr = self.pull_requests[number - 1]
963 return pr.reviews
964
Jan Hruban3b415922016-02-03 13:10:22 +0100965 def getUser(self, login):
966 data = {
967 'username': login,
968 'name': 'Github User',
969 'email': 'github.user@example.com'
970 }
971 return data
972
Jesse Keatingae4cd272017-01-30 17:10:44 -0800973 def getRepoPermission(self, project, login):
974 owner, proj = project.split('/')
975 for pr in self.pull_requests:
976 pr_owner, pr_project = pr.project.split('/')
977 if (pr_owner == owner and proj == pr_project):
978 if login in pr.writers:
979 return 'write'
980 else:
981 return 'read'
982
Gregory Haynes4fc12542015-04-22 20:38:06 -0700983 def getGitUrl(self, project):
984 return os.path.join(self.upstream_root, str(project))
985
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200986 def real_getGitUrl(self, project):
987 return super(FakeGithubConnection, self).getGitUrl(project)
988
Gregory Haynes4fc12542015-04-22 20:38:06 -0700989 def getProjectBranches(self, project):
990 """Masks getProjectBranches since we don't have a real github"""
991
992 # just returns master for now
993 return ['master']
994
Jan Hrubane252a732017-01-03 15:03:09 +0100995 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700996 pull_request = self.pull_requests[pr_number - 1]
997 pull_request.addComment(message)
998
Jan Hruban3b415922016-02-03 13:10:22 +0100999 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +01001000 pull_request = self.pull_requests[pr_number - 1]
1001 if self.merge_failure:
1002 raise Exception('Pull request was not merged')
1003 if self.merge_not_allowed_count > 0:
1004 self.merge_not_allowed_count -= 1
1005 raise MergeFailure('Merge was not successful due to mergeability'
1006 ' conflict')
1007 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001008 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001009
Jesse Keatingd96e5882017-01-19 13:55:50 -08001010 def getCommitStatuses(self, project, sha):
1011 owner, proj = project.split('/')
1012 for pr in self.pull_requests:
1013 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -07001014 # This is somewhat risky, if the same commit exists in multiple
1015 # PRs, we might grab the wrong one that doesn't have a status
1016 # that is expected to be there. Maybe re-work this so that there
1017 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001018 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001019 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001020 return pr.statuses[sha]
1021
Jan Hrubane252a732017-01-03 15:03:09 +01001022 def setCommitStatus(self, project, sha, state,
1023 url='', description='', context=''):
1024 owner, proj = project.split('/')
1025 for pr in self.pull_requests:
1026 pr_owner, pr_project = pr.project.split('/')
1027 if (pr_owner == owner and pr_project == proj and
1028 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001029 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001030
Jan Hruban16ad31f2015-11-07 14:39:07 +01001031 def labelPull(self, project, pr_number, label):
1032 pull_request = self.pull_requests[pr_number - 1]
1033 pull_request.addLabel(label)
1034
1035 def unlabelPull(self, project, pr_number, label):
1036 pull_request = self.pull_requests[pr_number - 1]
1037 pull_request.removeLabel(label)
1038
Gregory Haynes4fc12542015-04-22 20:38:06 -07001039
Clark Boylanb640e052014-04-03 16:41:46 -07001040class BuildHistory(object):
1041 def __init__(self, **kw):
1042 self.__dict__.update(kw)
1043
1044 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001045 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1046 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001047
1048
1049class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +02001050 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -07001051 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -07001052 self.url = url
1053
1054 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001055 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -07001056 path = res.path
1057 project = '/'.join(path.split('/')[2:-2])
1058 ret = '001e# service=git-upload-pack\n'
1059 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
1060 'multi_ack thin-pack side-band side-band-64k ofs-delta '
1061 'shallow no-progress include-tag multi_ack_detailed no-done\n')
1062 path = os.path.join(self.upstream_root, project)
1063 repo = git.Repo(path)
1064 for ref in repo.refs:
1065 r = ref.object.hexsha + ' ' + ref.path + '\n'
1066 ret += '%04x%s' % (len(r) + 4, r)
1067 ret += '0000'
1068 return ret
1069
1070
Clark Boylanb640e052014-04-03 16:41:46 -07001071class FakeStatsd(threading.Thread):
1072 def __init__(self):
1073 threading.Thread.__init__(self)
1074 self.daemon = True
1075 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1076 self.sock.bind(('', 0))
1077 self.port = self.sock.getsockname()[1]
1078 self.wake_read, self.wake_write = os.pipe()
1079 self.stats = []
1080
1081 def run(self):
1082 while True:
1083 poll = select.poll()
1084 poll.register(self.sock, select.POLLIN)
1085 poll.register(self.wake_read, select.POLLIN)
1086 ret = poll.poll()
1087 for (fd, event) in ret:
1088 if fd == self.sock.fileno():
1089 data = self.sock.recvfrom(1024)
1090 if not data:
1091 return
1092 self.stats.append(data[0])
1093 if fd == self.wake_read:
1094 return
1095
1096 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001097 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001098
1099
James E. Blaire1767bc2016-08-02 10:00:27 -07001100class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001101 log = logging.getLogger("zuul.test")
1102
Paul Belanger174a8272017-03-14 13:20:10 -04001103 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001104 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001105 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001106 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001107 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001108 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001109 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001110 # TODOv3(jeblair): self.node is really "the image of the node
1111 # assigned". We should rename it (self.node_image?) if we
1112 # keep using it like this, or we may end up exposing more of
1113 # the complexity around multi-node jobs here
1114 # (self.nodes[0].image?)
1115 self.node = None
1116 if len(self.parameters.get('nodes')) == 1:
1117 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001118 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001119 self.pipeline = self.parameters['ZUUL_PIPELINE']
1120 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001121 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.wait_condition = threading.Condition()
1123 self.waiting = False
1124 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001125 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001126 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001127 self.changes = None
1128 if 'ZUUL_CHANGE_IDS' in self.parameters:
1129 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001130
James E. Blair3158e282016-08-19 09:34:11 -07001131 def __repr__(self):
1132 waiting = ''
1133 if self.waiting:
1134 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001135 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1136 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001137
Clark Boylanb640e052014-04-03 16:41:46 -07001138 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001139 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001140 self.wait_condition.acquire()
1141 self.wait_condition.notify()
1142 self.waiting = False
1143 self.log.debug("Build %s released" % self.unique)
1144 self.wait_condition.release()
1145
1146 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001147 """Return whether this build is being held.
1148
1149 :returns: Whether the build is being held.
1150 :rtype: bool
1151 """
1152
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.wait_condition.acquire()
1154 if self.waiting:
1155 ret = True
1156 else:
1157 ret = False
1158 self.wait_condition.release()
1159 return ret
1160
1161 def _wait(self):
1162 self.wait_condition.acquire()
1163 self.waiting = True
1164 self.log.debug("Build %s waiting" % self.unique)
1165 self.wait_condition.wait()
1166 self.wait_condition.release()
1167
1168 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001169 self.log.debug('Running build %s' % self.unique)
1170
Paul Belanger174a8272017-03-14 13:20:10 -04001171 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001172 self.log.debug('Holding build %s' % self.unique)
1173 self._wait()
1174 self.log.debug("Build %s continuing" % self.unique)
1175
James E. Blair412fba82017-01-26 15:00:50 -08001176 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001177 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001178 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001179 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001180 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001181 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001182 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001183
James E. Blaire1767bc2016-08-02 10:00:27 -07001184 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001185
James E. Blaira5dba232016-08-08 15:53:24 -07001186 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001187 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001188 for change in changes:
1189 if self.hasChanges(change):
1190 return True
1191 return False
1192
James E. Blaire7b99a02016-08-05 14:27:34 -07001193 def hasChanges(self, *changes):
1194 """Return whether this build has certain changes in its git repos.
1195
1196 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001197 are expected to be present (in order) in the git repository of
1198 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001199
1200 :returns: Whether the build has the indicated changes.
1201 :rtype: bool
1202
1203 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001204 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001205 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001206 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001207 try:
1208 repo = git.Repo(path)
1209 except NoSuchPathError as e:
1210 self.log.debug('%s' % e)
1211 return False
1212 ref = self.parameters['ZUUL_REF']
1213 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1214 commit_message = '%s-1' % change.subject
1215 self.log.debug("Checking if build %s has changes; commit_message "
1216 "%s; repo_messages %s" % (self, commit_message,
1217 repo_messages))
1218 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001219 self.log.debug(" messages do not match")
1220 return False
1221 self.log.debug(" OK")
1222 return True
1223
Clark Boylanb640e052014-04-03 16:41:46 -07001224
Paul Belanger174a8272017-03-14 13:20:10 -04001225class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1226 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001227
Paul Belanger174a8272017-03-14 13:20:10 -04001228 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001229 they will report that they have started but then pause until
1230 released before reporting completion. This attribute may be
1231 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001232 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001233 be explicitly released.
1234
1235 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001236 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001237 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001238 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001239 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001240 self.hold_jobs_in_build = False
1241 self.lock = threading.Lock()
1242 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001243 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001244 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001245 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001246
James E. Blaira5dba232016-08-08 15:53:24 -07001247 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001248 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001249
1250 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001251 :arg Change change: The :py:class:`~tests.base.FakeChange`
1252 instance which should cause the job to fail. This job
1253 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001254
1255 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 l = self.fail_tests.get(name, [])
1257 l.append(change)
1258 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001259
James E. Blair962220f2016-08-03 11:22:38 -07001260 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001261 """Release a held build.
1262
1263 :arg str regex: A regular expression which, if supplied, will
1264 cause only builds with matching names to be released. If
1265 not supplied, all builds will be released.
1266
1267 """
James E. Blair962220f2016-08-03 11:22:38 -07001268 builds = self.running_builds[:]
1269 self.log.debug("Releasing build %s (%s)" % (regex,
1270 len(self.running_builds)))
1271 for build in builds:
1272 if not regex or re.match(regex, build.name):
1273 self.log.debug("Releasing build %s" %
1274 (build.parameters['ZUUL_UUID']))
1275 build.release()
1276 else:
1277 self.log.debug("Not releasing build %s" %
1278 (build.parameters['ZUUL_UUID']))
1279 self.log.debug("Done releasing builds %s (%s)" %
1280 (regex, len(self.running_builds)))
1281
Paul Belanger174a8272017-03-14 13:20:10 -04001282 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001283 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001284 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001285 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001286 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001287 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001288 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001289 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001290 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1291 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001292
1293 def stopJob(self, job):
1294 self.log.debug("handle stop")
1295 parameters = json.loads(job.arguments)
1296 uuid = parameters['uuid']
1297 for build in self.running_builds:
1298 if build.unique == uuid:
1299 build.aborted = True
1300 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001301 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001302
James E. Blaira002b032017-04-18 10:35:48 -07001303 def stop(self):
1304 for build in self.running_builds:
1305 build.release()
1306 super(RecordingExecutorServer, self).stop()
1307
Joshua Hesketh50c21782016-10-13 21:34:14 +11001308
Paul Belanger174a8272017-03-14 13:20:10 -04001309class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blair1960d682017-04-28 15:44:14 -07001310 def doMergeChanges(self, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001311 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001312 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1313 items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001314 if not commit: # merge conflict
1315 self.recordResult('MERGER_FAILURE')
1316 return commit
1317
1318 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001319 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001320 self.executor_server.lock.acquire()
1321 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001322 BuildHistory(name=build.name, result=result, changes=build.changes,
1323 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001324 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001325 pipeline=build.parameters['ZUUL_PIPELINE'])
1326 )
Paul Belanger174a8272017-03-14 13:20:10 -04001327 self.executor_server.running_builds.remove(build)
1328 del self.executor_server.job_builds[self.job.unique]
1329 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001330
1331 def runPlaybooks(self, args):
1332 build = self.executor_server.job_builds[self.job.unique]
1333 build.jobdir = self.jobdir
1334
1335 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1336 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001337 return result
1338
Monty Taylore6562aa2017-02-20 07:37:39 -05001339 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001340 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001341
Paul Belanger174a8272017-03-14 13:20:10 -04001342 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001343 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001344 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001345 else:
1346 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001347 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001348
James E. Blairad8dca02017-02-21 11:48:32 -05001349 def getHostList(self, args):
1350 self.log.debug("hostlist")
1351 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001352 for host in hosts:
1353 host['host_vars']['ansible_connection'] = 'local'
1354
1355 hosts.append(dict(
1356 name='localhost',
1357 host_vars=dict(ansible_connection='local'),
1358 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001359 return hosts
1360
James E. Blairf5dbd002015-12-23 15:26:17 -08001361
Clark Boylanb640e052014-04-03 16:41:46 -07001362class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001363 """A Gearman server for use in tests.
1364
1365 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1366 added to the queue but will not be distributed to workers
1367 until released. This attribute may be changed at any time and
1368 will take effect for subsequently enqueued jobs, but
1369 previously held jobs will still need to be explicitly
1370 released.
1371
1372 """
1373
Clark Boylanb640e052014-04-03 16:41:46 -07001374 def __init__(self):
1375 self.hold_jobs_in_queue = False
1376 super(FakeGearmanServer, self).__init__(0)
1377
1378 def getJobForConnection(self, connection, peek=False):
1379 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1380 for job in queue:
1381 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001382 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001383 job.waiting = self.hold_jobs_in_queue
1384 else:
1385 job.waiting = False
1386 if job.waiting:
1387 continue
1388 if job.name in connection.functions:
1389 if not peek:
1390 queue.remove(job)
1391 connection.related_jobs[job.handle] = job
1392 job.worker_connection = connection
1393 job.running = True
1394 return job
1395 return None
1396
1397 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001398 """Release a held job.
1399
1400 :arg str regex: A regular expression which, if supplied, will
1401 cause only jobs with matching names to be released. If
1402 not supplied, all jobs will be released.
1403 """
Clark Boylanb640e052014-04-03 16:41:46 -07001404 released = False
1405 qlen = (len(self.high_queue) + len(self.normal_queue) +
1406 len(self.low_queue))
1407 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1408 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001409 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001410 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001411 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001412 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001413 self.log.debug("releasing queued job %s" %
1414 job.unique)
1415 job.waiting = False
1416 released = True
1417 else:
1418 self.log.debug("not releasing queued job %s" %
1419 job.unique)
1420 if released:
1421 self.wakeConnections()
1422 qlen = (len(self.high_queue) + len(self.normal_queue) +
1423 len(self.low_queue))
1424 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1425
1426
1427class FakeSMTP(object):
1428 log = logging.getLogger('zuul.FakeSMTP')
1429
1430 def __init__(self, messages, server, port):
1431 self.server = server
1432 self.port = port
1433 self.messages = messages
1434
1435 def sendmail(self, from_email, to_email, msg):
1436 self.log.info("Sending email from %s, to %s, with msg %s" % (
1437 from_email, to_email, msg))
1438
1439 headers = msg.split('\n\n', 1)[0]
1440 body = msg.split('\n\n', 1)[1]
1441
1442 self.messages.append(dict(
1443 from_email=from_email,
1444 to_email=to_email,
1445 msg=msg,
1446 headers=headers,
1447 body=body,
1448 ))
1449
1450 return True
1451
1452 def quit(self):
1453 return True
1454
1455
James E. Blairdce6cea2016-12-20 16:45:32 -08001456class FakeNodepool(object):
1457 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001458 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001459
1460 log = logging.getLogger("zuul.test.FakeNodepool")
1461
1462 def __init__(self, host, port, chroot):
1463 self.client = kazoo.client.KazooClient(
1464 hosts='%s:%s%s' % (host, port, chroot))
1465 self.client.start()
1466 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001467 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001468 self.thread = threading.Thread(target=self.run)
1469 self.thread.daemon = True
1470 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001471 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001472
1473 def stop(self):
1474 self._running = False
1475 self.thread.join()
1476 self.client.stop()
1477 self.client.close()
1478
1479 def run(self):
1480 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001481 try:
1482 self._run()
1483 except Exception:
1484 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001485 time.sleep(0.1)
1486
1487 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001488 if self.paused:
1489 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001490 for req in self.getNodeRequests():
1491 self.fulfillRequest(req)
1492
1493 def getNodeRequests(self):
1494 try:
1495 reqids = self.client.get_children(self.REQUEST_ROOT)
1496 except kazoo.exceptions.NoNodeError:
1497 return []
1498 reqs = []
1499 for oid in sorted(reqids):
1500 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001501 try:
1502 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001503 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001504 data['_oid'] = oid
1505 reqs.append(data)
1506 except kazoo.exceptions.NoNodeError:
1507 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001508 return reqs
1509
James E. Blaire18d4602017-01-05 11:17:28 -08001510 def getNodes(self):
1511 try:
1512 nodeids = self.client.get_children(self.NODE_ROOT)
1513 except kazoo.exceptions.NoNodeError:
1514 return []
1515 nodes = []
1516 for oid in sorted(nodeids):
1517 path = self.NODE_ROOT + '/' + oid
1518 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001519 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001520 data['_oid'] = oid
1521 try:
1522 lockfiles = self.client.get_children(path + '/lock')
1523 except kazoo.exceptions.NoNodeError:
1524 lockfiles = []
1525 if lockfiles:
1526 data['_lock'] = True
1527 else:
1528 data['_lock'] = False
1529 nodes.append(data)
1530 return nodes
1531
James E. Blaira38c28e2017-01-04 10:33:20 -08001532 def makeNode(self, request_id, node_type):
1533 now = time.time()
1534 path = '/nodepool/nodes/'
1535 data = dict(type=node_type,
1536 provider='test-provider',
1537 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001538 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001539 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001540 public_ipv4='127.0.0.1',
1541 private_ipv4=None,
1542 public_ipv6=None,
1543 allocated_to=request_id,
1544 state='ready',
1545 state_time=now,
1546 created_time=now,
1547 updated_time=now,
1548 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001549 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001550 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001551 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001552 path = self.client.create(path, data,
1553 makepath=True,
1554 sequence=True)
1555 nodeid = path.split("/")[-1]
1556 return nodeid
1557
James E. Blair6ab79e02017-01-06 10:10:17 -08001558 def addFailRequest(self, request):
1559 self.fail_requests.add(request['_oid'])
1560
James E. Blairdce6cea2016-12-20 16:45:32 -08001561 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001562 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001563 return
1564 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001565 oid = request['_oid']
1566 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001567
James E. Blair6ab79e02017-01-06 10:10:17 -08001568 if oid in self.fail_requests:
1569 request['state'] = 'failed'
1570 else:
1571 request['state'] = 'fulfilled'
1572 nodes = []
1573 for node in request['node_types']:
1574 nodeid = self.makeNode(oid, node)
1575 nodes.append(nodeid)
1576 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001577
James E. Blaira38c28e2017-01-04 10:33:20 -08001578 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001579 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001580 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001581 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001582 try:
1583 self.client.set(path, data)
1584 except kazoo.exceptions.NoNodeError:
1585 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001586
1587
James E. Blair498059b2016-12-20 13:50:13 -08001588class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001589 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001590 super(ChrootedKazooFixture, self).__init__()
1591
1592 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1593 if ':' in zk_host:
1594 host, port = zk_host.split(':')
1595 else:
1596 host = zk_host
1597 port = None
1598
1599 self.zookeeper_host = host
1600
1601 if not port:
1602 self.zookeeper_port = 2181
1603 else:
1604 self.zookeeper_port = int(port)
1605
Clark Boylan621ec9a2017-04-07 17:41:33 -07001606 self.test_id = test_id
1607
James E. Blair498059b2016-12-20 13:50:13 -08001608 def _setUp(self):
1609 # Make sure the test chroot paths do not conflict
1610 random_bits = ''.join(random.choice(string.ascii_lowercase +
1611 string.ascii_uppercase)
1612 for x in range(8))
1613
Clark Boylan621ec9a2017-04-07 17:41:33 -07001614 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001615 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1616
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001617 self.addCleanup(self._cleanup)
1618
James E. Blair498059b2016-12-20 13:50:13 -08001619 # Ensure the chroot path exists and clean up any pre-existing znodes.
1620 _tmp_client = kazoo.client.KazooClient(
1621 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1622 _tmp_client.start()
1623
1624 if _tmp_client.exists(self.zookeeper_chroot):
1625 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1626
1627 _tmp_client.ensure_path(self.zookeeper_chroot)
1628 _tmp_client.stop()
1629 _tmp_client.close()
1630
James E. Blair498059b2016-12-20 13:50:13 -08001631 def _cleanup(self):
1632 '''Remove the chroot path.'''
1633 # Need a non-chroot'ed client to remove the chroot path
1634 _tmp_client = kazoo.client.KazooClient(
1635 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1636 _tmp_client.start()
1637 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1638 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001639 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001640
1641
Joshua Heskethd78b4482015-09-14 16:56:34 -06001642class MySQLSchemaFixture(fixtures.Fixture):
1643 def setUp(self):
1644 super(MySQLSchemaFixture, self).setUp()
1645
1646 random_bits = ''.join(random.choice(string.ascii_lowercase +
1647 string.ascii_uppercase)
1648 for x in range(8))
1649 self.name = '%s_%s' % (random_bits, os.getpid())
1650 self.passwd = uuid.uuid4().hex
1651 db = pymysql.connect(host="localhost",
1652 user="openstack_citest",
1653 passwd="openstack_citest",
1654 db="openstack_citest")
1655 cur = db.cursor()
1656 cur.execute("create database %s" % self.name)
1657 cur.execute(
1658 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1659 (self.name, self.name, self.passwd))
1660 cur.execute("flush privileges")
1661
1662 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1663 self.passwd,
1664 self.name)
1665 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1666 self.addCleanup(self.cleanup)
1667
1668 def cleanup(self):
1669 db = pymysql.connect(host="localhost",
1670 user="openstack_citest",
1671 passwd="openstack_citest",
1672 db="openstack_citest")
1673 cur = db.cursor()
1674 cur.execute("drop database %s" % self.name)
1675 cur.execute("drop user '%s'@'localhost'" % self.name)
1676 cur.execute("flush privileges")
1677
1678
Maru Newby3fe5f852015-01-13 04:22:14 +00001679class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001680 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001681 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001682
James E. Blair1c236df2017-02-01 14:07:24 -08001683 def attachLogs(self, *args):
1684 def reader():
1685 self._log_stream.seek(0)
1686 while True:
1687 x = self._log_stream.read(4096)
1688 if not x:
1689 break
1690 yield x.encode('utf8')
1691 content = testtools.content.content_from_reader(
1692 reader,
1693 testtools.content_type.UTF8_TEXT,
1694 False)
1695 self.addDetail('logging', content)
1696
Clark Boylanb640e052014-04-03 16:41:46 -07001697 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001698 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001699 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1700 try:
1701 test_timeout = int(test_timeout)
1702 except ValueError:
1703 # If timeout value is invalid do not set a timeout.
1704 test_timeout = 0
1705 if test_timeout > 0:
1706 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1707
1708 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1709 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1710 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1711 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1712 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1713 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1714 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1715 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1716 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1717 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001718 self._log_stream = StringIO()
1719 self.addOnException(self.attachLogs)
1720 else:
1721 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001722
James E. Blair73b41772017-05-22 13:22:55 -07001723 # NOTE(jeblair): this is temporary extra debugging to try to
1724 # track down a possible leak.
1725 orig_git_repo_init = git.Repo.__init__
1726
1727 def git_repo_init(myself, *args, **kw):
1728 orig_git_repo_init(myself, *args, **kw)
1729 self.log.debug("Created git repo 0x%x %s" %
1730 (id(myself), repr(myself)))
1731
1732 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1733 git_repo_init))
1734
James E. Blair1c236df2017-02-01 14:07:24 -08001735 handler = logging.StreamHandler(self._log_stream)
1736 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1737 '%(levelname)-8s %(message)s')
1738 handler.setFormatter(formatter)
1739
1740 logger = logging.getLogger()
1741 logger.setLevel(logging.DEBUG)
1742 logger.addHandler(handler)
1743
Clark Boylan3410d532017-04-25 12:35:29 -07001744 # Make sure we don't carry old handlers around in process state
1745 # which slows down test runs
1746 self.addCleanup(logger.removeHandler, handler)
1747 self.addCleanup(handler.close)
1748 self.addCleanup(handler.flush)
1749
James E. Blair1c236df2017-02-01 14:07:24 -08001750 # NOTE(notmorgan): Extract logging overrides for specific
1751 # libraries from the OS_LOG_DEFAULTS env and create loggers
1752 # for each. This is used to limit the output during test runs
1753 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001754 log_defaults_from_env = os.environ.get(
1755 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001756 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001757
James E. Blairdce6cea2016-12-20 16:45:32 -08001758 if log_defaults_from_env:
1759 for default in log_defaults_from_env.split(','):
1760 try:
1761 name, level_str = default.split('=', 1)
1762 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001763 logger = logging.getLogger(name)
1764 logger.setLevel(level)
1765 logger.addHandler(handler)
1766 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001767 except ValueError:
1768 # NOTE(notmorgan): Invalid format of the log default,
1769 # skip and don't try and apply a logger for the
1770 # specified module
1771 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001772
Maru Newby3fe5f852015-01-13 04:22:14 +00001773
1774class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001775 """A test case with a functioning Zuul.
1776
1777 The following class variables are used during test setup and can
1778 be overidden by subclasses but are effectively read-only once a
1779 test method starts running:
1780
1781 :cvar str config_file: This points to the main zuul config file
1782 within the fixtures directory. Subclasses may override this
1783 to obtain a different behavior.
1784
1785 :cvar str tenant_config_file: This is the tenant config file
1786 (which specifies from what git repos the configuration should
1787 be loaded). It defaults to the value specified in
1788 `config_file` but can be overidden by subclasses to obtain a
1789 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001790 configuration. See also the :py:func:`simple_layout`
1791 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001792
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001793 :cvar bool create_project_keys: Indicates whether Zuul should
1794 auto-generate keys for each project, or whether the test
1795 infrastructure should insert dummy keys to save time during
1796 startup. Defaults to False.
1797
James E. Blaire7b99a02016-08-05 14:27:34 -07001798 The following are instance variables that are useful within test
1799 methods:
1800
1801 :ivar FakeGerritConnection fake_<connection>:
1802 A :py:class:`~tests.base.FakeGerritConnection` will be
1803 instantiated for each connection present in the config file
1804 and stored here. For instance, `fake_gerrit` will hold the
1805 FakeGerritConnection object for a connection named `gerrit`.
1806
1807 :ivar FakeGearmanServer gearman_server: An instance of
1808 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1809 server that all of the Zuul components in this test use to
1810 communicate with each other.
1811
Paul Belanger174a8272017-03-14 13:20:10 -04001812 :ivar RecordingExecutorServer executor_server: An instance of
1813 :py:class:`~tests.base.RecordingExecutorServer` which is the
1814 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001815
1816 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1817 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001818 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001819 list upon completion.
1820
1821 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1822 objects representing completed builds. They are appended to
1823 the list in the order they complete.
1824
1825 """
1826
James E. Blair83005782015-12-11 14:46:03 -08001827 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001828 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001829 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001830
1831 def _startMerger(self):
1832 self.merge_server = zuul.merger.server.MergeServer(self.config,
1833 self.connections)
1834 self.merge_server.start()
1835
Maru Newby3fe5f852015-01-13 04:22:14 +00001836 def setUp(self):
1837 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001838
1839 self.setupZK()
1840
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001841 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001842 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001843 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1844 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001845 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001846 tmp_root = tempfile.mkdtemp(
1847 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001848 self.test_root = os.path.join(tmp_root, "zuul-test")
1849 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001850 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001851 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001852 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001853
1854 if os.path.exists(self.test_root):
1855 shutil.rmtree(self.test_root)
1856 os.makedirs(self.test_root)
1857 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001858 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001859
1860 # Make per test copy of Configuration.
1861 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001862 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1863 if not os.path.exists(self.private_key_file):
1864 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1865 shutil.copy(src_private_key_file, self.private_key_file)
1866 shutil.copy('{}.pub'.format(src_private_key_file),
1867 '{}.pub'.format(self.private_key_file))
1868 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001869 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001870 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001871 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001872 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001873 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001874 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001875 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001876
Clark Boylanb640e052014-04-03 16:41:46 -07001877 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001878 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1879 # see: https://github.com/jsocol/pystatsd/issues/61
1880 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001881 os.environ['STATSD_PORT'] = str(self.statsd.port)
1882 self.statsd.start()
1883 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001884 reload_module(statsd)
1885 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001886
1887 self.gearman_server = FakeGearmanServer()
1888
1889 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001890 self.log.info("Gearman server on port %s" %
1891 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001892
James E. Blaire511d2f2016-12-08 15:22:26 -08001893 gerritsource.GerritSource.replication_timeout = 1.5
1894 gerritsource.GerritSource.replication_retry_interval = 0.5
1895 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001896
Joshua Hesketh352264b2015-08-11 23:42:08 +10001897 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001898
Jan Hruban7083edd2015-08-21 14:00:54 +02001899 self.webapp = zuul.webapp.WebApp(
1900 self.sched, port=0, listen_address='127.0.0.1')
1901
Jan Hruban6b71aff2015-10-22 16:58:08 +02001902 self.event_queues = [
1903 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001904 self.sched.trigger_event_queue,
1905 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001906 ]
1907
James E. Blairfef78942016-03-11 16:28:56 -08001908 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001909 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001910
Clark Boylanb640e052014-04-03 16:41:46 -07001911 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001912 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001913 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001914 return FakeURLOpener(self.upstream_root, *args, **kw)
1915
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001916 old_urlopen = urllib.request.urlopen
1917 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001918
Paul Belanger174a8272017-03-14 13:20:10 -04001919 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001920 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001921 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001922 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001923 _test_root=self.test_root,
1924 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001925 self.executor_server.start()
1926 self.history = self.executor_server.build_history
1927 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001928
Paul Belanger174a8272017-03-14 13:20:10 -04001929 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001930 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001931 self.merge_client = zuul.merger.client.MergeClient(
1932 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001933 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001934 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001935 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001936
James E. Blair0d5a36e2017-02-21 10:53:44 -05001937 self.fake_nodepool = FakeNodepool(
1938 self.zk_chroot_fixture.zookeeper_host,
1939 self.zk_chroot_fixture.zookeeper_port,
1940 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001941
Paul Belanger174a8272017-03-14 13:20:10 -04001942 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001943 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001944 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001945 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001946
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001947 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001948
1949 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001950 self.webapp.start()
1951 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001952 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001953 # Cleanups are run in reverse order
1954 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001955 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001956 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001957
James E. Blairb9c0d772017-03-03 14:34:49 -08001958 self.sched.reconfigure(self.config)
1959 self.sched.resume()
1960
Tobias Henkel7df274b2017-05-26 17:41:11 +02001961 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001962 # Set up gerrit related fakes
1963 # Set a changes database so multiple FakeGerrit's can report back to
1964 # a virtual canonical database given by the configured hostname
1965 self.gerrit_changes_dbs = {}
1966
1967 def getGerritConnection(driver, name, config):
1968 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1969 con = FakeGerritConnection(driver, name, config,
1970 changes_db=db,
1971 upstream_root=self.upstream_root)
1972 self.event_queues.append(con.event_queue)
1973 setattr(self, 'fake_' + name, con)
1974 return con
1975
1976 self.useFixture(fixtures.MonkeyPatch(
1977 'zuul.driver.gerrit.GerritDriver.getConnection',
1978 getGerritConnection))
1979
Gregory Haynes4fc12542015-04-22 20:38:06 -07001980 def getGithubConnection(driver, name, config):
1981 con = FakeGithubConnection(driver, name, config,
1982 upstream_root=self.upstream_root)
1983 setattr(self, 'fake_' + name, con)
1984 return con
1985
1986 self.useFixture(fixtures.MonkeyPatch(
1987 'zuul.driver.github.GithubDriver.getConnection',
1988 getGithubConnection))
1989
James E. Blaire511d2f2016-12-08 15:22:26 -08001990 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001991 # TODO(jhesketh): This should come from lib.connections for better
1992 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001993 # Register connections from the config
1994 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001995
Joshua Hesketh352264b2015-08-11 23:42:08 +10001996 def FakeSMTPFactory(*args, **kw):
1997 args = [self.smtp_messages] + list(args)
1998 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001999
Joshua Hesketh352264b2015-08-11 23:42:08 +10002000 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002001
James E. Blaire511d2f2016-12-08 15:22:26 -08002002 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002003 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002004 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002005
James E. Blair83005782015-12-11 14:46:03 -08002006 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002007 # This creates the per-test configuration object. It can be
2008 # overriden by subclasses, but should not need to be since it
2009 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002010 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002011 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002012
2013 if not self.setupSimpleLayout():
2014 if hasattr(self, 'tenant_config_file'):
2015 self.config.set('zuul', 'tenant_config',
2016 self.tenant_config_file)
2017 git_path = os.path.join(
2018 os.path.dirname(
2019 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2020 'git')
2021 if os.path.exists(git_path):
2022 for reponame in os.listdir(git_path):
2023 project = reponame.replace('_', '/')
2024 self.copyDirToRepo(project,
2025 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002026 self.setupAllProjectKeys()
2027
James E. Blair06cc3922017-04-19 10:08:10 -07002028 def setupSimpleLayout(self):
2029 # If the test method has been decorated with a simple_layout,
2030 # use that instead of the class tenant_config_file. Set up a
2031 # single config-project with the specified layout, and
2032 # initialize repos for all of the 'project' entries which
2033 # appear in the layout.
2034 test_name = self.id().split('.')[-1]
2035 test = getattr(self, test_name)
2036 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002037 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002038 else:
2039 return False
2040
James E. Blairb70e55a2017-04-19 12:57:02 -07002041 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002042 path = os.path.join(FIXTURE_DIR, path)
2043 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002044 data = f.read()
2045 layout = yaml.safe_load(data)
2046 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002047 untrusted_projects = []
2048 for item in layout:
2049 if 'project' in item:
2050 name = item['project']['name']
2051 untrusted_projects.append(name)
2052 self.init_repo(name)
2053 self.addCommitToRepo(name, 'initial commit',
2054 files={'README': ''},
2055 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002056 if 'job' in item:
2057 jobname = item['job']['name']
2058 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002059
2060 root = os.path.join(self.test_root, "config")
2061 if not os.path.exists(root):
2062 os.makedirs(root)
2063 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2064 config = [{'tenant':
2065 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002066 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002067 {'config-projects': ['common-config'],
2068 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002069 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002070 f.close()
2071 self.config.set('zuul', 'tenant_config',
2072 os.path.join(FIXTURE_DIR, f.name))
2073
2074 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002075 self.addCommitToRepo('common-config', 'add content from fixture',
2076 files, branch='master', tag='init')
2077
2078 return True
2079
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002080 def setupAllProjectKeys(self):
2081 if self.create_project_keys:
2082 return
2083
2084 path = self.config.get('zuul', 'tenant_config')
2085 with open(os.path.join(FIXTURE_DIR, path)) as f:
2086 tenant_config = yaml.safe_load(f.read())
2087 for tenant in tenant_config:
2088 sources = tenant['tenant']['source']
2089 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002090 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002091 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002092 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002093 self.setupProjectKeys(source, project)
2094
2095 def setupProjectKeys(self, source, project):
2096 # Make sure we set up an RSA key for the project so that we
2097 # don't spend time generating one:
2098
2099 key_root = os.path.join(self.state_root, 'keys')
2100 if not os.path.isdir(key_root):
2101 os.mkdir(key_root, 0o700)
2102 private_key_file = os.path.join(key_root, source, project + '.pem')
2103 private_key_dir = os.path.dirname(private_key_file)
2104 self.log.debug("Installing test keys for project %s at %s" % (
2105 project, private_key_file))
2106 if not os.path.isdir(private_key_dir):
2107 os.makedirs(private_key_dir)
2108 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2109 with open(private_key_file, 'w') as o:
2110 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002111
James E. Blair498059b2016-12-20 13:50:13 -08002112 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002113 self.zk_chroot_fixture = self.useFixture(
2114 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002115 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002116 self.zk_chroot_fixture.zookeeper_host,
2117 self.zk_chroot_fixture.zookeeper_port,
2118 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002119
James E. Blair96c6bf82016-01-15 16:20:40 -08002120 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002121 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002122
2123 files = {}
2124 for (dirpath, dirnames, filenames) in os.walk(source_path):
2125 for filename in filenames:
2126 test_tree_filepath = os.path.join(dirpath, filename)
2127 common_path = os.path.commonprefix([test_tree_filepath,
2128 source_path])
2129 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2130 with open(test_tree_filepath, 'r') as f:
2131 content = f.read()
2132 files[relative_filepath] = content
2133 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002134 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002135
James E. Blaire18d4602017-01-05 11:17:28 -08002136 def assertNodepoolState(self):
2137 # Make sure that there are no pending requests
2138
2139 requests = self.fake_nodepool.getNodeRequests()
2140 self.assertEqual(len(requests), 0)
2141
2142 nodes = self.fake_nodepool.getNodes()
2143 for node in nodes:
2144 self.assertFalse(node['_lock'], "Node %s is locked" %
2145 (node['_oid'],))
2146
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002147 def assertNoGeneratedKeys(self):
2148 # Make sure that Zuul did not generate any project keys
2149 # (unless it was supposed to).
2150
2151 if self.create_project_keys:
2152 return
2153
2154 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2155 test_key = i.read()
2156
2157 key_root = os.path.join(self.state_root, 'keys')
2158 for root, dirname, files in os.walk(key_root):
2159 for fn in files:
2160 with open(os.path.join(root, fn)) as f:
2161 self.assertEqual(test_key, f.read())
2162
Clark Boylanb640e052014-04-03 16:41:46 -07002163 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002164 self.log.debug("Assert final state")
2165 # Make sure no jobs are running
2166 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002167 # Make sure that git.Repo objects have been garbage collected.
2168 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002169 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002170 gc.collect()
2171 for obj in gc.get_objects():
2172 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002173 self.log.debug("Leaked git repo object: 0x%x %s" %
2174 (id(obj), repr(obj)))
2175 for ref in gc.get_referrers(obj):
2176 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002177 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002178 if repos:
2179 for obj in gc.garbage:
2180 self.log.debug(" Garbage %s" % (repr(obj)))
2181 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002182 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002183 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002184 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002185 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002186 for tenant in self.sched.abide.tenants.values():
2187 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002188 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002189 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002190
2191 def shutdown(self):
2192 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002193 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002194 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002195 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002196 self.sched.stop()
2197 self.sched.join()
2198 self.statsd.stop()
2199 self.statsd.join()
2200 self.webapp.stop()
2201 self.webapp.join()
2202 self.rpc.stop()
2203 self.rpc.join()
2204 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002205 self.fake_nodepool.stop()
2206 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002207 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002208 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002209 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002210 # Further the pydevd threads also need to be whitelisted so debugging
2211 # e.g. in PyCharm is possible without breaking shutdown.
2212 whitelist = ['executor-watchdog',
2213 'pydevd.CommandThread',
2214 'pydevd.Reader',
2215 'pydevd.Writer',
2216 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002217 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002218 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002219 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002220 log_str = ""
2221 for thread_id, stack_frame in sys._current_frames().items():
2222 log_str += "Thread: %s\n" % thread_id
2223 log_str += "".join(traceback.format_stack(stack_frame))
2224 self.log.debug(log_str)
2225 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002226
James E. Blaira002b032017-04-18 10:35:48 -07002227 def assertCleanShutdown(self):
2228 pass
2229
James E. Blairc4ba97a2017-04-19 16:26:24 -07002230 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002231 parts = project.split('/')
2232 path = os.path.join(self.upstream_root, *parts[:-1])
2233 if not os.path.exists(path):
2234 os.makedirs(path)
2235 path = os.path.join(self.upstream_root, project)
2236 repo = git.Repo.init(path)
2237
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002238 with repo.config_writer() as config_writer:
2239 config_writer.set_value('user', 'email', 'user@example.com')
2240 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002241
Clark Boylanb640e052014-04-03 16:41:46 -07002242 repo.index.commit('initial commit')
2243 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002244 if tag:
2245 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002246
James E. Blair97d902e2014-08-21 13:25:56 -07002247 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002248 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002249 repo.git.clean('-x', '-f', '-d')
2250
James E. Blair97d902e2014-08-21 13:25:56 -07002251 def create_branch(self, project, branch):
2252 path = os.path.join(self.upstream_root, project)
2253 repo = git.Repo.init(path)
2254 fn = os.path.join(path, 'README')
2255
2256 branch_head = repo.create_head(branch)
2257 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002258 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002259 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002260 f.close()
2261 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002262 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002263
James E. Blair97d902e2014-08-21 13:25:56 -07002264 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002265 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002266 repo.git.clean('-x', '-f', '-d')
2267
Sachi King9f16d522016-03-16 12:20:45 +11002268 def create_commit(self, project):
2269 path = os.path.join(self.upstream_root, project)
2270 repo = git.Repo(path)
2271 repo.head.reference = repo.heads['master']
2272 file_name = os.path.join(path, 'README')
2273 with open(file_name, 'a') as f:
2274 f.write('creating fake commit\n')
2275 repo.index.add([file_name])
2276 commit = repo.index.commit('Creating a fake commit')
2277 return commit.hexsha
2278
James E. Blairf4a5f022017-04-18 14:01:10 -07002279 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002280 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002281 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002282 while len(self.builds):
2283 self.release(self.builds[0])
2284 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002285 i += 1
2286 if count is not None and i >= count:
2287 break
James E. Blairb8c16472015-05-05 14:55:26 -07002288
Clark Boylanb640e052014-04-03 16:41:46 -07002289 def release(self, job):
2290 if isinstance(job, FakeBuild):
2291 job.release()
2292 else:
2293 job.waiting = False
2294 self.log.debug("Queued job %s released" % job.unique)
2295 self.gearman_server.wakeConnections()
2296
2297 def getParameter(self, job, name):
2298 if isinstance(job, FakeBuild):
2299 return job.parameters[name]
2300 else:
2301 parameters = json.loads(job.arguments)
2302 return parameters[name]
2303
Clark Boylanb640e052014-04-03 16:41:46 -07002304 def haveAllBuildsReported(self):
2305 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002306 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002307 return False
2308 # Find out if every build that the worker has completed has been
2309 # reported back to Zuul. If it hasn't then that means a Gearman
2310 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002311 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002312 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002313 if not zbuild:
2314 # It has already been reported
2315 continue
2316 # It hasn't been reported yet.
2317 return False
2318 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002319 worker = self.executor_server.executor_worker
2320 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002321 if connection.state == 'GRAB_WAIT':
2322 return False
2323 return True
2324
2325 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002326 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002327 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002328 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002329 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002330 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002331 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002332 for j in conn.related_jobs.values():
2333 if j.unique == build.uuid:
2334 client_job = j
2335 break
2336 if not client_job:
2337 self.log.debug("%s is not known to the gearman client" %
2338 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002339 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002340 if not client_job.handle:
2341 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002342 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002343 server_job = self.gearman_server.jobs.get(client_job.handle)
2344 if not server_job:
2345 self.log.debug("%s is not known to the gearman server" %
2346 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002348 if not hasattr(server_job, 'waiting'):
2349 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002350 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002351 if server_job.waiting:
2352 continue
James E. Blair17302972016-08-10 16:11:42 -07002353 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002354 self.log.debug("%s has not reported start" % build)
2355 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002356 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002357 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002358 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002359 if worker_build:
2360 if worker_build.isWaiting():
2361 continue
2362 else:
2363 self.log.debug("%s is running" % worker_build)
2364 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002365 else:
James E. Blair962220f2016-08-03 11:22:38 -07002366 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002367 return False
James E. Blaira002b032017-04-18 10:35:48 -07002368 for (build_uuid, job_worker) in \
2369 self.executor_server.job_workers.items():
2370 if build_uuid not in seen_builds:
2371 self.log.debug("%s is not finalized" % build_uuid)
2372 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002373 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002374
James E. Blairdce6cea2016-12-20 16:45:32 -08002375 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002376 if self.fake_nodepool.paused:
2377 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002378 if self.sched.nodepool.requests:
2379 return False
2380 return True
2381
Jan Hruban6b71aff2015-10-22 16:58:08 +02002382 def eventQueuesEmpty(self):
2383 for queue in self.event_queues:
2384 yield queue.empty()
2385
2386 def eventQueuesJoin(self):
2387 for queue in self.event_queues:
2388 queue.join()
2389
Clark Boylanb640e052014-04-03 16:41:46 -07002390 def waitUntilSettled(self):
2391 self.log.debug("Waiting until settled...")
2392 start = time.time()
2393 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002394 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002395 self.log.error("Timeout waiting for Zuul to settle")
2396 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002397 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002398 self.log.error(" %s: %s" % (queue, queue.empty()))
2399 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002400 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002401 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002402 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002403 self.log.error("All requests completed: %s" %
2404 (self.areAllNodeRequestsComplete(),))
2405 self.log.error("Merge client jobs: %s" %
2406 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002407 raise Exception("Timeout waiting for Zuul to settle")
2408 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002409
Paul Belanger174a8272017-03-14 13:20:10 -04002410 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002411 # have all build states propogated to zuul?
2412 if self.haveAllBuildsReported():
2413 # Join ensures that the queue is empty _and_ events have been
2414 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002415 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002416 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002417 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002418 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002419 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002420 self.areAllNodeRequestsComplete() and
2421 all(self.eventQueuesEmpty())):
2422 # The queue empty check is placed at the end to
2423 # ensure that if a component adds an event between
2424 # when locked the run handler and checked that the
2425 # components were stable, we don't erroneously
2426 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002427 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002428 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002429 self.log.debug("...settled.")
2430 return
2431 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002432 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002433 self.sched.wake_event.wait(0.1)
2434
2435 def countJobResults(self, jobs, result):
2436 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002437 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002438
Monty Taylor0d926122017-05-24 08:07:56 -05002439 def getBuildByName(self, name):
2440 for build in self.builds:
2441 if build.name == name:
2442 return build
2443 raise Exception("Unable to find build %s" % name)
2444
James E. Blair96c6bf82016-01-15 16:20:40 -08002445 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002446 for job in self.history:
2447 if (job.name == name and
2448 (project is None or
2449 job.parameters['ZUUL_PROJECT'] == project)):
2450 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002451 raise Exception("Unable to find job %s in history" % name)
2452
2453 def assertEmptyQueues(self):
2454 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002455 for tenant in self.sched.abide.tenants.values():
2456 for pipeline in tenant.layout.pipelines.values():
2457 for queue in pipeline.queues:
2458 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002459 print('pipeline %s queue %s contents %s' % (
2460 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002461 self.assertEqual(len(queue.queue), 0,
2462 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002463
2464 def assertReportedStat(self, key, value=None, kind=None):
2465 start = time.time()
2466 while time.time() < (start + 5):
2467 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002468 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002469 if key == k:
2470 if value is None and kind is None:
2471 return
2472 elif value:
2473 if value == v:
2474 return
2475 elif kind:
2476 if v.endswith('|' + kind):
2477 return
2478 time.sleep(0.1)
2479
Clark Boylanb640e052014-04-03 16:41:46 -07002480 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002481
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002482 def assertBuilds(self, builds):
2483 """Assert that the running builds are as described.
2484
2485 The list of running builds is examined and must match exactly
2486 the list of builds described by the input.
2487
2488 :arg list builds: A list of dictionaries. Each item in the
2489 list must match the corresponding build in the build
2490 history, and each element of the dictionary must match the
2491 corresponding attribute of the build.
2492
2493 """
James E. Blair3158e282016-08-19 09:34:11 -07002494 try:
2495 self.assertEqual(len(self.builds), len(builds))
2496 for i, d in enumerate(builds):
2497 for k, v in d.items():
2498 self.assertEqual(
2499 getattr(self.builds[i], k), v,
2500 "Element %i in builds does not match" % (i,))
2501 except Exception:
2502 for build in self.builds:
2503 self.log.error("Running build: %s" % build)
2504 else:
2505 self.log.error("No running builds")
2506 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002507
James E. Blairb536ecc2016-08-31 10:11:42 -07002508 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002509 """Assert that the completed builds are as described.
2510
2511 The list of completed builds is examined and must match
2512 exactly the list of builds described by the input.
2513
2514 :arg list history: A list of dictionaries. Each item in the
2515 list must match the corresponding build in the build
2516 history, and each element of the dictionary must match the
2517 corresponding attribute of the build.
2518
James E. Blairb536ecc2016-08-31 10:11:42 -07002519 :arg bool ordered: If true, the history must match the order
2520 supplied, if false, the builds are permitted to have
2521 arrived in any order.
2522
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002523 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002524 def matches(history_item, item):
2525 for k, v in item.items():
2526 if getattr(history_item, k) != v:
2527 return False
2528 return True
James E. Blair3158e282016-08-19 09:34:11 -07002529 try:
2530 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002531 if ordered:
2532 for i, d in enumerate(history):
2533 if not matches(self.history[i], d):
2534 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002535 "Element %i in history does not match %s" %
2536 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002537 else:
2538 unseen = self.history[:]
2539 for i, d in enumerate(history):
2540 found = False
2541 for unseen_item in unseen:
2542 if matches(unseen_item, d):
2543 found = True
2544 unseen.remove(unseen_item)
2545 break
2546 if not found:
2547 raise Exception("No match found for element %i "
2548 "in history" % (i,))
2549 if unseen:
2550 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002551 except Exception:
2552 for build in self.history:
2553 self.log.error("Completed build: %s" % build)
2554 else:
2555 self.log.error("No completed builds")
2556 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002557
James E. Blair6ac368c2016-12-22 18:07:20 -08002558 def printHistory(self):
2559 """Log the build history.
2560
2561 This can be useful during tests to summarize what jobs have
2562 completed.
2563
2564 """
2565 self.log.debug("Build history:")
2566 for build in self.history:
2567 self.log.debug(build)
2568
James E. Blair59fdbac2015-12-07 17:08:06 -08002569 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002570 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2571
James E. Blair9ea70072017-04-19 16:05:30 -07002572 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002573 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002574 if not os.path.exists(root):
2575 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002576 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2577 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002578- tenant:
2579 name: openstack
2580 source:
2581 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002582 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002583 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002584 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002585 - org/project
2586 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002587 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002588 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002589 self.config.set('zuul', 'tenant_config',
2590 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002591 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002592
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002593 def addCommitToRepo(self, project, message, files,
2594 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002595 path = os.path.join(self.upstream_root, project)
2596 repo = git.Repo(path)
2597 repo.head.reference = branch
2598 zuul.merger.merger.reset_repo_to_head(repo)
2599 for fn, content in files.items():
2600 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002601 try:
2602 os.makedirs(os.path.dirname(fn))
2603 except OSError:
2604 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002605 with open(fn, 'w') as f:
2606 f.write(content)
2607 repo.index.add([fn])
2608 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002609 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002610 repo.heads[branch].commit = commit
2611 repo.head.reference = branch
2612 repo.git.clean('-x', '-f', '-d')
2613 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002614 if tag:
2615 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002616 return before
2617
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002618 def commitConfigUpdate(self, project_name, source_name):
2619 """Commit an update to zuul.yaml
2620
2621 This overwrites the zuul.yaml in the specificed project with
2622 the contents specified.
2623
2624 :arg str project_name: The name of the project containing
2625 zuul.yaml (e.g., common-config)
2626
2627 :arg str source_name: The path to the file (underneath the
2628 test fixture directory) whose contents should be used to
2629 replace zuul.yaml.
2630 """
2631
2632 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002633 files = {}
2634 with open(source_path, 'r') as f:
2635 data = f.read()
2636 layout = yaml.safe_load(data)
2637 files['zuul.yaml'] = data
2638 for item in layout:
2639 if 'job' in item:
2640 jobname = item['job']['name']
2641 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002642 before = self.addCommitToRepo(
2643 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002644 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002645 return before
2646
James E. Blair7fc8daa2016-08-08 15:37:15 -07002647 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002648
James E. Blair7fc8daa2016-08-08 15:37:15 -07002649 """Inject a Fake (Gerrit) event.
2650
2651 This method accepts a JSON-encoded event and simulates Zuul
2652 having received it from Gerrit. It could (and should)
2653 eventually apply to any connection type, but is currently only
2654 used with Gerrit connections. The name of the connection is
2655 used to look up the corresponding server, and the event is
2656 simulated as having been received by all Zuul connections
2657 attached to that server. So if two Gerrit connections in Zuul
2658 are connected to the same Gerrit server, and you invoke this
2659 method specifying the name of one of them, the event will be
2660 received by both.
2661
2662 .. note::
2663
2664 "self.fake_gerrit.addEvent" calls should be migrated to
2665 this method.
2666
2667 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002668 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002669 :arg str event: The JSON-encoded event.
2670
2671 """
2672 specified_conn = self.connections.connections[connection]
2673 for conn in self.connections.connections.values():
2674 if (isinstance(conn, specified_conn.__class__) and
2675 specified_conn.server == conn.server):
2676 conn.addEvent(event)
2677
James E. Blair3f876d52016-07-22 13:07:14 -07002678
2679class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002680 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002681 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002682
Joshua Heskethd78b4482015-09-14 16:56:34 -06002683
2684class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002685 def setup_config(self):
2686 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002687 for section_name in self.config.sections():
2688 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2689 section_name, re.I)
2690 if not con_match:
2691 continue
2692
2693 if self.config.get(section_name, 'driver') == 'sql':
2694 f = MySQLSchemaFixture()
2695 self.useFixture(f)
2696 if (self.config.get(section_name, 'dburi') ==
2697 '$MYSQL_FIXTURE_DBURI$'):
2698 self.config.set(section_name, 'dburi', f.dburi)