blob: d8f88b707cad62fb43629b0fd8d67c44ef38545e [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
Tobias Henkeld91b4d72017-05-23 15:43:40 +020072import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070073import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080074import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010075from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070076
77FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
78 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080079
80KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070081
Clark Boylanb640e052014-04-03 16:41:46 -070082
83def repack_repo(path):
84 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
85 output = subprocess.Popen(cmd, close_fds=True,
86 stdout=subprocess.PIPE,
87 stderr=subprocess.PIPE)
88 out = output.communicate()
89 if output.returncode:
90 raise Exception("git repack returned %d" % output.returncode)
91 return out
92
93
94def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040095 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070096
97
James E. Blaira190f3b2015-01-05 14:56:54 -080098def iterate_timeout(max_seconds, purpose):
99 start = time.time()
100 count = 0
101 while (time.time() < start + max_seconds):
102 count += 1
103 yield count
104 time.sleep(0)
105 raise Exception("Timeout waiting for %s" % purpose)
106
107
Jesse Keating436a5452017-04-20 11:48:41 -0700108def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700109 """Specify a layout file for use by a test method.
110
111 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700112 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700113
114 Some tests require only a very simple configuration. For those,
115 establishing a complete config directory hierachy is too much
116 work. In those cases, you can add a simple zuul.yaml file to the
117 test fixtures directory (in fixtures/layouts/foo.yaml) and use
118 this decorator to indicate the test method should use that rather
119 than the tenant config file specified by the test class.
120
121 The decorator will cause that layout file to be added to a
122 config-project called "common-config" and each "project" instance
123 referenced in the layout file will have a git repo automatically
124 initialized.
125 """
126
127 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700128 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700129 return test
130 return decorator
131
132
Gregory Haynes4fc12542015-04-22 20:38:06 -0700133class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700134 _common_path_default = "refs/changes"
135 _points_to_commits_only = True
136
137
Gregory Haynes4fc12542015-04-22 20:38:06 -0700138class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700139 categories = {'approved': ('Approved', -1, 1),
140 'code-review': ('Code-Review', -2, 2),
141 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700142
143 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700144 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700146 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700147 self.reported = 0
148 self.queried = 0
149 self.patchsets = []
150 self.number = number
151 self.project = project
152 self.branch = branch
153 self.subject = subject
154 self.latest_patchset = 0
155 self.depends_on_change = None
156 self.needed_by_changes = []
157 self.fail_merge = False
158 self.messages = []
159 self.data = {
160 'branch': branch,
161 'comments': [],
162 'commitMessage': subject,
163 'createdOn': time.time(),
164 'id': 'I' + random_sha1(),
165 'lastUpdated': time.time(),
166 'number': str(number),
167 'open': status == 'NEW',
168 'owner': {'email': 'user@example.com',
169 'name': 'User Name',
170 'username': 'username'},
171 'patchSets': self.patchsets,
172 'project': project,
173 'status': status,
174 'subject': subject,
175 'submitRecords': [],
176 'url': 'https://hostname/%s' % number}
177
178 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700179 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700180 self.data['submitRecords'] = self.getSubmitRecords()
181 self.open = status == 'NEW'
182
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700183 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700184 path = os.path.join(self.upstream_root, self.project)
185 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700186 ref = GerritChangeReference.create(
187 repo, '1/%s/%s' % (self.number, self.latest_patchset),
188 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700190 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 repo.git.clean('-x', '-f', '-d')
192
193 path = os.path.join(self.upstream_root, self.project)
194 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for fn, content in files.items():
196 fn = os.path.join(path, fn)
197 with open(fn, 'w') as f:
198 f.write(content)
199 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700200 else:
201 for fni in range(100):
202 fn = os.path.join(path, str(fni))
203 f = open(fn, 'w')
204 for ci in range(4096):
205 f.write(random.choice(string.printable))
206 f.close()
207 repo.index.add([fn])
208
209 r = repo.index.commit(msg)
210 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700211 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700212 repo.git.clean('-x', '-f', '-d')
213 repo.heads['master'].checkout()
214 return r
215
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700217 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700219 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700220 data = ("test %s %s %s\n" %
221 (self.branch, self.number, self.latest_patchset))
222 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700223 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700225 ps_files = [{'file': '/COMMIT_MSG',
226 'type': 'ADDED'},
227 {'file': 'README',
228 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700229 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700230 ps_files.append({'file': f, 'type': 'ADDED'})
231 d = {'approvals': [],
232 'createdOn': time.time(),
233 'files': ps_files,
234 'number': str(self.latest_patchset),
235 'ref': 'refs/changes/1/%s/%s' % (self.number,
236 self.latest_patchset),
237 'revision': c.hexsha,
238 'uploader': {'email': 'user@example.com',
239 'name': 'User name',
240 'username': 'user'}}
241 self.data['currentPatchSet'] = d
242 self.patchsets.append(d)
243 self.data['submitRecords'] = self.getSubmitRecords()
244
245 def getPatchsetCreatedEvent(self, patchset):
246 event = {"type": "patchset-created",
247 "change": {"project": self.project,
248 "branch": self.branch,
249 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
250 "number": str(self.number),
251 "subject": self.subject,
252 "owner": {"name": "User Name"},
253 "url": "https://hostname/3"},
254 "patchSet": self.patchsets[patchset - 1],
255 "uploader": {"name": "User Name"}}
256 return event
257
258 def getChangeRestoredEvent(self):
259 event = {"type": "change-restored",
260 "change": {"project": self.project,
261 "branch": self.branch,
262 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
263 "number": str(self.number),
264 "subject": self.subject,
265 "owner": {"name": "User Name"},
266 "url": "https://hostname/3"},
267 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100268 "patchSet": self.patchsets[-1],
269 "reason": ""}
270 return event
271
272 def getChangeAbandonedEvent(self):
273 event = {"type": "change-abandoned",
274 "change": {"project": self.project,
275 "branch": self.branch,
276 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
277 "number": str(self.number),
278 "subject": self.subject,
279 "owner": {"name": "User Name"},
280 "url": "https://hostname/3"},
281 "abandoner": {"name": "User Name"},
282 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700283 "reason": ""}
284 return event
285
286 def getChangeCommentEvent(self, patchset):
287 event = {"type": "comment-added",
288 "change": {"project": self.project,
289 "branch": self.branch,
290 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
291 "number": str(self.number),
292 "subject": self.subject,
293 "owner": {"name": "User Name"},
294 "url": "https://hostname/3"},
295 "patchSet": self.patchsets[patchset - 1],
296 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700297 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700298 "description": "Code-Review",
299 "value": "0"}],
300 "comment": "This is a comment"}
301 return event
302
James E. Blairc2a5ed72017-02-20 14:12:01 -0500303 def getChangeMergedEvent(self):
304 event = {"submitter": {"name": "Jenkins",
305 "username": "jenkins"},
306 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
307 "patchSet": self.patchsets[-1],
308 "change": self.data,
309 "type": "change-merged",
310 "eventCreatedOn": 1487613810}
311 return event
312
James E. Blair8cce42e2016-10-18 08:18:36 -0700313 def getRefUpdatedEvent(self):
314 path = os.path.join(self.upstream_root, self.project)
315 repo = git.Repo(path)
316 oldrev = repo.heads[self.branch].commit.hexsha
317
318 event = {
319 "type": "ref-updated",
320 "submitter": {
321 "name": "User Name",
322 },
323 "refUpdate": {
324 "oldRev": oldrev,
325 "newRev": self.patchsets[-1]['revision'],
326 "refName": self.branch,
327 "project": self.project,
328 }
329 }
330 return event
331
Joshua Hesketh642824b2014-07-01 17:54:59 +1000332 def addApproval(self, category, value, username='reviewer_john',
333 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700334 if not granted_on:
335 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000336 approval = {
337 'description': self.categories[category][0],
338 'type': category,
339 'value': str(value),
340 'by': {
341 'username': username,
342 'email': username + '@example.com',
343 },
344 'grantedOn': int(granted_on)
345 }
Clark Boylanb640e052014-04-03 16:41:46 -0700346 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
347 if x['by']['username'] == username and x['type'] == category:
348 del self.patchsets[-1]['approvals'][i]
349 self.patchsets[-1]['approvals'].append(approval)
350 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000351 'author': {'email': 'author@example.com',
352 'name': 'Patchset Author',
353 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700354 'change': {'branch': self.branch,
355 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
356 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000357 'owner': {'email': 'owner@example.com',
358 'name': 'Change Owner',
359 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'project': self.project,
361 'subject': self.subject,
362 'topic': 'master',
363 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000364 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700365 'patchSet': self.patchsets[-1],
366 'type': 'comment-added'}
367 self.data['submitRecords'] = self.getSubmitRecords()
368 return json.loads(json.dumps(event))
369
370 def getSubmitRecords(self):
371 status = {}
372 for cat in self.categories.keys():
373 status[cat] = 0
374
375 for a in self.patchsets[-1]['approvals']:
376 cur = status[a['type']]
377 cat_min, cat_max = self.categories[a['type']][1:]
378 new = int(a['value'])
379 if new == cat_min:
380 cur = new
381 elif abs(new) > abs(cur):
382 cur = new
383 status[a['type']] = cur
384
385 labels = []
386 ok = True
387 for typ, cat in self.categories.items():
388 cur = status[typ]
389 cat_min, cat_max = cat[1:]
390 if cur == cat_min:
391 value = 'REJECT'
392 ok = False
393 elif cur == cat_max:
394 value = 'OK'
395 else:
396 value = 'NEED'
397 ok = False
398 labels.append({'label': cat[0], 'status': value})
399 if ok:
400 return [{'status': 'OK'}]
401 return [{'status': 'NOT_READY',
402 'labels': labels}]
403
404 def setDependsOn(self, other, patchset):
405 self.depends_on_change = other
406 d = {'id': other.data['id'],
407 'number': other.data['number'],
408 'ref': other.patchsets[patchset - 1]['ref']
409 }
410 self.data['dependsOn'] = [d]
411
412 other.needed_by_changes.append(self)
413 needed = other.data.get('neededBy', [])
414 d = {'id': self.data['id'],
415 'number': self.data['number'],
416 'ref': self.patchsets[patchset - 1]['ref'],
417 'revision': self.patchsets[patchset - 1]['revision']
418 }
419 needed.append(d)
420 other.data['neededBy'] = needed
421
422 def query(self):
423 self.queried += 1
424 d = self.data.get('dependsOn')
425 if d:
426 d = d[0]
427 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
428 d['isCurrentPatchSet'] = True
429 else:
430 d['isCurrentPatchSet'] = False
431 return json.loads(json.dumps(self.data))
432
433 def setMerged(self):
434 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000435 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700436 return
437 if self.fail_merge:
438 return
439 self.data['status'] = 'MERGED'
440 self.open = False
441
442 path = os.path.join(self.upstream_root, self.project)
443 repo = git.Repo(path)
444 repo.heads[self.branch].commit = \
445 repo.commit(self.patchsets[-1]['revision'])
446
447 def setReported(self):
448 self.reported += 1
449
450
James E. Blaire511d2f2016-12-08 15:22:26 -0800451class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700452 """A Fake Gerrit connection for use in tests.
453
454 This subclasses
455 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
456 ability for tests to add changes to the fake Gerrit it represents.
457 """
458
Joshua Hesketh352264b2015-08-11 23:42:08 +1000459 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700460
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700462 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800463 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000464 connection_config)
465
James E. Blair7fc8daa2016-08-08 15:37:15 -0700466 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700467 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
468 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000469 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700470 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200471 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700472
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700473 def addFakeChange(self, project, branch, subject, status='NEW',
474 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700475 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700476 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700477 c = FakeGerritChange(self, self.change_number, project, branch,
478 subject, upstream_root=self.upstream_root,
479 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700480 self.changes[self.change_number] = c
481 return c
482
Clark Boylanb640e052014-04-03 16:41:46 -0700483 def review(self, project, changeid, message, action):
484 number, ps = changeid.split(',')
485 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000486
487 # Add the approval back onto the change (ie simulate what gerrit would
488 # do).
489 # Usually when zuul leaves a review it'll create a feedback loop where
490 # zuul's review enters another gerrit event (which is then picked up by
491 # zuul). However, we can't mimic this behaviour (by adding this
492 # approval event into the queue) as it stops jobs from checking what
493 # happens before this event is triggered. If a job needs to see what
494 # happens they can add their own verified event into the queue.
495 # Nevertheless, we can update change with the new review in gerrit.
496
James E. Blair8b5408c2016-08-08 15:37:46 -0700497 for cat in action.keys():
498 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000499 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000502
Clark Boylanb640e052014-04-03 16:41:46 -0700503 if 'submit' in action:
504 change.setMerged()
505 if message:
506 change.setReported()
507
508 def query(self, number):
509 change = self.changes.get(int(number))
510 if change:
511 return change.query()
512 return {}
513
James E. Blairc494d542014-08-06 09:23:52 -0700514 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700515 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700516 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800517 if query.startswith('change:'):
518 # Query a specific changeid
519 changeid = query[len('change:'):]
520 l = [change.query() for change in self.changes.values()
521 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700522 elif query.startswith('message:'):
523 # Query the content of a commit message
524 msg = query[len('message:'):].strip()
525 l = [change.query() for change in self.changes.values()
526 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800527 else:
528 # Query all open changes
529 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700530 return l
James E. Blairc494d542014-08-06 09:23:52 -0700531
Joshua Hesketh352264b2015-08-11 23:42:08 +1000532 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700533 pass
534
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200535 def _uploadPack(self, project):
536 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
537 'multi_ack thin-pack side-band side-band-64k ofs-delta '
538 'shallow no-progress include-tag multi_ack_detailed no-done\n')
539 path = os.path.join(self.upstream_root, project.name)
540 repo = git.Repo(path)
541 for ref in repo.refs:
542 r = ref.object.hexsha + ' ' + ref.path + '\n'
543 ret += '%04x%s' % (len(r) + 4, r)
544 ret += '0000'
545 return ret
546
Joshua Hesketh352264b2015-08-11 23:42:08 +1000547 def getGitUrl(self, project):
548 return os.path.join(self.upstream_root, project.name)
549
Clark Boylanb640e052014-04-03 16:41:46 -0700550
Gregory Haynes4fc12542015-04-22 20:38:06 -0700551class GithubChangeReference(git.Reference):
552 _common_path_default = "refs/pull"
553 _points_to_commits_only = True
554
555
556class FakeGithubPullRequest(object):
557
558 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800559 subject, upstream_root, files=[], number_of_commits=1,
560 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700561 """Creates a new PR with several commits.
562 Sends an event about opened PR."""
563 self.github = github
564 self.source = github
565 self.number = number
566 self.project = project
567 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100568 self.subject = subject
569 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700570 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100571 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700572 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100573 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100574 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800575 self.reviews = []
576 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700577 self.updated_at = None
578 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100579 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100580 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700581 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100583 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 self._updateTimeStamp()
585
Jan Hruban570d01c2016-03-10 21:51:32 +0100586 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700587 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 self._updateTimeStamp()
590
Jan Hruban570d01c2016-03-10 21:51:32 +0100591 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700592 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100593 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700594 self._updateTimeStamp()
595
596 def getPullRequestOpenedEvent(self):
597 return self._getPullRequestEvent('opened')
598
599 def getPullRequestSynchronizeEvent(self):
600 return self._getPullRequestEvent('synchronize')
601
602 def getPullRequestReopenedEvent(self):
603 return self._getPullRequestEvent('reopened')
604
605 def getPullRequestClosedEvent(self):
606 return self._getPullRequestEvent('closed')
607
Jesse Keating8c2eb572017-05-30 17:31:45 -0700608 def getPushEvent(self, old_sha, ref='refs/heads/master'):
609 name = 'push'
610 data = {
611 'ref': ref,
612 'before': old_sha,
613 'after': self.head_sha,
614 'repository': {
615 'full_name': self.project
616 },
617 'sender': {
618 'login': 'ghuser'
619 }
620 }
621 return (name, data)
622
Gregory Haynes4fc12542015-04-22 20:38:06 -0700623 def addComment(self, message):
624 self.comments.append(message)
625 self._updateTimeStamp()
626
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200627 def getCommentAddedEvent(self, text):
628 name = 'issue_comment'
629 data = {
630 'action': 'created',
631 'issue': {
632 'number': self.number
633 },
634 'comment': {
635 'body': text
636 },
637 'repository': {
638 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100639 },
640 'sender': {
641 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200642 }
643 }
644 return (name, data)
645
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800646 def getReviewAddedEvent(self, review):
647 name = 'pull_request_review'
648 data = {
649 'action': 'submitted',
650 'pull_request': {
651 'number': self.number,
652 'title': self.subject,
653 'updated_at': self.updated_at,
654 'base': {
655 'ref': self.branch,
656 'repo': {
657 'full_name': self.project
658 }
659 },
660 'head': {
661 'sha': self.head_sha
662 }
663 },
664 'review': {
665 'state': review
666 },
667 'repository': {
668 'full_name': self.project
669 },
670 'sender': {
671 'login': 'ghuser'
672 }
673 }
674 return (name, data)
675
Jan Hruban16ad31f2015-11-07 14:39:07 +0100676 def addLabel(self, name):
677 if name not in self.labels:
678 self.labels.append(name)
679 self._updateTimeStamp()
680 return self._getLabelEvent(name)
681
682 def removeLabel(self, name):
683 if name in self.labels:
684 self.labels.remove(name)
685 self._updateTimeStamp()
686 return self._getUnlabelEvent(name)
687
688 def _getLabelEvent(self, label):
689 name = 'pull_request'
690 data = {
691 'action': 'labeled',
692 'pull_request': {
693 'number': self.number,
694 'updated_at': self.updated_at,
695 'base': {
696 'ref': self.branch,
697 'repo': {
698 'full_name': self.project
699 }
700 },
701 'head': {
702 'sha': self.head_sha
703 }
704 },
705 'label': {
706 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100707 },
708 'sender': {
709 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100710 }
711 }
712 return (name, data)
713
714 def _getUnlabelEvent(self, label):
715 name = 'pull_request'
716 data = {
717 'action': 'unlabeled',
718 'pull_request': {
719 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100720 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100721 'updated_at': self.updated_at,
722 'base': {
723 'ref': self.branch,
724 'repo': {
725 'full_name': self.project
726 }
727 },
728 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800729 'sha': self.head_sha,
730 'repo': {
731 'full_name': self.project
732 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100733 }
734 },
735 'label': {
736 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100737 },
738 'sender': {
739 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100740 }
741 }
742 return (name, data)
743
Gregory Haynes4fc12542015-04-22 20:38:06 -0700744 def _getRepo(self):
745 repo_path = os.path.join(self.upstream_root, self.project)
746 return git.Repo(repo_path)
747
748 def _createPRRef(self):
749 repo = self._getRepo()
750 GithubChangeReference.create(
751 repo, self._getPRReference(), 'refs/tags/init')
752
Jan Hruban570d01c2016-03-10 21:51:32 +0100753 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700754 repo = self._getRepo()
755 ref = repo.references[self._getPRReference()]
756 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100757 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700758 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100759 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700760 repo.head.reference = ref
761 zuul.merger.merger.reset_repo_to_head(repo)
762 repo.git.clean('-x', '-f', '-d')
763
Jan Hruban570d01c2016-03-10 21:51:32 +0100764 if files:
765 fn = files[0]
766 self.files = files
767 else:
768 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
769 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100770 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700771 fn = os.path.join(repo.working_dir, fn)
772 f = open(fn, 'w')
773 with open(fn, 'w') as f:
774 f.write("test %s %s\n" %
775 (self.branch, self.number))
776 repo.index.add([fn])
777
778 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800779 # Create an empty set of statuses for the given sha,
780 # each sha on a PR may have a status set on it
781 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700782 repo.head.reference = 'master'
783 zuul.merger.merger.reset_repo_to_head(repo)
784 repo.git.clean('-x', '-f', '-d')
785 repo.heads['master'].checkout()
786
787 def _updateTimeStamp(self):
788 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
789
790 def getPRHeadSha(self):
791 repo = self._getRepo()
792 return repo.references[self._getPRReference()].commit.hexsha
793
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800794 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800795 # Since we're bypassing github API, which would require a user, we
796 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800797 # insert the status at the top of the list, to simulate that it
798 # is the most recent set status
799 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100800 'state': state,
801 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800802 'description': description,
803 'context': context,
804 'creator': {
805 'login': user
806 }
807 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100808
Jesse Keatingae4cd272017-01-30 17:10:44 -0800809 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800810 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
811 # convert the timestamp to a str format that would be returned
812 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800813
Adam Gandelmand81dd762017-02-09 15:15:49 -0800814 if granted_on:
815 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
816 submitted_at = time.strftime(
817 gh_time_format, granted_on.timetuple())
818 else:
819 # github timestamps only down to the second, so we need to make
820 # sure reviews that tests add appear to be added over a period of
821 # time in the past and not all at once.
822 if not self.reviews:
823 # the first review happens 10 mins ago
824 offset = 600
825 else:
826 # subsequent reviews happen 1 minute closer to now
827 offset = 600 - (len(self.reviews) * 60)
828
829 granted_on = datetime.datetime.utcfromtimestamp(
830 time.time() - offset)
831 submitted_at = time.strftime(
832 gh_time_format, granted_on.timetuple())
833
Jesse Keatingae4cd272017-01-30 17:10:44 -0800834 self.reviews.append({
835 'state': state,
836 'user': {
837 'login': user,
838 'email': user + "@derp.com",
839 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800840 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800841 })
842
Gregory Haynes4fc12542015-04-22 20:38:06 -0700843 def _getPRReference(self):
844 return '%s/head' % self.number
845
846 def _getPullRequestEvent(self, action):
847 name = 'pull_request'
848 data = {
849 'action': action,
850 'number': self.number,
851 'pull_request': {
852 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100853 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700854 'updated_at': self.updated_at,
855 'base': {
856 'ref': self.branch,
857 'repo': {
858 'full_name': self.project
859 }
860 },
861 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800862 'sha': self.head_sha,
863 'repo': {
864 'full_name': self.project
865 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700866 }
Jan Hruban3b415922016-02-03 13:10:22 +0100867 },
868 'sender': {
869 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700870 }
871 }
872 return (name, data)
873
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800874 def getCommitStatusEvent(self, context, state='success', user='zuul'):
875 name = 'status'
876 data = {
877 'state': state,
878 'sha': self.head_sha,
879 'description': 'Test results for %s: %s' % (self.head_sha, state),
880 'target_url': 'http://zuul/%s' % self.head_sha,
881 'branches': [],
882 'context': context,
883 'sender': {
884 'login': user
885 }
886 }
887 return (name, data)
888
Gregory Haynes4fc12542015-04-22 20:38:06 -0700889
890class FakeGithubConnection(githubconnection.GithubConnection):
891 log = logging.getLogger("zuul.test.FakeGithubConnection")
892
893 def __init__(self, driver, connection_name, connection_config,
894 upstream_root=None):
895 super(FakeGithubConnection, self).__init__(driver, connection_name,
896 connection_config)
897 self.connection_name = connection_name
898 self.pr_number = 0
899 self.pull_requests = []
900 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100901 self.merge_failure = False
902 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700903
Jan Hruban570d01c2016-03-10 21:51:32 +0100904 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700905 self.pr_number += 1
906 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100907 self, self.pr_number, project, branch, subject, self.upstream_root,
908 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700909 self.pull_requests.append(pull_request)
910 return pull_request
911
Wayne1a78c612015-06-11 17:14:13 -0700912 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
913 if not old_rev:
914 old_rev = '00000000000000000000000000000000'
915 if not new_rev:
916 new_rev = random_sha1()
917 name = 'push'
918 data = {
919 'ref': ref,
920 'before': old_rev,
921 'after': new_rev,
922 'repository': {
923 'full_name': project
924 }
925 }
926 return (name, data)
927
Gregory Haynes4fc12542015-04-22 20:38:06 -0700928 def emitEvent(self, event):
929 """Emulates sending the GitHub webhook event to the connection."""
930 port = self.webapp.server.socket.getsockname()[1]
931 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700932 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700933 headers = {'X-Github-Event': name}
934 req = urllib.request.Request(
935 'http://localhost:%s/connection/%s/payload'
936 % (port, self.connection_name),
937 data=payload, headers=headers)
938 urllib.request.urlopen(req)
939
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200940 def getPull(self, project, number):
941 pr = self.pull_requests[number - 1]
942 data = {
943 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100944 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200945 'updated_at': pr.updated_at,
946 'base': {
947 'repo': {
948 'full_name': pr.project
949 },
950 'ref': pr.branch,
951 },
Jan Hruban37615e52015-11-19 14:30:49 +0100952 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700953 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200954 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800955 'sha': pr.head_sha,
956 'repo': {
957 'full_name': pr.project
958 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200959 }
960 }
961 return data
962
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800963 def getPullBySha(self, sha):
964 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
965 if len(prs) > 1:
966 raise Exception('Multiple pulls found with head sha: %s' % sha)
967 pr = prs[0]
968 return self.getPull(pr.project, pr.number)
969
Jan Hruban570d01c2016-03-10 21:51:32 +0100970 def getPullFileNames(self, project, number):
971 pr = self.pull_requests[number - 1]
972 return pr.files
973
Jesse Keatingae4cd272017-01-30 17:10:44 -0800974 def _getPullReviews(self, owner, project, number):
975 pr = self.pull_requests[number - 1]
976 return pr.reviews
977
Jan Hruban3b415922016-02-03 13:10:22 +0100978 def getUser(self, login):
979 data = {
980 'username': login,
981 'name': 'Github User',
982 'email': 'github.user@example.com'
983 }
984 return data
985
Jesse Keatingae4cd272017-01-30 17:10:44 -0800986 def getRepoPermission(self, project, login):
987 owner, proj = project.split('/')
988 for pr in self.pull_requests:
989 pr_owner, pr_project = pr.project.split('/')
990 if (pr_owner == owner and proj == pr_project):
991 if login in pr.writers:
992 return 'write'
993 else:
994 return 'read'
995
Gregory Haynes4fc12542015-04-22 20:38:06 -0700996 def getGitUrl(self, project):
997 return os.path.join(self.upstream_root, str(project))
998
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200999 def real_getGitUrl(self, project):
1000 return super(FakeGithubConnection, self).getGitUrl(project)
1001
Gregory Haynes4fc12542015-04-22 20:38:06 -07001002 def getProjectBranches(self, project):
1003 """Masks getProjectBranches since we don't have a real github"""
1004
1005 # just returns master for now
1006 return ['master']
1007
Jan Hrubane252a732017-01-03 15:03:09 +01001008 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -07001009 pull_request = self.pull_requests[pr_number - 1]
1010 pull_request.addComment(message)
1011
Jan Hruban3b415922016-02-03 13:10:22 +01001012 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +01001013 pull_request = self.pull_requests[pr_number - 1]
1014 if self.merge_failure:
1015 raise Exception('Pull request was not merged')
1016 if self.merge_not_allowed_count > 0:
1017 self.merge_not_allowed_count -= 1
1018 raise MergeFailure('Merge was not successful due to mergeability'
1019 ' conflict')
1020 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001021 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001022
Jesse Keatingd96e5882017-01-19 13:55:50 -08001023 def getCommitStatuses(self, project, sha):
1024 owner, proj = project.split('/')
1025 for pr in self.pull_requests:
1026 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -07001027 # This is somewhat risky, if the same commit exists in multiple
1028 # PRs, we might grab the wrong one that doesn't have a status
1029 # that is expected to be there. Maybe re-work this so that there
1030 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001031 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001032 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001033 return pr.statuses[sha]
1034
Jan Hrubane252a732017-01-03 15:03:09 +01001035 def setCommitStatus(self, project, sha, state,
1036 url='', description='', context=''):
1037 owner, proj = project.split('/')
1038 for pr in self.pull_requests:
1039 pr_owner, pr_project = pr.project.split('/')
1040 if (pr_owner == owner and pr_project == proj and
1041 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001042 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001043
Jan Hruban16ad31f2015-11-07 14:39:07 +01001044 def labelPull(self, project, pr_number, label):
1045 pull_request = self.pull_requests[pr_number - 1]
1046 pull_request.addLabel(label)
1047
1048 def unlabelPull(self, project, pr_number, label):
1049 pull_request = self.pull_requests[pr_number - 1]
1050 pull_request.removeLabel(label)
1051
Gregory Haynes4fc12542015-04-22 20:38:06 -07001052
Clark Boylanb640e052014-04-03 16:41:46 -07001053class BuildHistory(object):
1054 def __init__(self, **kw):
1055 self.__dict__.update(kw)
1056
1057 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001058 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1059 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001060
1061
Clark Boylanb640e052014-04-03 16:41:46 -07001062class FakeStatsd(threading.Thread):
1063 def __init__(self):
1064 threading.Thread.__init__(self)
1065 self.daemon = True
1066 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1067 self.sock.bind(('', 0))
1068 self.port = self.sock.getsockname()[1]
1069 self.wake_read, self.wake_write = os.pipe()
1070 self.stats = []
1071
1072 def run(self):
1073 while True:
1074 poll = select.poll()
1075 poll.register(self.sock, select.POLLIN)
1076 poll.register(self.wake_read, select.POLLIN)
1077 ret = poll.poll()
1078 for (fd, event) in ret:
1079 if fd == self.sock.fileno():
1080 data = self.sock.recvfrom(1024)
1081 if not data:
1082 return
1083 self.stats.append(data[0])
1084 if fd == self.wake_read:
1085 return
1086
1087 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001088 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001089
1090
James E. Blaire1767bc2016-08-02 10:00:27 -07001091class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001092 log = logging.getLogger("zuul.test")
1093
Paul Belanger174a8272017-03-14 13:20:10 -04001094 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001095 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001096 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001097 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001098 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001099 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001100 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001101 # TODOv3(jeblair): self.node is really "the image of the node
1102 # assigned". We should rename it (self.node_image?) if we
1103 # keep using it like this, or we may end up exposing more of
1104 # the complexity around multi-node jobs here
1105 # (self.nodes[0].image?)
1106 self.node = None
1107 if len(self.parameters.get('nodes')) == 1:
1108 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001109 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001110 self.pipeline = self.parameters['ZUUL_PIPELINE']
1111 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001112 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001113 self.wait_condition = threading.Condition()
1114 self.waiting = False
1115 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001116 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001117 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001118 self.changes = None
1119 if 'ZUUL_CHANGE_IDS' in self.parameters:
1120 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001121
James E. Blair3158e282016-08-19 09:34:11 -07001122 def __repr__(self):
1123 waiting = ''
1124 if self.waiting:
1125 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001126 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1127 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001128
Clark Boylanb640e052014-04-03 16:41:46 -07001129 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001130 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001131 self.wait_condition.acquire()
1132 self.wait_condition.notify()
1133 self.waiting = False
1134 self.log.debug("Build %s released" % self.unique)
1135 self.wait_condition.release()
1136
1137 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001138 """Return whether this build is being held.
1139
1140 :returns: Whether the build is being held.
1141 :rtype: bool
1142 """
1143
Clark Boylanb640e052014-04-03 16:41:46 -07001144 self.wait_condition.acquire()
1145 if self.waiting:
1146 ret = True
1147 else:
1148 ret = False
1149 self.wait_condition.release()
1150 return ret
1151
1152 def _wait(self):
1153 self.wait_condition.acquire()
1154 self.waiting = True
1155 self.log.debug("Build %s waiting" % self.unique)
1156 self.wait_condition.wait()
1157 self.wait_condition.release()
1158
1159 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001160 self.log.debug('Running build %s' % self.unique)
1161
Paul Belanger174a8272017-03-14 13:20:10 -04001162 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001163 self.log.debug('Holding build %s' % self.unique)
1164 self._wait()
1165 self.log.debug("Build %s continuing" % self.unique)
1166
James E. Blair412fba82017-01-26 15:00:50 -08001167 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001168 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001169 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001170 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001171 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001172 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001173 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001174
James E. Blaire1767bc2016-08-02 10:00:27 -07001175 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001176
James E. Blaira5dba232016-08-08 15:53:24 -07001177 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001178 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001179 for change in changes:
1180 if self.hasChanges(change):
1181 return True
1182 return False
1183
James E. Blaire7b99a02016-08-05 14:27:34 -07001184 def hasChanges(self, *changes):
1185 """Return whether this build has certain changes in its git repos.
1186
1187 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001188 are expected to be present (in order) in the git repository of
1189 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001190
1191 :returns: Whether the build has the indicated changes.
1192 :rtype: bool
1193
1194 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001195 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001196 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001197 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001198 try:
1199 repo = git.Repo(path)
1200 except NoSuchPathError as e:
1201 self.log.debug('%s' % e)
1202 return False
1203 ref = self.parameters['ZUUL_REF']
1204 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1205 commit_message = '%s-1' % change.subject
1206 self.log.debug("Checking if build %s has changes; commit_message "
1207 "%s; repo_messages %s" % (self, commit_message,
1208 repo_messages))
1209 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001210 self.log.debug(" messages do not match")
1211 return False
1212 self.log.debug(" OK")
1213 return True
1214
James E. Blaird8af5422017-05-24 13:59:40 -07001215 def getWorkspaceRepos(self, projects):
1216 """Return workspace git repo objects for the listed projects
1217
1218 :arg list projects: A list of strings, each the canonical name
1219 of a project.
1220
1221 :returns: A dictionary of {name: repo} for every listed
1222 project.
1223 :rtype: dict
1224
1225 """
1226
1227 repos = {}
1228 for project in projects:
1229 path = os.path.join(self.jobdir.src_root, project)
1230 repo = git.Repo(path)
1231 repos[project] = repo
1232 return repos
1233
Clark Boylanb640e052014-04-03 16:41:46 -07001234
Paul Belanger174a8272017-03-14 13:20:10 -04001235class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1236 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001237
Paul Belanger174a8272017-03-14 13:20:10 -04001238 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001239 they will report that they have started but then pause until
1240 released before reporting completion. This attribute may be
1241 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001242 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001243 be explicitly released.
1244
1245 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001246 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001247 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001248 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001249 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001250 self.hold_jobs_in_build = False
1251 self.lock = threading.Lock()
1252 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001253 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001254 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001255 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001256
James E. Blaira5dba232016-08-08 15:53:24 -07001257 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001258 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001259
1260 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001261 :arg Change change: The :py:class:`~tests.base.FakeChange`
1262 instance which should cause the job to fail. This job
1263 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001264
1265 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001266 l = self.fail_tests.get(name, [])
1267 l.append(change)
1268 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001269
James E. Blair962220f2016-08-03 11:22:38 -07001270 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001271 """Release a held build.
1272
1273 :arg str regex: A regular expression which, if supplied, will
1274 cause only builds with matching names to be released. If
1275 not supplied, all builds will be released.
1276
1277 """
James E. Blair962220f2016-08-03 11:22:38 -07001278 builds = self.running_builds[:]
1279 self.log.debug("Releasing build %s (%s)" % (regex,
1280 len(self.running_builds)))
1281 for build in builds:
1282 if not regex or re.match(regex, build.name):
1283 self.log.debug("Releasing build %s" %
1284 (build.parameters['ZUUL_UUID']))
1285 build.release()
1286 else:
1287 self.log.debug("Not releasing build %s" %
1288 (build.parameters['ZUUL_UUID']))
1289 self.log.debug("Done releasing builds %s (%s)" %
1290 (regex, len(self.running_builds)))
1291
Paul Belanger174a8272017-03-14 13:20:10 -04001292 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001293 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001294 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001295 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001296 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001297 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001298 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001299 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001300 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1301 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001302
1303 def stopJob(self, job):
1304 self.log.debug("handle stop")
1305 parameters = json.loads(job.arguments)
1306 uuid = parameters['uuid']
1307 for build in self.running_builds:
1308 if build.unique == uuid:
1309 build.aborted = True
1310 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001311 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001312
James E. Blaira002b032017-04-18 10:35:48 -07001313 def stop(self):
1314 for build in self.running_builds:
1315 build.release()
1316 super(RecordingExecutorServer, self).stop()
1317
Joshua Hesketh50c21782016-10-13 21:34:14 +11001318
Paul Belanger174a8272017-03-14 13:20:10 -04001319class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001320 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001321 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001322 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001323 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001324 if not commit: # merge conflict
1325 self.recordResult('MERGER_FAILURE')
1326 return commit
1327
1328 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001329 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001330 self.executor_server.lock.acquire()
1331 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001332 BuildHistory(name=build.name, result=result, changes=build.changes,
1333 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001334 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001335 pipeline=build.parameters['ZUUL_PIPELINE'])
1336 )
Paul Belanger174a8272017-03-14 13:20:10 -04001337 self.executor_server.running_builds.remove(build)
1338 del self.executor_server.job_builds[self.job.unique]
1339 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001340
1341 def runPlaybooks(self, args):
1342 build = self.executor_server.job_builds[self.job.unique]
1343 build.jobdir = self.jobdir
1344
1345 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1346 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001347 return result
1348
Monty Taylore6562aa2017-02-20 07:37:39 -05001349 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001350 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001351
Paul Belanger174a8272017-03-14 13:20:10 -04001352 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001353 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001354 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001355 else:
1356 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001357 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001358
James E. Blairad8dca02017-02-21 11:48:32 -05001359 def getHostList(self, args):
1360 self.log.debug("hostlist")
1361 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001362 for host in hosts:
1363 host['host_vars']['ansible_connection'] = 'local'
1364
1365 hosts.append(dict(
1366 name='localhost',
1367 host_vars=dict(ansible_connection='local'),
1368 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001369 return hosts
1370
James E. Blairf5dbd002015-12-23 15:26:17 -08001371
Clark Boylanb640e052014-04-03 16:41:46 -07001372class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001373 """A Gearman server for use in tests.
1374
1375 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1376 added to the queue but will not be distributed to workers
1377 until released. This attribute may be changed at any time and
1378 will take effect for subsequently enqueued jobs, but
1379 previously held jobs will still need to be explicitly
1380 released.
1381
1382 """
1383
Clark Boylanb640e052014-04-03 16:41:46 -07001384 def __init__(self):
1385 self.hold_jobs_in_queue = False
1386 super(FakeGearmanServer, self).__init__(0)
1387
1388 def getJobForConnection(self, connection, peek=False):
1389 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1390 for job in queue:
1391 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001392 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001393 job.waiting = self.hold_jobs_in_queue
1394 else:
1395 job.waiting = False
1396 if job.waiting:
1397 continue
1398 if job.name in connection.functions:
1399 if not peek:
1400 queue.remove(job)
1401 connection.related_jobs[job.handle] = job
1402 job.worker_connection = connection
1403 job.running = True
1404 return job
1405 return None
1406
1407 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001408 """Release a held job.
1409
1410 :arg str regex: A regular expression which, if supplied, will
1411 cause only jobs with matching names to be released. If
1412 not supplied, all jobs will be released.
1413 """
Clark Boylanb640e052014-04-03 16:41:46 -07001414 released = False
1415 qlen = (len(self.high_queue) + len(self.normal_queue) +
1416 len(self.low_queue))
1417 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1418 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001419 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001420 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001421 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001422 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001423 self.log.debug("releasing queued job %s" %
1424 job.unique)
1425 job.waiting = False
1426 released = True
1427 else:
1428 self.log.debug("not releasing queued job %s" %
1429 job.unique)
1430 if released:
1431 self.wakeConnections()
1432 qlen = (len(self.high_queue) + len(self.normal_queue) +
1433 len(self.low_queue))
1434 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1435
1436
1437class FakeSMTP(object):
1438 log = logging.getLogger('zuul.FakeSMTP')
1439
1440 def __init__(self, messages, server, port):
1441 self.server = server
1442 self.port = port
1443 self.messages = messages
1444
1445 def sendmail(self, from_email, to_email, msg):
1446 self.log.info("Sending email from %s, to %s, with msg %s" % (
1447 from_email, to_email, msg))
1448
1449 headers = msg.split('\n\n', 1)[0]
1450 body = msg.split('\n\n', 1)[1]
1451
1452 self.messages.append(dict(
1453 from_email=from_email,
1454 to_email=to_email,
1455 msg=msg,
1456 headers=headers,
1457 body=body,
1458 ))
1459
1460 return True
1461
1462 def quit(self):
1463 return True
1464
1465
James E. Blairdce6cea2016-12-20 16:45:32 -08001466class FakeNodepool(object):
1467 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001468 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001469
1470 log = logging.getLogger("zuul.test.FakeNodepool")
1471
1472 def __init__(self, host, port, chroot):
1473 self.client = kazoo.client.KazooClient(
1474 hosts='%s:%s%s' % (host, port, chroot))
1475 self.client.start()
1476 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001477 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001478 self.thread = threading.Thread(target=self.run)
1479 self.thread.daemon = True
1480 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001481 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001482
1483 def stop(self):
1484 self._running = False
1485 self.thread.join()
1486 self.client.stop()
1487 self.client.close()
1488
1489 def run(self):
1490 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001491 try:
1492 self._run()
1493 except Exception:
1494 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001495 time.sleep(0.1)
1496
1497 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001498 if self.paused:
1499 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001500 for req in self.getNodeRequests():
1501 self.fulfillRequest(req)
1502
1503 def getNodeRequests(self):
1504 try:
1505 reqids = self.client.get_children(self.REQUEST_ROOT)
1506 except kazoo.exceptions.NoNodeError:
1507 return []
1508 reqs = []
1509 for oid in sorted(reqids):
1510 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001511 try:
1512 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001513 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001514 data['_oid'] = oid
1515 reqs.append(data)
1516 except kazoo.exceptions.NoNodeError:
1517 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001518 return reqs
1519
James E. Blaire18d4602017-01-05 11:17:28 -08001520 def getNodes(self):
1521 try:
1522 nodeids = self.client.get_children(self.NODE_ROOT)
1523 except kazoo.exceptions.NoNodeError:
1524 return []
1525 nodes = []
1526 for oid in sorted(nodeids):
1527 path = self.NODE_ROOT + '/' + oid
1528 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001529 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001530 data['_oid'] = oid
1531 try:
1532 lockfiles = self.client.get_children(path + '/lock')
1533 except kazoo.exceptions.NoNodeError:
1534 lockfiles = []
1535 if lockfiles:
1536 data['_lock'] = True
1537 else:
1538 data['_lock'] = False
1539 nodes.append(data)
1540 return nodes
1541
James E. Blaira38c28e2017-01-04 10:33:20 -08001542 def makeNode(self, request_id, node_type):
1543 now = time.time()
1544 path = '/nodepool/nodes/'
1545 data = dict(type=node_type,
1546 provider='test-provider',
1547 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001548 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001549 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001550 public_ipv4='127.0.0.1',
1551 private_ipv4=None,
1552 public_ipv6=None,
1553 allocated_to=request_id,
1554 state='ready',
1555 state_time=now,
1556 created_time=now,
1557 updated_time=now,
1558 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001559 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001560 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001561 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001562 path = self.client.create(path, data,
1563 makepath=True,
1564 sequence=True)
1565 nodeid = path.split("/")[-1]
1566 return nodeid
1567
James E. Blair6ab79e02017-01-06 10:10:17 -08001568 def addFailRequest(self, request):
1569 self.fail_requests.add(request['_oid'])
1570
James E. Blairdce6cea2016-12-20 16:45:32 -08001571 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001572 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001573 return
1574 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001575 oid = request['_oid']
1576 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001577
James E. Blair6ab79e02017-01-06 10:10:17 -08001578 if oid in self.fail_requests:
1579 request['state'] = 'failed'
1580 else:
1581 request['state'] = 'fulfilled'
1582 nodes = []
1583 for node in request['node_types']:
1584 nodeid = self.makeNode(oid, node)
1585 nodes.append(nodeid)
1586 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001587
James E. Blaira38c28e2017-01-04 10:33:20 -08001588 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001589 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001590 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001591 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001592 try:
1593 self.client.set(path, data)
1594 except kazoo.exceptions.NoNodeError:
1595 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001596
1597
James E. Blair498059b2016-12-20 13:50:13 -08001598class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001599 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001600 super(ChrootedKazooFixture, self).__init__()
1601
1602 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1603 if ':' in zk_host:
1604 host, port = zk_host.split(':')
1605 else:
1606 host = zk_host
1607 port = None
1608
1609 self.zookeeper_host = host
1610
1611 if not port:
1612 self.zookeeper_port = 2181
1613 else:
1614 self.zookeeper_port = int(port)
1615
Clark Boylan621ec9a2017-04-07 17:41:33 -07001616 self.test_id = test_id
1617
James E. Blair498059b2016-12-20 13:50:13 -08001618 def _setUp(self):
1619 # Make sure the test chroot paths do not conflict
1620 random_bits = ''.join(random.choice(string.ascii_lowercase +
1621 string.ascii_uppercase)
1622 for x in range(8))
1623
Clark Boylan621ec9a2017-04-07 17:41:33 -07001624 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001625 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1626
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001627 self.addCleanup(self._cleanup)
1628
James E. Blair498059b2016-12-20 13:50:13 -08001629 # Ensure the chroot path exists and clean up any pre-existing znodes.
1630 _tmp_client = kazoo.client.KazooClient(
1631 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1632 _tmp_client.start()
1633
1634 if _tmp_client.exists(self.zookeeper_chroot):
1635 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1636
1637 _tmp_client.ensure_path(self.zookeeper_chroot)
1638 _tmp_client.stop()
1639 _tmp_client.close()
1640
James E. Blair498059b2016-12-20 13:50:13 -08001641 def _cleanup(self):
1642 '''Remove the chroot path.'''
1643 # Need a non-chroot'ed client to remove the chroot path
1644 _tmp_client = kazoo.client.KazooClient(
1645 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1646 _tmp_client.start()
1647 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1648 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001649 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001650
1651
Joshua Heskethd78b4482015-09-14 16:56:34 -06001652class MySQLSchemaFixture(fixtures.Fixture):
1653 def setUp(self):
1654 super(MySQLSchemaFixture, self).setUp()
1655
1656 random_bits = ''.join(random.choice(string.ascii_lowercase +
1657 string.ascii_uppercase)
1658 for x in range(8))
1659 self.name = '%s_%s' % (random_bits, os.getpid())
1660 self.passwd = uuid.uuid4().hex
1661 db = pymysql.connect(host="localhost",
1662 user="openstack_citest",
1663 passwd="openstack_citest",
1664 db="openstack_citest")
1665 cur = db.cursor()
1666 cur.execute("create database %s" % self.name)
1667 cur.execute(
1668 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1669 (self.name, self.name, self.passwd))
1670 cur.execute("flush privileges")
1671
1672 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1673 self.passwd,
1674 self.name)
1675 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1676 self.addCleanup(self.cleanup)
1677
1678 def cleanup(self):
1679 db = pymysql.connect(host="localhost",
1680 user="openstack_citest",
1681 passwd="openstack_citest",
1682 db="openstack_citest")
1683 cur = db.cursor()
1684 cur.execute("drop database %s" % self.name)
1685 cur.execute("drop user '%s'@'localhost'" % self.name)
1686 cur.execute("flush privileges")
1687
1688
Maru Newby3fe5f852015-01-13 04:22:14 +00001689class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001690 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001691 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001692
James E. Blair1c236df2017-02-01 14:07:24 -08001693 def attachLogs(self, *args):
1694 def reader():
1695 self._log_stream.seek(0)
1696 while True:
1697 x = self._log_stream.read(4096)
1698 if not x:
1699 break
1700 yield x.encode('utf8')
1701 content = testtools.content.content_from_reader(
1702 reader,
1703 testtools.content_type.UTF8_TEXT,
1704 False)
1705 self.addDetail('logging', content)
1706
Clark Boylanb640e052014-04-03 16:41:46 -07001707 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001708 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001709 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1710 try:
1711 test_timeout = int(test_timeout)
1712 except ValueError:
1713 # If timeout value is invalid do not set a timeout.
1714 test_timeout = 0
1715 if test_timeout > 0:
1716 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1717
1718 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1719 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1720 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1721 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1722 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1723 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1724 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1725 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1726 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1727 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001728 self._log_stream = StringIO()
1729 self.addOnException(self.attachLogs)
1730 else:
1731 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001732
James E. Blair73b41772017-05-22 13:22:55 -07001733 # NOTE(jeblair): this is temporary extra debugging to try to
1734 # track down a possible leak.
1735 orig_git_repo_init = git.Repo.__init__
1736
1737 def git_repo_init(myself, *args, **kw):
1738 orig_git_repo_init(myself, *args, **kw)
1739 self.log.debug("Created git repo 0x%x %s" %
1740 (id(myself), repr(myself)))
1741
1742 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1743 git_repo_init))
1744
James E. Blair1c236df2017-02-01 14:07:24 -08001745 handler = logging.StreamHandler(self._log_stream)
1746 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1747 '%(levelname)-8s %(message)s')
1748 handler.setFormatter(formatter)
1749
1750 logger = logging.getLogger()
1751 logger.setLevel(logging.DEBUG)
1752 logger.addHandler(handler)
1753
Clark Boylan3410d532017-04-25 12:35:29 -07001754 # Make sure we don't carry old handlers around in process state
1755 # which slows down test runs
1756 self.addCleanup(logger.removeHandler, handler)
1757 self.addCleanup(handler.close)
1758 self.addCleanup(handler.flush)
1759
James E. Blair1c236df2017-02-01 14:07:24 -08001760 # NOTE(notmorgan): Extract logging overrides for specific
1761 # libraries from the OS_LOG_DEFAULTS env and create loggers
1762 # for each. This is used to limit the output during test runs
1763 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001764 log_defaults_from_env = os.environ.get(
1765 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001766 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001767
James E. Blairdce6cea2016-12-20 16:45:32 -08001768 if log_defaults_from_env:
1769 for default in log_defaults_from_env.split(','):
1770 try:
1771 name, level_str = default.split('=', 1)
1772 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001773 logger = logging.getLogger(name)
1774 logger.setLevel(level)
1775 logger.addHandler(handler)
1776 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001777 except ValueError:
1778 # NOTE(notmorgan): Invalid format of the log default,
1779 # skip and don't try and apply a logger for the
1780 # specified module
1781 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001782
Maru Newby3fe5f852015-01-13 04:22:14 +00001783
1784class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001785 """A test case with a functioning Zuul.
1786
1787 The following class variables are used during test setup and can
1788 be overidden by subclasses but are effectively read-only once a
1789 test method starts running:
1790
1791 :cvar str config_file: This points to the main zuul config file
1792 within the fixtures directory. Subclasses may override this
1793 to obtain a different behavior.
1794
1795 :cvar str tenant_config_file: This is the tenant config file
1796 (which specifies from what git repos the configuration should
1797 be loaded). It defaults to the value specified in
1798 `config_file` but can be overidden by subclasses to obtain a
1799 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001800 configuration. See also the :py:func:`simple_layout`
1801 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001802
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001803 :cvar bool create_project_keys: Indicates whether Zuul should
1804 auto-generate keys for each project, or whether the test
1805 infrastructure should insert dummy keys to save time during
1806 startup. Defaults to False.
1807
James E. Blaire7b99a02016-08-05 14:27:34 -07001808 The following are instance variables that are useful within test
1809 methods:
1810
1811 :ivar FakeGerritConnection fake_<connection>:
1812 A :py:class:`~tests.base.FakeGerritConnection` will be
1813 instantiated for each connection present in the config file
1814 and stored here. For instance, `fake_gerrit` will hold the
1815 FakeGerritConnection object for a connection named `gerrit`.
1816
1817 :ivar FakeGearmanServer gearman_server: An instance of
1818 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1819 server that all of the Zuul components in this test use to
1820 communicate with each other.
1821
Paul Belanger174a8272017-03-14 13:20:10 -04001822 :ivar RecordingExecutorServer executor_server: An instance of
1823 :py:class:`~tests.base.RecordingExecutorServer` which is the
1824 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001825
1826 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1827 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001828 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001829 list upon completion.
1830
1831 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1832 objects representing completed builds. They are appended to
1833 the list in the order they complete.
1834
1835 """
1836
James E. Blair83005782015-12-11 14:46:03 -08001837 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001838 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001839 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001840
1841 def _startMerger(self):
1842 self.merge_server = zuul.merger.server.MergeServer(self.config,
1843 self.connections)
1844 self.merge_server.start()
1845
Maru Newby3fe5f852015-01-13 04:22:14 +00001846 def setUp(self):
1847 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001848
1849 self.setupZK()
1850
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001851 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001852 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001853 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1854 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001855 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001856 tmp_root = tempfile.mkdtemp(
1857 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001858 self.test_root = os.path.join(tmp_root, "zuul-test")
1859 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001860 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001861 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001862 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001863
1864 if os.path.exists(self.test_root):
1865 shutil.rmtree(self.test_root)
1866 os.makedirs(self.test_root)
1867 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001868 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001869
1870 # Make per test copy of Configuration.
1871 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001872 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1873 if not os.path.exists(self.private_key_file):
1874 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1875 shutil.copy(src_private_key_file, self.private_key_file)
1876 shutil.copy('{}.pub'.format(src_private_key_file),
1877 '{}.pub'.format(self.private_key_file))
1878 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001879 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001880 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001881 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001882 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001883 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001884 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001885 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001886
Clark Boylanb640e052014-04-03 16:41:46 -07001887 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001888 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1889 # see: https://github.com/jsocol/pystatsd/issues/61
1890 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001891 os.environ['STATSD_PORT'] = str(self.statsd.port)
1892 self.statsd.start()
1893 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001894 reload_module(statsd)
1895 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001896
1897 self.gearman_server = FakeGearmanServer()
1898
1899 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001900 self.log.info("Gearman server on port %s" %
1901 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001902
James E. Blaire511d2f2016-12-08 15:22:26 -08001903 gerritsource.GerritSource.replication_timeout = 1.5
1904 gerritsource.GerritSource.replication_retry_interval = 0.5
1905 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001906
Joshua Hesketh352264b2015-08-11 23:42:08 +10001907 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001908
Jan Hruban7083edd2015-08-21 14:00:54 +02001909 self.webapp = zuul.webapp.WebApp(
1910 self.sched, port=0, listen_address='127.0.0.1')
1911
Jan Hruban6b71aff2015-10-22 16:58:08 +02001912 self.event_queues = [
1913 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001914 self.sched.trigger_event_queue,
1915 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001916 ]
1917
James E. Blairfef78942016-03-11 16:28:56 -08001918 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001919 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001920
Paul Belanger174a8272017-03-14 13:20:10 -04001921 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001922 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001923 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001924 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001925 _test_root=self.test_root,
1926 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001927 self.executor_server.start()
1928 self.history = self.executor_server.build_history
1929 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001930
Paul Belanger174a8272017-03-14 13:20:10 -04001931 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001932 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001933 self.merge_client = zuul.merger.client.MergeClient(
1934 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001935 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001936 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001937 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001938
James E. Blair0d5a36e2017-02-21 10:53:44 -05001939 self.fake_nodepool = FakeNodepool(
1940 self.zk_chroot_fixture.zookeeper_host,
1941 self.zk_chroot_fixture.zookeeper_port,
1942 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001943
Paul Belanger174a8272017-03-14 13:20:10 -04001944 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001945 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001946 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001947 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001948
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001949 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001950
1951 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001952 self.webapp.start()
1953 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001954 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001955 # Cleanups are run in reverse order
1956 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001957 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001958 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001959
James E. Blairb9c0d772017-03-03 14:34:49 -08001960 self.sched.reconfigure(self.config)
1961 self.sched.resume()
1962
Tobias Henkel7df274b2017-05-26 17:41:11 +02001963 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001964 # Set up gerrit related fakes
1965 # Set a changes database so multiple FakeGerrit's can report back to
1966 # a virtual canonical database given by the configured hostname
1967 self.gerrit_changes_dbs = {}
1968
1969 def getGerritConnection(driver, name, config):
1970 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1971 con = FakeGerritConnection(driver, name, config,
1972 changes_db=db,
1973 upstream_root=self.upstream_root)
1974 self.event_queues.append(con.event_queue)
1975 setattr(self, 'fake_' + name, con)
1976 return con
1977
1978 self.useFixture(fixtures.MonkeyPatch(
1979 'zuul.driver.gerrit.GerritDriver.getConnection',
1980 getGerritConnection))
1981
Gregory Haynes4fc12542015-04-22 20:38:06 -07001982 def getGithubConnection(driver, name, config):
1983 con = FakeGithubConnection(driver, name, config,
1984 upstream_root=self.upstream_root)
1985 setattr(self, 'fake_' + name, con)
1986 return con
1987
1988 self.useFixture(fixtures.MonkeyPatch(
1989 'zuul.driver.github.GithubDriver.getConnection',
1990 getGithubConnection))
1991
James E. Blaire511d2f2016-12-08 15:22:26 -08001992 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001993 # TODO(jhesketh): This should come from lib.connections for better
1994 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001995 # Register connections from the config
1996 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001997
Joshua Hesketh352264b2015-08-11 23:42:08 +10001998 def FakeSMTPFactory(*args, **kw):
1999 args = [self.smtp_messages] + list(args)
2000 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002001
Joshua Hesketh352264b2015-08-11 23:42:08 +10002002 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002003
James E. Blaire511d2f2016-12-08 15:22:26 -08002004 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002005 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002006 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002007
James E. Blair83005782015-12-11 14:46:03 -08002008 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002009 # This creates the per-test configuration object. It can be
2010 # overriden by subclasses, but should not need to be since it
2011 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002012 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002013 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002014
2015 if not self.setupSimpleLayout():
2016 if hasattr(self, 'tenant_config_file'):
2017 self.config.set('zuul', 'tenant_config',
2018 self.tenant_config_file)
2019 git_path = os.path.join(
2020 os.path.dirname(
2021 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2022 'git')
2023 if os.path.exists(git_path):
2024 for reponame in os.listdir(git_path):
2025 project = reponame.replace('_', '/')
2026 self.copyDirToRepo(project,
2027 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002028 self.setupAllProjectKeys()
2029
James E. Blair06cc3922017-04-19 10:08:10 -07002030 def setupSimpleLayout(self):
2031 # If the test method has been decorated with a simple_layout,
2032 # use that instead of the class tenant_config_file. Set up a
2033 # single config-project with the specified layout, and
2034 # initialize repos for all of the 'project' entries which
2035 # appear in the layout.
2036 test_name = self.id().split('.')[-1]
2037 test = getattr(self, test_name)
2038 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002039 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002040 else:
2041 return False
2042
James E. Blairb70e55a2017-04-19 12:57:02 -07002043 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002044 path = os.path.join(FIXTURE_DIR, path)
2045 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002046 data = f.read()
2047 layout = yaml.safe_load(data)
2048 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002049 untrusted_projects = []
2050 for item in layout:
2051 if 'project' in item:
2052 name = item['project']['name']
2053 untrusted_projects.append(name)
2054 self.init_repo(name)
2055 self.addCommitToRepo(name, 'initial commit',
2056 files={'README': ''},
2057 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002058 if 'job' in item:
2059 jobname = item['job']['name']
2060 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002061
2062 root = os.path.join(self.test_root, "config")
2063 if not os.path.exists(root):
2064 os.makedirs(root)
2065 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2066 config = [{'tenant':
2067 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002068 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002069 {'config-projects': ['common-config'],
2070 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002071 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002072 f.close()
2073 self.config.set('zuul', 'tenant_config',
2074 os.path.join(FIXTURE_DIR, f.name))
2075
2076 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002077 self.addCommitToRepo('common-config', 'add content from fixture',
2078 files, branch='master', tag='init')
2079
2080 return True
2081
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002082 def setupAllProjectKeys(self):
2083 if self.create_project_keys:
2084 return
2085
2086 path = self.config.get('zuul', 'tenant_config')
2087 with open(os.path.join(FIXTURE_DIR, path)) as f:
2088 tenant_config = yaml.safe_load(f.read())
2089 for tenant in tenant_config:
2090 sources = tenant['tenant']['source']
2091 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002092 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002093 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002094 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002095 self.setupProjectKeys(source, project)
2096
2097 def setupProjectKeys(self, source, project):
2098 # Make sure we set up an RSA key for the project so that we
2099 # don't spend time generating one:
2100
2101 key_root = os.path.join(self.state_root, 'keys')
2102 if not os.path.isdir(key_root):
2103 os.mkdir(key_root, 0o700)
2104 private_key_file = os.path.join(key_root, source, project + '.pem')
2105 private_key_dir = os.path.dirname(private_key_file)
2106 self.log.debug("Installing test keys for project %s at %s" % (
2107 project, private_key_file))
2108 if not os.path.isdir(private_key_dir):
2109 os.makedirs(private_key_dir)
2110 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2111 with open(private_key_file, 'w') as o:
2112 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002113
James E. Blair498059b2016-12-20 13:50:13 -08002114 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002115 self.zk_chroot_fixture = self.useFixture(
2116 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002117 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002118 self.zk_chroot_fixture.zookeeper_host,
2119 self.zk_chroot_fixture.zookeeper_port,
2120 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002121
James E. Blair96c6bf82016-01-15 16:20:40 -08002122 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002123 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002124
2125 files = {}
2126 for (dirpath, dirnames, filenames) in os.walk(source_path):
2127 for filename in filenames:
2128 test_tree_filepath = os.path.join(dirpath, filename)
2129 common_path = os.path.commonprefix([test_tree_filepath,
2130 source_path])
2131 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2132 with open(test_tree_filepath, 'r') as f:
2133 content = f.read()
2134 files[relative_filepath] = content
2135 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002136 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002137
James E. Blaire18d4602017-01-05 11:17:28 -08002138 def assertNodepoolState(self):
2139 # Make sure that there are no pending requests
2140
2141 requests = self.fake_nodepool.getNodeRequests()
2142 self.assertEqual(len(requests), 0)
2143
2144 nodes = self.fake_nodepool.getNodes()
2145 for node in nodes:
2146 self.assertFalse(node['_lock'], "Node %s is locked" %
2147 (node['_oid'],))
2148
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002149 def assertNoGeneratedKeys(self):
2150 # Make sure that Zuul did not generate any project keys
2151 # (unless it was supposed to).
2152
2153 if self.create_project_keys:
2154 return
2155
2156 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2157 test_key = i.read()
2158
2159 key_root = os.path.join(self.state_root, 'keys')
2160 for root, dirname, files in os.walk(key_root):
2161 for fn in files:
2162 with open(os.path.join(root, fn)) as f:
2163 self.assertEqual(test_key, f.read())
2164
Clark Boylanb640e052014-04-03 16:41:46 -07002165 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002166 self.log.debug("Assert final state")
2167 # Make sure no jobs are running
2168 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002169 # Make sure that git.Repo objects have been garbage collected.
2170 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002171 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002172 gc.collect()
2173 for obj in gc.get_objects():
2174 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002175 self.log.debug("Leaked git repo object: 0x%x %s" %
2176 (id(obj), repr(obj)))
2177 for ref in gc.get_referrers(obj):
2178 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002179 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002180 if repos:
2181 for obj in gc.garbage:
2182 self.log.debug(" Garbage %s" % (repr(obj)))
2183 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002184 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002185 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002186 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002187 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002188 for tenant in self.sched.abide.tenants.values():
2189 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002190 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002191 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002192
2193 def shutdown(self):
2194 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002195 self.executor_server.hold_jobs_in_build = False
2196 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002197 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002198 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002199 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002200 self.sched.stop()
2201 self.sched.join()
2202 self.statsd.stop()
2203 self.statsd.join()
2204 self.webapp.stop()
2205 self.webapp.join()
2206 self.rpc.stop()
2207 self.rpc.join()
2208 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002209 self.fake_nodepool.stop()
2210 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002211 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002212 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002213 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002214 # Further the pydevd threads also need to be whitelisted so debugging
2215 # e.g. in PyCharm is possible without breaking shutdown.
2216 whitelist = ['executor-watchdog',
2217 'pydevd.CommandThread',
2218 'pydevd.Reader',
2219 'pydevd.Writer',
2220 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002221 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002222 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002223 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002224 log_str = ""
2225 for thread_id, stack_frame in sys._current_frames().items():
2226 log_str += "Thread: %s\n" % thread_id
2227 log_str += "".join(traceback.format_stack(stack_frame))
2228 self.log.debug(log_str)
2229 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002230
James E. Blaira002b032017-04-18 10:35:48 -07002231 def assertCleanShutdown(self):
2232 pass
2233
James E. Blairc4ba97a2017-04-19 16:26:24 -07002234 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002235 parts = project.split('/')
2236 path = os.path.join(self.upstream_root, *parts[:-1])
2237 if not os.path.exists(path):
2238 os.makedirs(path)
2239 path = os.path.join(self.upstream_root, project)
2240 repo = git.Repo.init(path)
2241
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002242 with repo.config_writer() as config_writer:
2243 config_writer.set_value('user', 'email', 'user@example.com')
2244 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002245
Clark Boylanb640e052014-04-03 16:41:46 -07002246 repo.index.commit('initial commit')
2247 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002248 if tag:
2249 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002250
James E. Blair97d902e2014-08-21 13:25:56 -07002251 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002252 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002253 repo.git.clean('-x', '-f', '-d')
2254
James E. Blair97d902e2014-08-21 13:25:56 -07002255 def create_branch(self, project, branch):
2256 path = os.path.join(self.upstream_root, project)
2257 repo = git.Repo.init(path)
2258 fn = os.path.join(path, 'README')
2259
2260 branch_head = repo.create_head(branch)
2261 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002262 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002263 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002264 f.close()
2265 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002266 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002267
James E. Blair97d902e2014-08-21 13:25:56 -07002268 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002269 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002270 repo.git.clean('-x', '-f', '-d')
2271
Sachi King9f16d522016-03-16 12:20:45 +11002272 def create_commit(self, project):
2273 path = os.path.join(self.upstream_root, project)
2274 repo = git.Repo(path)
2275 repo.head.reference = repo.heads['master']
2276 file_name = os.path.join(path, 'README')
2277 with open(file_name, 'a') as f:
2278 f.write('creating fake commit\n')
2279 repo.index.add([file_name])
2280 commit = repo.index.commit('Creating a fake commit')
2281 return commit.hexsha
2282
James E. Blairf4a5f022017-04-18 14:01:10 -07002283 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002284 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002285 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002286 while len(self.builds):
2287 self.release(self.builds[0])
2288 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002289 i += 1
2290 if count is not None and i >= count:
2291 break
James E. Blairb8c16472015-05-05 14:55:26 -07002292
Clark Boylanb640e052014-04-03 16:41:46 -07002293 def release(self, job):
2294 if isinstance(job, FakeBuild):
2295 job.release()
2296 else:
2297 job.waiting = False
2298 self.log.debug("Queued job %s released" % job.unique)
2299 self.gearman_server.wakeConnections()
2300
2301 def getParameter(self, job, name):
2302 if isinstance(job, FakeBuild):
2303 return job.parameters[name]
2304 else:
2305 parameters = json.loads(job.arguments)
2306 return parameters[name]
2307
Clark Boylanb640e052014-04-03 16:41:46 -07002308 def haveAllBuildsReported(self):
2309 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002310 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002311 return False
2312 # Find out if every build that the worker has completed has been
2313 # reported back to Zuul. If it hasn't then that means a Gearman
2314 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002315 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002316 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002317 if not zbuild:
2318 # It has already been reported
2319 continue
2320 # It hasn't been reported yet.
2321 return False
2322 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002323 worker = self.executor_server.executor_worker
2324 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002325 if connection.state == 'GRAB_WAIT':
2326 return False
2327 return True
2328
2329 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002330 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002331 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002332 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002333 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002334 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002335 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002336 for j in conn.related_jobs.values():
2337 if j.unique == build.uuid:
2338 client_job = j
2339 break
2340 if not client_job:
2341 self.log.debug("%s is not known to the gearman client" %
2342 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002343 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002344 if not client_job.handle:
2345 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002346 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002347 server_job = self.gearman_server.jobs.get(client_job.handle)
2348 if not server_job:
2349 self.log.debug("%s is not known to the gearman server" %
2350 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002351 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002352 if not hasattr(server_job, 'waiting'):
2353 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002354 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002355 if server_job.waiting:
2356 continue
James E. Blair17302972016-08-10 16:11:42 -07002357 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002358 self.log.debug("%s has not reported start" % build)
2359 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002360 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002361 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002362 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002363 if worker_build:
2364 if worker_build.isWaiting():
2365 continue
2366 else:
2367 self.log.debug("%s is running" % worker_build)
2368 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002369 else:
James E. Blair962220f2016-08-03 11:22:38 -07002370 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002371 return False
James E. Blaira002b032017-04-18 10:35:48 -07002372 for (build_uuid, job_worker) in \
2373 self.executor_server.job_workers.items():
2374 if build_uuid not in seen_builds:
2375 self.log.debug("%s is not finalized" % build_uuid)
2376 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002377 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002378
James E. Blairdce6cea2016-12-20 16:45:32 -08002379 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002380 if self.fake_nodepool.paused:
2381 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002382 if self.sched.nodepool.requests:
2383 return False
2384 return True
2385
Jan Hruban6b71aff2015-10-22 16:58:08 +02002386 def eventQueuesEmpty(self):
2387 for queue in self.event_queues:
2388 yield queue.empty()
2389
2390 def eventQueuesJoin(self):
2391 for queue in self.event_queues:
2392 queue.join()
2393
Clark Boylanb640e052014-04-03 16:41:46 -07002394 def waitUntilSettled(self):
2395 self.log.debug("Waiting until settled...")
2396 start = time.time()
2397 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002398 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002399 self.log.error("Timeout waiting for Zuul to settle")
2400 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002401 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002402 self.log.error(" %s: %s" % (queue, queue.empty()))
2403 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002404 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002405 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002406 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002407 self.log.error("All requests completed: %s" %
2408 (self.areAllNodeRequestsComplete(),))
2409 self.log.error("Merge client jobs: %s" %
2410 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002411 raise Exception("Timeout waiting for Zuul to settle")
2412 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002413
Paul Belanger174a8272017-03-14 13:20:10 -04002414 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002415 # have all build states propogated to zuul?
2416 if self.haveAllBuildsReported():
2417 # Join ensures that the queue is empty _and_ events have been
2418 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002419 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002420 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002421 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002422 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002423 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002424 self.areAllNodeRequestsComplete() and
2425 all(self.eventQueuesEmpty())):
2426 # The queue empty check is placed at the end to
2427 # ensure that if a component adds an event between
2428 # when locked the run handler and checked that the
2429 # components were stable, we don't erroneously
2430 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002431 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002432 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002433 self.log.debug("...settled.")
2434 return
2435 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002436 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002437 self.sched.wake_event.wait(0.1)
2438
2439 def countJobResults(self, jobs, result):
2440 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002441 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002442
Monty Taylor0d926122017-05-24 08:07:56 -05002443 def getBuildByName(self, name):
2444 for build in self.builds:
2445 if build.name == name:
2446 return build
2447 raise Exception("Unable to find build %s" % name)
2448
James E. Blair96c6bf82016-01-15 16:20:40 -08002449 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002450 for job in self.history:
2451 if (job.name == name and
2452 (project is None or
2453 job.parameters['ZUUL_PROJECT'] == project)):
2454 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002455 raise Exception("Unable to find job %s in history" % name)
2456
2457 def assertEmptyQueues(self):
2458 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002459 for tenant in self.sched.abide.tenants.values():
2460 for pipeline in tenant.layout.pipelines.values():
2461 for queue in pipeline.queues:
2462 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002463 print('pipeline %s queue %s contents %s' % (
2464 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002465 self.assertEqual(len(queue.queue), 0,
2466 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002467
2468 def assertReportedStat(self, key, value=None, kind=None):
2469 start = time.time()
2470 while time.time() < (start + 5):
2471 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002472 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002473 if key == k:
2474 if value is None and kind is None:
2475 return
2476 elif value:
2477 if value == v:
2478 return
2479 elif kind:
2480 if v.endswith('|' + kind):
2481 return
2482 time.sleep(0.1)
2483
Clark Boylanb640e052014-04-03 16:41:46 -07002484 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002485
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002486 def assertBuilds(self, builds):
2487 """Assert that the running builds are as described.
2488
2489 The list of running builds is examined and must match exactly
2490 the list of builds described by the input.
2491
2492 :arg list builds: A list of dictionaries. Each item in the
2493 list must match the corresponding build in the build
2494 history, and each element of the dictionary must match the
2495 corresponding attribute of the build.
2496
2497 """
James E. Blair3158e282016-08-19 09:34:11 -07002498 try:
2499 self.assertEqual(len(self.builds), len(builds))
2500 for i, d in enumerate(builds):
2501 for k, v in d.items():
2502 self.assertEqual(
2503 getattr(self.builds[i], k), v,
2504 "Element %i in builds does not match" % (i,))
2505 except Exception:
2506 for build in self.builds:
2507 self.log.error("Running build: %s" % build)
2508 else:
2509 self.log.error("No running builds")
2510 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002511
James E. Blairb536ecc2016-08-31 10:11:42 -07002512 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002513 """Assert that the completed builds are as described.
2514
2515 The list of completed builds is examined and must match
2516 exactly the list of builds described by the input.
2517
2518 :arg list history: A list of dictionaries. Each item in the
2519 list must match the corresponding build in the build
2520 history, and each element of the dictionary must match the
2521 corresponding attribute of the build.
2522
James E. Blairb536ecc2016-08-31 10:11:42 -07002523 :arg bool ordered: If true, the history must match the order
2524 supplied, if false, the builds are permitted to have
2525 arrived in any order.
2526
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002527 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002528 def matches(history_item, item):
2529 for k, v in item.items():
2530 if getattr(history_item, k) != v:
2531 return False
2532 return True
James E. Blair3158e282016-08-19 09:34:11 -07002533 try:
2534 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002535 if ordered:
2536 for i, d in enumerate(history):
2537 if not matches(self.history[i], d):
2538 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002539 "Element %i in history does not match %s" %
2540 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002541 else:
2542 unseen = self.history[:]
2543 for i, d in enumerate(history):
2544 found = False
2545 for unseen_item in unseen:
2546 if matches(unseen_item, d):
2547 found = True
2548 unseen.remove(unseen_item)
2549 break
2550 if not found:
2551 raise Exception("No match found for element %i "
2552 "in history" % (i,))
2553 if unseen:
2554 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002555 except Exception:
2556 for build in self.history:
2557 self.log.error("Completed build: %s" % build)
2558 else:
2559 self.log.error("No completed builds")
2560 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002561
James E. Blair6ac368c2016-12-22 18:07:20 -08002562 def printHistory(self):
2563 """Log the build history.
2564
2565 This can be useful during tests to summarize what jobs have
2566 completed.
2567
2568 """
2569 self.log.debug("Build history:")
2570 for build in self.history:
2571 self.log.debug(build)
2572
James E. Blair59fdbac2015-12-07 17:08:06 -08002573 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002574 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2575
James E. Blair9ea70072017-04-19 16:05:30 -07002576 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002577 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002578 if not os.path.exists(root):
2579 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002580 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2581 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002582- tenant:
2583 name: openstack
2584 source:
2585 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002586 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002587 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002588 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002589 - org/project
2590 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002591 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002592 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002593 self.config.set('zuul', 'tenant_config',
2594 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002595 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002596
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002597 def addCommitToRepo(self, project, message, files,
2598 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002599 path = os.path.join(self.upstream_root, project)
2600 repo = git.Repo(path)
2601 repo.head.reference = branch
2602 zuul.merger.merger.reset_repo_to_head(repo)
2603 for fn, content in files.items():
2604 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002605 try:
2606 os.makedirs(os.path.dirname(fn))
2607 except OSError:
2608 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002609 with open(fn, 'w') as f:
2610 f.write(content)
2611 repo.index.add([fn])
2612 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002613 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002614 repo.heads[branch].commit = commit
2615 repo.head.reference = branch
2616 repo.git.clean('-x', '-f', '-d')
2617 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002618 if tag:
2619 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002620 return before
2621
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002622 def commitConfigUpdate(self, project_name, source_name):
2623 """Commit an update to zuul.yaml
2624
2625 This overwrites the zuul.yaml in the specificed project with
2626 the contents specified.
2627
2628 :arg str project_name: The name of the project containing
2629 zuul.yaml (e.g., common-config)
2630
2631 :arg str source_name: The path to the file (underneath the
2632 test fixture directory) whose contents should be used to
2633 replace zuul.yaml.
2634 """
2635
2636 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002637 files = {}
2638 with open(source_path, 'r') as f:
2639 data = f.read()
2640 layout = yaml.safe_load(data)
2641 files['zuul.yaml'] = data
2642 for item in layout:
2643 if 'job' in item:
2644 jobname = item['job']['name']
2645 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002646 before = self.addCommitToRepo(
2647 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002648 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002649 return before
2650
James E. Blair7fc8daa2016-08-08 15:37:15 -07002651 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002652
James E. Blair7fc8daa2016-08-08 15:37:15 -07002653 """Inject a Fake (Gerrit) event.
2654
2655 This method accepts a JSON-encoded event and simulates Zuul
2656 having received it from Gerrit. It could (and should)
2657 eventually apply to any connection type, but is currently only
2658 used with Gerrit connections. The name of the connection is
2659 used to look up the corresponding server, and the event is
2660 simulated as having been received by all Zuul connections
2661 attached to that server. So if two Gerrit connections in Zuul
2662 are connected to the same Gerrit server, and you invoke this
2663 method specifying the name of one of them, the event will be
2664 received by both.
2665
2666 .. note::
2667
2668 "self.fake_gerrit.addEvent" calls should be migrated to
2669 this method.
2670
2671 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002672 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002673 :arg str event: The JSON-encoded event.
2674
2675 """
2676 specified_conn = self.connections.connections[connection]
2677 for conn in self.connections.connections.values():
2678 if (isinstance(conn, specified_conn.__class__) and
2679 specified_conn.server == conn.server):
2680 conn.addEvent(event)
2681
James E. Blaird8af5422017-05-24 13:59:40 -07002682 def getUpstreamRepos(self, projects):
2683 """Return upstream git repo objects for the listed projects
2684
2685 :arg list projects: A list of strings, each the canonical name
2686 of a project.
2687
2688 :returns: A dictionary of {name: repo} for every listed
2689 project.
2690 :rtype: dict
2691
2692 """
2693
2694 repos = {}
2695 for project in projects:
2696 # FIXME(jeblair): the upstream root does not yet have a
2697 # hostname component; that needs to be added, and this
2698 # line removed:
2699 tmp_project_name = '/'.join(project.split('/')[1:])
2700 path = os.path.join(self.upstream_root, tmp_project_name)
2701 repo = git.Repo(path)
2702 repos[project] = repo
2703 return repos
2704
James E. Blair3f876d52016-07-22 13:07:14 -07002705
2706class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002707 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002708 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002709
Joshua Heskethd78b4482015-09-14 16:56:34 -06002710
2711class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002712 def setup_config(self):
2713 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002714 for section_name in self.config.sections():
2715 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2716 section_name, re.I)
2717 if not con_match:
2718 continue
2719
2720 if self.config.get(section_name, 'driver') == 'sql':
2721 f = MySQLSchemaFixture()
2722 self.useFixture(f)
2723 if (self.config.get(section_name, 'dburi') ==
2724 '$MYSQL_FIXTURE_DBURI$'):
2725 self.config.set(section_name, 'dburi', f.dburi)