blob: 0aac6bdd72c0e03784240253e78ca20fe9a0752b [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
22import json
23import logging
24import os
Christian Berendt12d4d722014-06-07 21:03:45 +020025from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070026from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070027import random
28import re
29import select
30import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030031from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070032try:
33 from cStringIO import StringIO
34except Exception:
35 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070036import socket
37import string
38import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080039import sys
James E. Blairf84026c2015-12-08 16:11:46 -080040import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070041import threading
Clark Boylan8208c192017-04-24 18:08:08 -070042import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070043import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060044import uuid
45
Clark Boylanb640e052014-04-03 16:41:46 -070046
47import git
48import gear
49import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080050import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080051import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060052import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070053import statsd
54import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080055import testtools.content
56import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080057from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000058import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070059
James E. Blaire511d2f2016-12-08 15:22:26 -080060import zuul.driver.gerrit.gerritsource as gerritsource
61import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070062import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.scheduler
64import zuul.webapp
65import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040066import zuul.executor.server
67import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080068import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070069import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070070import zuul.merger.merger
71import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070072import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080073import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010074from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070075
76FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
77 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080078
79KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070080
Clark Boylanb640e052014-04-03 16:41:46 -070081
82def repack_repo(path):
83 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
84 output = subprocess.Popen(cmd, close_fds=True,
85 stdout=subprocess.PIPE,
86 stderr=subprocess.PIPE)
87 out = output.communicate()
88 if output.returncode:
89 raise Exception("git repack returned %d" % output.returncode)
90 return out
91
92
93def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040094 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070095
96
James E. Blaira190f3b2015-01-05 14:56:54 -080097def iterate_timeout(max_seconds, purpose):
98 start = time.time()
99 count = 0
100 while (time.time() < start + max_seconds):
101 count += 1
102 yield count
103 time.sleep(0)
104 raise Exception("Timeout waiting for %s" % purpose)
105
106
Jesse Keating436a5452017-04-20 11:48:41 -0700107def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700108 """Specify a layout file for use by a test method.
109
110 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700111 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700112
113 Some tests require only a very simple configuration. For those,
114 establishing a complete config directory hierachy is too much
115 work. In those cases, you can add a simple zuul.yaml file to the
116 test fixtures directory (in fixtures/layouts/foo.yaml) and use
117 this decorator to indicate the test method should use that rather
118 than the tenant config file specified by the test class.
119
120 The decorator will cause that layout file to be added to a
121 config-project called "common-config" and each "project" instance
122 referenced in the layout file will have a git repo automatically
123 initialized.
124 """
125
126 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700127 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700128 return test
129 return decorator
130
131
Gregory Haynes4fc12542015-04-22 20:38:06 -0700132class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700133 _common_path_default = "refs/changes"
134 _points_to_commits_only = True
135
136
Gregory Haynes4fc12542015-04-22 20:38:06 -0700137class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700138 categories = {'approved': ('Approved', -1, 1),
139 'code-review': ('Code-Review', -2, 2),
140 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700141
142 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700143 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700145 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.reported = 0
147 self.queried = 0
148 self.patchsets = []
149 self.number = number
150 self.project = project
151 self.branch = branch
152 self.subject = subject
153 self.latest_patchset = 0
154 self.depends_on_change = None
155 self.needed_by_changes = []
156 self.fail_merge = False
157 self.messages = []
158 self.data = {
159 'branch': branch,
160 'comments': [],
161 'commitMessage': subject,
162 'createdOn': time.time(),
163 'id': 'I' + random_sha1(),
164 'lastUpdated': time.time(),
165 'number': str(number),
166 'open': status == 'NEW',
167 'owner': {'email': 'user@example.com',
168 'name': 'User Name',
169 'username': 'username'},
170 'patchSets': self.patchsets,
171 'project': project,
172 'status': status,
173 'subject': subject,
174 'submitRecords': [],
175 'url': 'https://hostname/%s' % number}
176
177 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700178 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 self.data['submitRecords'] = self.getSubmitRecords()
180 self.open = status == 'NEW'
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 path = os.path.join(self.upstream_root, self.project)
184 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700185 ref = GerritChangeReference.create(
186 repo, '1/%s/%s' % (self.number, self.latest_patchset),
187 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700189 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.git.clean('-x', '-f', '-d')
191
192 path = os.path.join(self.upstream_root, self.project)
193 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for fn, content in files.items():
195 fn = os.path.join(path, fn)
196 with open(fn, 'w') as f:
197 f.write(content)
198 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700199 else:
200 for fni in range(100):
201 fn = os.path.join(path, str(fni))
202 f = open(fn, 'w')
203 for ci in range(4096):
204 f.write(random.choice(string.printable))
205 f.close()
206 repo.index.add([fn])
207
208 r = repo.index.commit(msg)
209 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700210 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700211 repo.git.clean('-x', '-f', '-d')
212 repo.heads['master'].checkout()
213 return r
214
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700216 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700218 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700219 data = ("test %s %s %s\n" %
220 (self.branch, self.number, self.latest_patchset))
221 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700222 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700223 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700224 ps_files = [{'file': '/COMMIT_MSG',
225 'type': 'ADDED'},
226 {'file': 'README',
227 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700228 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700229 ps_files.append({'file': f, 'type': 'ADDED'})
230 d = {'approvals': [],
231 'createdOn': time.time(),
232 'files': ps_files,
233 'number': str(self.latest_patchset),
234 'ref': 'refs/changes/1/%s/%s' % (self.number,
235 self.latest_patchset),
236 'revision': c.hexsha,
237 'uploader': {'email': 'user@example.com',
238 'name': 'User name',
239 'username': 'user'}}
240 self.data['currentPatchSet'] = d
241 self.patchsets.append(d)
242 self.data['submitRecords'] = self.getSubmitRecords()
243
244 def getPatchsetCreatedEvent(self, patchset):
245 event = {"type": "patchset-created",
246 "change": {"project": self.project,
247 "branch": self.branch,
248 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
249 "number": str(self.number),
250 "subject": self.subject,
251 "owner": {"name": "User Name"},
252 "url": "https://hostname/3"},
253 "patchSet": self.patchsets[patchset - 1],
254 "uploader": {"name": "User Name"}}
255 return event
256
257 def getChangeRestoredEvent(self):
258 event = {"type": "change-restored",
259 "change": {"project": self.project,
260 "branch": self.branch,
261 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
262 "number": str(self.number),
263 "subject": self.subject,
264 "owner": {"name": "User Name"},
265 "url": "https://hostname/3"},
266 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100267 "patchSet": self.patchsets[-1],
268 "reason": ""}
269 return event
270
271 def getChangeAbandonedEvent(self):
272 event = {"type": "change-abandoned",
273 "change": {"project": self.project,
274 "branch": self.branch,
275 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
276 "number": str(self.number),
277 "subject": self.subject,
278 "owner": {"name": "User Name"},
279 "url": "https://hostname/3"},
280 "abandoner": {"name": "User Name"},
281 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700282 "reason": ""}
283 return event
284
285 def getChangeCommentEvent(self, patchset):
286 event = {"type": "comment-added",
287 "change": {"project": self.project,
288 "branch": self.branch,
289 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
290 "number": str(self.number),
291 "subject": self.subject,
292 "owner": {"name": "User Name"},
293 "url": "https://hostname/3"},
294 "patchSet": self.patchsets[patchset - 1],
295 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700296 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700297 "description": "Code-Review",
298 "value": "0"}],
299 "comment": "This is a comment"}
300 return event
301
James E. Blairc2a5ed72017-02-20 14:12:01 -0500302 def getChangeMergedEvent(self):
303 event = {"submitter": {"name": "Jenkins",
304 "username": "jenkins"},
305 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
306 "patchSet": self.patchsets[-1],
307 "change": self.data,
308 "type": "change-merged",
309 "eventCreatedOn": 1487613810}
310 return event
311
James E. Blair8cce42e2016-10-18 08:18:36 -0700312 def getRefUpdatedEvent(self):
313 path = os.path.join(self.upstream_root, self.project)
314 repo = git.Repo(path)
315 oldrev = repo.heads[self.branch].commit.hexsha
316
317 event = {
318 "type": "ref-updated",
319 "submitter": {
320 "name": "User Name",
321 },
322 "refUpdate": {
323 "oldRev": oldrev,
324 "newRev": self.patchsets[-1]['revision'],
325 "refName": self.branch,
326 "project": self.project,
327 }
328 }
329 return event
330
Joshua Hesketh642824b2014-07-01 17:54:59 +1000331 def addApproval(self, category, value, username='reviewer_john',
332 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700333 if not granted_on:
334 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000335 approval = {
336 'description': self.categories[category][0],
337 'type': category,
338 'value': str(value),
339 'by': {
340 'username': username,
341 'email': username + '@example.com',
342 },
343 'grantedOn': int(granted_on)
344 }
Clark Boylanb640e052014-04-03 16:41:46 -0700345 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
346 if x['by']['username'] == username and x['type'] == category:
347 del self.patchsets[-1]['approvals'][i]
348 self.patchsets[-1]['approvals'].append(approval)
349 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000350 'author': {'email': 'author@example.com',
351 'name': 'Patchset Author',
352 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700353 'change': {'branch': self.branch,
354 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
355 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000356 'owner': {'email': 'owner@example.com',
357 'name': 'Change Owner',
358 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700359 'project': self.project,
360 'subject': self.subject,
361 'topic': 'master',
362 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000363 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700364 'patchSet': self.patchsets[-1],
365 'type': 'comment-added'}
366 self.data['submitRecords'] = self.getSubmitRecords()
367 return json.loads(json.dumps(event))
368
369 def getSubmitRecords(self):
370 status = {}
371 for cat in self.categories.keys():
372 status[cat] = 0
373
374 for a in self.patchsets[-1]['approvals']:
375 cur = status[a['type']]
376 cat_min, cat_max = self.categories[a['type']][1:]
377 new = int(a['value'])
378 if new == cat_min:
379 cur = new
380 elif abs(new) > abs(cur):
381 cur = new
382 status[a['type']] = cur
383
384 labels = []
385 ok = True
386 for typ, cat in self.categories.items():
387 cur = status[typ]
388 cat_min, cat_max = cat[1:]
389 if cur == cat_min:
390 value = 'REJECT'
391 ok = False
392 elif cur == cat_max:
393 value = 'OK'
394 else:
395 value = 'NEED'
396 ok = False
397 labels.append({'label': cat[0], 'status': value})
398 if ok:
399 return [{'status': 'OK'}]
400 return [{'status': 'NOT_READY',
401 'labels': labels}]
402
403 def setDependsOn(self, other, patchset):
404 self.depends_on_change = other
405 d = {'id': other.data['id'],
406 'number': other.data['number'],
407 'ref': other.patchsets[patchset - 1]['ref']
408 }
409 self.data['dependsOn'] = [d]
410
411 other.needed_by_changes.append(self)
412 needed = other.data.get('neededBy', [])
413 d = {'id': self.data['id'],
414 'number': self.data['number'],
415 'ref': self.patchsets[patchset - 1]['ref'],
416 'revision': self.patchsets[patchset - 1]['revision']
417 }
418 needed.append(d)
419 other.data['neededBy'] = needed
420
421 def query(self):
422 self.queried += 1
423 d = self.data.get('dependsOn')
424 if d:
425 d = d[0]
426 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
427 d['isCurrentPatchSet'] = True
428 else:
429 d['isCurrentPatchSet'] = False
430 return json.loads(json.dumps(self.data))
431
432 def setMerged(self):
433 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000434 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700435 return
436 if self.fail_merge:
437 return
438 self.data['status'] = 'MERGED'
439 self.open = False
440
441 path = os.path.join(self.upstream_root, self.project)
442 repo = git.Repo(path)
443 repo.heads[self.branch].commit = \
444 repo.commit(self.patchsets[-1]['revision'])
445
446 def setReported(self):
447 self.reported += 1
448
449
James E. Blaire511d2f2016-12-08 15:22:26 -0800450class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700451 """A Fake Gerrit connection for use in tests.
452
453 This subclasses
454 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
455 ability for tests to add changes to the fake Gerrit it represents.
456 """
457
Joshua Hesketh352264b2015-08-11 23:42:08 +1000458 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700459
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700461 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800462 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000463 connection_config)
464
James E. Blair7fc8daa2016-08-08 15:37:15 -0700465 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700466 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
467 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000468 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700469 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200470 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700471
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700472 def addFakeChange(self, project, branch, subject, status='NEW',
473 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700474 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700475 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700476 c = FakeGerritChange(self, self.change_number, project, branch,
477 subject, upstream_root=self.upstream_root,
478 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700479 self.changes[self.change_number] = c
480 return c
481
Clark Boylanb640e052014-04-03 16:41:46 -0700482 def review(self, project, changeid, message, action):
483 number, ps = changeid.split(',')
484 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000485
486 # Add the approval back onto the change (ie simulate what gerrit would
487 # do).
488 # Usually when zuul leaves a review it'll create a feedback loop where
489 # zuul's review enters another gerrit event (which is then picked up by
490 # zuul). However, we can't mimic this behaviour (by adding this
491 # approval event into the queue) as it stops jobs from checking what
492 # happens before this event is triggered. If a job needs to see what
493 # happens they can add their own verified event into the queue.
494 # Nevertheless, we can update change with the new review in gerrit.
495
James E. Blair8b5408c2016-08-08 15:37:46 -0700496 for cat in action.keys():
497 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000498 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000501
Clark Boylanb640e052014-04-03 16:41:46 -0700502 if 'submit' in action:
503 change.setMerged()
504 if message:
505 change.setReported()
506
507 def query(self, number):
508 change = self.changes.get(int(number))
509 if change:
510 return change.query()
511 return {}
512
James E. Blairc494d542014-08-06 09:23:52 -0700513 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700514 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700515 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800516 if query.startswith('change:'):
517 # Query a specific changeid
518 changeid = query[len('change:'):]
519 l = [change.query() for change in self.changes.values()
520 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700521 elif query.startswith('message:'):
522 # Query the content of a commit message
523 msg = query[len('message:'):].strip()
524 l = [change.query() for change in self.changes.values()
525 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800526 else:
527 # Query all open changes
528 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700529 return l
James E. Blairc494d542014-08-06 09:23:52 -0700530
Joshua Hesketh352264b2015-08-11 23:42:08 +1000531 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700532 pass
533
Joshua Hesketh352264b2015-08-11 23:42:08 +1000534 def getGitUrl(self, project):
535 return os.path.join(self.upstream_root, project.name)
536
Clark Boylanb640e052014-04-03 16:41:46 -0700537
Gregory Haynes4fc12542015-04-22 20:38:06 -0700538class GithubChangeReference(git.Reference):
539 _common_path_default = "refs/pull"
540 _points_to_commits_only = True
541
542
543class FakeGithubPullRequest(object):
544
545 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800546 subject, upstream_root, files=[], number_of_commits=1,
547 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548 """Creates a new PR with several commits.
549 Sends an event about opened PR."""
550 self.github = github
551 self.source = github
552 self.number = number
553 self.project = project
554 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100555 self.subject = subject
556 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700557 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100558 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700559 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100560 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100561 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800562 self.reviews = []
563 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self.updated_at = None
565 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100566 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100567 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700568 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
572
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
577
Jan Hruban570d01c2016-03-10 21:51:32 +0100578 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100580 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581 self._updateTimeStamp()
582
583 def getPullRequestOpenedEvent(self):
584 return self._getPullRequestEvent('opened')
585
586 def getPullRequestSynchronizeEvent(self):
587 return self._getPullRequestEvent('synchronize')
588
589 def getPullRequestReopenedEvent(self):
590 return self._getPullRequestEvent('reopened')
591
592 def getPullRequestClosedEvent(self):
593 return self._getPullRequestEvent('closed')
594
595 def addComment(self, message):
596 self.comments.append(message)
597 self._updateTimeStamp()
598
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200599 def getCommentAddedEvent(self, text):
600 name = 'issue_comment'
601 data = {
602 'action': 'created',
603 'issue': {
604 'number': self.number
605 },
606 'comment': {
607 'body': text
608 },
609 'repository': {
610 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100611 },
612 'sender': {
613 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200614 }
615 }
616 return (name, data)
617
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800618 def getReviewAddedEvent(self, review):
619 name = 'pull_request_review'
620 data = {
621 'action': 'submitted',
622 'pull_request': {
623 'number': self.number,
624 'title': self.subject,
625 'updated_at': self.updated_at,
626 'base': {
627 'ref': self.branch,
628 'repo': {
629 'full_name': self.project
630 }
631 },
632 'head': {
633 'sha': self.head_sha
634 }
635 },
636 'review': {
637 'state': review
638 },
639 'repository': {
640 'full_name': self.project
641 },
642 'sender': {
643 'login': 'ghuser'
644 }
645 }
646 return (name, data)
647
Jan Hruban16ad31f2015-11-07 14:39:07 +0100648 def addLabel(self, name):
649 if name not in self.labels:
650 self.labels.append(name)
651 self._updateTimeStamp()
652 return self._getLabelEvent(name)
653
654 def removeLabel(self, name):
655 if name in self.labels:
656 self.labels.remove(name)
657 self._updateTimeStamp()
658 return self._getUnlabelEvent(name)
659
660 def _getLabelEvent(self, label):
661 name = 'pull_request'
662 data = {
663 'action': 'labeled',
664 'pull_request': {
665 'number': self.number,
666 'updated_at': self.updated_at,
667 'base': {
668 'ref': self.branch,
669 'repo': {
670 'full_name': self.project
671 }
672 },
673 'head': {
674 'sha': self.head_sha
675 }
676 },
677 'label': {
678 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100679 },
680 'sender': {
681 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100682 }
683 }
684 return (name, data)
685
686 def _getUnlabelEvent(self, label):
687 name = 'pull_request'
688 data = {
689 'action': 'unlabeled',
690 'pull_request': {
691 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100692 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100693 'updated_at': self.updated_at,
694 'base': {
695 'ref': self.branch,
696 'repo': {
697 'full_name': self.project
698 }
699 },
700 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800701 'sha': self.head_sha,
702 'repo': {
703 'full_name': self.project
704 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100705 }
706 },
707 'label': {
708 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100709 },
710 'sender': {
711 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100712 }
713 }
714 return (name, data)
715
Gregory Haynes4fc12542015-04-22 20:38:06 -0700716 def _getRepo(self):
717 repo_path = os.path.join(self.upstream_root, self.project)
718 return git.Repo(repo_path)
719
720 def _createPRRef(self):
721 repo = self._getRepo()
722 GithubChangeReference.create(
723 repo, self._getPRReference(), 'refs/tags/init')
724
Jan Hruban570d01c2016-03-10 21:51:32 +0100725 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700726 repo = self._getRepo()
727 ref = repo.references[self._getPRReference()]
728 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100729 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700730 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100731 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700732 repo.head.reference = ref
733 zuul.merger.merger.reset_repo_to_head(repo)
734 repo.git.clean('-x', '-f', '-d')
735
Jan Hruban570d01c2016-03-10 21:51:32 +0100736 if files:
737 fn = files[0]
738 self.files = files
739 else:
740 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
741 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100742 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700743 fn = os.path.join(repo.working_dir, fn)
744 f = open(fn, 'w')
745 with open(fn, 'w') as f:
746 f.write("test %s %s\n" %
747 (self.branch, self.number))
748 repo.index.add([fn])
749
750 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800751 # Create an empty set of statuses for the given sha,
752 # each sha on a PR may have a status set on it
753 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700754 repo.head.reference = 'master'
755 zuul.merger.merger.reset_repo_to_head(repo)
756 repo.git.clean('-x', '-f', '-d')
757 repo.heads['master'].checkout()
758
759 def _updateTimeStamp(self):
760 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
761
762 def getPRHeadSha(self):
763 repo = self._getRepo()
764 return repo.references[self._getPRReference()].commit.hexsha
765
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800766 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800767 # Since we're bypassing github API, which would require a user, we
768 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800769 # insert the status at the top of the list, to simulate that it
770 # is the most recent set status
771 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100772 'state': state,
773 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800774 'description': description,
775 'context': context,
776 'creator': {
777 'login': user
778 }
779 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100780
Jesse Keatingae4cd272017-01-30 17:10:44 -0800781 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800782 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
783 # convert the timestamp to a str format that would be returned
784 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800785
Adam Gandelmand81dd762017-02-09 15:15:49 -0800786 if granted_on:
787 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
788 submitted_at = time.strftime(
789 gh_time_format, granted_on.timetuple())
790 else:
791 # github timestamps only down to the second, so we need to make
792 # sure reviews that tests add appear to be added over a period of
793 # time in the past and not all at once.
794 if not self.reviews:
795 # the first review happens 10 mins ago
796 offset = 600
797 else:
798 # subsequent reviews happen 1 minute closer to now
799 offset = 600 - (len(self.reviews) * 60)
800
801 granted_on = datetime.datetime.utcfromtimestamp(
802 time.time() - offset)
803 submitted_at = time.strftime(
804 gh_time_format, granted_on.timetuple())
805
Jesse Keatingae4cd272017-01-30 17:10:44 -0800806 self.reviews.append({
807 'state': state,
808 'user': {
809 'login': user,
810 'email': user + "@derp.com",
811 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800812 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800813 })
814
Gregory Haynes4fc12542015-04-22 20:38:06 -0700815 def _getPRReference(self):
816 return '%s/head' % self.number
817
818 def _getPullRequestEvent(self, action):
819 name = 'pull_request'
820 data = {
821 'action': action,
822 'number': self.number,
823 'pull_request': {
824 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100825 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700826 'updated_at': self.updated_at,
827 'base': {
828 'ref': self.branch,
829 'repo': {
830 'full_name': self.project
831 }
832 },
833 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800834 'sha': self.head_sha,
835 'repo': {
836 'full_name': self.project
837 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700838 }
Jan Hruban3b415922016-02-03 13:10:22 +0100839 },
840 'sender': {
841 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700842 }
843 }
844 return (name, data)
845
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800846 def getCommitStatusEvent(self, context, state='success', user='zuul'):
847 name = 'status'
848 data = {
849 'state': state,
850 'sha': self.head_sha,
851 'description': 'Test results for %s: %s' % (self.head_sha, state),
852 'target_url': 'http://zuul/%s' % self.head_sha,
853 'branches': [],
854 'context': context,
855 'sender': {
856 'login': user
857 }
858 }
859 return (name, data)
860
Gregory Haynes4fc12542015-04-22 20:38:06 -0700861
862class FakeGithubConnection(githubconnection.GithubConnection):
863 log = logging.getLogger("zuul.test.FakeGithubConnection")
864
865 def __init__(self, driver, connection_name, connection_config,
866 upstream_root=None):
867 super(FakeGithubConnection, self).__init__(driver, connection_name,
868 connection_config)
869 self.connection_name = connection_name
870 self.pr_number = 0
871 self.pull_requests = []
872 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100873 self.merge_failure = False
874 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700875
Jan Hruban570d01c2016-03-10 21:51:32 +0100876 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 self.pr_number += 1
878 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100879 self, self.pr_number, project, branch, subject, self.upstream_root,
880 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700881 self.pull_requests.append(pull_request)
882 return pull_request
883
Wayne1a78c612015-06-11 17:14:13 -0700884 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
885 if not old_rev:
886 old_rev = '00000000000000000000000000000000'
887 if not new_rev:
888 new_rev = random_sha1()
889 name = 'push'
890 data = {
891 'ref': ref,
892 'before': old_rev,
893 'after': new_rev,
894 'repository': {
895 'full_name': project
896 }
897 }
898 return (name, data)
899
Gregory Haynes4fc12542015-04-22 20:38:06 -0700900 def emitEvent(self, event):
901 """Emulates sending the GitHub webhook event to the connection."""
902 port = self.webapp.server.socket.getsockname()[1]
903 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700904 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700905 headers = {'X-Github-Event': name}
906 req = urllib.request.Request(
907 'http://localhost:%s/connection/%s/payload'
908 % (port, self.connection_name),
909 data=payload, headers=headers)
910 urllib.request.urlopen(req)
911
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200912 def getPull(self, project, number):
913 pr = self.pull_requests[number - 1]
914 data = {
915 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100916 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200917 'updated_at': pr.updated_at,
918 'base': {
919 'repo': {
920 'full_name': pr.project
921 },
922 'ref': pr.branch,
923 },
Jan Hruban37615e52015-11-19 14:30:49 +0100924 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700925 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200926 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800927 'sha': pr.head_sha,
928 'repo': {
929 'full_name': pr.project
930 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200931 }
932 }
933 return data
934
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800935 def getPullBySha(self, sha):
936 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
937 if len(prs) > 1:
938 raise Exception('Multiple pulls found with head sha: %s' % sha)
939 pr = prs[0]
940 return self.getPull(pr.project, pr.number)
941
Jan Hruban570d01c2016-03-10 21:51:32 +0100942 def getPullFileNames(self, project, number):
943 pr = self.pull_requests[number - 1]
944 return pr.files
945
Jesse Keatingae4cd272017-01-30 17:10:44 -0800946 def _getPullReviews(self, owner, project, number):
947 pr = self.pull_requests[number - 1]
948 return pr.reviews
949
Jan Hruban3b415922016-02-03 13:10:22 +0100950 def getUser(self, login):
951 data = {
952 'username': login,
953 'name': 'Github User',
954 'email': 'github.user@example.com'
955 }
956 return data
957
Jesse Keatingae4cd272017-01-30 17:10:44 -0800958 def getRepoPermission(self, project, login):
959 owner, proj = project.split('/')
960 for pr in self.pull_requests:
961 pr_owner, pr_project = pr.project.split('/')
962 if (pr_owner == owner and proj == pr_project):
963 if login in pr.writers:
964 return 'write'
965 else:
966 return 'read'
967
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 def getGitUrl(self, project):
969 return os.path.join(self.upstream_root, str(project))
970
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200971 def real_getGitUrl(self, project):
972 return super(FakeGithubConnection, self).getGitUrl(project)
973
Gregory Haynes4fc12542015-04-22 20:38:06 -0700974 def getProjectBranches(self, project):
975 """Masks getProjectBranches since we don't have a real github"""
976
977 # just returns master for now
978 return ['master']
979
Jan Hrubane252a732017-01-03 15:03:09 +0100980 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700981 pull_request = self.pull_requests[pr_number - 1]
982 pull_request.addComment(message)
983
Jan Hruban3b415922016-02-03 13:10:22 +0100984 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100985 pull_request = self.pull_requests[pr_number - 1]
986 if self.merge_failure:
987 raise Exception('Pull request was not merged')
988 if self.merge_not_allowed_count > 0:
989 self.merge_not_allowed_count -= 1
990 raise MergeFailure('Merge was not successful due to mergeability'
991 ' conflict')
992 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100993 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100994
Jesse Keatingd96e5882017-01-19 13:55:50 -0800995 def getCommitStatuses(self, project, sha):
996 owner, proj = project.split('/')
997 for pr in self.pull_requests:
998 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -0700999 # This is somewhat risky, if the same commit exists in multiple
1000 # PRs, we might grab the wrong one that doesn't have a status
1001 # that is expected to be there. Maybe re-work this so that there
1002 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001003 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001004 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001005 return pr.statuses[sha]
1006
Jan Hrubane252a732017-01-03 15:03:09 +01001007 def setCommitStatus(self, project, sha, state,
1008 url='', description='', context=''):
1009 owner, proj = project.split('/')
1010 for pr in self.pull_requests:
1011 pr_owner, pr_project = pr.project.split('/')
1012 if (pr_owner == owner and pr_project == proj and
1013 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001014 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001015
Jan Hruban16ad31f2015-11-07 14:39:07 +01001016 def labelPull(self, project, pr_number, label):
1017 pull_request = self.pull_requests[pr_number - 1]
1018 pull_request.addLabel(label)
1019
1020 def unlabelPull(self, project, pr_number, label):
1021 pull_request = self.pull_requests[pr_number - 1]
1022 pull_request.removeLabel(label)
1023
Gregory Haynes4fc12542015-04-22 20:38:06 -07001024
Clark Boylanb640e052014-04-03 16:41:46 -07001025class BuildHistory(object):
1026 def __init__(self, **kw):
1027 self.__dict__.update(kw)
1028
1029 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001030 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1031 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001032
1033
1034class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +02001035 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -07001036 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -07001037 self.url = url
1038
1039 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001040 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -07001041 path = res.path
1042 project = '/'.join(path.split('/')[2:-2])
1043 ret = '001e# service=git-upload-pack\n'
1044 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
1045 'multi_ack thin-pack side-band side-band-64k ofs-delta '
1046 'shallow no-progress include-tag multi_ack_detailed no-done\n')
1047 path = os.path.join(self.upstream_root, project)
1048 repo = git.Repo(path)
1049 for ref in repo.refs:
1050 r = ref.object.hexsha + ' ' + ref.path + '\n'
1051 ret += '%04x%s' % (len(r) + 4, r)
1052 ret += '0000'
1053 return ret
1054
1055
Clark Boylanb640e052014-04-03 16:41:46 -07001056class FakeStatsd(threading.Thread):
1057 def __init__(self):
1058 threading.Thread.__init__(self)
1059 self.daemon = True
1060 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1061 self.sock.bind(('', 0))
1062 self.port = self.sock.getsockname()[1]
1063 self.wake_read, self.wake_write = os.pipe()
1064 self.stats = []
1065
1066 def run(self):
1067 while True:
1068 poll = select.poll()
1069 poll.register(self.sock, select.POLLIN)
1070 poll.register(self.wake_read, select.POLLIN)
1071 ret = poll.poll()
1072 for (fd, event) in ret:
1073 if fd == self.sock.fileno():
1074 data = self.sock.recvfrom(1024)
1075 if not data:
1076 return
1077 self.stats.append(data[0])
1078 if fd == self.wake_read:
1079 return
1080
1081 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001082 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001083
1084
James E. Blaire1767bc2016-08-02 10:00:27 -07001085class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001086 log = logging.getLogger("zuul.test")
1087
Paul Belanger174a8272017-03-14 13:20:10 -04001088 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001089 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001090 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001091 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001092 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001093 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001094 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001095 # TODOv3(jeblair): self.node is really "the image of the node
1096 # assigned". We should rename it (self.node_image?) if we
1097 # keep using it like this, or we may end up exposing more of
1098 # the complexity around multi-node jobs here
1099 # (self.nodes[0].image?)
1100 self.node = None
1101 if len(self.parameters.get('nodes')) == 1:
1102 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001103 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001104 self.pipeline = self.parameters['ZUUL_PIPELINE']
1105 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001106 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001107 self.wait_condition = threading.Condition()
1108 self.waiting = False
1109 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001110 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001111 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001112 self.changes = None
1113 if 'ZUUL_CHANGE_IDS' in self.parameters:
1114 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001115
James E. Blair3158e282016-08-19 09:34:11 -07001116 def __repr__(self):
1117 waiting = ''
1118 if self.waiting:
1119 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001120 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1121 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001122
Clark Boylanb640e052014-04-03 16:41:46 -07001123 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001124 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001125 self.wait_condition.acquire()
1126 self.wait_condition.notify()
1127 self.waiting = False
1128 self.log.debug("Build %s released" % self.unique)
1129 self.wait_condition.release()
1130
1131 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001132 """Return whether this build is being held.
1133
1134 :returns: Whether the build is being held.
1135 :rtype: bool
1136 """
1137
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.wait_condition.acquire()
1139 if self.waiting:
1140 ret = True
1141 else:
1142 ret = False
1143 self.wait_condition.release()
1144 return ret
1145
1146 def _wait(self):
1147 self.wait_condition.acquire()
1148 self.waiting = True
1149 self.log.debug("Build %s waiting" % self.unique)
1150 self.wait_condition.wait()
1151 self.wait_condition.release()
1152
1153 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001154 self.log.debug('Running build %s' % self.unique)
1155
Paul Belanger174a8272017-03-14 13:20:10 -04001156 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001157 self.log.debug('Holding build %s' % self.unique)
1158 self._wait()
1159 self.log.debug("Build %s continuing" % self.unique)
1160
James E. Blair412fba82017-01-26 15:00:50 -08001161 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001162 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001163 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001164 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001165 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001166 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001167 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001168
James E. Blaire1767bc2016-08-02 10:00:27 -07001169 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001170
James E. Blaira5dba232016-08-08 15:53:24 -07001171 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001172 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001173 for change in changes:
1174 if self.hasChanges(change):
1175 return True
1176 return False
1177
James E. Blaire7b99a02016-08-05 14:27:34 -07001178 def hasChanges(self, *changes):
1179 """Return whether this build has certain changes in its git repos.
1180
1181 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001182 are expected to be present (in order) in the git repository of
1183 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001184
1185 :returns: Whether the build has the indicated changes.
1186 :rtype: bool
1187
1188 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001189 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001190 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001191 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001192 try:
1193 repo = git.Repo(path)
1194 except NoSuchPathError as e:
1195 self.log.debug('%s' % e)
1196 return False
1197 ref = self.parameters['ZUUL_REF']
1198 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1199 commit_message = '%s-1' % change.subject
1200 self.log.debug("Checking if build %s has changes; commit_message "
1201 "%s; repo_messages %s" % (self, commit_message,
1202 repo_messages))
1203 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001204 self.log.debug(" messages do not match")
1205 return False
1206 self.log.debug(" OK")
1207 return True
1208
Clark Boylanb640e052014-04-03 16:41:46 -07001209
Paul Belanger174a8272017-03-14 13:20:10 -04001210class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1211 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001212
Paul Belanger174a8272017-03-14 13:20:10 -04001213 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001214 they will report that they have started but then pause until
1215 released before reporting completion. This attribute may be
1216 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001217 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001218 be explicitly released.
1219
1220 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001221 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001222 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001223 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001224 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001225 self.hold_jobs_in_build = False
1226 self.lock = threading.Lock()
1227 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001228 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001229 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001230 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001231
James E. Blaira5dba232016-08-08 15:53:24 -07001232 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001233 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001234
1235 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001236 :arg Change change: The :py:class:`~tests.base.FakeChange`
1237 instance which should cause the job to fail. This job
1238 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001239
1240 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001241 l = self.fail_tests.get(name, [])
1242 l.append(change)
1243 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001244
James E. Blair962220f2016-08-03 11:22:38 -07001245 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001246 """Release a held build.
1247
1248 :arg str regex: A regular expression which, if supplied, will
1249 cause only builds with matching names to be released. If
1250 not supplied, all builds will be released.
1251
1252 """
James E. Blair962220f2016-08-03 11:22:38 -07001253 builds = self.running_builds[:]
1254 self.log.debug("Releasing build %s (%s)" % (regex,
1255 len(self.running_builds)))
1256 for build in builds:
1257 if not regex or re.match(regex, build.name):
1258 self.log.debug("Releasing build %s" %
1259 (build.parameters['ZUUL_UUID']))
1260 build.release()
1261 else:
1262 self.log.debug("Not releasing build %s" %
1263 (build.parameters['ZUUL_UUID']))
1264 self.log.debug("Done releasing builds %s (%s)" %
1265 (regex, len(self.running_builds)))
1266
Paul Belanger174a8272017-03-14 13:20:10 -04001267 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001268 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001269 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001270 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001271 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001272 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001273 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001274 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001275 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1276 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001277
1278 def stopJob(self, job):
1279 self.log.debug("handle stop")
1280 parameters = json.loads(job.arguments)
1281 uuid = parameters['uuid']
1282 for build in self.running_builds:
1283 if build.unique == uuid:
1284 build.aborted = True
1285 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001286 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001287
James E. Blaira002b032017-04-18 10:35:48 -07001288 def stop(self):
1289 for build in self.running_builds:
1290 build.release()
1291 super(RecordingExecutorServer, self).stop()
1292
Joshua Hesketh50c21782016-10-13 21:34:14 +11001293
Paul Belanger174a8272017-03-14 13:20:10 -04001294class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blair1960d682017-04-28 15:44:14 -07001295 def doMergeChanges(self, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001296 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001297 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1298 items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001299 if not commit: # merge conflict
1300 self.recordResult('MERGER_FAILURE')
1301 return commit
1302
1303 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001304 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001305 self.executor_server.lock.acquire()
1306 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001307 BuildHistory(name=build.name, result=result, changes=build.changes,
1308 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001309 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 pipeline=build.parameters['ZUUL_PIPELINE'])
1311 )
Paul Belanger174a8272017-03-14 13:20:10 -04001312 self.executor_server.running_builds.remove(build)
1313 del self.executor_server.job_builds[self.job.unique]
1314 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001315
1316 def runPlaybooks(self, args):
1317 build = self.executor_server.job_builds[self.job.unique]
1318 build.jobdir = self.jobdir
1319
1320 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1321 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001322 return result
1323
Monty Taylore6562aa2017-02-20 07:37:39 -05001324 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001325 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001326
Paul Belanger174a8272017-03-14 13:20:10 -04001327 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001328 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001329 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001330 else:
1331 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001332 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001333
James E. Blairad8dca02017-02-21 11:48:32 -05001334 def getHostList(self, args):
1335 self.log.debug("hostlist")
1336 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001337 for host in hosts:
1338 host['host_vars']['ansible_connection'] = 'local'
1339
1340 hosts.append(dict(
1341 name='localhost',
1342 host_vars=dict(ansible_connection='local'),
1343 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001344 return hosts
1345
James E. Blairf5dbd002015-12-23 15:26:17 -08001346
Clark Boylanb640e052014-04-03 16:41:46 -07001347class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001348 """A Gearman server for use in tests.
1349
1350 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1351 added to the queue but will not be distributed to workers
1352 until released. This attribute may be changed at any time and
1353 will take effect for subsequently enqueued jobs, but
1354 previously held jobs will still need to be explicitly
1355 released.
1356
1357 """
1358
Clark Boylanb640e052014-04-03 16:41:46 -07001359 def __init__(self):
1360 self.hold_jobs_in_queue = False
1361 super(FakeGearmanServer, self).__init__(0)
1362
1363 def getJobForConnection(self, connection, peek=False):
1364 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1365 for job in queue:
1366 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001367 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001368 job.waiting = self.hold_jobs_in_queue
1369 else:
1370 job.waiting = False
1371 if job.waiting:
1372 continue
1373 if job.name in connection.functions:
1374 if not peek:
1375 queue.remove(job)
1376 connection.related_jobs[job.handle] = job
1377 job.worker_connection = connection
1378 job.running = True
1379 return job
1380 return None
1381
1382 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001383 """Release a held job.
1384
1385 :arg str regex: A regular expression which, if supplied, will
1386 cause only jobs with matching names to be released. If
1387 not supplied, all jobs will be released.
1388 """
Clark Boylanb640e052014-04-03 16:41:46 -07001389 released = False
1390 qlen = (len(self.high_queue) + len(self.normal_queue) +
1391 len(self.low_queue))
1392 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1393 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001394 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001395 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001396 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001397 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001398 self.log.debug("releasing queued job %s" %
1399 job.unique)
1400 job.waiting = False
1401 released = True
1402 else:
1403 self.log.debug("not releasing queued job %s" %
1404 job.unique)
1405 if released:
1406 self.wakeConnections()
1407 qlen = (len(self.high_queue) + len(self.normal_queue) +
1408 len(self.low_queue))
1409 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1410
1411
1412class FakeSMTP(object):
1413 log = logging.getLogger('zuul.FakeSMTP')
1414
1415 def __init__(self, messages, server, port):
1416 self.server = server
1417 self.port = port
1418 self.messages = messages
1419
1420 def sendmail(self, from_email, to_email, msg):
1421 self.log.info("Sending email from %s, to %s, with msg %s" % (
1422 from_email, to_email, msg))
1423
1424 headers = msg.split('\n\n', 1)[0]
1425 body = msg.split('\n\n', 1)[1]
1426
1427 self.messages.append(dict(
1428 from_email=from_email,
1429 to_email=to_email,
1430 msg=msg,
1431 headers=headers,
1432 body=body,
1433 ))
1434
1435 return True
1436
1437 def quit(self):
1438 return True
1439
1440
James E. Blairdce6cea2016-12-20 16:45:32 -08001441class FakeNodepool(object):
1442 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001443 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001444
1445 log = logging.getLogger("zuul.test.FakeNodepool")
1446
1447 def __init__(self, host, port, chroot):
1448 self.client = kazoo.client.KazooClient(
1449 hosts='%s:%s%s' % (host, port, chroot))
1450 self.client.start()
1451 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001452 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001453 self.thread = threading.Thread(target=self.run)
1454 self.thread.daemon = True
1455 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001456 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001457
1458 def stop(self):
1459 self._running = False
1460 self.thread.join()
1461 self.client.stop()
1462 self.client.close()
1463
1464 def run(self):
1465 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001466 try:
1467 self._run()
1468 except Exception:
1469 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001470 time.sleep(0.1)
1471
1472 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001473 if self.paused:
1474 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001475 for req in self.getNodeRequests():
1476 self.fulfillRequest(req)
1477
1478 def getNodeRequests(self):
1479 try:
1480 reqids = self.client.get_children(self.REQUEST_ROOT)
1481 except kazoo.exceptions.NoNodeError:
1482 return []
1483 reqs = []
1484 for oid in sorted(reqids):
1485 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001486 try:
1487 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001488 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001489 data['_oid'] = oid
1490 reqs.append(data)
1491 except kazoo.exceptions.NoNodeError:
1492 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001493 return reqs
1494
James E. Blaire18d4602017-01-05 11:17:28 -08001495 def getNodes(self):
1496 try:
1497 nodeids = self.client.get_children(self.NODE_ROOT)
1498 except kazoo.exceptions.NoNodeError:
1499 return []
1500 nodes = []
1501 for oid in sorted(nodeids):
1502 path = self.NODE_ROOT + '/' + oid
1503 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001504 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001505 data['_oid'] = oid
1506 try:
1507 lockfiles = self.client.get_children(path + '/lock')
1508 except kazoo.exceptions.NoNodeError:
1509 lockfiles = []
1510 if lockfiles:
1511 data['_lock'] = True
1512 else:
1513 data['_lock'] = False
1514 nodes.append(data)
1515 return nodes
1516
James E. Blaira38c28e2017-01-04 10:33:20 -08001517 def makeNode(self, request_id, node_type):
1518 now = time.time()
1519 path = '/nodepool/nodes/'
1520 data = dict(type=node_type,
1521 provider='test-provider',
1522 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001523 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001524 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001525 public_ipv4='127.0.0.1',
1526 private_ipv4=None,
1527 public_ipv6=None,
1528 allocated_to=request_id,
1529 state='ready',
1530 state_time=now,
1531 created_time=now,
1532 updated_time=now,
1533 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001534 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001535 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001536 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001537 path = self.client.create(path, data,
1538 makepath=True,
1539 sequence=True)
1540 nodeid = path.split("/")[-1]
1541 return nodeid
1542
James E. Blair6ab79e02017-01-06 10:10:17 -08001543 def addFailRequest(self, request):
1544 self.fail_requests.add(request['_oid'])
1545
James E. Blairdce6cea2016-12-20 16:45:32 -08001546 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001547 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001548 return
1549 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001550 oid = request['_oid']
1551 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001552
James E. Blair6ab79e02017-01-06 10:10:17 -08001553 if oid in self.fail_requests:
1554 request['state'] = 'failed'
1555 else:
1556 request['state'] = 'fulfilled'
1557 nodes = []
1558 for node in request['node_types']:
1559 nodeid = self.makeNode(oid, node)
1560 nodes.append(nodeid)
1561 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001562
James E. Blaira38c28e2017-01-04 10:33:20 -08001563 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001564 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001565 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001566 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001567 try:
1568 self.client.set(path, data)
1569 except kazoo.exceptions.NoNodeError:
1570 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001571
1572
James E. Blair498059b2016-12-20 13:50:13 -08001573class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001574 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001575 super(ChrootedKazooFixture, self).__init__()
1576
1577 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1578 if ':' in zk_host:
1579 host, port = zk_host.split(':')
1580 else:
1581 host = zk_host
1582 port = None
1583
1584 self.zookeeper_host = host
1585
1586 if not port:
1587 self.zookeeper_port = 2181
1588 else:
1589 self.zookeeper_port = int(port)
1590
Clark Boylan621ec9a2017-04-07 17:41:33 -07001591 self.test_id = test_id
1592
James E. Blair498059b2016-12-20 13:50:13 -08001593 def _setUp(self):
1594 # Make sure the test chroot paths do not conflict
1595 random_bits = ''.join(random.choice(string.ascii_lowercase +
1596 string.ascii_uppercase)
1597 for x in range(8))
1598
Clark Boylan621ec9a2017-04-07 17:41:33 -07001599 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001600 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1601
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001602 self.addCleanup(self._cleanup)
1603
James E. Blair498059b2016-12-20 13:50:13 -08001604 # Ensure the chroot path exists and clean up any pre-existing znodes.
1605 _tmp_client = kazoo.client.KazooClient(
1606 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1607 _tmp_client.start()
1608
1609 if _tmp_client.exists(self.zookeeper_chroot):
1610 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1611
1612 _tmp_client.ensure_path(self.zookeeper_chroot)
1613 _tmp_client.stop()
1614 _tmp_client.close()
1615
James E. Blair498059b2016-12-20 13:50:13 -08001616 def _cleanup(self):
1617 '''Remove the chroot path.'''
1618 # Need a non-chroot'ed client to remove the chroot path
1619 _tmp_client = kazoo.client.KazooClient(
1620 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1621 _tmp_client.start()
1622 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1623 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001624 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001625
1626
Joshua Heskethd78b4482015-09-14 16:56:34 -06001627class MySQLSchemaFixture(fixtures.Fixture):
1628 def setUp(self):
1629 super(MySQLSchemaFixture, self).setUp()
1630
1631 random_bits = ''.join(random.choice(string.ascii_lowercase +
1632 string.ascii_uppercase)
1633 for x in range(8))
1634 self.name = '%s_%s' % (random_bits, os.getpid())
1635 self.passwd = uuid.uuid4().hex
1636 db = pymysql.connect(host="localhost",
1637 user="openstack_citest",
1638 passwd="openstack_citest",
1639 db="openstack_citest")
1640 cur = db.cursor()
1641 cur.execute("create database %s" % self.name)
1642 cur.execute(
1643 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1644 (self.name, self.name, self.passwd))
1645 cur.execute("flush privileges")
1646
1647 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1648 self.passwd,
1649 self.name)
1650 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1651 self.addCleanup(self.cleanup)
1652
1653 def cleanup(self):
1654 db = pymysql.connect(host="localhost",
1655 user="openstack_citest",
1656 passwd="openstack_citest",
1657 db="openstack_citest")
1658 cur = db.cursor()
1659 cur.execute("drop database %s" % self.name)
1660 cur.execute("drop user '%s'@'localhost'" % self.name)
1661 cur.execute("flush privileges")
1662
1663
Maru Newby3fe5f852015-01-13 04:22:14 +00001664class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001665 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001666 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001667
James E. Blair1c236df2017-02-01 14:07:24 -08001668 def attachLogs(self, *args):
1669 def reader():
1670 self._log_stream.seek(0)
1671 while True:
1672 x = self._log_stream.read(4096)
1673 if not x:
1674 break
1675 yield x.encode('utf8')
1676 content = testtools.content.content_from_reader(
1677 reader,
1678 testtools.content_type.UTF8_TEXT,
1679 False)
1680 self.addDetail('logging', content)
1681
Clark Boylanb640e052014-04-03 16:41:46 -07001682 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001683 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001684 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1685 try:
1686 test_timeout = int(test_timeout)
1687 except ValueError:
1688 # If timeout value is invalid do not set a timeout.
1689 test_timeout = 0
1690 if test_timeout > 0:
1691 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1692
1693 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1694 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1695 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1696 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1697 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1698 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1699 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1700 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1701 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1702 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001703 self._log_stream = StringIO()
1704 self.addOnException(self.attachLogs)
1705 else:
1706 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001707
James E. Blair73b41772017-05-22 13:22:55 -07001708 # NOTE(jeblair): this is temporary extra debugging to try to
1709 # track down a possible leak.
1710 orig_git_repo_init = git.Repo.__init__
1711
1712 def git_repo_init(myself, *args, **kw):
1713 orig_git_repo_init(myself, *args, **kw)
1714 self.log.debug("Created git repo 0x%x %s" %
1715 (id(myself), repr(myself)))
1716
1717 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1718 git_repo_init))
1719
James E. Blair1c236df2017-02-01 14:07:24 -08001720 handler = logging.StreamHandler(self._log_stream)
1721 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1722 '%(levelname)-8s %(message)s')
1723 handler.setFormatter(formatter)
1724
1725 logger = logging.getLogger()
1726 logger.setLevel(logging.DEBUG)
1727 logger.addHandler(handler)
1728
Clark Boylan3410d532017-04-25 12:35:29 -07001729 # Make sure we don't carry old handlers around in process state
1730 # which slows down test runs
1731 self.addCleanup(logger.removeHandler, handler)
1732 self.addCleanup(handler.close)
1733 self.addCleanup(handler.flush)
1734
James E. Blair1c236df2017-02-01 14:07:24 -08001735 # NOTE(notmorgan): Extract logging overrides for specific
1736 # libraries from the OS_LOG_DEFAULTS env and create loggers
1737 # for each. This is used to limit the output during test runs
1738 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001739 log_defaults_from_env = os.environ.get(
1740 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001741 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001742
James E. Blairdce6cea2016-12-20 16:45:32 -08001743 if log_defaults_from_env:
1744 for default in log_defaults_from_env.split(','):
1745 try:
1746 name, level_str = default.split('=', 1)
1747 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001748 logger = logging.getLogger(name)
1749 logger.setLevel(level)
1750 logger.addHandler(handler)
1751 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001752 except ValueError:
1753 # NOTE(notmorgan): Invalid format of the log default,
1754 # skip and don't try and apply a logger for the
1755 # specified module
1756 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001757
Maru Newby3fe5f852015-01-13 04:22:14 +00001758
1759class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001760 """A test case with a functioning Zuul.
1761
1762 The following class variables are used during test setup and can
1763 be overidden by subclasses but are effectively read-only once a
1764 test method starts running:
1765
1766 :cvar str config_file: This points to the main zuul config file
1767 within the fixtures directory. Subclasses may override this
1768 to obtain a different behavior.
1769
1770 :cvar str tenant_config_file: This is the tenant config file
1771 (which specifies from what git repos the configuration should
1772 be loaded). It defaults to the value specified in
1773 `config_file` but can be overidden by subclasses to obtain a
1774 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001775 configuration. See also the :py:func:`simple_layout`
1776 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001777
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001778 :cvar bool create_project_keys: Indicates whether Zuul should
1779 auto-generate keys for each project, or whether the test
1780 infrastructure should insert dummy keys to save time during
1781 startup. Defaults to False.
1782
James E. Blaire7b99a02016-08-05 14:27:34 -07001783 The following are instance variables that are useful within test
1784 methods:
1785
1786 :ivar FakeGerritConnection fake_<connection>:
1787 A :py:class:`~tests.base.FakeGerritConnection` will be
1788 instantiated for each connection present in the config file
1789 and stored here. For instance, `fake_gerrit` will hold the
1790 FakeGerritConnection object for a connection named `gerrit`.
1791
1792 :ivar FakeGearmanServer gearman_server: An instance of
1793 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1794 server that all of the Zuul components in this test use to
1795 communicate with each other.
1796
Paul Belanger174a8272017-03-14 13:20:10 -04001797 :ivar RecordingExecutorServer executor_server: An instance of
1798 :py:class:`~tests.base.RecordingExecutorServer` which is the
1799 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001800
1801 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1802 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001803 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001804 list upon completion.
1805
1806 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1807 objects representing completed builds. They are appended to
1808 the list in the order they complete.
1809
1810 """
1811
James E. Blair83005782015-12-11 14:46:03 -08001812 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001813 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001814 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001815
1816 def _startMerger(self):
1817 self.merge_server = zuul.merger.server.MergeServer(self.config,
1818 self.connections)
1819 self.merge_server.start()
1820
Maru Newby3fe5f852015-01-13 04:22:14 +00001821 def setUp(self):
1822 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001823
1824 self.setupZK()
1825
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001826 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001827 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001828 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1829 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001830 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001831 tmp_root = tempfile.mkdtemp(
1832 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001833 self.test_root = os.path.join(tmp_root, "zuul-test")
1834 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001835 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001836 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001837 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001838
1839 if os.path.exists(self.test_root):
1840 shutil.rmtree(self.test_root)
1841 os.makedirs(self.test_root)
1842 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001843 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001844
1845 # Make per test copy of Configuration.
1846 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001847 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1848 if not os.path.exists(self.private_key_file):
1849 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1850 shutil.copy(src_private_key_file, self.private_key_file)
1851 shutil.copy('{}.pub'.format(src_private_key_file),
1852 '{}.pub'.format(self.private_key_file))
1853 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001854 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001855 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001856 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001857 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001858 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001859 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001860 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001861
Clark Boylanb640e052014-04-03 16:41:46 -07001862 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001863 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1864 # see: https://github.com/jsocol/pystatsd/issues/61
1865 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001866 os.environ['STATSD_PORT'] = str(self.statsd.port)
1867 self.statsd.start()
1868 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001869 reload_module(statsd)
1870 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001871
1872 self.gearman_server = FakeGearmanServer()
1873
1874 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001875 self.log.info("Gearman server on port %s" %
1876 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001877
James E. Blaire511d2f2016-12-08 15:22:26 -08001878 gerritsource.GerritSource.replication_timeout = 1.5
1879 gerritsource.GerritSource.replication_retry_interval = 0.5
1880 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001881
Joshua Hesketh352264b2015-08-11 23:42:08 +10001882 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001883
Jan Hruban7083edd2015-08-21 14:00:54 +02001884 self.webapp = zuul.webapp.WebApp(
1885 self.sched, port=0, listen_address='127.0.0.1')
1886
Jan Hruban6b71aff2015-10-22 16:58:08 +02001887 self.event_queues = [
1888 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001889 self.sched.trigger_event_queue,
1890 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001891 ]
1892
James E. Blairfef78942016-03-11 16:28:56 -08001893 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001894 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001895
Clark Boylanb640e052014-04-03 16:41:46 -07001896 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001897 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001898 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001899 return FakeURLOpener(self.upstream_root, *args, **kw)
1900
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001901 old_urlopen = urllib.request.urlopen
1902 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001903
Paul Belanger174a8272017-03-14 13:20:10 -04001904 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001905 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001906 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001907 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001908 _test_root=self.test_root,
1909 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001910 self.executor_server.start()
1911 self.history = self.executor_server.build_history
1912 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001913
Paul Belanger174a8272017-03-14 13:20:10 -04001914 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001915 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001916 self.merge_client = zuul.merger.client.MergeClient(
1917 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001918 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001919 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001920 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001921
James E. Blair0d5a36e2017-02-21 10:53:44 -05001922 self.fake_nodepool = FakeNodepool(
1923 self.zk_chroot_fixture.zookeeper_host,
1924 self.zk_chroot_fixture.zookeeper_port,
1925 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001926
Paul Belanger174a8272017-03-14 13:20:10 -04001927 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001928 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001929 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001930 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001931
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001932 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001933
1934 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001935 self.webapp.start()
1936 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001937 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001938 # Cleanups are run in reverse order
1939 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001940 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001941 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001942
James E. Blairb9c0d772017-03-03 14:34:49 -08001943 self.sched.reconfigure(self.config)
1944 self.sched.resume()
1945
Tobias Henkel7df274b2017-05-26 17:41:11 +02001946 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001947 # Set up gerrit related fakes
1948 # Set a changes database so multiple FakeGerrit's can report back to
1949 # a virtual canonical database given by the configured hostname
1950 self.gerrit_changes_dbs = {}
1951
1952 def getGerritConnection(driver, name, config):
1953 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1954 con = FakeGerritConnection(driver, name, config,
1955 changes_db=db,
1956 upstream_root=self.upstream_root)
1957 self.event_queues.append(con.event_queue)
1958 setattr(self, 'fake_' + name, con)
1959 return con
1960
1961 self.useFixture(fixtures.MonkeyPatch(
1962 'zuul.driver.gerrit.GerritDriver.getConnection',
1963 getGerritConnection))
1964
Gregory Haynes4fc12542015-04-22 20:38:06 -07001965 def getGithubConnection(driver, name, config):
1966 con = FakeGithubConnection(driver, name, config,
1967 upstream_root=self.upstream_root)
1968 setattr(self, 'fake_' + name, con)
1969 return con
1970
1971 self.useFixture(fixtures.MonkeyPatch(
1972 'zuul.driver.github.GithubDriver.getConnection',
1973 getGithubConnection))
1974
James E. Blaire511d2f2016-12-08 15:22:26 -08001975 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001976 # TODO(jhesketh): This should come from lib.connections for better
1977 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001978 # Register connections from the config
1979 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001980
Joshua Hesketh352264b2015-08-11 23:42:08 +10001981 def FakeSMTPFactory(*args, **kw):
1982 args = [self.smtp_messages] + list(args)
1983 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001984
Joshua Hesketh352264b2015-08-11 23:42:08 +10001985 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001986
James E. Blaire511d2f2016-12-08 15:22:26 -08001987 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001988 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02001989 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001990
James E. Blair83005782015-12-11 14:46:03 -08001991 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001992 # This creates the per-test configuration object. It can be
1993 # overriden by subclasses, but should not need to be since it
1994 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001995 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001996 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001997
1998 if not self.setupSimpleLayout():
1999 if hasattr(self, 'tenant_config_file'):
2000 self.config.set('zuul', 'tenant_config',
2001 self.tenant_config_file)
2002 git_path = os.path.join(
2003 os.path.dirname(
2004 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2005 'git')
2006 if os.path.exists(git_path):
2007 for reponame in os.listdir(git_path):
2008 project = reponame.replace('_', '/')
2009 self.copyDirToRepo(project,
2010 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002011 self.setupAllProjectKeys()
2012
James E. Blair06cc3922017-04-19 10:08:10 -07002013 def setupSimpleLayout(self):
2014 # If the test method has been decorated with a simple_layout,
2015 # use that instead of the class tenant_config_file. Set up a
2016 # single config-project with the specified layout, and
2017 # initialize repos for all of the 'project' entries which
2018 # appear in the layout.
2019 test_name = self.id().split('.')[-1]
2020 test = getattr(self, test_name)
2021 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002022 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002023 else:
2024 return False
2025
James E. Blairb70e55a2017-04-19 12:57:02 -07002026 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002027 path = os.path.join(FIXTURE_DIR, path)
2028 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002029 data = f.read()
2030 layout = yaml.safe_load(data)
2031 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002032 untrusted_projects = []
2033 for item in layout:
2034 if 'project' in item:
2035 name = item['project']['name']
2036 untrusted_projects.append(name)
2037 self.init_repo(name)
2038 self.addCommitToRepo(name, 'initial commit',
2039 files={'README': ''},
2040 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002041 if 'job' in item:
2042 jobname = item['job']['name']
2043 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002044
2045 root = os.path.join(self.test_root, "config")
2046 if not os.path.exists(root):
2047 os.makedirs(root)
2048 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2049 config = [{'tenant':
2050 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002051 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002052 {'config-projects': ['common-config'],
2053 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002054 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002055 f.close()
2056 self.config.set('zuul', 'tenant_config',
2057 os.path.join(FIXTURE_DIR, f.name))
2058
2059 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002060 self.addCommitToRepo('common-config', 'add content from fixture',
2061 files, branch='master', tag='init')
2062
2063 return True
2064
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002065 def setupAllProjectKeys(self):
2066 if self.create_project_keys:
2067 return
2068
2069 path = self.config.get('zuul', 'tenant_config')
2070 with open(os.path.join(FIXTURE_DIR, path)) as f:
2071 tenant_config = yaml.safe_load(f.read())
2072 for tenant in tenant_config:
2073 sources = tenant['tenant']['source']
2074 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002075 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002076 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002077 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002078 self.setupProjectKeys(source, project)
2079
2080 def setupProjectKeys(self, source, project):
2081 # Make sure we set up an RSA key for the project so that we
2082 # don't spend time generating one:
2083
2084 key_root = os.path.join(self.state_root, 'keys')
2085 if not os.path.isdir(key_root):
2086 os.mkdir(key_root, 0o700)
2087 private_key_file = os.path.join(key_root, source, project + '.pem')
2088 private_key_dir = os.path.dirname(private_key_file)
2089 self.log.debug("Installing test keys for project %s at %s" % (
2090 project, private_key_file))
2091 if not os.path.isdir(private_key_dir):
2092 os.makedirs(private_key_dir)
2093 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2094 with open(private_key_file, 'w') as o:
2095 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002096
James E. Blair498059b2016-12-20 13:50:13 -08002097 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002098 self.zk_chroot_fixture = self.useFixture(
2099 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002100 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002101 self.zk_chroot_fixture.zookeeper_host,
2102 self.zk_chroot_fixture.zookeeper_port,
2103 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002104
James E. Blair96c6bf82016-01-15 16:20:40 -08002105 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002106 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002107
2108 files = {}
2109 for (dirpath, dirnames, filenames) in os.walk(source_path):
2110 for filename in filenames:
2111 test_tree_filepath = os.path.join(dirpath, filename)
2112 common_path = os.path.commonprefix([test_tree_filepath,
2113 source_path])
2114 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2115 with open(test_tree_filepath, 'r') as f:
2116 content = f.read()
2117 files[relative_filepath] = content
2118 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002119 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002120
James E. Blaire18d4602017-01-05 11:17:28 -08002121 def assertNodepoolState(self):
2122 # Make sure that there are no pending requests
2123
2124 requests = self.fake_nodepool.getNodeRequests()
2125 self.assertEqual(len(requests), 0)
2126
2127 nodes = self.fake_nodepool.getNodes()
2128 for node in nodes:
2129 self.assertFalse(node['_lock'], "Node %s is locked" %
2130 (node['_oid'],))
2131
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002132 def assertNoGeneratedKeys(self):
2133 # Make sure that Zuul did not generate any project keys
2134 # (unless it was supposed to).
2135
2136 if self.create_project_keys:
2137 return
2138
2139 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2140 test_key = i.read()
2141
2142 key_root = os.path.join(self.state_root, 'keys')
2143 for root, dirname, files in os.walk(key_root):
2144 for fn in files:
2145 with open(os.path.join(root, fn)) as f:
2146 self.assertEqual(test_key, f.read())
2147
Clark Boylanb640e052014-04-03 16:41:46 -07002148 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002149 self.log.debug("Assert final state")
2150 # Make sure no jobs are running
2151 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002152 # Make sure that git.Repo objects have been garbage collected.
2153 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002154 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002155 gc.collect()
2156 for obj in gc.get_objects():
2157 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002158 self.log.debug("Leaked git repo object: 0x%x %s" %
2159 (id(obj), repr(obj)))
2160 for ref in gc.get_referrers(obj):
2161 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002162 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002163 if repos:
2164 for obj in gc.garbage:
2165 self.log.debug(" Garbage %s" % (repr(obj)))
2166 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002167 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002168 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002169 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002170 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002171 for tenant in self.sched.abide.tenants.values():
2172 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002173 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002174 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002175
2176 def shutdown(self):
2177 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002178 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002179 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002180 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002181 self.sched.stop()
2182 self.sched.join()
2183 self.statsd.stop()
2184 self.statsd.join()
2185 self.webapp.stop()
2186 self.webapp.join()
2187 self.rpc.stop()
2188 self.rpc.join()
2189 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002190 self.fake_nodepool.stop()
2191 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002192 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002193 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002194 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002195 # Further the pydevd threads also need to be whitelisted so debugging
2196 # e.g. in PyCharm is possible without breaking shutdown.
2197 whitelist = ['executor-watchdog',
2198 'pydevd.CommandThread',
2199 'pydevd.Reader',
2200 'pydevd.Writer',
2201 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002202 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002203 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002204 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002205 log_str = ""
2206 for thread_id, stack_frame in sys._current_frames().items():
2207 log_str += "Thread: %s\n" % thread_id
2208 log_str += "".join(traceback.format_stack(stack_frame))
2209 self.log.debug(log_str)
2210 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002211
James E. Blaira002b032017-04-18 10:35:48 -07002212 def assertCleanShutdown(self):
2213 pass
2214
James E. Blairc4ba97a2017-04-19 16:26:24 -07002215 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002216 parts = project.split('/')
2217 path = os.path.join(self.upstream_root, *parts[:-1])
2218 if not os.path.exists(path):
2219 os.makedirs(path)
2220 path = os.path.join(self.upstream_root, project)
2221 repo = git.Repo.init(path)
2222
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002223 with repo.config_writer() as config_writer:
2224 config_writer.set_value('user', 'email', 'user@example.com')
2225 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002226
Clark Boylanb640e052014-04-03 16:41:46 -07002227 repo.index.commit('initial commit')
2228 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002229 if tag:
2230 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002231
James E. Blair97d902e2014-08-21 13:25:56 -07002232 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002233 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002234 repo.git.clean('-x', '-f', '-d')
2235
James E. Blair97d902e2014-08-21 13:25:56 -07002236 def create_branch(self, project, branch):
2237 path = os.path.join(self.upstream_root, project)
2238 repo = git.Repo.init(path)
2239 fn = os.path.join(path, 'README')
2240
2241 branch_head = repo.create_head(branch)
2242 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002243 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002244 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002245 f.close()
2246 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002247 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002248
James E. Blair97d902e2014-08-21 13:25:56 -07002249 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002250 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002251 repo.git.clean('-x', '-f', '-d')
2252
Sachi King9f16d522016-03-16 12:20:45 +11002253 def create_commit(self, project):
2254 path = os.path.join(self.upstream_root, project)
2255 repo = git.Repo(path)
2256 repo.head.reference = repo.heads['master']
2257 file_name = os.path.join(path, 'README')
2258 with open(file_name, 'a') as f:
2259 f.write('creating fake commit\n')
2260 repo.index.add([file_name])
2261 commit = repo.index.commit('Creating a fake commit')
2262 return commit.hexsha
2263
James E. Blairf4a5f022017-04-18 14:01:10 -07002264 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002265 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002266 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002267 while len(self.builds):
2268 self.release(self.builds[0])
2269 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002270 i += 1
2271 if count is not None and i >= count:
2272 break
James E. Blairb8c16472015-05-05 14:55:26 -07002273
Clark Boylanb640e052014-04-03 16:41:46 -07002274 def release(self, job):
2275 if isinstance(job, FakeBuild):
2276 job.release()
2277 else:
2278 job.waiting = False
2279 self.log.debug("Queued job %s released" % job.unique)
2280 self.gearman_server.wakeConnections()
2281
2282 def getParameter(self, job, name):
2283 if isinstance(job, FakeBuild):
2284 return job.parameters[name]
2285 else:
2286 parameters = json.loads(job.arguments)
2287 return parameters[name]
2288
Clark Boylanb640e052014-04-03 16:41:46 -07002289 def haveAllBuildsReported(self):
2290 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002291 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002292 return False
2293 # Find out if every build that the worker has completed has been
2294 # reported back to Zuul. If it hasn't then that means a Gearman
2295 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002296 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002297 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002298 if not zbuild:
2299 # It has already been reported
2300 continue
2301 # It hasn't been reported yet.
2302 return False
2303 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002304 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002305 if connection.state == 'GRAB_WAIT':
2306 return False
2307 return True
2308
2309 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002310 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002311 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002312 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002313 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002314 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002315 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002316 for j in conn.related_jobs.values():
2317 if j.unique == build.uuid:
2318 client_job = j
2319 break
2320 if not client_job:
2321 self.log.debug("%s is not known to the gearman client" %
2322 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002323 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002324 if not client_job.handle:
2325 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002326 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002327 server_job = self.gearman_server.jobs.get(client_job.handle)
2328 if not server_job:
2329 self.log.debug("%s is not known to the gearman server" %
2330 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002331 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002332 if not hasattr(server_job, 'waiting'):
2333 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002334 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002335 if server_job.waiting:
2336 continue
James E. Blair17302972016-08-10 16:11:42 -07002337 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002338 self.log.debug("%s has not reported start" % build)
2339 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002340 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002341 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002342 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002343 if worker_build:
2344 if worker_build.isWaiting():
2345 continue
2346 else:
2347 self.log.debug("%s is running" % worker_build)
2348 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002349 else:
James E. Blair962220f2016-08-03 11:22:38 -07002350 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002351 return False
James E. Blaira002b032017-04-18 10:35:48 -07002352 for (build_uuid, job_worker) in \
2353 self.executor_server.job_workers.items():
2354 if build_uuid not in seen_builds:
2355 self.log.debug("%s is not finalized" % build_uuid)
2356 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002357 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002358
James E. Blairdce6cea2016-12-20 16:45:32 -08002359 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002360 if self.fake_nodepool.paused:
2361 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002362 if self.sched.nodepool.requests:
2363 return False
2364 return True
2365
Jan Hruban6b71aff2015-10-22 16:58:08 +02002366 def eventQueuesEmpty(self):
2367 for queue in self.event_queues:
2368 yield queue.empty()
2369
2370 def eventQueuesJoin(self):
2371 for queue in self.event_queues:
2372 queue.join()
2373
Clark Boylanb640e052014-04-03 16:41:46 -07002374 def waitUntilSettled(self):
2375 self.log.debug("Waiting until settled...")
2376 start = time.time()
2377 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002378 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002379 self.log.error("Timeout waiting for Zuul to settle")
2380 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002381 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002382 self.log.error(" %s: %s" % (queue, queue.empty()))
2383 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002384 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002385 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002386 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002387 self.log.error("All requests completed: %s" %
2388 (self.areAllNodeRequestsComplete(),))
2389 self.log.error("Merge client jobs: %s" %
2390 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002391 raise Exception("Timeout waiting for Zuul to settle")
2392 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002393
Paul Belanger174a8272017-03-14 13:20:10 -04002394 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002395 # have all build states propogated to zuul?
2396 if self.haveAllBuildsReported():
2397 # Join ensures that the queue is empty _and_ events have been
2398 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002399 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002400 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002401 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002402 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002403 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002404 self.areAllNodeRequestsComplete() and
2405 all(self.eventQueuesEmpty())):
2406 # The queue empty check is placed at the end to
2407 # ensure that if a component adds an event between
2408 # when locked the run handler and checked that the
2409 # components were stable, we don't erroneously
2410 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002411 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002412 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002413 self.log.debug("...settled.")
2414 return
2415 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002416 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002417 self.sched.wake_event.wait(0.1)
2418
2419 def countJobResults(self, jobs, result):
2420 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002421 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002422
James E. Blair96c6bf82016-01-15 16:20:40 -08002423 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002424 for job in self.history:
2425 if (job.name == name and
2426 (project is None or
2427 job.parameters['ZUUL_PROJECT'] == project)):
2428 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002429 raise Exception("Unable to find job %s in history" % name)
2430
2431 def assertEmptyQueues(self):
2432 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002433 for tenant in self.sched.abide.tenants.values():
2434 for pipeline in tenant.layout.pipelines.values():
2435 for queue in pipeline.queues:
2436 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002437 print('pipeline %s queue %s contents %s' % (
2438 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002439 self.assertEqual(len(queue.queue), 0,
2440 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002441
2442 def assertReportedStat(self, key, value=None, kind=None):
2443 start = time.time()
2444 while time.time() < (start + 5):
2445 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002446 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002447 if key == k:
2448 if value is None and kind is None:
2449 return
2450 elif value:
2451 if value == v:
2452 return
2453 elif kind:
2454 if v.endswith('|' + kind):
2455 return
2456 time.sleep(0.1)
2457
Clark Boylanb640e052014-04-03 16:41:46 -07002458 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002459
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002460 def assertBuilds(self, builds):
2461 """Assert that the running builds are as described.
2462
2463 The list of running builds is examined and must match exactly
2464 the list of builds described by the input.
2465
2466 :arg list builds: A list of dictionaries. Each item in the
2467 list must match the corresponding build in the build
2468 history, and each element of the dictionary must match the
2469 corresponding attribute of the build.
2470
2471 """
James E. Blair3158e282016-08-19 09:34:11 -07002472 try:
2473 self.assertEqual(len(self.builds), len(builds))
2474 for i, d in enumerate(builds):
2475 for k, v in d.items():
2476 self.assertEqual(
2477 getattr(self.builds[i], k), v,
2478 "Element %i in builds does not match" % (i,))
2479 except Exception:
2480 for build in self.builds:
2481 self.log.error("Running build: %s" % build)
2482 else:
2483 self.log.error("No running builds")
2484 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002485
James E. Blairb536ecc2016-08-31 10:11:42 -07002486 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002487 """Assert that the completed builds are as described.
2488
2489 The list of completed builds is examined and must match
2490 exactly the list of builds described by the input.
2491
2492 :arg list history: A list of dictionaries. Each item in the
2493 list must match the corresponding build in the build
2494 history, and each element of the dictionary must match the
2495 corresponding attribute of the build.
2496
James E. Blairb536ecc2016-08-31 10:11:42 -07002497 :arg bool ordered: If true, the history must match the order
2498 supplied, if false, the builds are permitted to have
2499 arrived in any order.
2500
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002501 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002502 def matches(history_item, item):
2503 for k, v in item.items():
2504 if getattr(history_item, k) != v:
2505 return False
2506 return True
James E. Blair3158e282016-08-19 09:34:11 -07002507 try:
2508 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002509 if ordered:
2510 for i, d in enumerate(history):
2511 if not matches(self.history[i], d):
2512 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002513 "Element %i in history does not match %s" %
2514 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002515 else:
2516 unseen = self.history[:]
2517 for i, d in enumerate(history):
2518 found = False
2519 for unseen_item in unseen:
2520 if matches(unseen_item, d):
2521 found = True
2522 unseen.remove(unseen_item)
2523 break
2524 if not found:
2525 raise Exception("No match found for element %i "
2526 "in history" % (i,))
2527 if unseen:
2528 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002529 except Exception:
2530 for build in self.history:
2531 self.log.error("Completed build: %s" % build)
2532 else:
2533 self.log.error("No completed builds")
2534 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002535
James E. Blair6ac368c2016-12-22 18:07:20 -08002536 def printHistory(self):
2537 """Log the build history.
2538
2539 This can be useful during tests to summarize what jobs have
2540 completed.
2541
2542 """
2543 self.log.debug("Build history:")
2544 for build in self.history:
2545 self.log.debug(build)
2546
James E. Blair59fdbac2015-12-07 17:08:06 -08002547 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002548 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2549
James E. Blair9ea70072017-04-19 16:05:30 -07002550 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002551 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002552 if not os.path.exists(root):
2553 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002554 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2555 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002556- tenant:
2557 name: openstack
2558 source:
2559 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002560 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002561 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002562 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002563 - org/project
2564 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002565 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002566 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002567 self.config.set('zuul', 'tenant_config',
2568 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002569 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002570
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002571 def addCommitToRepo(self, project, message, files,
2572 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002573 path = os.path.join(self.upstream_root, project)
2574 repo = git.Repo(path)
2575 repo.head.reference = branch
2576 zuul.merger.merger.reset_repo_to_head(repo)
2577 for fn, content in files.items():
2578 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002579 try:
2580 os.makedirs(os.path.dirname(fn))
2581 except OSError:
2582 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002583 with open(fn, 'w') as f:
2584 f.write(content)
2585 repo.index.add([fn])
2586 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002587 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002588 repo.heads[branch].commit = commit
2589 repo.head.reference = branch
2590 repo.git.clean('-x', '-f', '-d')
2591 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002592 if tag:
2593 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002594 return before
2595
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002596 def commitConfigUpdate(self, project_name, source_name):
2597 """Commit an update to zuul.yaml
2598
2599 This overwrites the zuul.yaml in the specificed project with
2600 the contents specified.
2601
2602 :arg str project_name: The name of the project containing
2603 zuul.yaml (e.g., common-config)
2604
2605 :arg str source_name: The path to the file (underneath the
2606 test fixture directory) whose contents should be used to
2607 replace zuul.yaml.
2608 """
2609
2610 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002611 files = {}
2612 with open(source_path, 'r') as f:
2613 data = f.read()
2614 layout = yaml.safe_load(data)
2615 files['zuul.yaml'] = data
2616 for item in layout:
2617 if 'job' in item:
2618 jobname = item['job']['name']
2619 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002620 before = self.addCommitToRepo(
2621 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002622 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002623 return before
2624
James E. Blair7fc8daa2016-08-08 15:37:15 -07002625 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002626
James E. Blair7fc8daa2016-08-08 15:37:15 -07002627 """Inject a Fake (Gerrit) event.
2628
2629 This method accepts a JSON-encoded event and simulates Zuul
2630 having received it from Gerrit. It could (and should)
2631 eventually apply to any connection type, but is currently only
2632 used with Gerrit connections. The name of the connection is
2633 used to look up the corresponding server, and the event is
2634 simulated as having been received by all Zuul connections
2635 attached to that server. So if two Gerrit connections in Zuul
2636 are connected to the same Gerrit server, and you invoke this
2637 method specifying the name of one of them, the event will be
2638 received by both.
2639
2640 .. note::
2641
2642 "self.fake_gerrit.addEvent" calls should be migrated to
2643 this method.
2644
2645 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002646 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002647 :arg str event: The JSON-encoded event.
2648
2649 """
2650 specified_conn = self.connections.connections[connection]
2651 for conn in self.connections.connections.values():
2652 if (isinstance(conn, specified_conn.__class__) and
2653 specified_conn.server == conn.server):
2654 conn.addEvent(event)
2655
James E. Blair3f876d52016-07-22 13:07:14 -07002656
2657class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002658 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002659 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002660
Joshua Heskethd78b4482015-09-14 16:56:34 -06002661
2662class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002663 def setup_config(self):
2664 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002665 for section_name in self.config.sections():
2666 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2667 section_name, re.I)
2668 if not con_match:
2669 continue
2670
2671 if self.config.get(section_name, 'driver') == 'sql':
2672 f = MySQLSchemaFixture()
2673 self.useFixture(f)
2674 if (self.config.get(section_name, 'dburi') ==
2675 '$MYSQL_FIXTURE_DBURI$'):
2676 self.config.set(section_name, 'dburi', f.dburi)