blob: 65ded500e8f4b2c6d0ef5cf180164729f8a5dc1c [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
22import json
23import logging
24import os
Christian Berendt12d4d722014-06-07 21:03:45 +020025from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070026from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070027import random
28import re
29import select
30import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030031from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070032try:
33 from cStringIO import StringIO
34except Exception:
35 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070036import socket
37import string
38import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080039import sys
James E. Blairf84026c2015-12-08 16:11:46 -080040import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070041import threading
Clark Boylan8208c192017-04-24 18:08:08 -070042import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070043import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060044import uuid
45
Clark Boylanb640e052014-04-03 16:41:46 -070046
47import git
48import gear
49import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080050import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080051import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060052import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070053import statsd
54import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080055import testtools.content
56import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080057from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000058import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070059
James E. Blaire511d2f2016-12-08 15:22:26 -080060import zuul.driver.gerrit.gerritsource as gerritsource
61import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070062import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.scheduler
64import zuul.webapp
65import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040066import zuul.executor.server
67import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080068import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070069import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070070import zuul.merger.merger
71import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070072import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080073import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010074from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070075
76FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
77 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080078
79KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070080
Clark Boylanb640e052014-04-03 16:41:46 -070081
82def repack_repo(path):
83 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
84 output = subprocess.Popen(cmd, close_fds=True,
85 stdout=subprocess.PIPE,
86 stderr=subprocess.PIPE)
87 out = output.communicate()
88 if output.returncode:
89 raise Exception("git repack returned %d" % output.returncode)
90 return out
91
92
93def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040094 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070095
96
James E. Blaira190f3b2015-01-05 14:56:54 -080097def iterate_timeout(max_seconds, purpose):
98 start = time.time()
99 count = 0
100 while (time.time() < start + max_seconds):
101 count += 1
102 yield count
103 time.sleep(0)
104 raise Exception("Timeout waiting for %s" % purpose)
105
106
Jesse Keating436a5452017-04-20 11:48:41 -0700107def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700108 """Specify a layout file for use by a test method.
109
110 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700111 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700112
113 Some tests require only a very simple configuration. For those,
114 establishing a complete config directory hierachy is too much
115 work. In those cases, you can add a simple zuul.yaml file to the
116 test fixtures directory (in fixtures/layouts/foo.yaml) and use
117 this decorator to indicate the test method should use that rather
118 than the tenant config file specified by the test class.
119
120 The decorator will cause that layout file to be added to a
121 config-project called "common-config" and each "project" instance
122 referenced in the layout file will have a git repo automatically
123 initialized.
124 """
125
126 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700127 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700128 return test
129 return decorator
130
131
Gregory Haynes4fc12542015-04-22 20:38:06 -0700132class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700133 _common_path_default = "refs/changes"
134 _points_to_commits_only = True
135
136
Gregory Haynes4fc12542015-04-22 20:38:06 -0700137class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700138 categories = {'approved': ('Approved', -1, 1),
139 'code-review': ('Code-Review', -2, 2),
140 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700141
142 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700143 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700145 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.reported = 0
147 self.queried = 0
148 self.patchsets = []
149 self.number = number
150 self.project = project
151 self.branch = branch
152 self.subject = subject
153 self.latest_patchset = 0
154 self.depends_on_change = None
155 self.needed_by_changes = []
156 self.fail_merge = False
157 self.messages = []
158 self.data = {
159 'branch': branch,
160 'comments': [],
161 'commitMessage': subject,
162 'createdOn': time.time(),
163 'id': 'I' + random_sha1(),
164 'lastUpdated': time.time(),
165 'number': str(number),
166 'open': status == 'NEW',
167 'owner': {'email': 'user@example.com',
168 'name': 'User Name',
169 'username': 'username'},
170 'patchSets': self.patchsets,
171 'project': project,
172 'status': status,
173 'subject': subject,
174 'submitRecords': [],
175 'url': 'https://hostname/%s' % number}
176
177 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700178 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 self.data['submitRecords'] = self.getSubmitRecords()
180 self.open = status == 'NEW'
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 path = os.path.join(self.upstream_root, self.project)
184 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700185 ref = GerritChangeReference.create(
186 repo, '1/%s/%s' % (self.number, self.latest_patchset),
187 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700189 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.git.clean('-x', '-f', '-d')
191
192 path = os.path.join(self.upstream_root, self.project)
193 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for fn, content in files.items():
195 fn = os.path.join(path, fn)
196 with open(fn, 'w') as f:
197 f.write(content)
198 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700199 else:
200 for fni in range(100):
201 fn = os.path.join(path, str(fni))
202 f = open(fn, 'w')
203 for ci in range(4096):
204 f.write(random.choice(string.printable))
205 f.close()
206 repo.index.add([fn])
207
208 r = repo.index.commit(msg)
209 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700210 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700211 repo.git.clean('-x', '-f', '-d')
212 repo.heads['master'].checkout()
213 return r
214
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700216 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700218 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700219 data = ("test %s %s %s\n" %
220 (self.branch, self.number, self.latest_patchset))
221 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700222 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700223 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700224 ps_files = [{'file': '/COMMIT_MSG',
225 'type': 'ADDED'},
226 {'file': 'README',
227 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700228 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700229 ps_files.append({'file': f, 'type': 'ADDED'})
230 d = {'approvals': [],
231 'createdOn': time.time(),
232 'files': ps_files,
233 'number': str(self.latest_patchset),
234 'ref': 'refs/changes/1/%s/%s' % (self.number,
235 self.latest_patchset),
236 'revision': c.hexsha,
237 'uploader': {'email': 'user@example.com',
238 'name': 'User name',
239 'username': 'user'}}
240 self.data['currentPatchSet'] = d
241 self.patchsets.append(d)
242 self.data['submitRecords'] = self.getSubmitRecords()
243
244 def getPatchsetCreatedEvent(self, patchset):
245 event = {"type": "patchset-created",
246 "change": {"project": self.project,
247 "branch": self.branch,
248 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
249 "number": str(self.number),
250 "subject": self.subject,
251 "owner": {"name": "User Name"},
252 "url": "https://hostname/3"},
253 "patchSet": self.patchsets[patchset - 1],
254 "uploader": {"name": "User Name"}}
255 return event
256
257 def getChangeRestoredEvent(self):
258 event = {"type": "change-restored",
259 "change": {"project": self.project,
260 "branch": self.branch,
261 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
262 "number": str(self.number),
263 "subject": self.subject,
264 "owner": {"name": "User Name"},
265 "url": "https://hostname/3"},
266 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100267 "patchSet": self.patchsets[-1],
268 "reason": ""}
269 return event
270
271 def getChangeAbandonedEvent(self):
272 event = {"type": "change-abandoned",
273 "change": {"project": self.project,
274 "branch": self.branch,
275 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
276 "number": str(self.number),
277 "subject": self.subject,
278 "owner": {"name": "User Name"},
279 "url": "https://hostname/3"},
280 "abandoner": {"name": "User Name"},
281 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700282 "reason": ""}
283 return event
284
285 def getChangeCommentEvent(self, patchset):
286 event = {"type": "comment-added",
287 "change": {"project": self.project,
288 "branch": self.branch,
289 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
290 "number": str(self.number),
291 "subject": self.subject,
292 "owner": {"name": "User Name"},
293 "url": "https://hostname/3"},
294 "patchSet": self.patchsets[patchset - 1],
295 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700296 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700297 "description": "Code-Review",
298 "value": "0"}],
299 "comment": "This is a comment"}
300 return event
301
James E. Blairc2a5ed72017-02-20 14:12:01 -0500302 def getChangeMergedEvent(self):
303 event = {"submitter": {"name": "Jenkins",
304 "username": "jenkins"},
305 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
306 "patchSet": self.patchsets[-1],
307 "change": self.data,
308 "type": "change-merged",
309 "eventCreatedOn": 1487613810}
310 return event
311
James E. Blair8cce42e2016-10-18 08:18:36 -0700312 def getRefUpdatedEvent(self):
313 path = os.path.join(self.upstream_root, self.project)
314 repo = git.Repo(path)
315 oldrev = repo.heads[self.branch].commit.hexsha
316
317 event = {
318 "type": "ref-updated",
319 "submitter": {
320 "name": "User Name",
321 },
322 "refUpdate": {
323 "oldRev": oldrev,
324 "newRev": self.patchsets[-1]['revision'],
325 "refName": self.branch,
326 "project": self.project,
327 }
328 }
329 return event
330
Joshua Hesketh642824b2014-07-01 17:54:59 +1000331 def addApproval(self, category, value, username='reviewer_john',
332 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700333 if not granted_on:
334 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000335 approval = {
336 'description': self.categories[category][0],
337 'type': category,
338 'value': str(value),
339 'by': {
340 'username': username,
341 'email': username + '@example.com',
342 },
343 'grantedOn': int(granted_on)
344 }
Clark Boylanb640e052014-04-03 16:41:46 -0700345 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
346 if x['by']['username'] == username and x['type'] == category:
347 del self.patchsets[-1]['approvals'][i]
348 self.patchsets[-1]['approvals'].append(approval)
349 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000350 'author': {'email': 'author@example.com',
351 'name': 'Patchset Author',
352 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700353 'change': {'branch': self.branch,
354 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
355 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000356 'owner': {'email': 'owner@example.com',
357 'name': 'Change Owner',
358 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700359 'project': self.project,
360 'subject': self.subject,
361 'topic': 'master',
362 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000363 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700364 'patchSet': self.patchsets[-1],
365 'type': 'comment-added'}
366 self.data['submitRecords'] = self.getSubmitRecords()
367 return json.loads(json.dumps(event))
368
369 def getSubmitRecords(self):
370 status = {}
371 for cat in self.categories.keys():
372 status[cat] = 0
373
374 for a in self.patchsets[-1]['approvals']:
375 cur = status[a['type']]
376 cat_min, cat_max = self.categories[a['type']][1:]
377 new = int(a['value'])
378 if new == cat_min:
379 cur = new
380 elif abs(new) > abs(cur):
381 cur = new
382 status[a['type']] = cur
383
384 labels = []
385 ok = True
386 for typ, cat in self.categories.items():
387 cur = status[typ]
388 cat_min, cat_max = cat[1:]
389 if cur == cat_min:
390 value = 'REJECT'
391 ok = False
392 elif cur == cat_max:
393 value = 'OK'
394 else:
395 value = 'NEED'
396 ok = False
397 labels.append({'label': cat[0], 'status': value})
398 if ok:
399 return [{'status': 'OK'}]
400 return [{'status': 'NOT_READY',
401 'labels': labels}]
402
403 def setDependsOn(self, other, patchset):
404 self.depends_on_change = other
405 d = {'id': other.data['id'],
406 'number': other.data['number'],
407 'ref': other.patchsets[patchset - 1]['ref']
408 }
409 self.data['dependsOn'] = [d]
410
411 other.needed_by_changes.append(self)
412 needed = other.data.get('neededBy', [])
413 d = {'id': self.data['id'],
414 'number': self.data['number'],
415 'ref': self.patchsets[patchset - 1]['ref'],
416 'revision': self.patchsets[patchset - 1]['revision']
417 }
418 needed.append(d)
419 other.data['neededBy'] = needed
420
421 def query(self):
422 self.queried += 1
423 d = self.data.get('dependsOn')
424 if d:
425 d = d[0]
426 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
427 d['isCurrentPatchSet'] = True
428 else:
429 d['isCurrentPatchSet'] = False
430 return json.loads(json.dumps(self.data))
431
432 def setMerged(self):
433 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000434 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700435 return
436 if self.fail_merge:
437 return
438 self.data['status'] = 'MERGED'
439 self.open = False
440
441 path = os.path.join(self.upstream_root, self.project)
442 repo = git.Repo(path)
443 repo.heads[self.branch].commit = \
444 repo.commit(self.patchsets[-1]['revision'])
445
446 def setReported(self):
447 self.reported += 1
448
449
James E. Blaire511d2f2016-12-08 15:22:26 -0800450class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700451 """A Fake Gerrit connection for use in tests.
452
453 This subclasses
454 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
455 ability for tests to add changes to the fake Gerrit it represents.
456 """
457
Joshua Hesketh352264b2015-08-11 23:42:08 +1000458 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700459
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700461 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800462 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000463 connection_config)
464
James E. Blair7fc8daa2016-08-08 15:37:15 -0700465 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700466 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
467 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000468 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700469 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200470 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700471
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700472 def addFakeChange(self, project, branch, subject, status='NEW',
473 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700474 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700475 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700476 c = FakeGerritChange(self, self.change_number, project, branch,
477 subject, upstream_root=self.upstream_root,
478 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700479 self.changes[self.change_number] = c
480 return c
481
Clark Boylanb640e052014-04-03 16:41:46 -0700482 def review(self, project, changeid, message, action):
483 number, ps = changeid.split(',')
484 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000485
486 # Add the approval back onto the change (ie simulate what gerrit would
487 # do).
488 # Usually when zuul leaves a review it'll create a feedback loop where
489 # zuul's review enters another gerrit event (which is then picked up by
490 # zuul). However, we can't mimic this behaviour (by adding this
491 # approval event into the queue) as it stops jobs from checking what
492 # happens before this event is triggered. If a job needs to see what
493 # happens they can add their own verified event into the queue.
494 # Nevertheless, we can update change with the new review in gerrit.
495
James E. Blair8b5408c2016-08-08 15:37:46 -0700496 for cat in action.keys():
497 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000498 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000501
Clark Boylanb640e052014-04-03 16:41:46 -0700502 if 'submit' in action:
503 change.setMerged()
504 if message:
505 change.setReported()
506
507 def query(self, number):
508 change = self.changes.get(int(number))
509 if change:
510 return change.query()
511 return {}
512
James E. Blairc494d542014-08-06 09:23:52 -0700513 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700514 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700515 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800516 if query.startswith('change:'):
517 # Query a specific changeid
518 changeid = query[len('change:'):]
519 l = [change.query() for change in self.changes.values()
520 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700521 elif query.startswith('message:'):
522 # Query the content of a commit message
523 msg = query[len('message:'):].strip()
524 l = [change.query() for change in self.changes.values()
525 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800526 else:
527 # Query all open changes
528 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700529 return l
James E. Blairc494d542014-08-06 09:23:52 -0700530
Joshua Hesketh352264b2015-08-11 23:42:08 +1000531 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700532 pass
533
Joshua Hesketh352264b2015-08-11 23:42:08 +1000534 def getGitUrl(self, project):
535 return os.path.join(self.upstream_root, project.name)
536
Clark Boylanb640e052014-04-03 16:41:46 -0700537
Gregory Haynes4fc12542015-04-22 20:38:06 -0700538class GithubChangeReference(git.Reference):
539 _common_path_default = "refs/pull"
540 _points_to_commits_only = True
541
542
543class FakeGithubPullRequest(object):
544
545 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800546 subject, upstream_root, files=[], number_of_commits=1,
547 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548 """Creates a new PR with several commits.
549 Sends an event about opened PR."""
550 self.github = github
551 self.source = github
552 self.number = number
553 self.project = project
554 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100555 self.subject = subject
556 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700557 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100558 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700559 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100560 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100561 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800562 self.reviews = []
563 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self.updated_at = None
565 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100566 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100567 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700568 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
572
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
577
Jan Hruban570d01c2016-03-10 21:51:32 +0100578 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100580 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581 self._updateTimeStamp()
582
583 def getPullRequestOpenedEvent(self):
584 return self._getPullRequestEvent('opened')
585
586 def getPullRequestSynchronizeEvent(self):
587 return self._getPullRequestEvent('synchronize')
588
589 def getPullRequestReopenedEvent(self):
590 return self._getPullRequestEvent('reopened')
591
592 def getPullRequestClosedEvent(self):
593 return self._getPullRequestEvent('closed')
594
Jesse Keating8c2eb572017-05-30 17:31:45 -0700595 def getPushEvent(self, old_sha, ref='refs/heads/master'):
596 name = 'push'
597 data = {
598 'ref': ref,
599 'before': old_sha,
600 'after': self.head_sha,
601 'repository': {
602 'full_name': self.project
603 },
604 'sender': {
605 'login': 'ghuser'
606 }
607 }
608 return (name, data)
609
Gregory Haynes4fc12542015-04-22 20:38:06 -0700610 def addComment(self, message):
611 self.comments.append(message)
612 self._updateTimeStamp()
613
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200614 def getCommentAddedEvent(self, text):
615 name = 'issue_comment'
616 data = {
617 'action': 'created',
618 'issue': {
619 'number': self.number
620 },
621 'comment': {
622 'body': text
623 },
624 'repository': {
625 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100626 },
627 'sender': {
628 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200629 }
630 }
631 return (name, data)
632
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800633 def getReviewAddedEvent(self, review):
634 name = 'pull_request_review'
635 data = {
636 'action': 'submitted',
637 'pull_request': {
638 'number': self.number,
639 'title': self.subject,
640 'updated_at': self.updated_at,
641 'base': {
642 'ref': self.branch,
643 'repo': {
644 'full_name': self.project
645 }
646 },
647 'head': {
648 'sha': self.head_sha
649 }
650 },
651 'review': {
652 'state': review
653 },
654 'repository': {
655 'full_name': self.project
656 },
657 'sender': {
658 'login': 'ghuser'
659 }
660 }
661 return (name, data)
662
Jan Hruban16ad31f2015-11-07 14:39:07 +0100663 def addLabel(self, name):
664 if name not in self.labels:
665 self.labels.append(name)
666 self._updateTimeStamp()
667 return self._getLabelEvent(name)
668
669 def removeLabel(self, name):
670 if name in self.labels:
671 self.labels.remove(name)
672 self._updateTimeStamp()
673 return self._getUnlabelEvent(name)
674
675 def _getLabelEvent(self, label):
676 name = 'pull_request'
677 data = {
678 'action': 'labeled',
679 'pull_request': {
680 'number': self.number,
681 'updated_at': self.updated_at,
682 'base': {
683 'ref': self.branch,
684 'repo': {
685 'full_name': self.project
686 }
687 },
688 'head': {
689 'sha': self.head_sha
690 }
691 },
692 'label': {
693 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100694 },
695 'sender': {
696 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100697 }
698 }
699 return (name, data)
700
701 def _getUnlabelEvent(self, label):
702 name = 'pull_request'
703 data = {
704 'action': 'unlabeled',
705 'pull_request': {
706 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100707 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100708 'updated_at': self.updated_at,
709 'base': {
710 'ref': self.branch,
711 'repo': {
712 'full_name': self.project
713 }
714 },
715 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800716 'sha': self.head_sha,
717 'repo': {
718 'full_name': self.project
719 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100720 }
721 },
722 'label': {
723 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100724 },
725 'sender': {
726 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100727 }
728 }
729 return (name, data)
730
Gregory Haynes4fc12542015-04-22 20:38:06 -0700731 def _getRepo(self):
732 repo_path = os.path.join(self.upstream_root, self.project)
733 return git.Repo(repo_path)
734
735 def _createPRRef(self):
736 repo = self._getRepo()
737 GithubChangeReference.create(
738 repo, self._getPRReference(), 'refs/tags/init')
739
Jan Hruban570d01c2016-03-10 21:51:32 +0100740 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700741 repo = self._getRepo()
742 ref = repo.references[self._getPRReference()]
743 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100744 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700745 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100746 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700747 repo.head.reference = ref
748 zuul.merger.merger.reset_repo_to_head(repo)
749 repo.git.clean('-x', '-f', '-d')
750
Jan Hruban570d01c2016-03-10 21:51:32 +0100751 if files:
752 fn = files[0]
753 self.files = files
754 else:
755 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
756 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100757 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700758 fn = os.path.join(repo.working_dir, fn)
759 f = open(fn, 'w')
760 with open(fn, 'w') as f:
761 f.write("test %s %s\n" %
762 (self.branch, self.number))
763 repo.index.add([fn])
764
765 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800766 # Create an empty set of statuses for the given sha,
767 # each sha on a PR may have a status set on it
768 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700769 repo.head.reference = 'master'
770 zuul.merger.merger.reset_repo_to_head(repo)
771 repo.git.clean('-x', '-f', '-d')
772 repo.heads['master'].checkout()
773
774 def _updateTimeStamp(self):
775 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
776
777 def getPRHeadSha(self):
778 repo = self._getRepo()
779 return repo.references[self._getPRReference()].commit.hexsha
780
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800781 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800782 # Since we're bypassing github API, which would require a user, we
783 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800784 # insert the status at the top of the list, to simulate that it
785 # is the most recent set status
786 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100787 'state': state,
788 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800789 'description': description,
790 'context': context,
791 'creator': {
792 'login': user
793 }
794 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100795
Jesse Keatingae4cd272017-01-30 17:10:44 -0800796 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800797 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
798 # convert the timestamp to a str format that would be returned
799 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800800
Adam Gandelmand81dd762017-02-09 15:15:49 -0800801 if granted_on:
802 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
803 submitted_at = time.strftime(
804 gh_time_format, granted_on.timetuple())
805 else:
806 # github timestamps only down to the second, so we need to make
807 # sure reviews that tests add appear to be added over a period of
808 # time in the past and not all at once.
809 if not self.reviews:
810 # the first review happens 10 mins ago
811 offset = 600
812 else:
813 # subsequent reviews happen 1 minute closer to now
814 offset = 600 - (len(self.reviews) * 60)
815
816 granted_on = datetime.datetime.utcfromtimestamp(
817 time.time() - offset)
818 submitted_at = time.strftime(
819 gh_time_format, granted_on.timetuple())
820
Jesse Keatingae4cd272017-01-30 17:10:44 -0800821 self.reviews.append({
822 'state': state,
823 'user': {
824 'login': user,
825 'email': user + "@derp.com",
826 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800827 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800828 })
829
Gregory Haynes4fc12542015-04-22 20:38:06 -0700830 def _getPRReference(self):
831 return '%s/head' % self.number
832
833 def _getPullRequestEvent(self, action):
834 name = 'pull_request'
835 data = {
836 'action': action,
837 'number': self.number,
838 'pull_request': {
839 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100840 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700841 'updated_at': self.updated_at,
842 'base': {
843 'ref': self.branch,
844 'repo': {
845 'full_name': self.project
846 }
847 },
848 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800849 'sha': self.head_sha,
850 'repo': {
851 'full_name': self.project
852 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700853 }
Jan Hruban3b415922016-02-03 13:10:22 +0100854 },
855 'sender': {
856 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700857 }
858 }
859 return (name, data)
860
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800861 def getCommitStatusEvent(self, context, state='success', user='zuul'):
862 name = 'status'
863 data = {
864 'state': state,
865 'sha': self.head_sha,
866 'description': 'Test results for %s: %s' % (self.head_sha, state),
867 'target_url': 'http://zuul/%s' % self.head_sha,
868 'branches': [],
869 'context': context,
870 'sender': {
871 'login': user
872 }
873 }
874 return (name, data)
875
Gregory Haynes4fc12542015-04-22 20:38:06 -0700876
877class FakeGithubConnection(githubconnection.GithubConnection):
878 log = logging.getLogger("zuul.test.FakeGithubConnection")
879
880 def __init__(self, driver, connection_name, connection_config,
881 upstream_root=None):
882 super(FakeGithubConnection, self).__init__(driver, connection_name,
883 connection_config)
884 self.connection_name = connection_name
885 self.pr_number = 0
886 self.pull_requests = []
887 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100888 self.merge_failure = False
889 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700890
Jan Hruban570d01c2016-03-10 21:51:32 +0100891 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700892 self.pr_number += 1
893 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100894 self, self.pr_number, project, branch, subject, self.upstream_root,
895 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700896 self.pull_requests.append(pull_request)
897 return pull_request
898
Wayne1a78c612015-06-11 17:14:13 -0700899 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
900 if not old_rev:
901 old_rev = '00000000000000000000000000000000'
902 if not new_rev:
903 new_rev = random_sha1()
904 name = 'push'
905 data = {
906 'ref': ref,
907 'before': old_rev,
908 'after': new_rev,
909 'repository': {
910 'full_name': project
911 }
912 }
913 return (name, data)
914
Gregory Haynes4fc12542015-04-22 20:38:06 -0700915 def emitEvent(self, event):
916 """Emulates sending the GitHub webhook event to the connection."""
917 port = self.webapp.server.socket.getsockname()[1]
918 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700919 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700920 headers = {'X-Github-Event': name}
921 req = urllib.request.Request(
922 'http://localhost:%s/connection/%s/payload'
923 % (port, self.connection_name),
924 data=payload, headers=headers)
925 urllib.request.urlopen(req)
926
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200927 def getPull(self, project, number):
928 pr = self.pull_requests[number - 1]
929 data = {
930 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100931 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200932 'updated_at': pr.updated_at,
933 'base': {
934 'repo': {
935 'full_name': pr.project
936 },
937 'ref': pr.branch,
938 },
Jan Hruban37615e52015-11-19 14:30:49 +0100939 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700940 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200941 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800942 'sha': pr.head_sha,
943 'repo': {
944 'full_name': pr.project
945 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200946 }
947 }
948 return data
949
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800950 def getPullBySha(self, sha):
951 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
952 if len(prs) > 1:
953 raise Exception('Multiple pulls found with head sha: %s' % sha)
954 pr = prs[0]
955 return self.getPull(pr.project, pr.number)
956
Jan Hruban570d01c2016-03-10 21:51:32 +0100957 def getPullFileNames(self, project, number):
958 pr = self.pull_requests[number - 1]
959 return pr.files
960
Jesse Keatingae4cd272017-01-30 17:10:44 -0800961 def _getPullReviews(self, owner, project, number):
962 pr = self.pull_requests[number - 1]
963 return pr.reviews
964
Jan Hruban3b415922016-02-03 13:10:22 +0100965 def getUser(self, login):
966 data = {
967 'username': login,
968 'name': 'Github User',
969 'email': 'github.user@example.com'
970 }
971 return data
972
Jesse Keatingae4cd272017-01-30 17:10:44 -0800973 def getRepoPermission(self, project, login):
974 owner, proj = project.split('/')
975 for pr in self.pull_requests:
976 pr_owner, pr_project = pr.project.split('/')
977 if (pr_owner == owner and proj == pr_project):
978 if login in pr.writers:
979 return 'write'
980 else:
981 return 'read'
982
Gregory Haynes4fc12542015-04-22 20:38:06 -0700983 def getGitUrl(self, project):
984 return os.path.join(self.upstream_root, str(project))
985
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200986 def real_getGitUrl(self, project):
987 return super(FakeGithubConnection, self).getGitUrl(project)
988
Gregory Haynes4fc12542015-04-22 20:38:06 -0700989 def getProjectBranches(self, project):
990 """Masks getProjectBranches since we don't have a real github"""
991
992 # just returns master for now
993 return ['master']
994
Jan Hrubane252a732017-01-03 15:03:09 +0100995 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700996 pull_request = self.pull_requests[pr_number - 1]
997 pull_request.addComment(message)
998
Jan Hruban3b415922016-02-03 13:10:22 +0100999 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +01001000 pull_request = self.pull_requests[pr_number - 1]
1001 if self.merge_failure:
1002 raise Exception('Pull request was not merged')
1003 if self.merge_not_allowed_count > 0:
1004 self.merge_not_allowed_count -= 1
1005 raise MergeFailure('Merge was not successful due to mergeability'
1006 ' conflict')
1007 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001008 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001009
Jesse Keatingd96e5882017-01-19 13:55:50 -08001010 def getCommitStatuses(self, project, sha):
1011 owner, proj = project.split('/')
1012 for pr in self.pull_requests:
1013 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -07001014 # This is somewhat risky, if the same commit exists in multiple
1015 # PRs, we might grab the wrong one that doesn't have a status
1016 # that is expected to be there. Maybe re-work this so that there
1017 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001018 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001019 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001020 return pr.statuses[sha]
1021
Jan Hrubane252a732017-01-03 15:03:09 +01001022 def setCommitStatus(self, project, sha, state,
1023 url='', description='', context=''):
1024 owner, proj = project.split('/')
1025 for pr in self.pull_requests:
1026 pr_owner, pr_project = pr.project.split('/')
1027 if (pr_owner == owner and pr_project == proj and
1028 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001029 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001030
Jan Hruban16ad31f2015-11-07 14:39:07 +01001031 def labelPull(self, project, pr_number, label):
1032 pull_request = self.pull_requests[pr_number - 1]
1033 pull_request.addLabel(label)
1034
1035 def unlabelPull(self, project, pr_number, label):
1036 pull_request = self.pull_requests[pr_number - 1]
1037 pull_request.removeLabel(label)
1038
Gregory Haynes4fc12542015-04-22 20:38:06 -07001039
Clark Boylanb640e052014-04-03 16:41:46 -07001040class BuildHistory(object):
1041 def __init__(self, **kw):
1042 self.__dict__.update(kw)
1043
1044 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001045 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1046 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001047
1048
1049class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +02001050 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -07001051 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -07001052 self.url = url
1053
1054 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001055 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -07001056 path = res.path
1057 project = '/'.join(path.split('/')[2:-2])
1058 ret = '001e# service=git-upload-pack\n'
1059 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
1060 'multi_ack thin-pack side-band side-band-64k ofs-delta '
1061 'shallow no-progress include-tag multi_ack_detailed no-done\n')
1062 path = os.path.join(self.upstream_root, project)
1063 repo = git.Repo(path)
1064 for ref in repo.refs:
1065 r = ref.object.hexsha + ' ' + ref.path + '\n'
1066 ret += '%04x%s' % (len(r) + 4, r)
1067 ret += '0000'
1068 return ret
1069
1070
Clark Boylanb640e052014-04-03 16:41:46 -07001071class FakeStatsd(threading.Thread):
1072 def __init__(self):
1073 threading.Thread.__init__(self)
1074 self.daemon = True
1075 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1076 self.sock.bind(('', 0))
1077 self.port = self.sock.getsockname()[1]
1078 self.wake_read, self.wake_write = os.pipe()
1079 self.stats = []
1080
1081 def run(self):
1082 while True:
1083 poll = select.poll()
1084 poll.register(self.sock, select.POLLIN)
1085 poll.register(self.wake_read, select.POLLIN)
1086 ret = poll.poll()
1087 for (fd, event) in ret:
1088 if fd == self.sock.fileno():
1089 data = self.sock.recvfrom(1024)
1090 if not data:
1091 return
1092 self.stats.append(data[0])
1093 if fd == self.wake_read:
1094 return
1095
1096 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001097 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001098
1099
James E. Blaire1767bc2016-08-02 10:00:27 -07001100class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001101 log = logging.getLogger("zuul.test")
1102
Paul Belanger174a8272017-03-14 13:20:10 -04001103 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001104 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001105 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001106 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001107 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001108 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001109 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001110 # TODOv3(jeblair): self.node is really "the image of the node
1111 # assigned". We should rename it (self.node_image?) if we
1112 # keep using it like this, or we may end up exposing more of
1113 # the complexity around multi-node jobs here
1114 # (self.nodes[0].image?)
1115 self.node = None
1116 if len(self.parameters.get('nodes')) == 1:
1117 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001118 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001119 self.pipeline = self.parameters['ZUUL_PIPELINE']
1120 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001121 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.wait_condition = threading.Condition()
1123 self.waiting = False
1124 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001125 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001126 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001127 self.changes = None
1128 if 'ZUUL_CHANGE_IDS' in self.parameters:
1129 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001130
James E. Blair3158e282016-08-19 09:34:11 -07001131 def __repr__(self):
1132 waiting = ''
1133 if self.waiting:
1134 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001135 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1136 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001137
Clark Boylanb640e052014-04-03 16:41:46 -07001138 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001139 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001140 self.wait_condition.acquire()
1141 self.wait_condition.notify()
1142 self.waiting = False
1143 self.log.debug("Build %s released" % self.unique)
1144 self.wait_condition.release()
1145
1146 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001147 """Return whether this build is being held.
1148
1149 :returns: Whether the build is being held.
1150 :rtype: bool
1151 """
1152
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.wait_condition.acquire()
1154 if self.waiting:
1155 ret = True
1156 else:
1157 ret = False
1158 self.wait_condition.release()
1159 return ret
1160
1161 def _wait(self):
1162 self.wait_condition.acquire()
1163 self.waiting = True
1164 self.log.debug("Build %s waiting" % self.unique)
1165 self.wait_condition.wait()
1166 self.wait_condition.release()
1167
1168 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001169 self.log.debug('Running build %s' % self.unique)
1170
Paul Belanger174a8272017-03-14 13:20:10 -04001171 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001172 self.log.debug('Holding build %s' % self.unique)
1173 self._wait()
1174 self.log.debug("Build %s continuing" % self.unique)
1175
James E. Blair412fba82017-01-26 15:00:50 -08001176 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001177 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001178 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001179 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001180 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001181 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001182 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001183
James E. Blaire1767bc2016-08-02 10:00:27 -07001184 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001185
James E. Blaira5dba232016-08-08 15:53:24 -07001186 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001187 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001188 for change in changes:
1189 if self.hasChanges(change):
1190 return True
1191 return False
1192
James E. Blaire7b99a02016-08-05 14:27:34 -07001193 def hasChanges(self, *changes):
1194 """Return whether this build has certain changes in its git repos.
1195
1196 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001197 are expected to be present (in order) in the git repository of
1198 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001199
1200 :returns: Whether the build has the indicated changes.
1201 :rtype: bool
1202
1203 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001204 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001205 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001206 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001207 try:
1208 repo = git.Repo(path)
1209 except NoSuchPathError as e:
1210 self.log.debug('%s' % e)
1211 return False
1212 ref = self.parameters['ZUUL_REF']
1213 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1214 commit_message = '%s-1' % change.subject
1215 self.log.debug("Checking if build %s has changes; commit_message "
1216 "%s; repo_messages %s" % (self, commit_message,
1217 repo_messages))
1218 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001219 self.log.debug(" messages do not match")
1220 return False
1221 self.log.debug(" OK")
1222 return True
1223
James E. Blaird8af5422017-05-24 13:59:40 -07001224 def getWorkspaceRepos(self, projects):
1225 """Return workspace git repo objects for the listed projects
1226
1227 :arg list projects: A list of strings, each the canonical name
1228 of a project.
1229
1230 :returns: A dictionary of {name: repo} for every listed
1231 project.
1232 :rtype: dict
1233
1234 """
1235
1236 repos = {}
1237 for project in projects:
1238 path = os.path.join(self.jobdir.src_root, project)
1239 repo = git.Repo(path)
1240 repos[project] = repo
1241 return repos
1242
Clark Boylanb640e052014-04-03 16:41:46 -07001243
Paul Belanger174a8272017-03-14 13:20:10 -04001244class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1245 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001246
Paul Belanger174a8272017-03-14 13:20:10 -04001247 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001248 they will report that they have started but then pause until
1249 released before reporting completion. This attribute may be
1250 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001251 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001252 be explicitly released.
1253
1254 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001255 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001257 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001258 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001259 self.hold_jobs_in_build = False
1260 self.lock = threading.Lock()
1261 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001262 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001263 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001264 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001265
James E. Blaira5dba232016-08-08 15:53:24 -07001266 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001267 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001268
1269 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001270 :arg Change change: The :py:class:`~tests.base.FakeChange`
1271 instance which should cause the job to fail. This job
1272 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001273
1274 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001275 l = self.fail_tests.get(name, [])
1276 l.append(change)
1277 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001278
James E. Blair962220f2016-08-03 11:22:38 -07001279 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001280 """Release a held build.
1281
1282 :arg str regex: A regular expression which, if supplied, will
1283 cause only builds with matching names to be released. If
1284 not supplied, all builds will be released.
1285
1286 """
James E. Blair962220f2016-08-03 11:22:38 -07001287 builds = self.running_builds[:]
1288 self.log.debug("Releasing build %s (%s)" % (regex,
1289 len(self.running_builds)))
1290 for build in builds:
1291 if not regex or re.match(regex, build.name):
1292 self.log.debug("Releasing build %s" %
1293 (build.parameters['ZUUL_UUID']))
1294 build.release()
1295 else:
1296 self.log.debug("Not releasing build %s" %
1297 (build.parameters['ZUUL_UUID']))
1298 self.log.debug("Done releasing builds %s (%s)" %
1299 (regex, len(self.running_builds)))
1300
Paul Belanger174a8272017-03-14 13:20:10 -04001301 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001302 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001303 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001304 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001305 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001306 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001307 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001308 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001309 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1310 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001311
1312 def stopJob(self, job):
1313 self.log.debug("handle stop")
1314 parameters = json.loads(job.arguments)
1315 uuid = parameters['uuid']
1316 for build in self.running_builds:
1317 if build.unique == uuid:
1318 build.aborted = True
1319 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001320 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001321
James E. Blaira002b032017-04-18 10:35:48 -07001322 def stop(self):
1323 for build in self.running_builds:
1324 build.release()
1325 super(RecordingExecutorServer, self).stop()
1326
Joshua Hesketh50c21782016-10-13 21:34:14 +11001327
Paul Belanger174a8272017-03-14 13:20:10 -04001328class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001329 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001330 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001331 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001332 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001333 if not commit: # merge conflict
1334 self.recordResult('MERGER_FAILURE')
1335 return commit
1336
1337 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001338 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001339 self.executor_server.lock.acquire()
1340 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001341 BuildHistory(name=build.name, result=result, changes=build.changes,
1342 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001343 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001344 pipeline=build.parameters['ZUUL_PIPELINE'])
1345 )
Paul Belanger174a8272017-03-14 13:20:10 -04001346 self.executor_server.running_builds.remove(build)
1347 del self.executor_server.job_builds[self.job.unique]
1348 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001349
1350 def runPlaybooks(self, args):
1351 build = self.executor_server.job_builds[self.job.unique]
1352 build.jobdir = self.jobdir
1353
1354 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1355 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001356 return result
1357
Monty Taylore6562aa2017-02-20 07:37:39 -05001358 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001359 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001360
Paul Belanger174a8272017-03-14 13:20:10 -04001361 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001362 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001363 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001364 else:
1365 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001366 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001367
James E. Blairad8dca02017-02-21 11:48:32 -05001368 def getHostList(self, args):
1369 self.log.debug("hostlist")
1370 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001371 for host in hosts:
1372 host['host_vars']['ansible_connection'] = 'local'
1373
1374 hosts.append(dict(
1375 name='localhost',
1376 host_vars=dict(ansible_connection='local'),
1377 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001378 return hosts
1379
James E. Blairf5dbd002015-12-23 15:26:17 -08001380
Clark Boylanb640e052014-04-03 16:41:46 -07001381class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001382 """A Gearman server for use in tests.
1383
1384 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1385 added to the queue but will not be distributed to workers
1386 until released. This attribute may be changed at any time and
1387 will take effect for subsequently enqueued jobs, but
1388 previously held jobs will still need to be explicitly
1389 released.
1390
1391 """
1392
Clark Boylanb640e052014-04-03 16:41:46 -07001393 def __init__(self):
1394 self.hold_jobs_in_queue = False
1395 super(FakeGearmanServer, self).__init__(0)
1396
1397 def getJobForConnection(self, connection, peek=False):
1398 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1399 for job in queue:
1400 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001401 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001402 job.waiting = self.hold_jobs_in_queue
1403 else:
1404 job.waiting = False
1405 if job.waiting:
1406 continue
1407 if job.name in connection.functions:
1408 if not peek:
1409 queue.remove(job)
1410 connection.related_jobs[job.handle] = job
1411 job.worker_connection = connection
1412 job.running = True
1413 return job
1414 return None
1415
1416 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001417 """Release a held job.
1418
1419 :arg str regex: A regular expression which, if supplied, will
1420 cause only jobs with matching names to be released. If
1421 not supplied, all jobs will be released.
1422 """
Clark Boylanb640e052014-04-03 16:41:46 -07001423 released = False
1424 qlen = (len(self.high_queue) + len(self.normal_queue) +
1425 len(self.low_queue))
1426 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1427 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001428 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001429 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001430 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001431 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001432 self.log.debug("releasing queued job %s" %
1433 job.unique)
1434 job.waiting = False
1435 released = True
1436 else:
1437 self.log.debug("not releasing queued job %s" %
1438 job.unique)
1439 if released:
1440 self.wakeConnections()
1441 qlen = (len(self.high_queue) + len(self.normal_queue) +
1442 len(self.low_queue))
1443 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1444
1445
1446class FakeSMTP(object):
1447 log = logging.getLogger('zuul.FakeSMTP')
1448
1449 def __init__(self, messages, server, port):
1450 self.server = server
1451 self.port = port
1452 self.messages = messages
1453
1454 def sendmail(self, from_email, to_email, msg):
1455 self.log.info("Sending email from %s, to %s, with msg %s" % (
1456 from_email, to_email, msg))
1457
1458 headers = msg.split('\n\n', 1)[0]
1459 body = msg.split('\n\n', 1)[1]
1460
1461 self.messages.append(dict(
1462 from_email=from_email,
1463 to_email=to_email,
1464 msg=msg,
1465 headers=headers,
1466 body=body,
1467 ))
1468
1469 return True
1470
1471 def quit(self):
1472 return True
1473
1474
James E. Blairdce6cea2016-12-20 16:45:32 -08001475class FakeNodepool(object):
1476 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001477 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001478
1479 log = logging.getLogger("zuul.test.FakeNodepool")
1480
1481 def __init__(self, host, port, chroot):
1482 self.client = kazoo.client.KazooClient(
1483 hosts='%s:%s%s' % (host, port, chroot))
1484 self.client.start()
1485 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001486 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001487 self.thread = threading.Thread(target=self.run)
1488 self.thread.daemon = True
1489 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001490 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001491
1492 def stop(self):
1493 self._running = False
1494 self.thread.join()
1495 self.client.stop()
1496 self.client.close()
1497
1498 def run(self):
1499 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001500 try:
1501 self._run()
1502 except Exception:
1503 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001504 time.sleep(0.1)
1505
1506 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001507 if self.paused:
1508 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001509 for req in self.getNodeRequests():
1510 self.fulfillRequest(req)
1511
1512 def getNodeRequests(self):
1513 try:
1514 reqids = self.client.get_children(self.REQUEST_ROOT)
1515 except kazoo.exceptions.NoNodeError:
1516 return []
1517 reqs = []
1518 for oid in sorted(reqids):
1519 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001520 try:
1521 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001522 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001523 data['_oid'] = oid
1524 reqs.append(data)
1525 except kazoo.exceptions.NoNodeError:
1526 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001527 return reqs
1528
James E. Blaire18d4602017-01-05 11:17:28 -08001529 def getNodes(self):
1530 try:
1531 nodeids = self.client.get_children(self.NODE_ROOT)
1532 except kazoo.exceptions.NoNodeError:
1533 return []
1534 nodes = []
1535 for oid in sorted(nodeids):
1536 path = self.NODE_ROOT + '/' + oid
1537 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001538 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001539 data['_oid'] = oid
1540 try:
1541 lockfiles = self.client.get_children(path + '/lock')
1542 except kazoo.exceptions.NoNodeError:
1543 lockfiles = []
1544 if lockfiles:
1545 data['_lock'] = True
1546 else:
1547 data['_lock'] = False
1548 nodes.append(data)
1549 return nodes
1550
James E. Blaira38c28e2017-01-04 10:33:20 -08001551 def makeNode(self, request_id, node_type):
1552 now = time.time()
1553 path = '/nodepool/nodes/'
1554 data = dict(type=node_type,
1555 provider='test-provider',
1556 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001557 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001558 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001559 public_ipv4='127.0.0.1',
1560 private_ipv4=None,
1561 public_ipv6=None,
1562 allocated_to=request_id,
1563 state='ready',
1564 state_time=now,
1565 created_time=now,
1566 updated_time=now,
1567 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001568 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001569 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001570 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001571 path = self.client.create(path, data,
1572 makepath=True,
1573 sequence=True)
1574 nodeid = path.split("/")[-1]
1575 return nodeid
1576
James E. Blair6ab79e02017-01-06 10:10:17 -08001577 def addFailRequest(self, request):
1578 self.fail_requests.add(request['_oid'])
1579
James E. Blairdce6cea2016-12-20 16:45:32 -08001580 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001581 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001582 return
1583 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001584 oid = request['_oid']
1585 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001586
James E. Blair6ab79e02017-01-06 10:10:17 -08001587 if oid in self.fail_requests:
1588 request['state'] = 'failed'
1589 else:
1590 request['state'] = 'fulfilled'
1591 nodes = []
1592 for node in request['node_types']:
1593 nodeid = self.makeNode(oid, node)
1594 nodes.append(nodeid)
1595 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001596
James E. Blaira38c28e2017-01-04 10:33:20 -08001597 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001598 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001599 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001600 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001601 try:
1602 self.client.set(path, data)
1603 except kazoo.exceptions.NoNodeError:
1604 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001605
1606
James E. Blair498059b2016-12-20 13:50:13 -08001607class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001608 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001609 super(ChrootedKazooFixture, self).__init__()
1610
1611 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1612 if ':' in zk_host:
1613 host, port = zk_host.split(':')
1614 else:
1615 host = zk_host
1616 port = None
1617
1618 self.zookeeper_host = host
1619
1620 if not port:
1621 self.zookeeper_port = 2181
1622 else:
1623 self.zookeeper_port = int(port)
1624
Clark Boylan621ec9a2017-04-07 17:41:33 -07001625 self.test_id = test_id
1626
James E. Blair498059b2016-12-20 13:50:13 -08001627 def _setUp(self):
1628 # Make sure the test chroot paths do not conflict
1629 random_bits = ''.join(random.choice(string.ascii_lowercase +
1630 string.ascii_uppercase)
1631 for x in range(8))
1632
Clark Boylan621ec9a2017-04-07 17:41:33 -07001633 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001634 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1635
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001636 self.addCleanup(self._cleanup)
1637
James E. Blair498059b2016-12-20 13:50:13 -08001638 # Ensure the chroot path exists and clean up any pre-existing znodes.
1639 _tmp_client = kazoo.client.KazooClient(
1640 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1641 _tmp_client.start()
1642
1643 if _tmp_client.exists(self.zookeeper_chroot):
1644 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1645
1646 _tmp_client.ensure_path(self.zookeeper_chroot)
1647 _tmp_client.stop()
1648 _tmp_client.close()
1649
James E. Blair498059b2016-12-20 13:50:13 -08001650 def _cleanup(self):
1651 '''Remove the chroot path.'''
1652 # Need a non-chroot'ed client to remove the chroot path
1653 _tmp_client = kazoo.client.KazooClient(
1654 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1655 _tmp_client.start()
1656 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1657 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001658 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001659
1660
Joshua Heskethd78b4482015-09-14 16:56:34 -06001661class MySQLSchemaFixture(fixtures.Fixture):
1662 def setUp(self):
1663 super(MySQLSchemaFixture, self).setUp()
1664
1665 random_bits = ''.join(random.choice(string.ascii_lowercase +
1666 string.ascii_uppercase)
1667 for x in range(8))
1668 self.name = '%s_%s' % (random_bits, os.getpid())
1669 self.passwd = uuid.uuid4().hex
1670 db = pymysql.connect(host="localhost",
1671 user="openstack_citest",
1672 passwd="openstack_citest",
1673 db="openstack_citest")
1674 cur = db.cursor()
1675 cur.execute("create database %s" % self.name)
1676 cur.execute(
1677 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1678 (self.name, self.name, self.passwd))
1679 cur.execute("flush privileges")
1680
1681 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1682 self.passwd,
1683 self.name)
1684 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1685 self.addCleanup(self.cleanup)
1686
1687 def cleanup(self):
1688 db = pymysql.connect(host="localhost",
1689 user="openstack_citest",
1690 passwd="openstack_citest",
1691 db="openstack_citest")
1692 cur = db.cursor()
1693 cur.execute("drop database %s" % self.name)
1694 cur.execute("drop user '%s'@'localhost'" % self.name)
1695 cur.execute("flush privileges")
1696
1697
Maru Newby3fe5f852015-01-13 04:22:14 +00001698class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001699 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001700 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001701
James E. Blair1c236df2017-02-01 14:07:24 -08001702 def attachLogs(self, *args):
1703 def reader():
1704 self._log_stream.seek(0)
1705 while True:
1706 x = self._log_stream.read(4096)
1707 if not x:
1708 break
1709 yield x.encode('utf8')
1710 content = testtools.content.content_from_reader(
1711 reader,
1712 testtools.content_type.UTF8_TEXT,
1713 False)
1714 self.addDetail('logging', content)
1715
Clark Boylanb640e052014-04-03 16:41:46 -07001716 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001717 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001718 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1719 try:
1720 test_timeout = int(test_timeout)
1721 except ValueError:
1722 # If timeout value is invalid do not set a timeout.
1723 test_timeout = 0
1724 if test_timeout > 0:
1725 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1726
1727 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1728 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1729 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1730 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1731 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1732 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1733 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1734 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1735 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1736 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001737 self._log_stream = StringIO()
1738 self.addOnException(self.attachLogs)
1739 else:
1740 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001741
James E. Blair73b41772017-05-22 13:22:55 -07001742 # NOTE(jeblair): this is temporary extra debugging to try to
1743 # track down a possible leak.
1744 orig_git_repo_init = git.Repo.__init__
1745
1746 def git_repo_init(myself, *args, **kw):
1747 orig_git_repo_init(myself, *args, **kw)
1748 self.log.debug("Created git repo 0x%x %s" %
1749 (id(myself), repr(myself)))
1750
1751 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1752 git_repo_init))
1753
James E. Blair1c236df2017-02-01 14:07:24 -08001754 handler = logging.StreamHandler(self._log_stream)
1755 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1756 '%(levelname)-8s %(message)s')
1757 handler.setFormatter(formatter)
1758
1759 logger = logging.getLogger()
1760 logger.setLevel(logging.DEBUG)
1761 logger.addHandler(handler)
1762
Clark Boylan3410d532017-04-25 12:35:29 -07001763 # Make sure we don't carry old handlers around in process state
1764 # which slows down test runs
1765 self.addCleanup(logger.removeHandler, handler)
1766 self.addCleanup(handler.close)
1767 self.addCleanup(handler.flush)
1768
James E. Blair1c236df2017-02-01 14:07:24 -08001769 # NOTE(notmorgan): Extract logging overrides for specific
1770 # libraries from the OS_LOG_DEFAULTS env and create loggers
1771 # for each. This is used to limit the output during test runs
1772 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001773 log_defaults_from_env = os.environ.get(
1774 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001775 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001776
James E. Blairdce6cea2016-12-20 16:45:32 -08001777 if log_defaults_from_env:
1778 for default in log_defaults_from_env.split(','):
1779 try:
1780 name, level_str = default.split('=', 1)
1781 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001782 logger = logging.getLogger(name)
1783 logger.setLevel(level)
1784 logger.addHandler(handler)
1785 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001786 except ValueError:
1787 # NOTE(notmorgan): Invalid format of the log default,
1788 # skip and don't try and apply a logger for the
1789 # specified module
1790 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001791
Maru Newby3fe5f852015-01-13 04:22:14 +00001792
1793class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001794 """A test case with a functioning Zuul.
1795
1796 The following class variables are used during test setup and can
1797 be overidden by subclasses but are effectively read-only once a
1798 test method starts running:
1799
1800 :cvar str config_file: This points to the main zuul config file
1801 within the fixtures directory. Subclasses may override this
1802 to obtain a different behavior.
1803
1804 :cvar str tenant_config_file: This is the tenant config file
1805 (which specifies from what git repos the configuration should
1806 be loaded). It defaults to the value specified in
1807 `config_file` but can be overidden by subclasses to obtain a
1808 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001809 configuration. See also the :py:func:`simple_layout`
1810 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001811
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001812 :cvar bool create_project_keys: Indicates whether Zuul should
1813 auto-generate keys for each project, or whether the test
1814 infrastructure should insert dummy keys to save time during
1815 startup. Defaults to False.
1816
James E. Blaire7b99a02016-08-05 14:27:34 -07001817 The following are instance variables that are useful within test
1818 methods:
1819
1820 :ivar FakeGerritConnection fake_<connection>:
1821 A :py:class:`~tests.base.FakeGerritConnection` will be
1822 instantiated for each connection present in the config file
1823 and stored here. For instance, `fake_gerrit` will hold the
1824 FakeGerritConnection object for a connection named `gerrit`.
1825
1826 :ivar FakeGearmanServer gearman_server: An instance of
1827 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1828 server that all of the Zuul components in this test use to
1829 communicate with each other.
1830
Paul Belanger174a8272017-03-14 13:20:10 -04001831 :ivar RecordingExecutorServer executor_server: An instance of
1832 :py:class:`~tests.base.RecordingExecutorServer` which is the
1833 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001834
1835 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1836 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001837 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001838 list upon completion.
1839
1840 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1841 objects representing completed builds. They are appended to
1842 the list in the order they complete.
1843
1844 """
1845
James E. Blair83005782015-12-11 14:46:03 -08001846 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001847 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001848 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001849
1850 def _startMerger(self):
1851 self.merge_server = zuul.merger.server.MergeServer(self.config,
1852 self.connections)
1853 self.merge_server.start()
1854
Maru Newby3fe5f852015-01-13 04:22:14 +00001855 def setUp(self):
1856 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001857
1858 self.setupZK()
1859
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001860 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001861 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001862 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1863 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001864 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001865 tmp_root = tempfile.mkdtemp(
1866 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001867 self.test_root = os.path.join(tmp_root, "zuul-test")
1868 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001869 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001870 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001871 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001872
1873 if os.path.exists(self.test_root):
1874 shutil.rmtree(self.test_root)
1875 os.makedirs(self.test_root)
1876 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001877 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001878
1879 # Make per test copy of Configuration.
1880 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001881 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1882 if not os.path.exists(self.private_key_file):
1883 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1884 shutil.copy(src_private_key_file, self.private_key_file)
1885 shutil.copy('{}.pub'.format(src_private_key_file),
1886 '{}.pub'.format(self.private_key_file))
1887 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001888 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001889 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001890 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001891 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001892 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001893 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001894 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001895
Clark Boylanb640e052014-04-03 16:41:46 -07001896 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001897 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1898 # see: https://github.com/jsocol/pystatsd/issues/61
1899 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001900 os.environ['STATSD_PORT'] = str(self.statsd.port)
1901 self.statsd.start()
1902 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001903 reload_module(statsd)
1904 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001905
1906 self.gearman_server = FakeGearmanServer()
1907
1908 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001909 self.log.info("Gearman server on port %s" %
1910 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001911
James E. Blaire511d2f2016-12-08 15:22:26 -08001912 gerritsource.GerritSource.replication_timeout = 1.5
1913 gerritsource.GerritSource.replication_retry_interval = 0.5
1914 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001915
Joshua Hesketh352264b2015-08-11 23:42:08 +10001916 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001917
Jan Hruban7083edd2015-08-21 14:00:54 +02001918 self.webapp = zuul.webapp.WebApp(
1919 self.sched, port=0, listen_address='127.0.0.1')
1920
Jan Hruban6b71aff2015-10-22 16:58:08 +02001921 self.event_queues = [
1922 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001923 self.sched.trigger_event_queue,
1924 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001925 ]
1926
James E. Blairfef78942016-03-11 16:28:56 -08001927 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001928 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001929
Clark Boylanb640e052014-04-03 16:41:46 -07001930 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001931 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001932 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001933 return FakeURLOpener(self.upstream_root, *args, **kw)
1934
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001935 old_urlopen = urllib.request.urlopen
1936 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001937
Paul Belanger174a8272017-03-14 13:20:10 -04001938 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001939 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001940 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001941 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001942 _test_root=self.test_root,
1943 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001944 self.executor_server.start()
1945 self.history = self.executor_server.build_history
1946 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001947
Paul Belanger174a8272017-03-14 13:20:10 -04001948 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001949 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001950 self.merge_client = zuul.merger.client.MergeClient(
1951 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001952 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001953 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001954 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001955
James E. Blair0d5a36e2017-02-21 10:53:44 -05001956 self.fake_nodepool = FakeNodepool(
1957 self.zk_chroot_fixture.zookeeper_host,
1958 self.zk_chroot_fixture.zookeeper_port,
1959 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001960
Paul Belanger174a8272017-03-14 13:20:10 -04001961 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001962 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001963 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001964 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001965
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001966 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001967
1968 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001969 self.webapp.start()
1970 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001971 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001972 # Cleanups are run in reverse order
1973 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001974 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001975 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001976
James E. Blairb9c0d772017-03-03 14:34:49 -08001977 self.sched.reconfigure(self.config)
1978 self.sched.resume()
1979
Tobias Henkel7df274b2017-05-26 17:41:11 +02001980 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001981 # Set up gerrit related fakes
1982 # Set a changes database so multiple FakeGerrit's can report back to
1983 # a virtual canonical database given by the configured hostname
1984 self.gerrit_changes_dbs = {}
1985
1986 def getGerritConnection(driver, name, config):
1987 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1988 con = FakeGerritConnection(driver, name, config,
1989 changes_db=db,
1990 upstream_root=self.upstream_root)
1991 self.event_queues.append(con.event_queue)
1992 setattr(self, 'fake_' + name, con)
1993 return con
1994
1995 self.useFixture(fixtures.MonkeyPatch(
1996 'zuul.driver.gerrit.GerritDriver.getConnection',
1997 getGerritConnection))
1998
Gregory Haynes4fc12542015-04-22 20:38:06 -07001999 def getGithubConnection(driver, name, config):
2000 con = FakeGithubConnection(driver, name, config,
2001 upstream_root=self.upstream_root)
2002 setattr(self, 'fake_' + name, con)
2003 return con
2004
2005 self.useFixture(fixtures.MonkeyPatch(
2006 'zuul.driver.github.GithubDriver.getConnection',
2007 getGithubConnection))
2008
James E. Blaire511d2f2016-12-08 15:22:26 -08002009 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002010 # TODO(jhesketh): This should come from lib.connections for better
2011 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002012 # Register connections from the config
2013 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002014
Joshua Hesketh352264b2015-08-11 23:42:08 +10002015 def FakeSMTPFactory(*args, **kw):
2016 args = [self.smtp_messages] + list(args)
2017 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002018
Joshua Hesketh352264b2015-08-11 23:42:08 +10002019 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002020
James E. Blaire511d2f2016-12-08 15:22:26 -08002021 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002022 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002023 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002024
James E. Blair83005782015-12-11 14:46:03 -08002025 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002026 # This creates the per-test configuration object. It can be
2027 # overriden by subclasses, but should not need to be since it
2028 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002029 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002030 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002031
2032 if not self.setupSimpleLayout():
2033 if hasattr(self, 'tenant_config_file'):
2034 self.config.set('zuul', 'tenant_config',
2035 self.tenant_config_file)
2036 git_path = os.path.join(
2037 os.path.dirname(
2038 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2039 'git')
2040 if os.path.exists(git_path):
2041 for reponame in os.listdir(git_path):
2042 project = reponame.replace('_', '/')
2043 self.copyDirToRepo(project,
2044 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002045 self.setupAllProjectKeys()
2046
James E. Blair06cc3922017-04-19 10:08:10 -07002047 def setupSimpleLayout(self):
2048 # If the test method has been decorated with a simple_layout,
2049 # use that instead of the class tenant_config_file. Set up a
2050 # single config-project with the specified layout, and
2051 # initialize repos for all of the 'project' entries which
2052 # appear in the layout.
2053 test_name = self.id().split('.')[-1]
2054 test = getattr(self, test_name)
2055 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002056 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002057 else:
2058 return False
2059
James E. Blairb70e55a2017-04-19 12:57:02 -07002060 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002061 path = os.path.join(FIXTURE_DIR, path)
2062 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002063 data = f.read()
2064 layout = yaml.safe_load(data)
2065 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002066 untrusted_projects = []
2067 for item in layout:
2068 if 'project' in item:
2069 name = item['project']['name']
2070 untrusted_projects.append(name)
2071 self.init_repo(name)
2072 self.addCommitToRepo(name, 'initial commit',
2073 files={'README': ''},
2074 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002075 if 'job' in item:
2076 jobname = item['job']['name']
2077 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002078
2079 root = os.path.join(self.test_root, "config")
2080 if not os.path.exists(root):
2081 os.makedirs(root)
2082 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2083 config = [{'tenant':
2084 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002085 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002086 {'config-projects': ['common-config'],
2087 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002088 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002089 f.close()
2090 self.config.set('zuul', 'tenant_config',
2091 os.path.join(FIXTURE_DIR, f.name))
2092
2093 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002094 self.addCommitToRepo('common-config', 'add content from fixture',
2095 files, branch='master', tag='init')
2096
2097 return True
2098
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002099 def setupAllProjectKeys(self):
2100 if self.create_project_keys:
2101 return
2102
2103 path = self.config.get('zuul', 'tenant_config')
2104 with open(os.path.join(FIXTURE_DIR, path)) as f:
2105 tenant_config = yaml.safe_load(f.read())
2106 for tenant in tenant_config:
2107 sources = tenant['tenant']['source']
2108 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002109 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002110 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002111 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002112 self.setupProjectKeys(source, project)
2113
2114 def setupProjectKeys(self, source, project):
2115 # Make sure we set up an RSA key for the project so that we
2116 # don't spend time generating one:
2117
2118 key_root = os.path.join(self.state_root, 'keys')
2119 if not os.path.isdir(key_root):
2120 os.mkdir(key_root, 0o700)
2121 private_key_file = os.path.join(key_root, source, project + '.pem')
2122 private_key_dir = os.path.dirname(private_key_file)
2123 self.log.debug("Installing test keys for project %s at %s" % (
2124 project, private_key_file))
2125 if not os.path.isdir(private_key_dir):
2126 os.makedirs(private_key_dir)
2127 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2128 with open(private_key_file, 'w') as o:
2129 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002130
James E. Blair498059b2016-12-20 13:50:13 -08002131 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002132 self.zk_chroot_fixture = self.useFixture(
2133 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002134 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002135 self.zk_chroot_fixture.zookeeper_host,
2136 self.zk_chroot_fixture.zookeeper_port,
2137 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002138
James E. Blair96c6bf82016-01-15 16:20:40 -08002139 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002140 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002141
2142 files = {}
2143 for (dirpath, dirnames, filenames) in os.walk(source_path):
2144 for filename in filenames:
2145 test_tree_filepath = os.path.join(dirpath, filename)
2146 common_path = os.path.commonprefix([test_tree_filepath,
2147 source_path])
2148 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2149 with open(test_tree_filepath, 'r') as f:
2150 content = f.read()
2151 files[relative_filepath] = content
2152 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002153 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002154
James E. Blaire18d4602017-01-05 11:17:28 -08002155 def assertNodepoolState(self):
2156 # Make sure that there are no pending requests
2157
2158 requests = self.fake_nodepool.getNodeRequests()
2159 self.assertEqual(len(requests), 0)
2160
2161 nodes = self.fake_nodepool.getNodes()
2162 for node in nodes:
2163 self.assertFalse(node['_lock'], "Node %s is locked" %
2164 (node['_oid'],))
2165
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002166 def assertNoGeneratedKeys(self):
2167 # Make sure that Zuul did not generate any project keys
2168 # (unless it was supposed to).
2169
2170 if self.create_project_keys:
2171 return
2172
2173 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2174 test_key = i.read()
2175
2176 key_root = os.path.join(self.state_root, 'keys')
2177 for root, dirname, files in os.walk(key_root):
2178 for fn in files:
2179 with open(os.path.join(root, fn)) as f:
2180 self.assertEqual(test_key, f.read())
2181
Clark Boylanb640e052014-04-03 16:41:46 -07002182 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002183 self.log.debug("Assert final state")
2184 # Make sure no jobs are running
2185 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002186 # Make sure that git.Repo objects have been garbage collected.
2187 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002188 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002189 gc.collect()
2190 for obj in gc.get_objects():
2191 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002192 self.log.debug("Leaked git repo object: 0x%x %s" %
2193 (id(obj), repr(obj)))
2194 for ref in gc.get_referrers(obj):
2195 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002196 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002197 if repos:
2198 for obj in gc.garbage:
2199 self.log.debug(" Garbage %s" % (repr(obj)))
2200 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002201 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002202 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002203 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002204 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002205 for tenant in self.sched.abide.tenants.values():
2206 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002207 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002208 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002209
2210 def shutdown(self):
2211 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002212 self.executor_server.hold_jobs_in_build = False
2213 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002214 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002215 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002216 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002217 self.sched.stop()
2218 self.sched.join()
2219 self.statsd.stop()
2220 self.statsd.join()
2221 self.webapp.stop()
2222 self.webapp.join()
2223 self.rpc.stop()
2224 self.rpc.join()
2225 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002226 self.fake_nodepool.stop()
2227 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002228 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002229 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002230 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002231 # Further the pydevd threads also need to be whitelisted so debugging
2232 # e.g. in PyCharm is possible without breaking shutdown.
2233 whitelist = ['executor-watchdog',
2234 'pydevd.CommandThread',
2235 'pydevd.Reader',
2236 'pydevd.Writer',
2237 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002238 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002239 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002240 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002241 log_str = ""
2242 for thread_id, stack_frame in sys._current_frames().items():
2243 log_str += "Thread: %s\n" % thread_id
2244 log_str += "".join(traceback.format_stack(stack_frame))
2245 self.log.debug(log_str)
2246 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002247
James E. Blaira002b032017-04-18 10:35:48 -07002248 def assertCleanShutdown(self):
2249 pass
2250
James E. Blairc4ba97a2017-04-19 16:26:24 -07002251 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002252 parts = project.split('/')
2253 path = os.path.join(self.upstream_root, *parts[:-1])
2254 if not os.path.exists(path):
2255 os.makedirs(path)
2256 path = os.path.join(self.upstream_root, project)
2257 repo = git.Repo.init(path)
2258
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002259 with repo.config_writer() as config_writer:
2260 config_writer.set_value('user', 'email', 'user@example.com')
2261 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002262
Clark Boylanb640e052014-04-03 16:41:46 -07002263 repo.index.commit('initial commit')
2264 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002265 if tag:
2266 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002267
James E. Blair97d902e2014-08-21 13:25:56 -07002268 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002269 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002270 repo.git.clean('-x', '-f', '-d')
2271
James E. Blair97d902e2014-08-21 13:25:56 -07002272 def create_branch(self, project, branch):
2273 path = os.path.join(self.upstream_root, project)
2274 repo = git.Repo.init(path)
2275 fn = os.path.join(path, 'README')
2276
2277 branch_head = repo.create_head(branch)
2278 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002279 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002280 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002281 f.close()
2282 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002283 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002284
James E. Blair97d902e2014-08-21 13:25:56 -07002285 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002286 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002287 repo.git.clean('-x', '-f', '-d')
2288
Sachi King9f16d522016-03-16 12:20:45 +11002289 def create_commit(self, project):
2290 path = os.path.join(self.upstream_root, project)
2291 repo = git.Repo(path)
2292 repo.head.reference = repo.heads['master']
2293 file_name = os.path.join(path, 'README')
2294 with open(file_name, 'a') as f:
2295 f.write('creating fake commit\n')
2296 repo.index.add([file_name])
2297 commit = repo.index.commit('Creating a fake commit')
2298 return commit.hexsha
2299
James E. Blairf4a5f022017-04-18 14:01:10 -07002300 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002301 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002302 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002303 while len(self.builds):
2304 self.release(self.builds[0])
2305 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002306 i += 1
2307 if count is not None and i >= count:
2308 break
James E. Blairb8c16472015-05-05 14:55:26 -07002309
Clark Boylanb640e052014-04-03 16:41:46 -07002310 def release(self, job):
2311 if isinstance(job, FakeBuild):
2312 job.release()
2313 else:
2314 job.waiting = False
2315 self.log.debug("Queued job %s released" % job.unique)
2316 self.gearman_server.wakeConnections()
2317
2318 def getParameter(self, job, name):
2319 if isinstance(job, FakeBuild):
2320 return job.parameters[name]
2321 else:
2322 parameters = json.loads(job.arguments)
2323 return parameters[name]
2324
Clark Boylanb640e052014-04-03 16:41:46 -07002325 def haveAllBuildsReported(self):
2326 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002327 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002328 return False
2329 # Find out if every build that the worker has completed has been
2330 # reported back to Zuul. If it hasn't then that means a Gearman
2331 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002332 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002333 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002334 if not zbuild:
2335 # It has already been reported
2336 continue
2337 # It hasn't been reported yet.
2338 return False
2339 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002340 worker = self.executor_server.executor_worker
2341 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002342 if connection.state == 'GRAB_WAIT':
2343 return False
2344 return True
2345
2346 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002347 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002348 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002349 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002350 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002351 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002352 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002353 for j in conn.related_jobs.values():
2354 if j.unique == build.uuid:
2355 client_job = j
2356 break
2357 if not client_job:
2358 self.log.debug("%s is not known to the gearman client" %
2359 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002360 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002361 if not client_job.handle:
2362 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002363 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002364 server_job = self.gearman_server.jobs.get(client_job.handle)
2365 if not server_job:
2366 self.log.debug("%s is not known to the gearman server" %
2367 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002368 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002369 if not hasattr(server_job, 'waiting'):
2370 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002371 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002372 if server_job.waiting:
2373 continue
James E. Blair17302972016-08-10 16:11:42 -07002374 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002375 self.log.debug("%s has not reported start" % build)
2376 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002377 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002378 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002379 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002380 if worker_build:
2381 if worker_build.isWaiting():
2382 continue
2383 else:
2384 self.log.debug("%s is running" % worker_build)
2385 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002386 else:
James E. Blair962220f2016-08-03 11:22:38 -07002387 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002388 return False
James E. Blaira002b032017-04-18 10:35:48 -07002389 for (build_uuid, job_worker) in \
2390 self.executor_server.job_workers.items():
2391 if build_uuid not in seen_builds:
2392 self.log.debug("%s is not finalized" % build_uuid)
2393 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002394 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002395
James E. Blairdce6cea2016-12-20 16:45:32 -08002396 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002397 if self.fake_nodepool.paused:
2398 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002399 if self.sched.nodepool.requests:
2400 return False
2401 return True
2402
Jan Hruban6b71aff2015-10-22 16:58:08 +02002403 def eventQueuesEmpty(self):
2404 for queue in self.event_queues:
2405 yield queue.empty()
2406
2407 def eventQueuesJoin(self):
2408 for queue in self.event_queues:
2409 queue.join()
2410
Clark Boylanb640e052014-04-03 16:41:46 -07002411 def waitUntilSettled(self):
2412 self.log.debug("Waiting until settled...")
2413 start = time.time()
2414 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002415 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002416 self.log.error("Timeout waiting for Zuul to settle")
2417 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002418 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002419 self.log.error(" %s: %s" % (queue, queue.empty()))
2420 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002421 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002422 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002423 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002424 self.log.error("All requests completed: %s" %
2425 (self.areAllNodeRequestsComplete(),))
2426 self.log.error("Merge client jobs: %s" %
2427 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002428 raise Exception("Timeout waiting for Zuul to settle")
2429 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002430
Paul Belanger174a8272017-03-14 13:20:10 -04002431 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002432 # have all build states propogated to zuul?
2433 if self.haveAllBuildsReported():
2434 # Join ensures that the queue is empty _and_ events have been
2435 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002436 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002437 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002438 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002439 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002440 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002441 self.areAllNodeRequestsComplete() and
2442 all(self.eventQueuesEmpty())):
2443 # The queue empty check is placed at the end to
2444 # ensure that if a component adds an event between
2445 # when locked the run handler and checked that the
2446 # components were stable, we don't erroneously
2447 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002448 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002449 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002450 self.log.debug("...settled.")
2451 return
2452 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002453 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002454 self.sched.wake_event.wait(0.1)
2455
2456 def countJobResults(self, jobs, result):
2457 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002458 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002459
James E. Blair96c6bf82016-01-15 16:20:40 -08002460 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002461 for job in self.history:
2462 if (job.name == name and
2463 (project is None or
2464 job.parameters['ZUUL_PROJECT'] == project)):
2465 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002466 raise Exception("Unable to find job %s in history" % name)
2467
2468 def assertEmptyQueues(self):
2469 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002470 for tenant in self.sched.abide.tenants.values():
2471 for pipeline in tenant.layout.pipelines.values():
2472 for queue in pipeline.queues:
2473 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002474 print('pipeline %s queue %s contents %s' % (
2475 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002476 self.assertEqual(len(queue.queue), 0,
2477 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002478
2479 def assertReportedStat(self, key, value=None, kind=None):
2480 start = time.time()
2481 while time.time() < (start + 5):
2482 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002483 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002484 if key == k:
2485 if value is None and kind is None:
2486 return
2487 elif value:
2488 if value == v:
2489 return
2490 elif kind:
2491 if v.endswith('|' + kind):
2492 return
2493 time.sleep(0.1)
2494
Clark Boylanb640e052014-04-03 16:41:46 -07002495 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002496
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002497 def assertBuilds(self, builds):
2498 """Assert that the running builds are as described.
2499
2500 The list of running builds is examined and must match exactly
2501 the list of builds described by the input.
2502
2503 :arg list builds: A list of dictionaries. Each item in the
2504 list must match the corresponding build in the build
2505 history, and each element of the dictionary must match the
2506 corresponding attribute of the build.
2507
2508 """
James E. Blair3158e282016-08-19 09:34:11 -07002509 try:
2510 self.assertEqual(len(self.builds), len(builds))
2511 for i, d in enumerate(builds):
2512 for k, v in d.items():
2513 self.assertEqual(
2514 getattr(self.builds[i], k), v,
2515 "Element %i in builds does not match" % (i,))
2516 except Exception:
2517 for build in self.builds:
2518 self.log.error("Running build: %s" % build)
2519 else:
2520 self.log.error("No running builds")
2521 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002522
James E. Blairb536ecc2016-08-31 10:11:42 -07002523 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002524 """Assert that the completed builds are as described.
2525
2526 The list of completed builds is examined and must match
2527 exactly the list of builds described by the input.
2528
2529 :arg list history: A list of dictionaries. Each item in the
2530 list must match the corresponding build in the build
2531 history, and each element of the dictionary must match the
2532 corresponding attribute of the build.
2533
James E. Blairb536ecc2016-08-31 10:11:42 -07002534 :arg bool ordered: If true, the history must match the order
2535 supplied, if false, the builds are permitted to have
2536 arrived in any order.
2537
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002538 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002539 def matches(history_item, item):
2540 for k, v in item.items():
2541 if getattr(history_item, k) != v:
2542 return False
2543 return True
James E. Blair3158e282016-08-19 09:34:11 -07002544 try:
2545 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002546 if ordered:
2547 for i, d in enumerate(history):
2548 if not matches(self.history[i], d):
2549 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002550 "Element %i in history does not match %s" %
2551 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002552 else:
2553 unseen = self.history[:]
2554 for i, d in enumerate(history):
2555 found = False
2556 for unseen_item in unseen:
2557 if matches(unseen_item, d):
2558 found = True
2559 unseen.remove(unseen_item)
2560 break
2561 if not found:
2562 raise Exception("No match found for element %i "
2563 "in history" % (i,))
2564 if unseen:
2565 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002566 except Exception:
2567 for build in self.history:
2568 self.log.error("Completed build: %s" % build)
2569 else:
2570 self.log.error("No completed builds")
2571 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002572
James E. Blair6ac368c2016-12-22 18:07:20 -08002573 def printHistory(self):
2574 """Log the build history.
2575
2576 This can be useful during tests to summarize what jobs have
2577 completed.
2578
2579 """
2580 self.log.debug("Build history:")
2581 for build in self.history:
2582 self.log.debug(build)
2583
James E. Blair59fdbac2015-12-07 17:08:06 -08002584 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002585 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2586
James E. Blair9ea70072017-04-19 16:05:30 -07002587 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002588 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002589 if not os.path.exists(root):
2590 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002591 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2592 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002593- tenant:
2594 name: openstack
2595 source:
2596 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002597 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002598 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002599 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002600 - org/project
2601 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002602 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002603 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002604 self.config.set('zuul', 'tenant_config',
2605 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002606 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002607
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002608 def addCommitToRepo(self, project, message, files,
2609 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002610 path = os.path.join(self.upstream_root, project)
2611 repo = git.Repo(path)
2612 repo.head.reference = branch
2613 zuul.merger.merger.reset_repo_to_head(repo)
2614 for fn, content in files.items():
2615 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002616 try:
2617 os.makedirs(os.path.dirname(fn))
2618 except OSError:
2619 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002620 with open(fn, 'w') as f:
2621 f.write(content)
2622 repo.index.add([fn])
2623 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002624 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002625 repo.heads[branch].commit = commit
2626 repo.head.reference = branch
2627 repo.git.clean('-x', '-f', '-d')
2628 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002629 if tag:
2630 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002631 return before
2632
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002633 def commitConfigUpdate(self, project_name, source_name):
2634 """Commit an update to zuul.yaml
2635
2636 This overwrites the zuul.yaml in the specificed project with
2637 the contents specified.
2638
2639 :arg str project_name: The name of the project containing
2640 zuul.yaml (e.g., common-config)
2641
2642 :arg str source_name: The path to the file (underneath the
2643 test fixture directory) whose contents should be used to
2644 replace zuul.yaml.
2645 """
2646
2647 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002648 files = {}
2649 with open(source_path, 'r') as f:
2650 data = f.read()
2651 layout = yaml.safe_load(data)
2652 files['zuul.yaml'] = data
2653 for item in layout:
2654 if 'job' in item:
2655 jobname = item['job']['name']
2656 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002657 before = self.addCommitToRepo(
2658 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002659 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002660 return before
2661
James E. Blair7fc8daa2016-08-08 15:37:15 -07002662 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002663
James E. Blair7fc8daa2016-08-08 15:37:15 -07002664 """Inject a Fake (Gerrit) event.
2665
2666 This method accepts a JSON-encoded event and simulates Zuul
2667 having received it from Gerrit. It could (and should)
2668 eventually apply to any connection type, but is currently only
2669 used with Gerrit connections. The name of the connection is
2670 used to look up the corresponding server, and the event is
2671 simulated as having been received by all Zuul connections
2672 attached to that server. So if two Gerrit connections in Zuul
2673 are connected to the same Gerrit server, and you invoke this
2674 method specifying the name of one of them, the event will be
2675 received by both.
2676
2677 .. note::
2678
2679 "self.fake_gerrit.addEvent" calls should be migrated to
2680 this method.
2681
2682 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002683 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002684 :arg str event: The JSON-encoded event.
2685
2686 """
2687 specified_conn = self.connections.connections[connection]
2688 for conn in self.connections.connections.values():
2689 if (isinstance(conn, specified_conn.__class__) and
2690 specified_conn.server == conn.server):
2691 conn.addEvent(event)
2692
James E. Blaird8af5422017-05-24 13:59:40 -07002693 def getUpstreamRepos(self, projects):
2694 """Return upstream git repo objects for the listed projects
2695
2696 :arg list projects: A list of strings, each the canonical name
2697 of a project.
2698
2699 :returns: A dictionary of {name: repo} for every listed
2700 project.
2701 :rtype: dict
2702
2703 """
2704
2705 repos = {}
2706 for project in projects:
2707 # FIXME(jeblair): the upstream root does not yet have a
2708 # hostname component; that needs to be added, and this
2709 # line removed:
2710 tmp_project_name = '/'.join(project.split('/')[1:])
2711 path = os.path.join(self.upstream_root, tmp_project_name)
2712 repo = git.Repo(path)
2713 repos[project] = repo
2714 return repos
2715
James E. Blair3f876d52016-07-22 13:07:14 -07002716
2717class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002718 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002719 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002720
Joshua Heskethd78b4482015-09-14 16:56:34 -06002721
2722class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002723 def setup_config(self):
2724 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002725 for section_name in self.config.sections():
2726 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2727 section_name, re.I)
2728 if not con_match:
2729 continue
2730
2731 if self.config.get(section_name, 'driver') == 'sql':
2732 f = MySQLSchemaFixture()
2733 self.useFixture(f)
2734 if (self.config.get(section_name, 'dburi') ==
2735 '$MYSQL_FIXTURE_DBURI$'):
2736 self.config.set(section_name, 'dburi', f.dburi)