blob: 31d6f0d7d5ea1dc93c783e60622ad21767f7ffa7 [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 }
Jesse Keating61040e72017-06-08 15:08:27 -0700938 },
939 'files': pr.files
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200940 }
941 return data
942
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800943 def getPullBySha(self, sha):
944 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
945 if len(prs) > 1:
946 raise Exception('Multiple pulls found with head sha: %s' % sha)
947 pr = prs[0]
948 return self.getPull(pr.project, pr.number)
949
Jesse Keatingae4cd272017-01-30 17:10:44 -0800950 def _getPullReviews(self, owner, project, number):
951 pr = self.pull_requests[number - 1]
952 return pr.reviews
953
Jan Hruban3b415922016-02-03 13:10:22 +0100954 def getUser(self, login):
955 data = {
956 'username': login,
957 'name': 'Github User',
958 'email': 'github.user@example.com'
959 }
960 return data
961
Jesse Keatingae4cd272017-01-30 17:10:44 -0800962 def getRepoPermission(self, project, login):
963 owner, proj = project.split('/')
964 for pr in self.pull_requests:
965 pr_owner, pr_project = pr.project.split('/')
966 if (pr_owner == owner and proj == pr_project):
967 if login in pr.writers:
968 return 'write'
969 else:
970 return 'read'
971
Gregory Haynes4fc12542015-04-22 20:38:06 -0700972 def getGitUrl(self, project):
973 return os.path.join(self.upstream_root, str(project))
974
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200975 def real_getGitUrl(self, project):
976 return super(FakeGithubConnection, self).getGitUrl(project)
977
Gregory Haynes4fc12542015-04-22 20:38:06 -0700978 def getProjectBranches(self, project):
979 """Masks getProjectBranches since we don't have a real github"""
980
981 # just returns master for now
982 return ['master']
983
Jan Hrubane252a732017-01-03 15:03:09 +0100984 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700985 pull_request = self.pull_requests[pr_number - 1]
986 pull_request.addComment(message)
987
Jan Hruban3b415922016-02-03 13:10:22 +0100988 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100989 pull_request = self.pull_requests[pr_number - 1]
990 if self.merge_failure:
991 raise Exception('Pull request was not merged')
992 if self.merge_not_allowed_count > 0:
993 self.merge_not_allowed_count -= 1
994 raise MergeFailure('Merge was not successful due to mergeability'
995 ' conflict')
996 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100997 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100998
Jesse Keatingd96e5882017-01-19 13:55:50 -0800999 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001000 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001001
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001002 def setCommitStatus(self, project, sha, state, url='', description='',
1003 context='default', user='zuul'):
1004 # always insert a status to the front of the list, to represent
1005 # the last status provided for a commit.
1006 # Since we're bypassing github API, which would require a user, we
1007 # default the user as 'zuul' here.
1008 self.statuses.setdefault(project, {}).setdefault(sha, [])
1009 self.statuses[project][sha].insert(0, {
1010 'state': state,
1011 'url': url,
1012 'description': description,
1013 'context': context,
1014 'creator': {
1015 'login': user
1016 }
1017 })
Jan Hrubane252a732017-01-03 15:03:09 +01001018
Jan Hruban16ad31f2015-11-07 14:39:07 +01001019 def labelPull(self, project, pr_number, label):
1020 pull_request = self.pull_requests[pr_number - 1]
1021 pull_request.addLabel(label)
1022
1023 def unlabelPull(self, project, pr_number, label):
1024 pull_request = self.pull_requests[pr_number - 1]
1025 pull_request.removeLabel(label)
1026
Gregory Haynes4fc12542015-04-22 20:38:06 -07001027
Clark Boylanb640e052014-04-03 16:41:46 -07001028class BuildHistory(object):
1029 def __init__(self, **kw):
1030 self.__dict__.update(kw)
1031
1032 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001033 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1034 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001035
1036
Clark Boylanb640e052014-04-03 16:41:46 -07001037class FakeStatsd(threading.Thread):
1038 def __init__(self):
1039 threading.Thread.__init__(self)
1040 self.daemon = True
1041 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1042 self.sock.bind(('', 0))
1043 self.port = self.sock.getsockname()[1]
1044 self.wake_read, self.wake_write = os.pipe()
1045 self.stats = []
1046
1047 def run(self):
1048 while True:
1049 poll = select.poll()
1050 poll.register(self.sock, select.POLLIN)
1051 poll.register(self.wake_read, select.POLLIN)
1052 ret = poll.poll()
1053 for (fd, event) in ret:
1054 if fd == self.sock.fileno():
1055 data = self.sock.recvfrom(1024)
1056 if not data:
1057 return
1058 self.stats.append(data[0])
1059 if fd == self.wake_read:
1060 return
1061
1062 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001063 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001064
1065
James E. Blaire1767bc2016-08-02 10:00:27 -07001066class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001067 log = logging.getLogger("zuul.test")
1068
Paul Belanger174a8272017-03-14 13:20:10 -04001069 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001070 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001071 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001072 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001073 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001074 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001075 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001076 # TODOv3(jeblair): self.node is really "the label of the node
1077 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001078 # keep using it like this, or we may end up exposing more of
1079 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001080 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001081 self.node = None
1082 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001083 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001084 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001085 self.pipeline = self.parameters['ZUUL_PIPELINE']
1086 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001087 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001088 self.wait_condition = threading.Condition()
1089 self.waiting = False
1090 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001091 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001092 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001093 self.changes = None
1094 if 'ZUUL_CHANGE_IDS' in self.parameters:
1095 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001096
James E. Blair3158e282016-08-19 09:34:11 -07001097 def __repr__(self):
1098 waiting = ''
1099 if self.waiting:
1100 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001101 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1102 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001103
Clark Boylanb640e052014-04-03 16:41:46 -07001104 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001105 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001106 self.wait_condition.acquire()
1107 self.wait_condition.notify()
1108 self.waiting = False
1109 self.log.debug("Build %s released" % self.unique)
1110 self.wait_condition.release()
1111
1112 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001113 """Return whether this build is being held.
1114
1115 :returns: Whether the build is being held.
1116 :rtype: bool
1117 """
1118
Clark Boylanb640e052014-04-03 16:41:46 -07001119 self.wait_condition.acquire()
1120 if self.waiting:
1121 ret = True
1122 else:
1123 ret = False
1124 self.wait_condition.release()
1125 return ret
1126
1127 def _wait(self):
1128 self.wait_condition.acquire()
1129 self.waiting = True
1130 self.log.debug("Build %s waiting" % self.unique)
1131 self.wait_condition.wait()
1132 self.wait_condition.release()
1133
1134 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001135 self.log.debug('Running build %s' % self.unique)
1136
Paul Belanger174a8272017-03-14 13:20:10 -04001137 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.log.debug('Holding build %s' % self.unique)
1139 self._wait()
1140 self.log.debug("Build %s continuing" % self.unique)
1141
James E. Blair412fba82017-01-26 15:00:50 -08001142 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001143 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001144 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001145 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001146 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001147 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001148 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001149
James E. Blaire1767bc2016-08-02 10:00:27 -07001150 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001151
James E. Blaira5dba232016-08-08 15:53:24 -07001152 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001153 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001154 for change in changes:
1155 if self.hasChanges(change):
1156 return True
1157 return False
1158
James E. Blaire7b99a02016-08-05 14:27:34 -07001159 def hasChanges(self, *changes):
1160 """Return whether this build has certain changes in its git repos.
1161
1162 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001163 are expected to be present (in order) in the git repository of
1164 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001165
1166 :returns: Whether the build has the indicated changes.
1167 :rtype: bool
1168
1169 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001170 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001171 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001172 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001173 try:
1174 repo = git.Repo(path)
1175 except NoSuchPathError as e:
1176 self.log.debug('%s' % e)
1177 return False
1178 ref = self.parameters['ZUUL_REF']
1179 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1180 commit_message = '%s-1' % change.subject
1181 self.log.debug("Checking if build %s has changes; commit_message "
1182 "%s; repo_messages %s" % (self, commit_message,
1183 repo_messages))
1184 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001185 self.log.debug(" messages do not match")
1186 return False
1187 self.log.debug(" OK")
1188 return True
1189
James E. Blaird8af5422017-05-24 13:59:40 -07001190 def getWorkspaceRepos(self, projects):
1191 """Return workspace git repo objects for the listed projects
1192
1193 :arg list projects: A list of strings, each the canonical name
1194 of a project.
1195
1196 :returns: A dictionary of {name: repo} for every listed
1197 project.
1198 :rtype: dict
1199
1200 """
1201
1202 repos = {}
1203 for project in projects:
1204 path = os.path.join(self.jobdir.src_root, project)
1205 repo = git.Repo(path)
1206 repos[project] = repo
1207 return repos
1208
Clark Boylanb640e052014-04-03 16:41:46 -07001209
Paul Belanger174a8272017-03-14 13:20:10 -04001210class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1211 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001212
Paul Belanger174a8272017-03-14 13:20:10 -04001213 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001214 they will report that they have started but then pause until
1215 released before reporting completion. This attribute may be
1216 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001217 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001218 be explicitly released.
1219
1220 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001221 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001222 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001223 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001224 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001225 self.hold_jobs_in_build = False
1226 self.lock = threading.Lock()
1227 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001228 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001229 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001230 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001231
James E. Blaira5dba232016-08-08 15:53:24 -07001232 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001233 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001234
1235 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001236 :arg Change change: The :py:class:`~tests.base.FakeChange`
1237 instance which should cause the job to fail. This job
1238 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001239
1240 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001241 l = self.fail_tests.get(name, [])
1242 l.append(change)
1243 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001244
James E. Blair962220f2016-08-03 11:22:38 -07001245 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001246 """Release a held build.
1247
1248 :arg str regex: A regular expression which, if supplied, will
1249 cause only builds with matching names to be released. If
1250 not supplied, all builds will be released.
1251
1252 """
James E. Blair962220f2016-08-03 11:22:38 -07001253 builds = self.running_builds[:]
1254 self.log.debug("Releasing build %s (%s)" % (regex,
1255 len(self.running_builds)))
1256 for build in builds:
1257 if not regex or re.match(regex, build.name):
1258 self.log.debug("Releasing build %s" %
1259 (build.parameters['ZUUL_UUID']))
1260 build.release()
1261 else:
1262 self.log.debug("Not releasing build %s" %
1263 (build.parameters['ZUUL_UUID']))
1264 self.log.debug("Done releasing builds %s (%s)" %
1265 (regex, len(self.running_builds)))
1266
Paul Belanger174a8272017-03-14 13:20:10 -04001267 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001268 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001269 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001270 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001271 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001272 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001273 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001274 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001275 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1276 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001277
1278 def stopJob(self, job):
1279 self.log.debug("handle stop")
1280 parameters = json.loads(job.arguments)
1281 uuid = parameters['uuid']
1282 for build in self.running_builds:
1283 if build.unique == uuid:
1284 build.aborted = True
1285 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001286 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001287
James E. Blaira002b032017-04-18 10:35:48 -07001288 def stop(self):
1289 for build in self.running_builds:
1290 build.release()
1291 super(RecordingExecutorServer, self).stop()
1292
Joshua Hesketh50c21782016-10-13 21:34:14 +11001293
Paul Belanger174a8272017-03-14 13:20:10 -04001294class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001295 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001296 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001297 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001298 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001299 if not commit: # merge conflict
1300 self.recordResult('MERGER_FAILURE')
1301 return commit
1302
1303 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001304 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001305 self.executor_server.lock.acquire()
1306 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001307 BuildHistory(name=build.name, result=result, changes=build.changes,
1308 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001309 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 pipeline=build.parameters['ZUUL_PIPELINE'])
1311 )
Paul Belanger174a8272017-03-14 13:20:10 -04001312 self.executor_server.running_builds.remove(build)
1313 del self.executor_server.job_builds[self.job.unique]
1314 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001315
1316 def runPlaybooks(self, args):
1317 build = self.executor_server.job_builds[self.job.unique]
1318 build.jobdir = self.jobdir
1319
1320 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1321 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001322 return result
1323
Monty Taylore6562aa2017-02-20 07:37:39 -05001324 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001325 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001326
Paul Belanger174a8272017-03-14 13:20:10 -04001327 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001328 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001329 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001330 else:
1331 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001332 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001333
James E. Blairad8dca02017-02-21 11:48:32 -05001334 def getHostList(self, args):
1335 self.log.debug("hostlist")
1336 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001337 for host in hosts:
1338 host['host_vars']['ansible_connection'] = 'local'
1339
1340 hosts.append(dict(
1341 name='localhost',
1342 host_vars=dict(ansible_connection='local'),
1343 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001344 return hosts
1345
James E. Blairf5dbd002015-12-23 15:26:17 -08001346
Clark Boylanb640e052014-04-03 16:41:46 -07001347class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001348 """A Gearman server for use in tests.
1349
1350 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1351 added to the queue but will not be distributed to workers
1352 until released. This attribute may be changed at any time and
1353 will take effect for subsequently enqueued jobs, but
1354 previously held jobs will still need to be explicitly
1355 released.
1356
1357 """
1358
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001359 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001360 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001361 if use_ssl:
1362 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1363 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1364 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1365 else:
1366 ssl_ca = None
1367 ssl_cert = None
1368 ssl_key = None
1369
1370 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1371 ssl_cert=ssl_cert,
1372 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001373
1374 def getJobForConnection(self, connection, peek=False):
1375 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1376 for job in queue:
1377 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001378 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001379 job.waiting = self.hold_jobs_in_queue
1380 else:
1381 job.waiting = False
1382 if job.waiting:
1383 continue
1384 if job.name in connection.functions:
1385 if not peek:
1386 queue.remove(job)
1387 connection.related_jobs[job.handle] = job
1388 job.worker_connection = connection
1389 job.running = True
1390 return job
1391 return None
1392
1393 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001394 """Release a held job.
1395
1396 :arg str regex: A regular expression which, if supplied, will
1397 cause only jobs with matching names to be released. If
1398 not supplied, all jobs will be released.
1399 """
Clark Boylanb640e052014-04-03 16:41:46 -07001400 released = False
1401 qlen = (len(self.high_queue) + len(self.normal_queue) +
1402 len(self.low_queue))
1403 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1404 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001405 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001406 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001407 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001408 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001409 self.log.debug("releasing queued job %s" %
1410 job.unique)
1411 job.waiting = False
1412 released = True
1413 else:
1414 self.log.debug("not releasing queued job %s" %
1415 job.unique)
1416 if released:
1417 self.wakeConnections()
1418 qlen = (len(self.high_queue) + len(self.normal_queue) +
1419 len(self.low_queue))
1420 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1421
1422
1423class FakeSMTP(object):
1424 log = logging.getLogger('zuul.FakeSMTP')
1425
1426 def __init__(self, messages, server, port):
1427 self.server = server
1428 self.port = port
1429 self.messages = messages
1430
1431 def sendmail(self, from_email, to_email, msg):
1432 self.log.info("Sending email from %s, to %s, with msg %s" % (
1433 from_email, to_email, msg))
1434
1435 headers = msg.split('\n\n', 1)[0]
1436 body = msg.split('\n\n', 1)[1]
1437
1438 self.messages.append(dict(
1439 from_email=from_email,
1440 to_email=to_email,
1441 msg=msg,
1442 headers=headers,
1443 body=body,
1444 ))
1445
1446 return True
1447
1448 def quit(self):
1449 return True
1450
1451
James E. Blairdce6cea2016-12-20 16:45:32 -08001452class FakeNodepool(object):
1453 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001454 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001455
1456 log = logging.getLogger("zuul.test.FakeNodepool")
1457
1458 def __init__(self, host, port, chroot):
1459 self.client = kazoo.client.KazooClient(
1460 hosts='%s:%s%s' % (host, port, chroot))
1461 self.client.start()
1462 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001463 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001464 self.thread = threading.Thread(target=self.run)
1465 self.thread.daemon = True
1466 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001467 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001468
1469 def stop(self):
1470 self._running = False
1471 self.thread.join()
1472 self.client.stop()
1473 self.client.close()
1474
1475 def run(self):
1476 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001477 try:
1478 self._run()
1479 except Exception:
1480 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001481 time.sleep(0.1)
1482
1483 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001484 if self.paused:
1485 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001486 for req in self.getNodeRequests():
1487 self.fulfillRequest(req)
1488
1489 def getNodeRequests(self):
1490 try:
1491 reqids = self.client.get_children(self.REQUEST_ROOT)
1492 except kazoo.exceptions.NoNodeError:
1493 return []
1494 reqs = []
1495 for oid in sorted(reqids):
1496 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001497 try:
1498 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001499 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001500 data['_oid'] = oid
1501 reqs.append(data)
1502 except kazoo.exceptions.NoNodeError:
1503 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001504 return reqs
1505
James E. Blaire18d4602017-01-05 11:17:28 -08001506 def getNodes(self):
1507 try:
1508 nodeids = self.client.get_children(self.NODE_ROOT)
1509 except kazoo.exceptions.NoNodeError:
1510 return []
1511 nodes = []
1512 for oid in sorted(nodeids):
1513 path = self.NODE_ROOT + '/' + oid
1514 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001515 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001516 data['_oid'] = oid
1517 try:
1518 lockfiles = self.client.get_children(path + '/lock')
1519 except kazoo.exceptions.NoNodeError:
1520 lockfiles = []
1521 if lockfiles:
1522 data['_lock'] = True
1523 else:
1524 data['_lock'] = False
1525 nodes.append(data)
1526 return nodes
1527
James E. Blaira38c28e2017-01-04 10:33:20 -08001528 def makeNode(self, request_id, node_type):
1529 now = time.time()
1530 path = '/nodepool/nodes/'
1531 data = dict(type=node_type,
1532 provider='test-provider',
1533 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001534 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001535 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001536 public_ipv4='127.0.0.1',
1537 private_ipv4=None,
1538 public_ipv6=None,
1539 allocated_to=request_id,
1540 state='ready',
1541 state_time=now,
1542 created_time=now,
1543 updated_time=now,
1544 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001545 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001546 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001547 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001548 path = self.client.create(path, data,
1549 makepath=True,
1550 sequence=True)
1551 nodeid = path.split("/")[-1]
1552 return nodeid
1553
James E. Blair6ab79e02017-01-06 10:10:17 -08001554 def addFailRequest(self, request):
1555 self.fail_requests.add(request['_oid'])
1556
James E. Blairdce6cea2016-12-20 16:45:32 -08001557 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001558 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001559 return
1560 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001561 oid = request['_oid']
1562 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001563
James E. Blair6ab79e02017-01-06 10:10:17 -08001564 if oid in self.fail_requests:
1565 request['state'] = 'failed'
1566 else:
1567 request['state'] = 'fulfilled'
1568 nodes = []
1569 for node in request['node_types']:
1570 nodeid = self.makeNode(oid, node)
1571 nodes.append(nodeid)
1572 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001573
James E. Blaira38c28e2017-01-04 10:33:20 -08001574 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001575 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001576 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001577 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001578 try:
1579 self.client.set(path, data)
1580 except kazoo.exceptions.NoNodeError:
1581 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001582
1583
James E. Blair498059b2016-12-20 13:50:13 -08001584class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001585 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001586 super(ChrootedKazooFixture, self).__init__()
1587
1588 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1589 if ':' in zk_host:
1590 host, port = zk_host.split(':')
1591 else:
1592 host = zk_host
1593 port = None
1594
1595 self.zookeeper_host = host
1596
1597 if not port:
1598 self.zookeeper_port = 2181
1599 else:
1600 self.zookeeper_port = int(port)
1601
Clark Boylan621ec9a2017-04-07 17:41:33 -07001602 self.test_id = test_id
1603
James E. Blair498059b2016-12-20 13:50:13 -08001604 def _setUp(self):
1605 # Make sure the test chroot paths do not conflict
1606 random_bits = ''.join(random.choice(string.ascii_lowercase +
1607 string.ascii_uppercase)
1608 for x in range(8))
1609
Clark Boylan621ec9a2017-04-07 17:41:33 -07001610 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001611 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1612
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001613 self.addCleanup(self._cleanup)
1614
James E. Blair498059b2016-12-20 13:50:13 -08001615 # Ensure the chroot path exists and clean up any pre-existing znodes.
1616 _tmp_client = kazoo.client.KazooClient(
1617 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1618 _tmp_client.start()
1619
1620 if _tmp_client.exists(self.zookeeper_chroot):
1621 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1622
1623 _tmp_client.ensure_path(self.zookeeper_chroot)
1624 _tmp_client.stop()
1625 _tmp_client.close()
1626
James E. Blair498059b2016-12-20 13:50:13 -08001627 def _cleanup(self):
1628 '''Remove the chroot path.'''
1629 # Need a non-chroot'ed client to remove the chroot path
1630 _tmp_client = kazoo.client.KazooClient(
1631 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1632 _tmp_client.start()
1633 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1634 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001635 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001636
1637
Joshua Heskethd78b4482015-09-14 16:56:34 -06001638class MySQLSchemaFixture(fixtures.Fixture):
1639 def setUp(self):
1640 super(MySQLSchemaFixture, self).setUp()
1641
1642 random_bits = ''.join(random.choice(string.ascii_lowercase +
1643 string.ascii_uppercase)
1644 for x in range(8))
1645 self.name = '%s_%s' % (random_bits, os.getpid())
1646 self.passwd = uuid.uuid4().hex
1647 db = pymysql.connect(host="localhost",
1648 user="openstack_citest",
1649 passwd="openstack_citest",
1650 db="openstack_citest")
1651 cur = db.cursor()
1652 cur.execute("create database %s" % self.name)
1653 cur.execute(
1654 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1655 (self.name, self.name, self.passwd))
1656 cur.execute("flush privileges")
1657
1658 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1659 self.passwd,
1660 self.name)
1661 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1662 self.addCleanup(self.cleanup)
1663
1664 def cleanup(self):
1665 db = pymysql.connect(host="localhost",
1666 user="openstack_citest",
1667 passwd="openstack_citest",
1668 db="openstack_citest")
1669 cur = db.cursor()
1670 cur.execute("drop database %s" % self.name)
1671 cur.execute("drop user '%s'@'localhost'" % self.name)
1672 cur.execute("flush privileges")
1673
1674
Maru Newby3fe5f852015-01-13 04:22:14 +00001675class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001676 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001677 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001678
James E. Blair1c236df2017-02-01 14:07:24 -08001679 def attachLogs(self, *args):
1680 def reader():
1681 self._log_stream.seek(0)
1682 while True:
1683 x = self._log_stream.read(4096)
1684 if not x:
1685 break
1686 yield x.encode('utf8')
1687 content = testtools.content.content_from_reader(
1688 reader,
1689 testtools.content_type.UTF8_TEXT,
1690 False)
1691 self.addDetail('logging', content)
1692
Clark Boylanb640e052014-04-03 16:41:46 -07001693 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001694 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001695 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1696 try:
1697 test_timeout = int(test_timeout)
1698 except ValueError:
1699 # If timeout value is invalid do not set a timeout.
1700 test_timeout = 0
1701 if test_timeout > 0:
1702 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1703
1704 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1705 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1706 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1707 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1708 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1709 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1710 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1711 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1712 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1713 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001714 self._log_stream = StringIO()
1715 self.addOnException(self.attachLogs)
1716 else:
1717 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001718
James E. Blair73b41772017-05-22 13:22:55 -07001719 # NOTE(jeblair): this is temporary extra debugging to try to
1720 # track down a possible leak.
1721 orig_git_repo_init = git.Repo.__init__
1722
1723 def git_repo_init(myself, *args, **kw):
1724 orig_git_repo_init(myself, *args, **kw)
1725 self.log.debug("Created git repo 0x%x %s" %
1726 (id(myself), repr(myself)))
1727
1728 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1729 git_repo_init))
1730
James E. Blair1c236df2017-02-01 14:07:24 -08001731 handler = logging.StreamHandler(self._log_stream)
1732 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1733 '%(levelname)-8s %(message)s')
1734 handler.setFormatter(formatter)
1735
1736 logger = logging.getLogger()
1737 logger.setLevel(logging.DEBUG)
1738 logger.addHandler(handler)
1739
Clark Boylan3410d532017-04-25 12:35:29 -07001740 # Make sure we don't carry old handlers around in process state
1741 # which slows down test runs
1742 self.addCleanup(logger.removeHandler, handler)
1743 self.addCleanup(handler.close)
1744 self.addCleanup(handler.flush)
1745
James E. Blair1c236df2017-02-01 14:07:24 -08001746 # NOTE(notmorgan): Extract logging overrides for specific
1747 # libraries from the OS_LOG_DEFAULTS env and create loggers
1748 # for each. This is used to limit the output during test runs
1749 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001750 log_defaults_from_env = os.environ.get(
1751 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001752 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001753
James E. Blairdce6cea2016-12-20 16:45:32 -08001754 if log_defaults_from_env:
1755 for default in log_defaults_from_env.split(','):
1756 try:
1757 name, level_str = default.split('=', 1)
1758 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001759 logger = logging.getLogger(name)
1760 logger.setLevel(level)
1761 logger.addHandler(handler)
1762 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001763 except ValueError:
1764 # NOTE(notmorgan): Invalid format of the log default,
1765 # skip and don't try and apply a logger for the
1766 # specified module
1767 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001768
Maru Newby3fe5f852015-01-13 04:22:14 +00001769
1770class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001771 """A test case with a functioning Zuul.
1772
1773 The following class variables are used during test setup and can
1774 be overidden by subclasses but are effectively read-only once a
1775 test method starts running:
1776
1777 :cvar str config_file: This points to the main zuul config file
1778 within the fixtures directory. Subclasses may override this
1779 to obtain a different behavior.
1780
1781 :cvar str tenant_config_file: This is the tenant config file
1782 (which specifies from what git repos the configuration should
1783 be loaded). It defaults to the value specified in
1784 `config_file` but can be overidden by subclasses to obtain a
1785 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001786 configuration. See also the :py:func:`simple_layout`
1787 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001788
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001789 :cvar bool create_project_keys: Indicates whether Zuul should
1790 auto-generate keys for each project, or whether the test
1791 infrastructure should insert dummy keys to save time during
1792 startup. Defaults to False.
1793
James E. Blaire7b99a02016-08-05 14:27:34 -07001794 The following are instance variables that are useful within test
1795 methods:
1796
1797 :ivar FakeGerritConnection fake_<connection>:
1798 A :py:class:`~tests.base.FakeGerritConnection` will be
1799 instantiated for each connection present in the config file
1800 and stored here. For instance, `fake_gerrit` will hold the
1801 FakeGerritConnection object for a connection named `gerrit`.
1802
1803 :ivar FakeGearmanServer gearman_server: An instance of
1804 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1805 server that all of the Zuul components in this test use to
1806 communicate with each other.
1807
Paul Belanger174a8272017-03-14 13:20:10 -04001808 :ivar RecordingExecutorServer executor_server: An instance of
1809 :py:class:`~tests.base.RecordingExecutorServer` which is the
1810 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001811
1812 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1813 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001814 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001815 list upon completion.
1816
1817 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1818 objects representing completed builds. They are appended to
1819 the list in the order they complete.
1820
1821 """
1822
James E. Blair83005782015-12-11 14:46:03 -08001823 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001824 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001825 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001826 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001827
1828 def _startMerger(self):
1829 self.merge_server = zuul.merger.server.MergeServer(self.config,
1830 self.connections)
1831 self.merge_server.start()
1832
Maru Newby3fe5f852015-01-13 04:22:14 +00001833 def setUp(self):
1834 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001835
1836 self.setupZK()
1837
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001838 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001839 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001840 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1841 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001842 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001843 tmp_root = tempfile.mkdtemp(
1844 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001845 self.test_root = os.path.join(tmp_root, "zuul-test")
1846 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001847 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001848 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001849 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001850
1851 if os.path.exists(self.test_root):
1852 shutil.rmtree(self.test_root)
1853 os.makedirs(self.test_root)
1854 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001855 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001856
1857 # Make per test copy of Configuration.
1858 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001859 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1860 if not os.path.exists(self.private_key_file):
1861 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1862 shutil.copy(src_private_key_file, self.private_key_file)
1863 shutil.copy('{}.pub'.format(src_private_key_file),
1864 '{}.pub'.format(self.private_key_file))
1865 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001866 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001867 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001868 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001869 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001870 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001871 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001872 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001873
Clark Boylanb640e052014-04-03 16:41:46 -07001874 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001875 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1876 # see: https://github.com/jsocol/pystatsd/issues/61
1877 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001878 os.environ['STATSD_PORT'] = str(self.statsd.port)
1879 self.statsd.start()
1880 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001881 reload_module(statsd)
1882 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001883
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001884 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001885
1886 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001887 self.log.info("Gearman server on port %s" %
1888 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001889 if self.use_ssl:
1890 self.log.info('SSL enabled for gearman')
1891 self.config.set(
1892 'gearman', 'ssl_ca',
1893 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1894 self.config.set(
1895 'gearman', 'ssl_cert',
1896 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1897 self.config.set(
1898 'gearman', 'ssl_key',
1899 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001900
James E. Blaire511d2f2016-12-08 15:22:26 -08001901 gerritsource.GerritSource.replication_timeout = 1.5
1902 gerritsource.GerritSource.replication_retry_interval = 0.5
1903 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001904
Joshua Hesketh352264b2015-08-11 23:42:08 +10001905 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001906
Jan Hruban7083edd2015-08-21 14:00:54 +02001907 self.webapp = zuul.webapp.WebApp(
1908 self.sched, port=0, listen_address='127.0.0.1')
1909
Jan Hruban6b71aff2015-10-22 16:58:08 +02001910 self.event_queues = [
1911 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001912 self.sched.trigger_event_queue,
1913 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001914 ]
1915
James E. Blairfef78942016-03-11 16:28:56 -08001916 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001917 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001918
Paul Belanger174a8272017-03-14 13:20:10 -04001919 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001920 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001921 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001922 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001923 _test_root=self.test_root,
1924 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001925 self.executor_server.start()
1926 self.history = self.executor_server.build_history
1927 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001928
Paul Belanger174a8272017-03-14 13:20:10 -04001929 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001930 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001931 self.merge_client = zuul.merger.client.MergeClient(
1932 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001933 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001934 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001935 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001936
James E. Blair0d5a36e2017-02-21 10:53:44 -05001937 self.fake_nodepool = FakeNodepool(
1938 self.zk_chroot_fixture.zookeeper_host,
1939 self.zk_chroot_fixture.zookeeper_port,
1940 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001941
Paul Belanger174a8272017-03-14 13:20:10 -04001942 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001943 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001944 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001945 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001946
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001947 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001948
1949 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001950 self.webapp.start()
1951 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001952 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001953 # Cleanups are run in reverse order
1954 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001955 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001956 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001957
James E. Blairb9c0d772017-03-03 14:34:49 -08001958 self.sched.reconfigure(self.config)
1959 self.sched.resume()
1960
Tobias Henkel7df274b2017-05-26 17:41:11 +02001961 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001962 # Set up gerrit related fakes
1963 # Set a changes database so multiple FakeGerrit's can report back to
1964 # a virtual canonical database given by the configured hostname
1965 self.gerrit_changes_dbs = {}
1966
1967 def getGerritConnection(driver, name, config):
1968 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1969 con = FakeGerritConnection(driver, name, config,
1970 changes_db=db,
1971 upstream_root=self.upstream_root)
1972 self.event_queues.append(con.event_queue)
1973 setattr(self, 'fake_' + name, con)
1974 return con
1975
1976 self.useFixture(fixtures.MonkeyPatch(
1977 'zuul.driver.gerrit.GerritDriver.getConnection',
1978 getGerritConnection))
1979
Gregory Haynes4fc12542015-04-22 20:38:06 -07001980 def getGithubConnection(driver, name, config):
1981 con = FakeGithubConnection(driver, name, config,
1982 upstream_root=self.upstream_root)
1983 setattr(self, 'fake_' + name, con)
1984 return con
1985
1986 self.useFixture(fixtures.MonkeyPatch(
1987 'zuul.driver.github.GithubDriver.getConnection',
1988 getGithubConnection))
1989
James E. Blaire511d2f2016-12-08 15:22:26 -08001990 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001991 # TODO(jhesketh): This should come from lib.connections for better
1992 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001993 # Register connections from the config
1994 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001995
Joshua Hesketh352264b2015-08-11 23:42:08 +10001996 def FakeSMTPFactory(*args, **kw):
1997 args = [self.smtp_messages] + list(args)
1998 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001999
Joshua Hesketh352264b2015-08-11 23:42:08 +10002000 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002001
James E. Blaire511d2f2016-12-08 15:22:26 -08002002 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002003 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002004 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002005
James E. Blair83005782015-12-11 14:46:03 -08002006 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002007 # This creates the per-test configuration object. It can be
2008 # overriden by subclasses, but should not need to be since it
2009 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002010 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002011 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002012
2013 if not self.setupSimpleLayout():
2014 if hasattr(self, 'tenant_config_file'):
2015 self.config.set('zuul', 'tenant_config',
2016 self.tenant_config_file)
2017 git_path = os.path.join(
2018 os.path.dirname(
2019 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2020 'git')
2021 if os.path.exists(git_path):
2022 for reponame in os.listdir(git_path):
2023 project = reponame.replace('_', '/')
2024 self.copyDirToRepo(project,
2025 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002026 self.setupAllProjectKeys()
2027
James E. Blair06cc3922017-04-19 10:08:10 -07002028 def setupSimpleLayout(self):
2029 # If the test method has been decorated with a simple_layout,
2030 # use that instead of the class tenant_config_file. Set up a
2031 # single config-project with the specified layout, and
2032 # initialize repos for all of the 'project' entries which
2033 # appear in the layout.
2034 test_name = self.id().split('.')[-1]
2035 test = getattr(self, test_name)
2036 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002037 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002038 else:
2039 return False
2040
James E. Blairb70e55a2017-04-19 12:57:02 -07002041 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002042 path = os.path.join(FIXTURE_DIR, path)
2043 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002044 data = f.read()
2045 layout = yaml.safe_load(data)
2046 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002047 untrusted_projects = []
2048 for item in layout:
2049 if 'project' in item:
2050 name = item['project']['name']
2051 untrusted_projects.append(name)
2052 self.init_repo(name)
2053 self.addCommitToRepo(name, 'initial commit',
2054 files={'README': ''},
2055 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002056 if 'job' in item:
2057 jobname = item['job']['name']
2058 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002059
2060 root = os.path.join(self.test_root, "config")
2061 if not os.path.exists(root):
2062 os.makedirs(root)
2063 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2064 config = [{'tenant':
2065 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002066 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002067 {'config-projects': ['common-config'],
2068 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002069 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002070 f.close()
2071 self.config.set('zuul', 'tenant_config',
2072 os.path.join(FIXTURE_DIR, f.name))
2073
2074 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002075 self.addCommitToRepo('common-config', 'add content from fixture',
2076 files, branch='master', tag='init')
2077
2078 return True
2079
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002080 def setupAllProjectKeys(self):
2081 if self.create_project_keys:
2082 return
2083
2084 path = self.config.get('zuul', 'tenant_config')
2085 with open(os.path.join(FIXTURE_DIR, path)) as f:
2086 tenant_config = yaml.safe_load(f.read())
2087 for tenant in tenant_config:
2088 sources = tenant['tenant']['source']
2089 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002090 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002091 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002092 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002093 self.setupProjectKeys(source, project)
2094
2095 def setupProjectKeys(self, source, project):
2096 # Make sure we set up an RSA key for the project so that we
2097 # don't spend time generating one:
2098
2099 key_root = os.path.join(self.state_root, 'keys')
2100 if not os.path.isdir(key_root):
2101 os.mkdir(key_root, 0o700)
2102 private_key_file = os.path.join(key_root, source, project + '.pem')
2103 private_key_dir = os.path.dirname(private_key_file)
2104 self.log.debug("Installing test keys for project %s at %s" % (
2105 project, private_key_file))
2106 if not os.path.isdir(private_key_dir):
2107 os.makedirs(private_key_dir)
2108 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2109 with open(private_key_file, 'w') as o:
2110 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002111
James E. Blair498059b2016-12-20 13:50:13 -08002112 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002113 self.zk_chroot_fixture = self.useFixture(
2114 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002115 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002116 self.zk_chroot_fixture.zookeeper_host,
2117 self.zk_chroot_fixture.zookeeper_port,
2118 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002119
James E. Blair96c6bf82016-01-15 16:20:40 -08002120 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002121 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002122
2123 files = {}
2124 for (dirpath, dirnames, filenames) in os.walk(source_path):
2125 for filename in filenames:
2126 test_tree_filepath = os.path.join(dirpath, filename)
2127 common_path = os.path.commonprefix([test_tree_filepath,
2128 source_path])
2129 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2130 with open(test_tree_filepath, 'r') as f:
2131 content = f.read()
2132 files[relative_filepath] = content
2133 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002134 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002135
James E. Blaire18d4602017-01-05 11:17:28 -08002136 def assertNodepoolState(self):
2137 # Make sure that there are no pending requests
2138
2139 requests = self.fake_nodepool.getNodeRequests()
2140 self.assertEqual(len(requests), 0)
2141
2142 nodes = self.fake_nodepool.getNodes()
2143 for node in nodes:
2144 self.assertFalse(node['_lock'], "Node %s is locked" %
2145 (node['_oid'],))
2146
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002147 def assertNoGeneratedKeys(self):
2148 # Make sure that Zuul did not generate any project keys
2149 # (unless it was supposed to).
2150
2151 if self.create_project_keys:
2152 return
2153
2154 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2155 test_key = i.read()
2156
2157 key_root = os.path.join(self.state_root, 'keys')
2158 for root, dirname, files in os.walk(key_root):
2159 for fn in files:
2160 with open(os.path.join(root, fn)) as f:
2161 self.assertEqual(test_key, f.read())
2162
Clark Boylanb640e052014-04-03 16:41:46 -07002163 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002164 self.log.debug("Assert final state")
2165 # Make sure no jobs are running
2166 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002167 # Make sure that git.Repo objects have been garbage collected.
2168 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002169 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002170 gc.collect()
2171 for obj in gc.get_objects():
2172 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002173 self.log.debug("Leaked git repo object: 0x%x %s" %
2174 (id(obj), repr(obj)))
2175 for ref in gc.get_referrers(obj):
2176 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002177 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002178 if repos:
2179 for obj in gc.garbage:
2180 self.log.debug(" Garbage %s" % (repr(obj)))
2181 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002182 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002183 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002184 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002185 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002186 for tenant in self.sched.abide.tenants.values():
2187 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002188 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002189 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002190
2191 def shutdown(self):
2192 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002193 self.executor_server.hold_jobs_in_build = False
2194 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002195 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002196 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002197 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002198 self.sched.stop()
2199 self.sched.join()
2200 self.statsd.stop()
2201 self.statsd.join()
2202 self.webapp.stop()
2203 self.webapp.join()
2204 self.rpc.stop()
2205 self.rpc.join()
2206 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002207 self.fake_nodepool.stop()
2208 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002209 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002210 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002211 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002212 # Further the pydevd threads also need to be whitelisted so debugging
2213 # e.g. in PyCharm is possible without breaking shutdown.
2214 whitelist = ['executor-watchdog',
2215 'pydevd.CommandThread',
2216 'pydevd.Reader',
2217 'pydevd.Writer',
2218 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002219 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002220 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002221 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002222 log_str = ""
2223 for thread_id, stack_frame in sys._current_frames().items():
2224 log_str += "Thread: %s\n" % thread_id
2225 log_str += "".join(traceback.format_stack(stack_frame))
2226 self.log.debug(log_str)
2227 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002228
James E. Blaira002b032017-04-18 10:35:48 -07002229 def assertCleanShutdown(self):
2230 pass
2231
James E. Blairc4ba97a2017-04-19 16:26:24 -07002232 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002233 parts = project.split('/')
2234 path = os.path.join(self.upstream_root, *parts[:-1])
2235 if not os.path.exists(path):
2236 os.makedirs(path)
2237 path = os.path.join(self.upstream_root, project)
2238 repo = git.Repo.init(path)
2239
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002240 with repo.config_writer() as config_writer:
2241 config_writer.set_value('user', 'email', 'user@example.com')
2242 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002243
Clark Boylanb640e052014-04-03 16:41:46 -07002244 repo.index.commit('initial commit')
2245 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002246 if tag:
2247 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002248
James E. Blair97d902e2014-08-21 13:25:56 -07002249 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002250 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002251 repo.git.clean('-x', '-f', '-d')
2252
James E. Blair97d902e2014-08-21 13:25:56 -07002253 def create_branch(self, project, branch):
2254 path = os.path.join(self.upstream_root, project)
2255 repo = git.Repo.init(path)
2256 fn = os.path.join(path, 'README')
2257
2258 branch_head = repo.create_head(branch)
2259 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002260 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002261 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002262 f.close()
2263 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002264 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002265
James E. Blair97d902e2014-08-21 13:25:56 -07002266 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002267 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002268 repo.git.clean('-x', '-f', '-d')
2269
Sachi King9f16d522016-03-16 12:20:45 +11002270 def create_commit(self, project):
2271 path = os.path.join(self.upstream_root, project)
2272 repo = git.Repo(path)
2273 repo.head.reference = repo.heads['master']
2274 file_name = os.path.join(path, 'README')
2275 with open(file_name, 'a') as f:
2276 f.write('creating fake commit\n')
2277 repo.index.add([file_name])
2278 commit = repo.index.commit('Creating a fake commit')
2279 return commit.hexsha
2280
James E. Blairf4a5f022017-04-18 14:01:10 -07002281 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002282 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002283 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002284 while len(self.builds):
2285 self.release(self.builds[0])
2286 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002287 i += 1
2288 if count is not None and i >= count:
2289 break
James E. Blairb8c16472015-05-05 14:55:26 -07002290
Clark Boylanb640e052014-04-03 16:41:46 -07002291 def release(self, job):
2292 if isinstance(job, FakeBuild):
2293 job.release()
2294 else:
2295 job.waiting = False
2296 self.log.debug("Queued job %s released" % job.unique)
2297 self.gearman_server.wakeConnections()
2298
2299 def getParameter(self, job, name):
2300 if isinstance(job, FakeBuild):
2301 return job.parameters[name]
2302 else:
2303 parameters = json.loads(job.arguments)
2304 return parameters[name]
2305
Clark Boylanb640e052014-04-03 16:41:46 -07002306 def haveAllBuildsReported(self):
2307 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002308 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002309 return False
2310 # Find out if every build that the worker has completed has been
2311 # reported back to Zuul. If it hasn't then that means a Gearman
2312 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002313 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002314 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002315 if not zbuild:
2316 # It has already been reported
2317 continue
2318 # It hasn't been reported yet.
2319 return False
2320 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002321 worker = self.executor_server.executor_worker
2322 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002323 if connection.state == 'GRAB_WAIT':
2324 return False
2325 return True
2326
2327 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002328 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002329 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002330 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002331 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002332 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002333 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002334 for j in conn.related_jobs.values():
2335 if j.unique == build.uuid:
2336 client_job = j
2337 break
2338 if not client_job:
2339 self.log.debug("%s is not known to the gearman client" %
2340 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002341 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002342 if not client_job.handle:
2343 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002344 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002345 server_job = self.gearman_server.jobs.get(client_job.handle)
2346 if not server_job:
2347 self.log.debug("%s is not known to the gearman server" %
2348 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002349 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002350 if not hasattr(server_job, 'waiting'):
2351 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002352 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002353 if server_job.waiting:
2354 continue
James E. Blair17302972016-08-10 16:11:42 -07002355 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002356 self.log.debug("%s has not reported start" % build)
2357 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002358 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002359 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002360 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002361 if worker_build:
2362 if worker_build.isWaiting():
2363 continue
2364 else:
2365 self.log.debug("%s is running" % worker_build)
2366 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002367 else:
James E. Blair962220f2016-08-03 11:22:38 -07002368 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002369 return False
James E. Blaira002b032017-04-18 10:35:48 -07002370 for (build_uuid, job_worker) in \
2371 self.executor_server.job_workers.items():
2372 if build_uuid not in seen_builds:
2373 self.log.debug("%s is not finalized" % build_uuid)
2374 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002375 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002376
James E. Blairdce6cea2016-12-20 16:45:32 -08002377 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002378 if self.fake_nodepool.paused:
2379 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002380 if self.sched.nodepool.requests:
2381 return False
2382 return True
2383
Jan Hruban6b71aff2015-10-22 16:58:08 +02002384 def eventQueuesEmpty(self):
2385 for queue in self.event_queues:
2386 yield queue.empty()
2387
2388 def eventQueuesJoin(self):
2389 for queue in self.event_queues:
2390 queue.join()
2391
Clark Boylanb640e052014-04-03 16:41:46 -07002392 def waitUntilSettled(self):
2393 self.log.debug("Waiting until settled...")
2394 start = time.time()
2395 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002396 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002397 self.log.error("Timeout waiting for Zuul to settle")
2398 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002399 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002400 self.log.error(" %s: %s" % (queue, queue.empty()))
2401 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002402 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002403 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002404 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002405 self.log.error("All requests completed: %s" %
2406 (self.areAllNodeRequestsComplete(),))
2407 self.log.error("Merge client jobs: %s" %
2408 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002409 raise Exception("Timeout waiting for Zuul to settle")
2410 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002411
Paul Belanger174a8272017-03-14 13:20:10 -04002412 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002413 # have all build states propogated to zuul?
2414 if self.haveAllBuildsReported():
2415 # Join ensures that the queue is empty _and_ events have been
2416 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002417 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002418 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002419 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002420 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002421 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002422 self.areAllNodeRequestsComplete() and
2423 all(self.eventQueuesEmpty())):
2424 # The queue empty check is placed at the end to
2425 # ensure that if a component adds an event between
2426 # when locked the run handler and checked that the
2427 # components were stable, we don't erroneously
2428 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002429 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002430 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002431 self.log.debug("...settled.")
2432 return
2433 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002434 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002435 self.sched.wake_event.wait(0.1)
2436
2437 def countJobResults(self, jobs, result):
2438 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002439 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002440
Monty Taylor0d926122017-05-24 08:07:56 -05002441 def getBuildByName(self, name):
2442 for build in self.builds:
2443 if build.name == name:
2444 return build
2445 raise Exception("Unable to find build %s" % name)
2446
James E. Blair96c6bf82016-01-15 16:20:40 -08002447 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002448 for job in self.history:
2449 if (job.name == name and
2450 (project is None or
2451 job.parameters['ZUUL_PROJECT'] == project)):
2452 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002453 raise Exception("Unable to find job %s in history" % name)
2454
2455 def assertEmptyQueues(self):
2456 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002457 for tenant in self.sched.abide.tenants.values():
2458 for pipeline in tenant.layout.pipelines.values():
2459 for queue in pipeline.queues:
2460 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002461 print('pipeline %s queue %s contents %s' % (
2462 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002463 self.assertEqual(len(queue.queue), 0,
2464 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002465
2466 def assertReportedStat(self, key, value=None, kind=None):
2467 start = time.time()
2468 while time.time() < (start + 5):
2469 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002470 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002471 if key == k:
2472 if value is None and kind is None:
2473 return
2474 elif value:
2475 if value == v:
2476 return
2477 elif kind:
2478 if v.endswith('|' + kind):
2479 return
2480 time.sleep(0.1)
2481
Clark Boylanb640e052014-04-03 16:41:46 -07002482 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002483
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002484 def assertBuilds(self, builds):
2485 """Assert that the running builds are as described.
2486
2487 The list of running builds is examined and must match exactly
2488 the list of builds described by the input.
2489
2490 :arg list builds: A list of dictionaries. Each item in the
2491 list must match the corresponding build in the build
2492 history, and each element of the dictionary must match the
2493 corresponding attribute of the build.
2494
2495 """
James E. Blair3158e282016-08-19 09:34:11 -07002496 try:
2497 self.assertEqual(len(self.builds), len(builds))
2498 for i, d in enumerate(builds):
2499 for k, v in d.items():
2500 self.assertEqual(
2501 getattr(self.builds[i], k), v,
2502 "Element %i in builds does not match" % (i,))
2503 except Exception:
2504 for build in self.builds:
2505 self.log.error("Running build: %s" % build)
2506 else:
2507 self.log.error("No running builds")
2508 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002509
James E. Blairb536ecc2016-08-31 10:11:42 -07002510 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002511 """Assert that the completed builds are as described.
2512
2513 The list of completed builds is examined and must match
2514 exactly the list of builds described by the input.
2515
2516 :arg list history: A list of dictionaries. Each item in the
2517 list must match the corresponding build in the build
2518 history, and each element of the dictionary must match the
2519 corresponding attribute of the build.
2520
James E. Blairb536ecc2016-08-31 10:11:42 -07002521 :arg bool ordered: If true, the history must match the order
2522 supplied, if false, the builds are permitted to have
2523 arrived in any order.
2524
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002525 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002526 def matches(history_item, item):
2527 for k, v in item.items():
2528 if getattr(history_item, k) != v:
2529 return False
2530 return True
James E. Blair3158e282016-08-19 09:34:11 -07002531 try:
2532 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002533 if ordered:
2534 for i, d in enumerate(history):
2535 if not matches(self.history[i], d):
2536 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002537 "Element %i in history does not match %s" %
2538 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002539 else:
2540 unseen = self.history[:]
2541 for i, d in enumerate(history):
2542 found = False
2543 for unseen_item in unseen:
2544 if matches(unseen_item, d):
2545 found = True
2546 unseen.remove(unseen_item)
2547 break
2548 if not found:
2549 raise Exception("No match found for element %i "
2550 "in history" % (i,))
2551 if unseen:
2552 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002553 except Exception:
2554 for build in self.history:
2555 self.log.error("Completed build: %s" % build)
2556 else:
2557 self.log.error("No completed builds")
2558 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002559
James E. Blair6ac368c2016-12-22 18:07:20 -08002560 def printHistory(self):
2561 """Log the build history.
2562
2563 This can be useful during tests to summarize what jobs have
2564 completed.
2565
2566 """
2567 self.log.debug("Build history:")
2568 for build in self.history:
2569 self.log.debug(build)
2570
James E. Blair59fdbac2015-12-07 17:08:06 -08002571 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002572 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2573
James E. Blair9ea70072017-04-19 16:05:30 -07002574 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002575 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002576 if not os.path.exists(root):
2577 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002578 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2579 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002580- tenant:
2581 name: openstack
2582 source:
2583 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002584 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002585 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002586 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002587 - org/project
2588 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002589 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002590 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002591 self.config.set('zuul', 'tenant_config',
2592 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002593 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002594
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002595 def addCommitToRepo(self, project, message, files,
2596 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002597 path = os.path.join(self.upstream_root, project)
2598 repo = git.Repo(path)
2599 repo.head.reference = branch
2600 zuul.merger.merger.reset_repo_to_head(repo)
2601 for fn, content in files.items():
2602 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002603 try:
2604 os.makedirs(os.path.dirname(fn))
2605 except OSError:
2606 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002607 with open(fn, 'w') as f:
2608 f.write(content)
2609 repo.index.add([fn])
2610 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002611 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002612 repo.heads[branch].commit = commit
2613 repo.head.reference = branch
2614 repo.git.clean('-x', '-f', '-d')
2615 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002616 if tag:
2617 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002618 return before
2619
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002620 def commitConfigUpdate(self, project_name, source_name):
2621 """Commit an update to zuul.yaml
2622
2623 This overwrites the zuul.yaml in the specificed project with
2624 the contents specified.
2625
2626 :arg str project_name: The name of the project containing
2627 zuul.yaml (e.g., common-config)
2628
2629 :arg str source_name: The path to the file (underneath the
2630 test fixture directory) whose contents should be used to
2631 replace zuul.yaml.
2632 """
2633
2634 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002635 files = {}
2636 with open(source_path, 'r') as f:
2637 data = f.read()
2638 layout = yaml.safe_load(data)
2639 files['zuul.yaml'] = data
2640 for item in layout:
2641 if 'job' in item:
2642 jobname = item['job']['name']
2643 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002644 before = self.addCommitToRepo(
2645 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002646 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002647 return before
2648
James E. Blair7fc8daa2016-08-08 15:37:15 -07002649 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002650
James E. Blair7fc8daa2016-08-08 15:37:15 -07002651 """Inject a Fake (Gerrit) event.
2652
2653 This method accepts a JSON-encoded event and simulates Zuul
2654 having received it from Gerrit. It could (and should)
2655 eventually apply to any connection type, but is currently only
2656 used with Gerrit connections. The name of the connection is
2657 used to look up the corresponding server, and the event is
2658 simulated as having been received by all Zuul connections
2659 attached to that server. So if two Gerrit connections in Zuul
2660 are connected to the same Gerrit server, and you invoke this
2661 method specifying the name of one of them, the event will be
2662 received by both.
2663
2664 .. note::
2665
2666 "self.fake_gerrit.addEvent" calls should be migrated to
2667 this method.
2668
2669 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002670 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002671 :arg str event: The JSON-encoded event.
2672
2673 """
2674 specified_conn = self.connections.connections[connection]
2675 for conn in self.connections.connections.values():
2676 if (isinstance(conn, specified_conn.__class__) and
2677 specified_conn.server == conn.server):
2678 conn.addEvent(event)
2679
James E. Blaird8af5422017-05-24 13:59:40 -07002680 def getUpstreamRepos(self, projects):
2681 """Return upstream git repo objects for the listed projects
2682
2683 :arg list projects: A list of strings, each the canonical name
2684 of a project.
2685
2686 :returns: A dictionary of {name: repo} for every listed
2687 project.
2688 :rtype: dict
2689
2690 """
2691
2692 repos = {}
2693 for project in projects:
2694 # FIXME(jeblair): the upstream root does not yet have a
2695 # hostname component; that needs to be added, and this
2696 # line removed:
2697 tmp_project_name = '/'.join(project.split('/')[1:])
2698 path = os.path.join(self.upstream_root, tmp_project_name)
2699 repo = git.Repo(path)
2700 repos[project] = repo
2701 return repos
2702
James E. Blair3f876d52016-07-22 13:07:14 -07002703
2704class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002705 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002706 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002707
Joshua Heskethd78b4482015-09-14 16:56:34 -06002708
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002709class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002710 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002711 use_ssl = True
2712
2713
Joshua Heskethd78b4482015-09-14 16:56:34 -06002714class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002715 def setup_config(self):
2716 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002717 for section_name in self.config.sections():
2718 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2719 section_name, re.I)
2720 if not con_match:
2721 continue
2722
2723 if self.config.get(section_name, 'driver') == 'sql':
2724 f = MySQLSchemaFixture()
2725 self.useFixture(f)
2726 if (self.config.get(section_name, 'dburi') ==
2727 '$MYSQL_FIXTURE_DBURI$'):
2728 self.config.set(section_name, 'dburi', f.dburi)