blob: 1fe97391f0171cc6eb43cd802e7e0ffab0a257cc [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
608 def addComment(self, message):
609 self.comments.append(message)
610 self._updateTimeStamp()
611
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200612 def getCommentAddedEvent(self, text):
613 name = 'issue_comment'
614 data = {
615 'action': 'created',
616 'issue': {
617 'number': self.number
618 },
619 'comment': {
620 'body': text
621 },
622 'repository': {
623 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100624 },
625 'sender': {
626 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200627 }
628 }
629 return (name, data)
630
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800631 def getReviewAddedEvent(self, review):
632 name = 'pull_request_review'
633 data = {
634 'action': 'submitted',
635 'pull_request': {
636 'number': self.number,
637 'title': self.subject,
638 'updated_at': self.updated_at,
639 'base': {
640 'ref': self.branch,
641 'repo': {
642 'full_name': self.project
643 }
644 },
645 'head': {
646 'sha': self.head_sha
647 }
648 },
649 'review': {
650 'state': review
651 },
652 'repository': {
653 'full_name': self.project
654 },
655 'sender': {
656 'login': 'ghuser'
657 }
658 }
659 return (name, data)
660
Jan Hruban16ad31f2015-11-07 14:39:07 +0100661 def addLabel(self, name):
662 if name not in self.labels:
663 self.labels.append(name)
664 self._updateTimeStamp()
665 return self._getLabelEvent(name)
666
667 def removeLabel(self, name):
668 if name in self.labels:
669 self.labels.remove(name)
670 self._updateTimeStamp()
671 return self._getUnlabelEvent(name)
672
673 def _getLabelEvent(self, label):
674 name = 'pull_request'
675 data = {
676 'action': 'labeled',
677 'pull_request': {
678 'number': self.number,
679 'updated_at': self.updated_at,
680 'base': {
681 'ref': self.branch,
682 'repo': {
683 'full_name': self.project
684 }
685 },
686 'head': {
687 'sha': self.head_sha
688 }
689 },
690 'label': {
691 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100692 },
693 'sender': {
694 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100695 }
696 }
697 return (name, data)
698
699 def _getUnlabelEvent(self, label):
700 name = 'pull_request'
701 data = {
702 'action': 'unlabeled',
703 'pull_request': {
704 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100705 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100706 'updated_at': self.updated_at,
707 'base': {
708 'ref': self.branch,
709 'repo': {
710 'full_name': self.project
711 }
712 },
713 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800714 'sha': self.head_sha,
715 'repo': {
716 'full_name': self.project
717 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100718 }
719 },
720 'label': {
721 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100722 },
723 'sender': {
724 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100725 }
726 }
727 return (name, data)
728
Gregory Haynes4fc12542015-04-22 20:38:06 -0700729 def _getRepo(self):
730 repo_path = os.path.join(self.upstream_root, self.project)
731 return git.Repo(repo_path)
732
733 def _createPRRef(self):
734 repo = self._getRepo()
735 GithubChangeReference.create(
736 repo, self._getPRReference(), 'refs/tags/init')
737
Jan Hruban570d01c2016-03-10 21:51:32 +0100738 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700739 repo = self._getRepo()
740 ref = repo.references[self._getPRReference()]
741 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100742 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700743 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100744 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700745 repo.head.reference = ref
746 zuul.merger.merger.reset_repo_to_head(repo)
747 repo.git.clean('-x', '-f', '-d')
748
Jan Hruban570d01c2016-03-10 21:51:32 +0100749 if files:
750 fn = files[0]
751 self.files = files
752 else:
753 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
754 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100755 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700756 fn = os.path.join(repo.working_dir, fn)
757 f = open(fn, 'w')
758 with open(fn, 'w') as f:
759 f.write("test %s %s\n" %
760 (self.branch, self.number))
761 repo.index.add([fn])
762
763 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800764 # Create an empty set of statuses for the given sha,
765 # each sha on a PR may have a status set on it
766 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700767 repo.head.reference = 'master'
768 zuul.merger.merger.reset_repo_to_head(repo)
769 repo.git.clean('-x', '-f', '-d')
770 repo.heads['master'].checkout()
771
772 def _updateTimeStamp(self):
773 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
774
775 def getPRHeadSha(self):
776 repo = self._getRepo()
777 return repo.references[self._getPRReference()].commit.hexsha
778
Jesse Keatingae4cd272017-01-30 17:10:44 -0800779 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800780 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
781 # convert the timestamp to a str format that would be returned
782 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800783
Adam Gandelmand81dd762017-02-09 15:15:49 -0800784 if granted_on:
785 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
786 submitted_at = time.strftime(
787 gh_time_format, granted_on.timetuple())
788 else:
789 # github timestamps only down to the second, so we need to make
790 # sure reviews that tests add appear to be added over a period of
791 # time in the past and not all at once.
792 if not self.reviews:
793 # the first review happens 10 mins ago
794 offset = 600
795 else:
796 # subsequent reviews happen 1 minute closer to now
797 offset = 600 - (len(self.reviews) * 60)
798
799 granted_on = datetime.datetime.utcfromtimestamp(
800 time.time() - offset)
801 submitted_at = time.strftime(
802 gh_time_format, granted_on.timetuple())
803
Jesse Keatingae4cd272017-01-30 17:10:44 -0800804 self.reviews.append({
805 'state': state,
806 'user': {
807 'login': user,
808 'email': user + "@derp.com",
809 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800810 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800811 })
812
Gregory Haynes4fc12542015-04-22 20:38:06 -0700813 def _getPRReference(self):
814 return '%s/head' % self.number
815
816 def _getPullRequestEvent(self, action):
817 name = 'pull_request'
818 data = {
819 'action': action,
820 'number': self.number,
821 'pull_request': {
822 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100823 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700824 'updated_at': self.updated_at,
825 'base': {
826 'ref': self.branch,
827 'repo': {
828 'full_name': self.project
829 }
830 },
831 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800832 'sha': self.head_sha,
833 'repo': {
834 'full_name': self.project
835 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700836 }
Jan Hruban3b415922016-02-03 13:10:22 +0100837 },
838 'sender': {
839 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700840 }
841 }
842 return (name, data)
843
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800844 def getCommitStatusEvent(self, context, state='success', user='zuul'):
845 name = 'status'
846 data = {
847 'state': state,
848 'sha': self.head_sha,
849 'description': 'Test results for %s: %s' % (self.head_sha, state),
850 'target_url': 'http://zuul/%s' % self.head_sha,
851 'branches': [],
852 'context': context,
853 'sender': {
854 'login': user
855 }
856 }
857 return (name, data)
858
Gregory Haynes4fc12542015-04-22 20:38:06 -0700859
860class FakeGithubConnection(githubconnection.GithubConnection):
861 log = logging.getLogger("zuul.test.FakeGithubConnection")
862
863 def __init__(self, driver, connection_name, connection_config,
864 upstream_root=None):
865 super(FakeGithubConnection, self).__init__(driver, connection_name,
866 connection_config)
867 self.connection_name = connection_name
868 self.pr_number = 0
869 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700870 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700871 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100872 self.merge_failure = False
873 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700874
Jan Hruban570d01c2016-03-10 21:51:32 +0100875 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700876 self.pr_number += 1
877 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100878 self, self.pr_number, project, branch, subject, self.upstream_root,
879 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700880 self.pull_requests.append(pull_request)
881 return pull_request
882
Jesse Keating71a47ff2017-06-06 11:36:43 -0700883 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
884 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700885 if not old_rev:
886 old_rev = '00000000000000000000000000000000'
887 if not new_rev:
888 new_rev = random_sha1()
889 name = 'push'
890 data = {
891 'ref': ref,
892 'before': old_rev,
893 'after': new_rev,
894 'repository': {
895 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700896 },
897 'commits': [
898 {
899 'added': added_files,
900 'removed': removed_files,
901 'modified': modified_files
902 }
903 ]
Wayne1a78c612015-06-11 17:14:13 -0700904 }
905 return (name, data)
906
Gregory Haynes4fc12542015-04-22 20:38:06 -0700907 def emitEvent(self, event):
908 """Emulates sending the GitHub webhook event to the connection."""
909 port = self.webapp.server.socket.getsockname()[1]
910 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700911 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700912 headers = {'X-Github-Event': name}
913 req = urllib.request.Request(
914 'http://localhost:%s/connection/%s/payload'
915 % (port, self.connection_name),
916 data=payload, headers=headers)
917 urllib.request.urlopen(req)
918
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200919 def getPull(self, project, number):
920 pr = self.pull_requests[number - 1]
921 data = {
922 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100923 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200924 'updated_at': pr.updated_at,
925 'base': {
926 'repo': {
927 'full_name': pr.project
928 },
929 'ref': pr.branch,
930 },
Jan Hruban37615e52015-11-19 14:30:49 +0100931 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700932 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200933 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800934 'sha': pr.head_sha,
935 'repo': {
936 'full_name': pr.project
937 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200938 }
939 }
940 return data
941
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800942 def getPullBySha(self, sha):
943 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
944 if len(prs) > 1:
945 raise Exception('Multiple pulls found with head sha: %s' % sha)
946 pr = prs[0]
947 return self.getPull(pr.project, pr.number)
948
Jan Hruban570d01c2016-03-10 21:51:32 +0100949 def getPullFileNames(self, project, number):
950 pr = self.pull_requests[number - 1]
951 return pr.files
952
Jesse Keatingae4cd272017-01-30 17:10:44 -0800953 def _getPullReviews(self, owner, project, number):
954 pr = self.pull_requests[number - 1]
955 return pr.reviews
956
Jan Hruban3b415922016-02-03 13:10:22 +0100957 def getUser(self, login):
958 data = {
959 'username': login,
960 'name': 'Github User',
961 'email': 'github.user@example.com'
962 }
963 return data
964
Jesse Keatingae4cd272017-01-30 17:10:44 -0800965 def getRepoPermission(self, project, login):
966 owner, proj = project.split('/')
967 for pr in self.pull_requests:
968 pr_owner, pr_project = pr.project.split('/')
969 if (pr_owner == owner and proj == pr_project):
970 if login in pr.writers:
971 return 'write'
972 else:
973 return 'read'
974
Gregory Haynes4fc12542015-04-22 20:38:06 -0700975 def getGitUrl(self, project):
976 return os.path.join(self.upstream_root, str(project))
977
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200978 def real_getGitUrl(self, project):
979 return super(FakeGithubConnection, self).getGitUrl(project)
980
Gregory Haynes4fc12542015-04-22 20:38:06 -0700981 def getProjectBranches(self, project):
982 """Masks getProjectBranches since we don't have a real github"""
983
984 # just returns master for now
985 return ['master']
986
Jan Hrubane252a732017-01-03 15:03:09 +0100987 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700988 pull_request = self.pull_requests[pr_number - 1]
989 pull_request.addComment(message)
990
Jan Hruban3b415922016-02-03 13:10:22 +0100991 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100992 pull_request = self.pull_requests[pr_number - 1]
993 if self.merge_failure:
994 raise Exception('Pull request was not merged')
995 if self.merge_not_allowed_count > 0:
996 self.merge_not_allowed_count -= 1
997 raise MergeFailure('Merge was not successful due to mergeability'
998 ' conflict')
999 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001000 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001001
Jesse Keatingd96e5882017-01-19 13:55:50 -08001002 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001003 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001004
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001005 def setCommitStatus(self, project, sha, state, url='', description='',
1006 context='default', user='zuul'):
1007 # always insert a status to the front of the list, to represent
1008 # the last status provided for a commit.
1009 # Since we're bypassing github API, which would require a user, we
1010 # default the user as 'zuul' here.
1011 self.statuses.setdefault(project, {}).setdefault(sha, [])
1012 self.statuses[project][sha].insert(0, {
1013 'state': state,
1014 'url': url,
1015 'description': description,
1016 'context': context,
1017 'creator': {
1018 'login': user
1019 }
1020 })
Jan Hrubane252a732017-01-03 15:03:09 +01001021
Jan Hruban16ad31f2015-11-07 14:39:07 +01001022 def labelPull(self, project, pr_number, label):
1023 pull_request = self.pull_requests[pr_number - 1]
1024 pull_request.addLabel(label)
1025
1026 def unlabelPull(self, project, pr_number, label):
1027 pull_request = self.pull_requests[pr_number - 1]
1028 pull_request.removeLabel(label)
1029
Gregory Haynes4fc12542015-04-22 20:38:06 -07001030
Clark Boylanb640e052014-04-03 16:41:46 -07001031class BuildHistory(object):
1032 def __init__(self, **kw):
1033 self.__dict__.update(kw)
1034
1035 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001036 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1037 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001038
1039
Clark Boylanb640e052014-04-03 16:41:46 -07001040class FakeStatsd(threading.Thread):
1041 def __init__(self):
1042 threading.Thread.__init__(self)
1043 self.daemon = True
1044 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1045 self.sock.bind(('', 0))
1046 self.port = self.sock.getsockname()[1]
1047 self.wake_read, self.wake_write = os.pipe()
1048 self.stats = []
1049
1050 def run(self):
1051 while True:
1052 poll = select.poll()
1053 poll.register(self.sock, select.POLLIN)
1054 poll.register(self.wake_read, select.POLLIN)
1055 ret = poll.poll()
1056 for (fd, event) in ret:
1057 if fd == self.sock.fileno():
1058 data = self.sock.recvfrom(1024)
1059 if not data:
1060 return
1061 self.stats.append(data[0])
1062 if fd == self.wake_read:
1063 return
1064
1065 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001066 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001067
1068
James E. Blaire1767bc2016-08-02 10:00:27 -07001069class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001070 log = logging.getLogger("zuul.test")
1071
Paul Belanger174a8272017-03-14 13:20:10 -04001072 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001073 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001074 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001075 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001076 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001077 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001078 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001079 # TODOv3(jeblair): self.node is really "the label of the node
1080 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001081 # keep using it like this, or we may end up exposing more of
1082 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001083 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001084 self.node = None
1085 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001086 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001087 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001088 self.pipeline = self.parameters['ZUUL_PIPELINE']
1089 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001090 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001091 self.wait_condition = threading.Condition()
1092 self.waiting = False
1093 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001094 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001095 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001096 self.changes = None
1097 if 'ZUUL_CHANGE_IDS' in self.parameters:
1098 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001099
James E. Blair3158e282016-08-19 09:34:11 -07001100 def __repr__(self):
1101 waiting = ''
1102 if self.waiting:
1103 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001104 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1105 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001106
Clark Boylanb640e052014-04-03 16:41:46 -07001107 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001108 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001109 self.wait_condition.acquire()
1110 self.wait_condition.notify()
1111 self.waiting = False
1112 self.log.debug("Build %s released" % self.unique)
1113 self.wait_condition.release()
1114
1115 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001116 """Return whether this build is being held.
1117
1118 :returns: Whether the build is being held.
1119 :rtype: bool
1120 """
1121
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.wait_condition.acquire()
1123 if self.waiting:
1124 ret = True
1125 else:
1126 ret = False
1127 self.wait_condition.release()
1128 return ret
1129
1130 def _wait(self):
1131 self.wait_condition.acquire()
1132 self.waiting = True
1133 self.log.debug("Build %s waiting" % self.unique)
1134 self.wait_condition.wait()
1135 self.wait_condition.release()
1136
1137 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.log.debug('Running build %s' % self.unique)
1139
Paul Belanger174a8272017-03-14 13:20:10 -04001140 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001141 self.log.debug('Holding build %s' % self.unique)
1142 self._wait()
1143 self.log.debug("Build %s continuing" % self.unique)
1144
James E. Blair412fba82017-01-26 15:00:50 -08001145 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001146 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001147 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001148 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001149 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001150 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001151 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001152
James E. Blaire1767bc2016-08-02 10:00:27 -07001153 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001154
James E. Blaira5dba232016-08-08 15:53:24 -07001155 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001156 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001157 for change in changes:
1158 if self.hasChanges(change):
1159 return True
1160 return False
1161
James E. Blaire7b99a02016-08-05 14:27:34 -07001162 def hasChanges(self, *changes):
1163 """Return whether this build has certain changes in its git repos.
1164
1165 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001166 are expected to be present (in order) in the git repository of
1167 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001168
1169 :returns: Whether the build has the indicated changes.
1170 :rtype: bool
1171
1172 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001173 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001174 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001175 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001176 try:
1177 repo = git.Repo(path)
1178 except NoSuchPathError as e:
1179 self.log.debug('%s' % e)
1180 return False
1181 ref = self.parameters['ZUUL_REF']
1182 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1183 commit_message = '%s-1' % change.subject
1184 self.log.debug("Checking if build %s has changes; commit_message "
1185 "%s; repo_messages %s" % (self, commit_message,
1186 repo_messages))
1187 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001188 self.log.debug(" messages do not match")
1189 return False
1190 self.log.debug(" OK")
1191 return True
1192
James E. Blaird8af5422017-05-24 13:59:40 -07001193 def getWorkspaceRepos(self, projects):
1194 """Return workspace git repo objects for the listed projects
1195
1196 :arg list projects: A list of strings, each the canonical name
1197 of a project.
1198
1199 :returns: A dictionary of {name: repo} for every listed
1200 project.
1201 :rtype: dict
1202
1203 """
1204
1205 repos = {}
1206 for project in projects:
1207 path = os.path.join(self.jobdir.src_root, project)
1208 repo = git.Repo(path)
1209 repos[project] = repo
1210 return repos
1211
Clark Boylanb640e052014-04-03 16:41:46 -07001212
Paul Belanger174a8272017-03-14 13:20:10 -04001213class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1214 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001215
Paul Belanger174a8272017-03-14 13:20:10 -04001216 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001217 they will report that they have started but then pause until
1218 released before reporting completion. This attribute may be
1219 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001220 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001221 be explicitly released.
1222
1223 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001224 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001225 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001226 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001227 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001228 self.hold_jobs_in_build = False
1229 self.lock = threading.Lock()
1230 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001231 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001232 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001233 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001234
James E. Blaira5dba232016-08-08 15:53:24 -07001235 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001236 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001237
1238 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001239 :arg Change change: The :py:class:`~tests.base.FakeChange`
1240 instance which should cause the job to fail. This job
1241 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001242
1243 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001244 l = self.fail_tests.get(name, [])
1245 l.append(change)
1246 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001247
James E. Blair962220f2016-08-03 11:22:38 -07001248 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001249 """Release a held build.
1250
1251 :arg str regex: A regular expression which, if supplied, will
1252 cause only builds with matching names to be released. If
1253 not supplied, all builds will be released.
1254
1255 """
James E. Blair962220f2016-08-03 11:22:38 -07001256 builds = self.running_builds[:]
1257 self.log.debug("Releasing build %s (%s)" % (regex,
1258 len(self.running_builds)))
1259 for build in builds:
1260 if not regex or re.match(regex, build.name):
1261 self.log.debug("Releasing build %s" %
1262 (build.parameters['ZUUL_UUID']))
1263 build.release()
1264 else:
1265 self.log.debug("Not releasing build %s" %
1266 (build.parameters['ZUUL_UUID']))
1267 self.log.debug("Done releasing builds %s (%s)" %
1268 (regex, len(self.running_builds)))
1269
Paul Belanger174a8272017-03-14 13:20:10 -04001270 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001271 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001272 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001273 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001274 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001275 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001276 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001277 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001278 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1279 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001280
1281 def stopJob(self, job):
1282 self.log.debug("handle stop")
1283 parameters = json.loads(job.arguments)
1284 uuid = parameters['uuid']
1285 for build in self.running_builds:
1286 if build.unique == uuid:
1287 build.aborted = True
1288 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001289 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001290
James E. Blaira002b032017-04-18 10:35:48 -07001291 def stop(self):
1292 for build in self.running_builds:
1293 build.release()
1294 super(RecordingExecutorServer, self).stop()
1295
Joshua Hesketh50c21782016-10-13 21:34:14 +11001296
Paul Belanger174a8272017-03-14 13:20:10 -04001297class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001298 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001299 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001300 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001301 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001302 if not commit: # merge conflict
1303 self.recordResult('MERGER_FAILURE')
1304 return commit
1305
1306 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001307 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001308 self.executor_server.lock.acquire()
1309 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001310 BuildHistory(name=build.name, result=result, changes=build.changes,
1311 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001312 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001313 pipeline=build.parameters['ZUUL_PIPELINE'])
1314 )
Paul Belanger174a8272017-03-14 13:20:10 -04001315 self.executor_server.running_builds.remove(build)
1316 del self.executor_server.job_builds[self.job.unique]
1317 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001318
1319 def runPlaybooks(self, args):
1320 build = self.executor_server.job_builds[self.job.unique]
1321 build.jobdir = self.jobdir
1322
1323 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1324 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001325 return result
1326
Monty Taylore6562aa2017-02-20 07:37:39 -05001327 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001328 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001329
Paul Belanger174a8272017-03-14 13:20:10 -04001330 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001331 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001332 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001333 else:
1334 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001335 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001336
James E. Blairad8dca02017-02-21 11:48:32 -05001337 def getHostList(self, args):
1338 self.log.debug("hostlist")
1339 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001340 for host in hosts:
1341 host['host_vars']['ansible_connection'] = 'local'
1342
1343 hosts.append(dict(
1344 name='localhost',
1345 host_vars=dict(ansible_connection='local'),
1346 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001347 return hosts
1348
James E. Blairf5dbd002015-12-23 15:26:17 -08001349
Clark Boylanb640e052014-04-03 16:41:46 -07001350class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001351 """A Gearman server for use in tests.
1352
1353 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1354 added to the queue but will not be distributed to workers
1355 until released. This attribute may be changed at any time and
1356 will take effect for subsequently enqueued jobs, but
1357 previously held jobs will still need to be explicitly
1358 released.
1359
1360 """
1361
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001362 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001363 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001364 if use_ssl:
1365 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1366 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1367 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1368 else:
1369 ssl_ca = None
1370 ssl_cert = None
1371 ssl_key = None
1372
1373 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1374 ssl_cert=ssl_cert,
1375 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001376
1377 def getJobForConnection(self, connection, peek=False):
1378 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1379 for job in queue:
1380 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001381 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001382 job.waiting = self.hold_jobs_in_queue
1383 else:
1384 job.waiting = False
1385 if job.waiting:
1386 continue
1387 if job.name in connection.functions:
1388 if not peek:
1389 queue.remove(job)
1390 connection.related_jobs[job.handle] = job
1391 job.worker_connection = connection
1392 job.running = True
1393 return job
1394 return None
1395
1396 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001397 """Release a held job.
1398
1399 :arg str regex: A regular expression which, if supplied, will
1400 cause only jobs with matching names to be released. If
1401 not supplied, all jobs will be released.
1402 """
Clark Boylanb640e052014-04-03 16:41:46 -07001403 released = False
1404 qlen = (len(self.high_queue) + len(self.normal_queue) +
1405 len(self.low_queue))
1406 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1407 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001408 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001409 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001410 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001411 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001412 self.log.debug("releasing queued job %s" %
1413 job.unique)
1414 job.waiting = False
1415 released = True
1416 else:
1417 self.log.debug("not releasing queued job %s" %
1418 job.unique)
1419 if released:
1420 self.wakeConnections()
1421 qlen = (len(self.high_queue) + len(self.normal_queue) +
1422 len(self.low_queue))
1423 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1424
1425
1426class FakeSMTP(object):
1427 log = logging.getLogger('zuul.FakeSMTP')
1428
1429 def __init__(self, messages, server, port):
1430 self.server = server
1431 self.port = port
1432 self.messages = messages
1433
1434 def sendmail(self, from_email, to_email, msg):
1435 self.log.info("Sending email from %s, to %s, with msg %s" % (
1436 from_email, to_email, msg))
1437
1438 headers = msg.split('\n\n', 1)[0]
1439 body = msg.split('\n\n', 1)[1]
1440
1441 self.messages.append(dict(
1442 from_email=from_email,
1443 to_email=to_email,
1444 msg=msg,
1445 headers=headers,
1446 body=body,
1447 ))
1448
1449 return True
1450
1451 def quit(self):
1452 return True
1453
1454
James E. Blairdce6cea2016-12-20 16:45:32 -08001455class FakeNodepool(object):
1456 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001457 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001458
1459 log = logging.getLogger("zuul.test.FakeNodepool")
1460
1461 def __init__(self, host, port, chroot):
1462 self.client = kazoo.client.KazooClient(
1463 hosts='%s:%s%s' % (host, port, chroot))
1464 self.client.start()
1465 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001466 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001467 self.thread = threading.Thread(target=self.run)
1468 self.thread.daemon = True
1469 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001470 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001471
1472 def stop(self):
1473 self._running = False
1474 self.thread.join()
1475 self.client.stop()
1476 self.client.close()
1477
1478 def run(self):
1479 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001480 try:
1481 self._run()
1482 except Exception:
1483 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001484 time.sleep(0.1)
1485
1486 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001487 if self.paused:
1488 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001489 for req in self.getNodeRequests():
1490 self.fulfillRequest(req)
1491
1492 def getNodeRequests(self):
1493 try:
1494 reqids = self.client.get_children(self.REQUEST_ROOT)
1495 except kazoo.exceptions.NoNodeError:
1496 return []
1497 reqs = []
1498 for oid in sorted(reqids):
1499 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001500 try:
1501 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001502 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001503 data['_oid'] = oid
1504 reqs.append(data)
1505 except kazoo.exceptions.NoNodeError:
1506 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001507 return reqs
1508
James E. Blaire18d4602017-01-05 11:17:28 -08001509 def getNodes(self):
1510 try:
1511 nodeids = self.client.get_children(self.NODE_ROOT)
1512 except kazoo.exceptions.NoNodeError:
1513 return []
1514 nodes = []
1515 for oid in sorted(nodeids):
1516 path = self.NODE_ROOT + '/' + oid
1517 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001518 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001519 data['_oid'] = oid
1520 try:
1521 lockfiles = self.client.get_children(path + '/lock')
1522 except kazoo.exceptions.NoNodeError:
1523 lockfiles = []
1524 if lockfiles:
1525 data['_lock'] = True
1526 else:
1527 data['_lock'] = False
1528 nodes.append(data)
1529 return nodes
1530
James E. Blaira38c28e2017-01-04 10:33:20 -08001531 def makeNode(self, request_id, node_type):
1532 now = time.time()
1533 path = '/nodepool/nodes/'
1534 data = dict(type=node_type,
1535 provider='test-provider',
1536 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001537 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001538 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001539 public_ipv4='127.0.0.1',
1540 private_ipv4=None,
1541 public_ipv6=None,
1542 allocated_to=request_id,
1543 state='ready',
1544 state_time=now,
1545 created_time=now,
1546 updated_time=now,
1547 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001548 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001549 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001550 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001551 path = self.client.create(path, data,
1552 makepath=True,
1553 sequence=True)
1554 nodeid = path.split("/")[-1]
1555 return nodeid
1556
James E. Blair6ab79e02017-01-06 10:10:17 -08001557 def addFailRequest(self, request):
1558 self.fail_requests.add(request['_oid'])
1559
James E. Blairdce6cea2016-12-20 16:45:32 -08001560 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001561 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001562 return
1563 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001564 oid = request['_oid']
1565 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001566
James E. Blair6ab79e02017-01-06 10:10:17 -08001567 if oid in self.fail_requests:
1568 request['state'] = 'failed'
1569 else:
1570 request['state'] = 'fulfilled'
1571 nodes = []
1572 for node in request['node_types']:
1573 nodeid = self.makeNode(oid, node)
1574 nodes.append(nodeid)
1575 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001576
James E. Blaira38c28e2017-01-04 10:33:20 -08001577 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001578 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001579 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001580 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001581 try:
1582 self.client.set(path, data)
1583 except kazoo.exceptions.NoNodeError:
1584 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001585
1586
James E. Blair498059b2016-12-20 13:50:13 -08001587class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001588 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001589 super(ChrootedKazooFixture, self).__init__()
1590
1591 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1592 if ':' in zk_host:
1593 host, port = zk_host.split(':')
1594 else:
1595 host = zk_host
1596 port = None
1597
1598 self.zookeeper_host = host
1599
1600 if not port:
1601 self.zookeeper_port = 2181
1602 else:
1603 self.zookeeper_port = int(port)
1604
Clark Boylan621ec9a2017-04-07 17:41:33 -07001605 self.test_id = test_id
1606
James E. Blair498059b2016-12-20 13:50:13 -08001607 def _setUp(self):
1608 # Make sure the test chroot paths do not conflict
1609 random_bits = ''.join(random.choice(string.ascii_lowercase +
1610 string.ascii_uppercase)
1611 for x in range(8))
1612
Clark Boylan621ec9a2017-04-07 17:41:33 -07001613 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001614 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1615
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001616 self.addCleanup(self._cleanup)
1617
James E. Blair498059b2016-12-20 13:50:13 -08001618 # Ensure the chroot path exists and clean up any pre-existing znodes.
1619 _tmp_client = kazoo.client.KazooClient(
1620 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1621 _tmp_client.start()
1622
1623 if _tmp_client.exists(self.zookeeper_chroot):
1624 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1625
1626 _tmp_client.ensure_path(self.zookeeper_chroot)
1627 _tmp_client.stop()
1628 _tmp_client.close()
1629
James E. Blair498059b2016-12-20 13:50:13 -08001630 def _cleanup(self):
1631 '''Remove the chroot path.'''
1632 # Need a non-chroot'ed client to remove the chroot path
1633 _tmp_client = kazoo.client.KazooClient(
1634 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1635 _tmp_client.start()
1636 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1637 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001638 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001639
1640
Joshua Heskethd78b4482015-09-14 16:56:34 -06001641class MySQLSchemaFixture(fixtures.Fixture):
1642 def setUp(self):
1643 super(MySQLSchemaFixture, self).setUp()
1644
1645 random_bits = ''.join(random.choice(string.ascii_lowercase +
1646 string.ascii_uppercase)
1647 for x in range(8))
1648 self.name = '%s_%s' % (random_bits, os.getpid())
1649 self.passwd = uuid.uuid4().hex
1650 db = pymysql.connect(host="localhost",
1651 user="openstack_citest",
1652 passwd="openstack_citest",
1653 db="openstack_citest")
1654 cur = db.cursor()
1655 cur.execute("create database %s" % self.name)
1656 cur.execute(
1657 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1658 (self.name, self.name, self.passwd))
1659 cur.execute("flush privileges")
1660
1661 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1662 self.passwd,
1663 self.name)
1664 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1665 self.addCleanup(self.cleanup)
1666
1667 def cleanup(self):
1668 db = pymysql.connect(host="localhost",
1669 user="openstack_citest",
1670 passwd="openstack_citest",
1671 db="openstack_citest")
1672 cur = db.cursor()
1673 cur.execute("drop database %s" % self.name)
1674 cur.execute("drop user '%s'@'localhost'" % self.name)
1675 cur.execute("flush privileges")
1676
1677
Maru Newby3fe5f852015-01-13 04:22:14 +00001678class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001679 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001680 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001681
James E. Blair1c236df2017-02-01 14:07:24 -08001682 def attachLogs(self, *args):
1683 def reader():
1684 self._log_stream.seek(0)
1685 while True:
1686 x = self._log_stream.read(4096)
1687 if not x:
1688 break
1689 yield x.encode('utf8')
1690 content = testtools.content.content_from_reader(
1691 reader,
1692 testtools.content_type.UTF8_TEXT,
1693 False)
1694 self.addDetail('logging', content)
1695
Clark Boylanb640e052014-04-03 16:41:46 -07001696 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001697 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001698 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1699 try:
1700 test_timeout = int(test_timeout)
1701 except ValueError:
1702 # If timeout value is invalid do not set a timeout.
1703 test_timeout = 0
1704 if test_timeout > 0:
1705 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1706
1707 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1708 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1709 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1710 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1711 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1712 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1713 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1714 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1715 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1716 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001717 self._log_stream = StringIO()
1718 self.addOnException(self.attachLogs)
1719 else:
1720 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001721
James E. Blair73b41772017-05-22 13:22:55 -07001722 # NOTE(jeblair): this is temporary extra debugging to try to
1723 # track down a possible leak.
1724 orig_git_repo_init = git.Repo.__init__
1725
1726 def git_repo_init(myself, *args, **kw):
1727 orig_git_repo_init(myself, *args, **kw)
1728 self.log.debug("Created git repo 0x%x %s" %
1729 (id(myself), repr(myself)))
1730
1731 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1732 git_repo_init))
1733
James E. Blair1c236df2017-02-01 14:07:24 -08001734 handler = logging.StreamHandler(self._log_stream)
1735 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1736 '%(levelname)-8s %(message)s')
1737 handler.setFormatter(formatter)
1738
1739 logger = logging.getLogger()
1740 logger.setLevel(logging.DEBUG)
1741 logger.addHandler(handler)
1742
Clark Boylan3410d532017-04-25 12:35:29 -07001743 # Make sure we don't carry old handlers around in process state
1744 # which slows down test runs
1745 self.addCleanup(logger.removeHandler, handler)
1746 self.addCleanup(handler.close)
1747 self.addCleanup(handler.flush)
1748
James E. Blair1c236df2017-02-01 14:07:24 -08001749 # NOTE(notmorgan): Extract logging overrides for specific
1750 # libraries from the OS_LOG_DEFAULTS env and create loggers
1751 # for each. This is used to limit the output during test runs
1752 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001753 log_defaults_from_env = os.environ.get(
1754 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001755 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001756
James E. Blairdce6cea2016-12-20 16:45:32 -08001757 if log_defaults_from_env:
1758 for default in log_defaults_from_env.split(','):
1759 try:
1760 name, level_str = default.split('=', 1)
1761 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001762 logger = logging.getLogger(name)
1763 logger.setLevel(level)
1764 logger.addHandler(handler)
1765 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001766 except ValueError:
1767 # NOTE(notmorgan): Invalid format of the log default,
1768 # skip and don't try and apply a logger for the
1769 # specified module
1770 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001771
Maru Newby3fe5f852015-01-13 04:22:14 +00001772
1773class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001774 """A test case with a functioning Zuul.
1775
1776 The following class variables are used during test setup and can
1777 be overidden by subclasses but are effectively read-only once a
1778 test method starts running:
1779
1780 :cvar str config_file: This points to the main zuul config file
1781 within the fixtures directory. Subclasses may override this
1782 to obtain a different behavior.
1783
1784 :cvar str tenant_config_file: This is the tenant config file
1785 (which specifies from what git repos the configuration should
1786 be loaded). It defaults to the value specified in
1787 `config_file` but can be overidden by subclasses to obtain a
1788 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001789 configuration. See also the :py:func:`simple_layout`
1790 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001791
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001792 :cvar bool create_project_keys: Indicates whether Zuul should
1793 auto-generate keys for each project, or whether the test
1794 infrastructure should insert dummy keys to save time during
1795 startup. Defaults to False.
1796
James E. Blaire7b99a02016-08-05 14:27:34 -07001797 The following are instance variables that are useful within test
1798 methods:
1799
1800 :ivar FakeGerritConnection fake_<connection>:
1801 A :py:class:`~tests.base.FakeGerritConnection` will be
1802 instantiated for each connection present in the config file
1803 and stored here. For instance, `fake_gerrit` will hold the
1804 FakeGerritConnection object for a connection named `gerrit`.
1805
1806 :ivar FakeGearmanServer gearman_server: An instance of
1807 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1808 server that all of the Zuul components in this test use to
1809 communicate with each other.
1810
Paul Belanger174a8272017-03-14 13:20:10 -04001811 :ivar RecordingExecutorServer executor_server: An instance of
1812 :py:class:`~tests.base.RecordingExecutorServer` which is the
1813 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001814
1815 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1816 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001817 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001818 list upon completion.
1819
1820 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1821 objects representing completed builds. They are appended to
1822 the list in the order they complete.
1823
1824 """
1825
James E. Blair83005782015-12-11 14:46:03 -08001826 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001827 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001828 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001829 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001830
1831 def _startMerger(self):
1832 self.merge_server = zuul.merger.server.MergeServer(self.config,
1833 self.connections)
1834 self.merge_server.start()
1835
Maru Newby3fe5f852015-01-13 04:22:14 +00001836 def setUp(self):
1837 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001838
1839 self.setupZK()
1840
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001841 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001842 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001843 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1844 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001845 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001846 tmp_root = tempfile.mkdtemp(
1847 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001848 self.test_root = os.path.join(tmp_root, "zuul-test")
1849 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001850 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001851 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001852 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001853
1854 if os.path.exists(self.test_root):
1855 shutil.rmtree(self.test_root)
1856 os.makedirs(self.test_root)
1857 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001858 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001859
1860 # Make per test copy of Configuration.
1861 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001862 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1863 if not os.path.exists(self.private_key_file):
1864 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1865 shutil.copy(src_private_key_file, self.private_key_file)
1866 shutil.copy('{}.pub'.format(src_private_key_file),
1867 '{}.pub'.format(self.private_key_file))
1868 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001869 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001870 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001871 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001872 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001873 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001874 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001875 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001876
Clark Boylanb640e052014-04-03 16:41:46 -07001877 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001878 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1879 # see: https://github.com/jsocol/pystatsd/issues/61
1880 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001881 os.environ['STATSD_PORT'] = str(self.statsd.port)
1882 self.statsd.start()
1883 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001884 reload_module(statsd)
1885 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001886
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001887 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001888
1889 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001890 self.log.info("Gearman server on port %s" %
1891 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001892 if self.use_ssl:
1893 self.log.info('SSL enabled for gearman')
1894 self.config.set(
1895 'gearman', 'ssl_ca',
1896 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1897 self.config.set(
1898 'gearman', 'ssl_cert',
1899 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1900 self.config.set(
1901 'gearman', 'ssl_key',
1902 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001903
James E. Blaire511d2f2016-12-08 15:22:26 -08001904 gerritsource.GerritSource.replication_timeout = 1.5
1905 gerritsource.GerritSource.replication_retry_interval = 0.5
1906 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001907
Joshua Hesketh352264b2015-08-11 23:42:08 +10001908 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001909
Jan Hruban7083edd2015-08-21 14:00:54 +02001910 self.webapp = zuul.webapp.WebApp(
1911 self.sched, port=0, listen_address='127.0.0.1')
1912
Jan Hruban6b71aff2015-10-22 16:58:08 +02001913 self.event_queues = [
1914 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001915 self.sched.trigger_event_queue,
1916 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001917 ]
1918
James E. Blairfef78942016-03-11 16:28:56 -08001919 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001920 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001921
Paul Belanger174a8272017-03-14 13:20:10 -04001922 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001923 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001924 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001925 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001926 _test_root=self.test_root,
1927 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001928 self.executor_server.start()
1929 self.history = self.executor_server.build_history
1930 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001931
Paul Belanger174a8272017-03-14 13:20:10 -04001932 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001933 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001934 self.merge_client = zuul.merger.client.MergeClient(
1935 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001936 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001937 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001938 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001939
James E. Blair0d5a36e2017-02-21 10:53:44 -05001940 self.fake_nodepool = FakeNodepool(
1941 self.zk_chroot_fixture.zookeeper_host,
1942 self.zk_chroot_fixture.zookeeper_port,
1943 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001944
Paul Belanger174a8272017-03-14 13:20:10 -04001945 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001946 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001947 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001948 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001949
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001950 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001951
1952 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001953 self.webapp.start()
1954 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001955 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001956 # Cleanups are run in reverse order
1957 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001958 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001959 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001960
James E. Blairb9c0d772017-03-03 14:34:49 -08001961 self.sched.reconfigure(self.config)
1962 self.sched.resume()
1963
Tobias Henkel7df274b2017-05-26 17:41:11 +02001964 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001965 # Set up gerrit related fakes
1966 # Set a changes database so multiple FakeGerrit's can report back to
1967 # a virtual canonical database given by the configured hostname
1968 self.gerrit_changes_dbs = {}
1969
1970 def getGerritConnection(driver, name, config):
1971 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1972 con = FakeGerritConnection(driver, name, config,
1973 changes_db=db,
1974 upstream_root=self.upstream_root)
1975 self.event_queues.append(con.event_queue)
1976 setattr(self, 'fake_' + name, con)
1977 return con
1978
1979 self.useFixture(fixtures.MonkeyPatch(
1980 'zuul.driver.gerrit.GerritDriver.getConnection',
1981 getGerritConnection))
1982
Gregory Haynes4fc12542015-04-22 20:38:06 -07001983 def getGithubConnection(driver, name, config):
1984 con = FakeGithubConnection(driver, name, config,
1985 upstream_root=self.upstream_root)
1986 setattr(self, 'fake_' + name, con)
1987 return con
1988
1989 self.useFixture(fixtures.MonkeyPatch(
1990 'zuul.driver.github.GithubDriver.getConnection',
1991 getGithubConnection))
1992
James E. Blaire511d2f2016-12-08 15:22:26 -08001993 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001994 # TODO(jhesketh): This should come from lib.connections for better
1995 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001996 # Register connections from the config
1997 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001998
Joshua Hesketh352264b2015-08-11 23:42:08 +10001999 def FakeSMTPFactory(*args, **kw):
2000 args = [self.smtp_messages] + list(args)
2001 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002002
Joshua Hesketh352264b2015-08-11 23:42:08 +10002003 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002004
James E. Blaire511d2f2016-12-08 15:22:26 -08002005 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002006 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002007 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002008
James E. Blair83005782015-12-11 14:46:03 -08002009 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002010 # This creates the per-test configuration object. It can be
2011 # overriden by subclasses, but should not need to be since it
2012 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002013 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002014 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002015
2016 if not self.setupSimpleLayout():
2017 if hasattr(self, 'tenant_config_file'):
2018 self.config.set('zuul', 'tenant_config',
2019 self.tenant_config_file)
2020 git_path = os.path.join(
2021 os.path.dirname(
2022 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2023 'git')
2024 if os.path.exists(git_path):
2025 for reponame in os.listdir(git_path):
2026 project = reponame.replace('_', '/')
2027 self.copyDirToRepo(project,
2028 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002029 self.setupAllProjectKeys()
2030
James E. Blair06cc3922017-04-19 10:08:10 -07002031 def setupSimpleLayout(self):
2032 # If the test method has been decorated with a simple_layout,
2033 # use that instead of the class tenant_config_file. Set up a
2034 # single config-project with the specified layout, and
2035 # initialize repos for all of the 'project' entries which
2036 # appear in the layout.
2037 test_name = self.id().split('.')[-1]
2038 test = getattr(self, test_name)
2039 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002040 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002041 else:
2042 return False
2043
James E. Blairb70e55a2017-04-19 12:57:02 -07002044 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002045 path = os.path.join(FIXTURE_DIR, path)
2046 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002047 data = f.read()
2048 layout = yaml.safe_load(data)
2049 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002050 untrusted_projects = []
2051 for item in layout:
2052 if 'project' in item:
2053 name = item['project']['name']
2054 untrusted_projects.append(name)
2055 self.init_repo(name)
2056 self.addCommitToRepo(name, 'initial commit',
2057 files={'README': ''},
2058 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002059 if 'job' in item:
2060 jobname = item['job']['name']
2061 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002062
2063 root = os.path.join(self.test_root, "config")
2064 if not os.path.exists(root):
2065 os.makedirs(root)
2066 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2067 config = [{'tenant':
2068 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002069 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002070 {'config-projects': ['common-config'],
2071 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002072 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002073 f.close()
2074 self.config.set('zuul', 'tenant_config',
2075 os.path.join(FIXTURE_DIR, f.name))
2076
2077 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002078 self.addCommitToRepo('common-config', 'add content from fixture',
2079 files, branch='master', tag='init')
2080
2081 return True
2082
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002083 def setupAllProjectKeys(self):
2084 if self.create_project_keys:
2085 return
2086
2087 path = self.config.get('zuul', 'tenant_config')
2088 with open(os.path.join(FIXTURE_DIR, path)) as f:
2089 tenant_config = yaml.safe_load(f.read())
2090 for tenant in tenant_config:
2091 sources = tenant['tenant']['source']
2092 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002093 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002094 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002095 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002096 self.setupProjectKeys(source, project)
2097
2098 def setupProjectKeys(self, source, project):
2099 # Make sure we set up an RSA key for the project so that we
2100 # don't spend time generating one:
2101
2102 key_root = os.path.join(self.state_root, 'keys')
2103 if not os.path.isdir(key_root):
2104 os.mkdir(key_root, 0o700)
2105 private_key_file = os.path.join(key_root, source, project + '.pem')
2106 private_key_dir = os.path.dirname(private_key_file)
2107 self.log.debug("Installing test keys for project %s at %s" % (
2108 project, private_key_file))
2109 if not os.path.isdir(private_key_dir):
2110 os.makedirs(private_key_dir)
2111 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2112 with open(private_key_file, 'w') as o:
2113 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002114
James E. Blair498059b2016-12-20 13:50:13 -08002115 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002116 self.zk_chroot_fixture = self.useFixture(
2117 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002118 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002119 self.zk_chroot_fixture.zookeeper_host,
2120 self.zk_chroot_fixture.zookeeper_port,
2121 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002122
James E. Blair96c6bf82016-01-15 16:20:40 -08002123 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002124 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002125
2126 files = {}
2127 for (dirpath, dirnames, filenames) in os.walk(source_path):
2128 for filename in filenames:
2129 test_tree_filepath = os.path.join(dirpath, filename)
2130 common_path = os.path.commonprefix([test_tree_filepath,
2131 source_path])
2132 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2133 with open(test_tree_filepath, 'r') as f:
2134 content = f.read()
2135 files[relative_filepath] = content
2136 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002137 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002138
James E. Blaire18d4602017-01-05 11:17:28 -08002139 def assertNodepoolState(self):
2140 # Make sure that there are no pending requests
2141
2142 requests = self.fake_nodepool.getNodeRequests()
2143 self.assertEqual(len(requests), 0)
2144
2145 nodes = self.fake_nodepool.getNodes()
2146 for node in nodes:
2147 self.assertFalse(node['_lock'], "Node %s is locked" %
2148 (node['_oid'],))
2149
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002150 def assertNoGeneratedKeys(self):
2151 # Make sure that Zuul did not generate any project keys
2152 # (unless it was supposed to).
2153
2154 if self.create_project_keys:
2155 return
2156
2157 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2158 test_key = i.read()
2159
2160 key_root = os.path.join(self.state_root, 'keys')
2161 for root, dirname, files in os.walk(key_root):
2162 for fn in files:
2163 with open(os.path.join(root, fn)) as f:
2164 self.assertEqual(test_key, f.read())
2165
Clark Boylanb640e052014-04-03 16:41:46 -07002166 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002167 self.log.debug("Assert final state")
2168 # Make sure no jobs are running
2169 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002170 # Make sure that git.Repo objects have been garbage collected.
2171 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002172 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002173 gc.collect()
2174 for obj in gc.get_objects():
2175 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002176 self.log.debug("Leaked git repo object: 0x%x %s" %
2177 (id(obj), repr(obj)))
2178 for ref in gc.get_referrers(obj):
2179 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002180 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002181 if repos:
2182 for obj in gc.garbage:
2183 self.log.debug(" Garbage %s" % (repr(obj)))
2184 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002185 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002186 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002187 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002188 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002189 for tenant in self.sched.abide.tenants.values():
2190 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002191 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002192 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002193
2194 def shutdown(self):
2195 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002196 self.executor_server.hold_jobs_in_build = False
2197 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002198 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002199 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002200 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002201 self.sched.stop()
2202 self.sched.join()
2203 self.statsd.stop()
2204 self.statsd.join()
2205 self.webapp.stop()
2206 self.webapp.join()
2207 self.rpc.stop()
2208 self.rpc.join()
2209 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002210 self.fake_nodepool.stop()
2211 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002212 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002213 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002214 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002215 # Further the pydevd threads also need to be whitelisted so debugging
2216 # e.g. in PyCharm is possible without breaking shutdown.
2217 whitelist = ['executor-watchdog',
2218 'pydevd.CommandThread',
2219 'pydevd.Reader',
2220 'pydevd.Writer',
2221 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002222 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002223 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002224 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002225 log_str = ""
2226 for thread_id, stack_frame in sys._current_frames().items():
2227 log_str += "Thread: %s\n" % thread_id
2228 log_str += "".join(traceback.format_stack(stack_frame))
2229 self.log.debug(log_str)
2230 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002231
James E. Blaira002b032017-04-18 10:35:48 -07002232 def assertCleanShutdown(self):
2233 pass
2234
James E. Blairc4ba97a2017-04-19 16:26:24 -07002235 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002236 parts = project.split('/')
2237 path = os.path.join(self.upstream_root, *parts[:-1])
2238 if not os.path.exists(path):
2239 os.makedirs(path)
2240 path = os.path.join(self.upstream_root, project)
2241 repo = git.Repo.init(path)
2242
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002243 with repo.config_writer() as config_writer:
2244 config_writer.set_value('user', 'email', 'user@example.com')
2245 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002246
Clark Boylanb640e052014-04-03 16:41:46 -07002247 repo.index.commit('initial commit')
2248 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002249 if tag:
2250 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002251
James E. Blair97d902e2014-08-21 13:25:56 -07002252 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002253 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002254 repo.git.clean('-x', '-f', '-d')
2255
James E. Blair97d902e2014-08-21 13:25:56 -07002256 def create_branch(self, project, branch):
2257 path = os.path.join(self.upstream_root, project)
2258 repo = git.Repo.init(path)
2259 fn = os.path.join(path, 'README')
2260
2261 branch_head = repo.create_head(branch)
2262 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002263 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002264 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002265 f.close()
2266 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002267 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002268
James E. Blair97d902e2014-08-21 13:25:56 -07002269 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002270 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002271 repo.git.clean('-x', '-f', '-d')
2272
Sachi King9f16d522016-03-16 12:20:45 +11002273 def create_commit(self, project):
2274 path = os.path.join(self.upstream_root, project)
2275 repo = git.Repo(path)
2276 repo.head.reference = repo.heads['master']
2277 file_name = os.path.join(path, 'README')
2278 with open(file_name, 'a') as f:
2279 f.write('creating fake commit\n')
2280 repo.index.add([file_name])
2281 commit = repo.index.commit('Creating a fake commit')
2282 return commit.hexsha
2283
James E. Blairf4a5f022017-04-18 14:01:10 -07002284 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002285 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002286 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002287 while len(self.builds):
2288 self.release(self.builds[0])
2289 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002290 i += 1
2291 if count is not None and i >= count:
2292 break
James E. Blairb8c16472015-05-05 14:55:26 -07002293
Clark Boylanb640e052014-04-03 16:41:46 -07002294 def release(self, job):
2295 if isinstance(job, FakeBuild):
2296 job.release()
2297 else:
2298 job.waiting = False
2299 self.log.debug("Queued job %s released" % job.unique)
2300 self.gearman_server.wakeConnections()
2301
2302 def getParameter(self, job, name):
2303 if isinstance(job, FakeBuild):
2304 return job.parameters[name]
2305 else:
2306 parameters = json.loads(job.arguments)
2307 return parameters[name]
2308
Clark Boylanb640e052014-04-03 16:41:46 -07002309 def haveAllBuildsReported(self):
2310 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002311 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002312 return False
2313 # Find out if every build that the worker has completed has been
2314 # reported back to Zuul. If it hasn't then that means a Gearman
2315 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002316 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002317 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002318 if not zbuild:
2319 # It has already been reported
2320 continue
2321 # It hasn't been reported yet.
2322 return False
2323 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002324 worker = self.executor_server.executor_worker
2325 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002326 if connection.state == 'GRAB_WAIT':
2327 return False
2328 return True
2329
2330 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002331 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002332 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002333 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002334 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002335 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002336 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002337 for j in conn.related_jobs.values():
2338 if j.unique == build.uuid:
2339 client_job = j
2340 break
2341 if not client_job:
2342 self.log.debug("%s is not known to the gearman client" %
2343 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002344 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002345 if not client_job.handle:
2346 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002348 server_job = self.gearman_server.jobs.get(client_job.handle)
2349 if not server_job:
2350 self.log.debug("%s is not known to the gearman server" %
2351 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002352 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002353 if not hasattr(server_job, 'waiting'):
2354 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002355 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002356 if server_job.waiting:
2357 continue
James E. Blair17302972016-08-10 16:11:42 -07002358 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002359 self.log.debug("%s has not reported start" % build)
2360 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002361 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002362 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002363 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002364 if worker_build:
2365 if worker_build.isWaiting():
2366 continue
2367 else:
2368 self.log.debug("%s is running" % worker_build)
2369 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002370 else:
James E. Blair962220f2016-08-03 11:22:38 -07002371 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002372 return False
James E. Blaira002b032017-04-18 10:35:48 -07002373 for (build_uuid, job_worker) in \
2374 self.executor_server.job_workers.items():
2375 if build_uuid not in seen_builds:
2376 self.log.debug("%s is not finalized" % build_uuid)
2377 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002378 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002379
James E. Blairdce6cea2016-12-20 16:45:32 -08002380 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002381 if self.fake_nodepool.paused:
2382 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002383 if self.sched.nodepool.requests:
2384 return False
2385 return True
2386
Jan Hruban6b71aff2015-10-22 16:58:08 +02002387 def eventQueuesEmpty(self):
2388 for queue in self.event_queues:
2389 yield queue.empty()
2390
2391 def eventQueuesJoin(self):
2392 for queue in self.event_queues:
2393 queue.join()
2394
Clark Boylanb640e052014-04-03 16:41:46 -07002395 def waitUntilSettled(self):
2396 self.log.debug("Waiting until settled...")
2397 start = time.time()
2398 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002399 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002400 self.log.error("Timeout waiting for Zuul to settle")
2401 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002402 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002403 self.log.error(" %s: %s" % (queue, queue.empty()))
2404 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002405 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002406 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002407 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002408 self.log.error("All requests completed: %s" %
2409 (self.areAllNodeRequestsComplete(),))
2410 self.log.error("Merge client jobs: %s" %
2411 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002412 raise Exception("Timeout waiting for Zuul to settle")
2413 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002414
Paul Belanger174a8272017-03-14 13:20:10 -04002415 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002416 # have all build states propogated to zuul?
2417 if self.haveAllBuildsReported():
2418 # Join ensures that the queue is empty _and_ events have been
2419 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002420 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002421 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002422 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002423 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002424 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002425 self.areAllNodeRequestsComplete() and
2426 all(self.eventQueuesEmpty())):
2427 # The queue empty check is placed at the end to
2428 # ensure that if a component adds an event between
2429 # when locked the run handler and checked that the
2430 # components were stable, we don't erroneously
2431 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002432 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002433 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002434 self.log.debug("...settled.")
2435 return
2436 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002437 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002438 self.sched.wake_event.wait(0.1)
2439
2440 def countJobResults(self, jobs, result):
2441 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002442 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002443
Monty Taylor0d926122017-05-24 08:07:56 -05002444 def getBuildByName(self, name):
2445 for build in self.builds:
2446 if build.name == name:
2447 return build
2448 raise Exception("Unable to find build %s" % name)
2449
James E. Blair96c6bf82016-01-15 16:20:40 -08002450 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002451 for job in self.history:
2452 if (job.name == name and
2453 (project is None or
2454 job.parameters['ZUUL_PROJECT'] == project)):
2455 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002456 raise Exception("Unable to find job %s in history" % name)
2457
2458 def assertEmptyQueues(self):
2459 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002460 for tenant in self.sched.abide.tenants.values():
2461 for pipeline in tenant.layout.pipelines.values():
2462 for queue in pipeline.queues:
2463 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002464 print('pipeline %s queue %s contents %s' % (
2465 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002466 self.assertEqual(len(queue.queue), 0,
2467 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002468
2469 def assertReportedStat(self, key, value=None, kind=None):
2470 start = time.time()
2471 while time.time() < (start + 5):
2472 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002473 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002474 if key == k:
2475 if value is None and kind is None:
2476 return
2477 elif value:
2478 if value == v:
2479 return
2480 elif kind:
2481 if v.endswith('|' + kind):
2482 return
2483 time.sleep(0.1)
2484
Clark Boylanb640e052014-04-03 16:41:46 -07002485 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002486
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002487 def assertBuilds(self, builds):
2488 """Assert that the running builds are as described.
2489
2490 The list of running builds is examined and must match exactly
2491 the list of builds described by the input.
2492
2493 :arg list builds: A list of dictionaries. Each item in the
2494 list must match the corresponding build in the build
2495 history, and each element of the dictionary must match the
2496 corresponding attribute of the build.
2497
2498 """
James E. Blair3158e282016-08-19 09:34:11 -07002499 try:
2500 self.assertEqual(len(self.builds), len(builds))
2501 for i, d in enumerate(builds):
2502 for k, v in d.items():
2503 self.assertEqual(
2504 getattr(self.builds[i], k), v,
2505 "Element %i in builds does not match" % (i,))
2506 except Exception:
2507 for build in self.builds:
2508 self.log.error("Running build: %s" % build)
2509 else:
2510 self.log.error("No running builds")
2511 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002512
James E. Blairb536ecc2016-08-31 10:11:42 -07002513 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002514 """Assert that the completed builds are as described.
2515
2516 The list of completed builds is examined and must match
2517 exactly the list of builds described by the input.
2518
2519 :arg list history: A list of dictionaries. Each item in the
2520 list must match the corresponding build in the build
2521 history, and each element of the dictionary must match the
2522 corresponding attribute of the build.
2523
James E. Blairb536ecc2016-08-31 10:11:42 -07002524 :arg bool ordered: If true, the history must match the order
2525 supplied, if false, the builds are permitted to have
2526 arrived in any order.
2527
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002528 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002529 def matches(history_item, item):
2530 for k, v in item.items():
2531 if getattr(history_item, k) != v:
2532 return False
2533 return True
James E. Blair3158e282016-08-19 09:34:11 -07002534 try:
2535 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002536 if ordered:
2537 for i, d in enumerate(history):
2538 if not matches(self.history[i], d):
2539 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002540 "Element %i in history does not match %s" %
2541 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002542 else:
2543 unseen = self.history[:]
2544 for i, d in enumerate(history):
2545 found = False
2546 for unseen_item in unseen:
2547 if matches(unseen_item, d):
2548 found = True
2549 unseen.remove(unseen_item)
2550 break
2551 if not found:
2552 raise Exception("No match found for element %i "
2553 "in history" % (i,))
2554 if unseen:
2555 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002556 except Exception:
2557 for build in self.history:
2558 self.log.error("Completed build: %s" % build)
2559 else:
2560 self.log.error("No completed builds")
2561 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002562
James E. Blair6ac368c2016-12-22 18:07:20 -08002563 def printHistory(self):
2564 """Log the build history.
2565
2566 This can be useful during tests to summarize what jobs have
2567 completed.
2568
2569 """
2570 self.log.debug("Build history:")
2571 for build in self.history:
2572 self.log.debug(build)
2573
James E. Blair59fdbac2015-12-07 17:08:06 -08002574 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002575 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2576
James E. Blair9ea70072017-04-19 16:05:30 -07002577 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002578 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002579 if not os.path.exists(root):
2580 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002581 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2582 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002583- tenant:
2584 name: openstack
2585 source:
2586 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002587 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002588 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002589 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002590 - org/project
2591 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002592 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002593 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002594 self.config.set('zuul', 'tenant_config',
2595 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002596 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002597
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002598 def addCommitToRepo(self, project, message, files,
2599 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002600 path = os.path.join(self.upstream_root, project)
2601 repo = git.Repo(path)
2602 repo.head.reference = branch
2603 zuul.merger.merger.reset_repo_to_head(repo)
2604 for fn, content in files.items():
2605 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002606 try:
2607 os.makedirs(os.path.dirname(fn))
2608 except OSError:
2609 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002610 with open(fn, 'w') as f:
2611 f.write(content)
2612 repo.index.add([fn])
2613 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002614 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002615 repo.heads[branch].commit = commit
2616 repo.head.reference = branch
2617 repo.git.clean('-x', '-f', '-d')
2618 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002619 if tag:
2620 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002621 return before
2622
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002623 def commitConfigUpdate(self, project_name, source_name):
2624 """Commit an update to zuul.yaml
2625
2626 This overwrites the zuul.yaml in the specificed project with
2627 the contents specified.
2628
2629 :arg str project_name: The name of the project containing
2630 zuul.yaml (e.g., common-config)
2631
2632 :arg str source_name: The path to the file (underneath the
2633 test fixture directory) whose contents should be used to
2634 replace zuul.yaml.
2635 """
2636
2637 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002638 files = {}
2639 with open(source_path, 'r') as f:
2640 data = f.read()
2641 layout = yaml.safe_load(data)
2642 files['zuul.yaml'] = data
2643 for item in layout:
2644 if 'job' in item:
2645 jobname = item['job']['name']
2646 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002647 before = self.addCommitToRepo(
2648 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002649 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002650 return before
2651
James E. Blair7fc8daa2016-08-08 15:37:15 -07002652 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002653
James E. Blair7fc8daa2016-08-08 15:37:15 -07002654 """Inject a Fake (Gerrit) event.
2655
2656 This method accepts a JSON-encoded event and simulates Zuul
2657 having received it from Gerrit. It could (and should)
2658 eventually apply to any connection type, but is currently only
2659 used with Gerrit connections. The name of the connection is
2660 used to look up the corresponding server, and the event is
2661 simulated as having been received by all Zuul connections
2662 attached to that server. So if two Gerrit connections in Zuul
2663 are connected to the same Gerrit server, and you invoke this
2664 method specifying the name of one of them, the event will be
2665 received by both.
2666
2667 .. note::
2668
2669 "self.fake_gerrit.addEvent" calls should be migrated to
2670 this method.
2671
2672 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002673 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002674 :arg str event: The JSON-encoded event.
2675
2676 """
2677 specified_conn = self.connections.connections[connection]
2678 for conn in self.connections.connections.values():
2679 if (isinstance(conn, specified_conn.__class__) and
2680 specified_conn.server == conn.server):
2681 conn.addEvent(event)
2682
James E. Blaird8af5422017-05-24 13:59:40 -07002683 def getUpstreamRepos(self, projects):
2684 """Return upstream git repo objects for the listed projects
2685
2686 :arg list projects: A list of strings, each the canonical name
2687 of a project.
2688
2689 :returns: A dictionary of {name: repo} for every listed
2690 project.
2691 :rtype: dict
2692
2693 """
2694
2695 repos = {}
2696 for project in projects:
2697 # FIXME(jeblair): the upstream root does not yet have a
2698 # hostname component; that needs to be added, and this
2699 # line removed:
2700 tmp_project_name = '/'.join(project.split('/')[1:])
2701 path = os.path.join(self.upstream_root, tmp_project_name)
2702 repo = git.Repo(path)
2703 repos[project] = repo
2704 return repos
2705
James E. Blair3f876d52016-07-22 13:07:14 -07002706
2707class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002708 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002709 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002710
Joshua Heskethd78b4482015-09-14 16:56:34 -06002711
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002712class SSLZuulTestCase(ZuulTestCase):
2713 """ZuulTestCase but with an but using SSL when possible"""
2714 use_ssl = True
2715
2716
Joshua Heskethd78b4482015-09-14 16:56:34 -06002717class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002718 def setup_config(self):
2719 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002720 for section_name in self.config.sections():
2721 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2722 section_name, re.I)
2723 if not con_match:
2724 continue
2725
2726 if self.config.get(section_name, 'driver') == 'sql':
2727 f = MySQLSchemaFixture()
2728 self.useFixture(f)
2729 if (self.config.get(section_name, 'dburi') ==
2730 '$MYSQL_FIXTURE_DBURI$'):
2731 self.config.set(section_name, 'dburi', f.dburi)