blob: ecbe5c8904671fc8b43ff7694cfb8964bc3e6141 [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
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800779 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800780 # Since we're bypassing github API, which would require a user, we
781 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800782 # insert the status at the top of the list, to simulate that it
783 # is the most recent set status
784 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100785 'state': state,
786 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800787 'description': description,
788 'context': context,
789 'creator': {
790 'login': user
791 }
792 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100793
Jesse Keatingae4cd272017-01-30 17:10:44 -0800794 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800795 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
796 # convert the timestamp to a str format that would be returned
797 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800798
Adam Gandelmand81dd762017-02-09 15:15:49 -0800799 if granted_on:
800 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
801 submitted_at = time.strftime(
802 gh_time_format, granted_on.timetuple())
803 else:
804 # github timestamps only down to the second, so we need to make
805 # sure reviews that tests add appear to be added over a period of
806 # time in the past and not all at once.
807 if not self.reviews:
808 # the first review happens 10 mins ago
809 offset = 600
810 else:
811 # subsequent reviews happen 1 minute closer to now
812 offset = 600 - (len(self.reviews) * 60)
813
814 granted_on = datetime.datetime.utcfromtimestamp(
815 time.time() - offset)
816 submitted_at = time.strftime(
817 gh_time_format, granted_on.timetuple())
818
Jesse Keatingae4cd272017-01-30 17:10:44 -0800819 self.reviews.append({
820 'state': state,
821 'user': {
822 'login': user,
823 'email': user + "@derp.com",
824 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800825 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800826 })
827
Gregory Haynes4fc12542015-04-22 20:38:06 -0700828 def _getPRReference(self):
829 return '%s/head' % self.number
830
831 def _getPullRequestEvent(self, action):
832 name = 'pull_request'
833 data = {
834 'action': action,
835 'number': self.number,
836 'pull_request': {
837 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100838 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 'updated_at': self.updated_at,
840 'base': {
841 'ref': self.branch,
842 'repo': {
843 'full_name': self.project
844 }
845 },
846 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800847 'sha': self.head_sha,
848 'repo': {
849 'full_name': self.project
850 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700851 }
Jan Hruban3b415922016-02-03 13:10:22 +0100852 },
853 'sender': {
854 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700855 }
856 }
857 return (name, data)
858
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800859 def getCommitStatusEvent(self, context, state='success', user='zuul'):
860 name = 'status'
861 data = {
862 'state': state,
863 'sha': self.head_sha,
864 'description': 'Test results for %s: %s' % (self.head_sha, state),
865 'target_url': 'http://zuul/%s' % self.head_sha,
866 'branches': [],
867 'context': context,
868 'sender': {
869 'login': user
870 }
871 }
872 return (name, data)
873
Gregory Haynes4fc12542015-04-22 20:38:06 -0700874
875class FakeGithubConnection(githubconnection.GithubConnection):
876 log = logging.getLogger("zuul.test.FakeGithubConnection")
877
878 def __init__(self, driver, connection_name, connection_config,
879 upstream_root=None):
880 super(FakeGithubConnection, self).__init__(driver, connection_name,
881 connection_config)
882 self.connection_name = connection_name
883 self.pr_number = 0
884 self.pull_requests = []
885 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100886 self.merge_failure = False
887 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700888
Jan Hruban570d01c2016-03-10 21:51:32 +0100889 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700890 self.pr_number += 1
891 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100892 self, self.pr_number, project, branch, subject, self.upstream_root,
893 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700894 self.pull_requests.append(pull_request)
895 return pull_request
896
Jesse Keating71a47ff2017-06-06 11:36:43 -0700897 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
898 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700899 if not old_rev:
900 old_rev = '00000000000000000000000000000000'
901 if not new_rev:
902 new_rev = random_sha1()
903 name = 'push'
904 data = {
905 'ref': ref,
906 'before': old_rev,
907 'after': new_rev,
908 'repository': {
909 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700910 },
911 'commits': [
912 {
913 'added': added_files,
914 'removed': removed_files,
915 'modified': modified_files
916 }
917 ]
Wayne1a78c612015-06-11 17:14:13 -0700918 }
919 return (name, data)
920
Gregory Haynes4fc12542015-04-22 20:38:06 -0700921 def emitEvent(self, event):
922 """Emulates sending the GitHub webhook event to the connection."""
923 port = self.webapp.server.socket.getsockname()[1]
924 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700925 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926 headers = {'X-Github-Event': name}
927 req = urllib.request.Request(
928 'http://localhost:%s/connection/%s/payload'
929 % (port, self.connection_name),
930 data=payload, headers=headers)
931 urllib.request.urlopen(req)
932
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200933 def getPull(self, project, number):
934 pr = self.pull_requests[number - 1]
935 data = {
936 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100937 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200938 'updated_at': pr.updated_at,
939 'base': {
940 'repo': {
941 'full_name': pr.project
942 },
943 'ref': pr.branch,
944 },
Jan Hruban37615e52015-11-19 14:30:49 +0100945 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700946 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200947 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800948 'sha': pr.head_sha,
949 'repo': {
950 'full_name': pr.project
951 }
Jesse Keating61040e72017-06-08 15:08:27 -0700952 },
953 'files': pr.files
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200954 }
955 return data
956
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800957 def getPullBySha(self, sha):
958 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
959 if len(prs) > 1:
960 raise Exception('Multiple pulls found with head sha: %s' % sha)
961 pr = prs[0]
962 return self.getPull(pr.project, pr.number)
963
Jesse Keatingae4cd272017-01-30 17:10:44 -0800964 def _getPullReviews(self, owner, project, number):
965 pr = self.pull_requests[number - 1]
966 return pr.reviews
967
Jan Hruban3b415922016-02-03 13:10:22 +0100968 def getUser(self, login):
969 data = {
970 'username': login,
971 'name': 'Github User',
972 'email': 'github.user@example.com'
973 }
974 return data
975
Jesse Keatingae4cd272017-01-30 17:10:44 -0800976 def getRepoPermission(self, project, login):
977 owner, proj = project.split('/')
978 for pr in self.pull_requests:
979 pr_owner, pr_project = pr.project.split('/')
980 if (pr_owner == owner and proj == pr_project):
981 if login in pr.writers:
982 return 'write'
983 else:
984 return 'read'
985
Gregory Haynes4fc12542015-04-22 20:38:06 -0700986 def getGitUrl(self, project):
987 return os.path.join(self.upstream_root, str(project))
988
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200989 def real_getGitUrl(self, project):
990 return super(FakeGithubConnection, self).getGitUrl(project)
991
Gregory Haynes4fc12542015-04-22 20:38:06 -0700992 def getProjectBranches(self, project):
993 """Masks getProjectBranches since we don't have a real github"""
994
995 # just returns master for now
996 return ['master']
997
Jan Hrubane252a732017-01-03 15:03:09 +0100998 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700999 pull_request = self.pull_requests[pr_number - 1]
1000 pull_request.addComment(message)
1001
Jan Hruban3b415922016-02-03 13:10:22 +01001002 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +01001003 pull_request = self.pull_requests[pr_number - 1]
1004 if self.merge_failure:
1005 raise Exception('Pull request was not merged')
1006 if self.merge_not_allowed_count > 0:
1007 self.merge_not_allowed_count -= 1
1008 raise MergeFailure('Merge was not successful due to mergeability'
1009 ' conflict')
1010 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001011 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001012
Jesse Keatingd96e5882017-01-19 13:55:50 -08001013 def getCommitStatuses(self, project, sha):
1014 owner, proj = project.split('/')
1015 for pr in self.pull_requests:
1016 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -07001017 # This is somewhat risky, if the same commit exists in multiple
1018 # PRs, we might grab the wrong one that doesn't have a status
1019 # that is expected to be there. Maybe re-work this so that there
1020 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001021 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001022 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001023 return pr.statuses[sha]
1024
Jan Hrubane252a732017-01-03 15:03:09 +01001025 def setCommitStatus(self, project, sha, state,
1026 url='', description='', context=''):
1027 owner, proj = project.split('/')
1028 for pr in self.pull_requests:
1029 pr_owner, pr_project = pr.project.split('/')
1030 if (pr_owner == owner and pr_project == proj and
1031 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001032 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001033
Jan Hruban16ad31f2015-11-07 14:39:07 +01001034 def labelPull(self, project, pr_number, label):
1035 pull_request = self.pull_requests[pr_number - 1]
1036 pull_request.addLabel(label)
1037
1038 def unlabelPull(self, project, pr_number, label):
1039 pull_request = self.pull_requests[pr_number - 1]
1040 pull_request.removeLabel(label)
1041
Gregory Haynes4fc12542015-04-22 20:38:06 -07001042
Clark Boylanb640e052014-04-03 16:41:46 -07001043class BuildHistory(object):
1044 def __init__(self, **kw):
1045 self.__dict__.update(kw)
1046
1047 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001048 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1049 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001050
1051
Clark Boylanb640e052014-04-03 16:41:46 -07001052class FakeStatsd(threading.Thread):
1053 def __init__(self):
1054 threading.Thread.__init__(self)
1055 self.daemon = True
1056 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1057 self.sock.bind(('', 0))
1058 self.port = self.sock.getsockname()[1]
1059 self.wake_read, self.wake_write = os.pipe()
1060 self.stats = []
1061
1062 def run(self):
1063 while True:
1064 poll = select.poll()
1065 poll.register(self.sock, select.POLLIN)
1066 poll.register(self.wake_read, select.POLLIN)
1067 ret = poll.poll()
1068 for (fd, event) in ret:
1069 if fd == self.sock.fileno():
1070 data = self.sock.recvfrom(1024)
1071 if not data:
1072 return
1073 self.stats.append(data[0])
1074 if fd == self.wake_read:
1075 return
1076
1077 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001078 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001079
1080
James E. Blaire1767bc2016-08-02 10:00:27 -07001081class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001082 log = logging.getLogger("zuul.test")
1083
Paul Belanger174a8272017-03-14 13:20:10 -04001084 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001085 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001086 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001087 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001088 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001089 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001090 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001091 # TODOv3(jeblair): self.node is really "the image of the node
1092 # assigned". We should rename it (self.node_image?) if we
1093 # keep using it like this, or we may end up exposing more of
1094 # the complexity around multi-node jobs here
1095 # (self.nodes[0].image?)
1096 self.node = None
1097 if len(self.parameters.get('nodes')) == 1:
1098 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001099 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001100 self.pipeline = self.parameters['ZUUL_PIPELINE']
1101 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001102 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001103 self.wait_condition = threading.Condition()
1104 self.waiting = False
1105 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001106 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001107 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001108 self.changes = None
1109 if 'ZUUL_CHANGE_IDS' in self.parameters:
1110 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001111
James E. Blair3158e282016-08-19 09:34:11 -07001112 def __repr__(self):
1113 waiting = ''
1114 if self.waiting:
1115 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001116 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1117 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001118
Clark Boylanb640e052014-04-03 16:41:46 -07001119 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001120 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001121 self.wait_condition.acquire()
1122 self.wait_condition.notify()
1123 self.waiting = False
1124 self.log.debug("Build %s released" % self.unique)
1125 self.wait_condition.release()
1126
1127 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001128 """Return whether this build is being held.
1129
1130 :returns: Whether the build is being held.
1131 :rtype: bool
1132 """
1133
Clark Boylanb640e052014-04-03 16:41:46 -07001134 self.wait_condition.acquire()
1135 if self.waiting:
1136 ret = True
1137 else:
1138 ret = False
1139 self.wait_condition.release()
1140 return ret
1141
1142 def _wait(self):
1143 self.wait_condition.acquire()
1144 self.waiting = True
1145 self.log.debug("Build %s waiting" % self.unique)
1146 self.wait_condition.wait()
1147 self.wait_condition.release()
1148
1149 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001150 self.log.debug('Running build %s' % self.unique)
1151
Paul Belanger174a8272017-03-14 13:20:10 -04001152 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.log.debug('Holding build %s' % self.unique)
1154 self._wait()
1155 self.log.debug("Build %s continuing" % self.unique)
1156
James E. Blair412fba82017-01-26 15:00:50 -08001157 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001158 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001159 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001160 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001161 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001162 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001163 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001164
James E. Blaire1767bc2016-08-02 10:00:27 -07001165 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001166
James E. Blaira5dba232016-08-08 15:53:24 -07001167 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001168 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001169 for change in changes:
1170 if self.hasChanges(change):
1171 return True
1172 return False
1173
James E. Blaire7b99a02016-08-05 14:27:34 -07001174 def hasChanges(self, *changes):
1175 """Return whether this build has certain changes in its git repos.
1176
1177 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001178 are expected to be present (in order) in the git repository of
1179 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001180
1181 :returns: Whether the build has the indicated changes.
1182 :rtype: bool
1183
1184 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001185 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001186 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001187 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001188 try:
1189 repo = git.Repo(path)
1190 except NoSuchPathError as e:
1191 self.log.debug('%s' % e)
1192 return False
1193 ref = self.parameters['ZUUL_REF']
1194 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1195 commit_message = '%s-1' % change.subject
1196 self.log.debug("Checking if build %s has changes; commit_message "
1197 "%s; repo_messages %s" % (self, commit_message,
1198 repo_messages))
1199 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001200 self.log.debug(" messages do not match")
1201 return False
1202 self.log.debug(" OK")
1203 return True
1204
James E. Blaird8af5422017-05-24 13:59:40 -07001205 def getWorkspaceRepos(self, projects):
1206 """Return workspace git repo objects for the listed projects
1207
1208 :arg list projects: A list of strings, each the canonical name
1209 of a project.
1210
1211 :returns: A dictionary of {name: repo} for every listed
1212 project.
1213 :rtype: dict
1214
1215 """
1216
1217 repos = {}
1218 for project in projects:
1219 path = os.path.join(self.jobdir.src_root, project)
1220 repo = git.Repo(path)
1221 repos[project] = repo
1222 return repos
1223
Clark Boylanb640e052014-04-03 16:41:46 -07001224
Paul Belanger174a8272017-03-14 13:20:10 -04001225class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1226 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001227
Paul Belanger174a8272017-03-14 13:20:10 -04001228 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001229 they will report that they have started but then pause until
1230 released before reporting completion. This attribute may be
1231 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001232 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001233 be explicitly released.
1234
1235 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001236 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001237 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001238 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001239 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001240 self.hold_jobs_in_build = False
1241 self.lock = threading.Lock()
1242 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001243 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001244 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001245 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001246
James E. Blaira5dba232016-08-08 15:53:24 -07001247 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001248 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001249
1250 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001251 :arg Change change: The :py:class:`~tests.base.FakeChange`
1252 instance which should cause the job to fail. This job
1253 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001254
1255 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 l = self.fail_tests.get(name, [])
1257 l.append(change)
1258 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001259
James E. Blair962220f2016-08-03 11:22:38 -07001260 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001261 """Release a held build.
1262
1263 :arg str regex: A regular expression which, if supplied, will
1264 cause only builds with matching names to be released. If
1265 not supplied, all builds will be released.
1266
1267 """
James E. Blair962220f2016-08-03 11:22:38 -07001268 builds = self.running_builds[:]
1269 self.log.debug("Releasing build %s (%s)" % (regex,
1270 len(self.running_builds)))
1271 for build in builds:
1272 if not regex or re.match(regex, build.name):
1273 self.log.debug("Releasing build %s" %
1274 (build.parameters['ZUUL_UUID']))
1275 build.release()
1276 else:
1277 self.log.debug("Not releasing build %s" %
1278 (build.parameters['ZUUL_UUID']))
1279 self.log.debug("Done releasing builds %s (%s)" %
1280 (regex, len(self.running_builds)))
1281
Paul Belanger174a8272017-03-14 13:20:10 -04001282 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001283 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001284 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001285 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001286 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001287 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001288 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001289 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001290 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1291 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001292
1293 def stopJob(self, job):
1294 self.log.debug("handle stop")
1295 parameters = json.loads(job.arguments)
1296 uuid = parameters['uuid']
1297 for build in self.running_builds:
1298 if build.unique == uuid:
1299 build.aborted = True
1300 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001301 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001302
James E. Blaira002b032017-04-18 10:35:48 -07001303 def stop(self):
1304 for build in self.running_builds:
1305 build.release()
1306 super(RecordingExecutorServer, self).stop()
1307
Joshua Hesketh50c21782016-10-13 21:34:14 +11001308
Paul Belanger174a8272017-03-14 13:20:10 -04001309class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001310 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001311 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001312 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001313 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001314 if not commit: # merge conflict
1315 self.recordResult('MERGER_FAILURE')
1316 return commit
1317
1318 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001319 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001320 self.executor_server.lock.acquire()
1321 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001322 BuildHistory(name=build.name, result=result, changes=build.changes,
1323 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001324 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001325 pipeline=build.parameters['ZUUL_PIPELINE'])
1326 )
Paul Belanger174a8272017-03-14 13:20:10 -04001327 self.executor_server.running_builds.remove(build)
1328 del self.executor_server.job_builds[self.job.unique]
1329 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001330
1331 def runPlaybooks(self, args):
1332 build = self.executor_server.job_builds[self.job.unique]
1333 build.jobdir = self.jobdir
1334
1335 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1336 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001337 return result
1338
Monty Taylore6562aa2017-02-20 07:37:39 -05001339 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001340 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001341
Paul Belanger174a8272017-03-14 13:20:10 -04001342 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001343 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001344 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001345 else:
1346 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001347 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001348
James E. Blairad8dca02017-02-21 11:48:32 -05001349 def getHostList(self, args):
1350 self.log.debug("hostlist")
1351 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001352 for host in hosts:
1353 host['host_vars']['ansible_connection'] = 'local'
1354
1355 hosts.append(dict(
1356 name='localhost',
1357 host_vars=dict(ansible_connection='local'),
1358 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001359 return hosts
1360
James E. Blairf5dbd002015-12-23 15:26:17 -08001361
Clark Boylanb640e052014-04-03 16:41:46 -07001362class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001363 """A Gearman server for use in tests.
1364
1365 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1366 added to the queue but will not be distributed to workers
1367 until released. This attribute may be changed at any time and
1368 will take effect for subsequently enqueued jobs, but
1369 previously held jobs will still need to be explicitly
1370 released.
1371
1372 """
1373
Clark Boylanb640e052014-04-03 16:41:46 -07001374 def __init__(self):
1375 self.hold_jobs_in_queue = False
1376 super(FakeGearmanServer, self).__init__(0)
1377
1378 def getJobForConnection(self, connection, peek=False):
1379 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1380 for job in queue:
1381 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001382 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001383 job.waiting = self.hold_jobs_in_queue
1384 else:
1385 job.waiting = False
1386 if job.waiting:
1387 continue
1388 if job.name in connection.functions:
1389 if not peek:
1390 queue.remove(job)
1391 connection.related_jobs[job.handle] = job
1392 job.worker_connection = connection
1393 job.running = True
1394 return job
1395 return None
1396
1397 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001398 """Release a held job.
1399
1400 :arg str regex: A regular expression which, if supplied, will
1401 cause only jobs with matching names to be released. If
1402 not supplied, all jobs will be released.
1403 """
Clark Boylanb640e052014-04-03 16:41:46 -07001404 released = False
1405 qlen = (len(self.high_queue) + len(self.normal_queue) +
1406 len(self.low_queue))
1407 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1408 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001409 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001410 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001411 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001412 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001413 self.log.debug("releasing queued job %s" %
1414 job.unique)
1415 job.waiting = False
1416 released = True
1417 else:
1418 self.log.debug("not releasing queued job %s" %
1419 job.unique)
1420 if released:
1421 self.wakeConnections()
1422 qlen = (len(self.high_queue) + len(self.normal_queue) +
1423 len(self.low_queue))
1424 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1425
1426
1427class FakeSMTP(object):
1428 log = logging.getLogger('zuul.FakeSMTP')
1429
1430 def __init__(self, messages, server, port):
1431 self.server = server
1432 self.port = port
1433 self.messages = messages
1434
1435 def sendmail(self, from_email, to_email, msg):
1436 self.log.info("Sending email from %s, to %s, with msg %s" % (
1437 from_email, to_email, msg))
1438
1439 headers = msg.split('\n\n', 1)[0]
1440 body = msg.split('\n\n', 1)[1]
1441
1442 self.messages.append(dict(
1443 from_email=from_email,
1444 to_email=to_email,
1445 msg=msg,
1446 headers=headers,
1447 body=body,
1448 ))
1449
1450 return True
1451
1452 def quit(self):
1453 return True
1454
1455
James E. Blairdce6cea2016-12-20 16:45:32 -08001456class FakeNodepool(object):
1457 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001458 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001459
1460 log = logging.getLogger("zuul.test.FakeNodepool")
1461
1462 def __init__(self, host, port, chroot):
1463 self.client = kazoo.client.KazooClient(
1464 hosts='%s:%s%s' % (host, port, chroot))
1465 self.client.start()
1466 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001467 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001468 self.thread = threading.Thread(target=self.run)
1469 self.thread.daemon = True
1470 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001471 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001472
1473 def stop(self):
1474 self._running = False
1475 self.thread.join()
1476 self.client.stop()
1477 self.client.close()
1478
1479 def run(self):
1480 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001481 try:
1482 self._run()
1483 except Exception:
1484 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001485 time.sleep(0.1)
1486
1487 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001488 if self.paused:
1489 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001490 for req in self.getNodeRequests():
1491 self.fulfillRequest(req)
1492
1493 def getNodeRequests(self):
1494 try:
1495 reqids = self.client.get_children(self.REQUEST_ROOT)
1496 except kazoo.exceptions.NoNodeError:
1497 return []
1498 reqs = []
1499 for oid in sorted(reqids):
1500 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001501 try:
1502 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001503 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001504 data['_oid'] = oid
1505 reqs.append(data)
1506 except kazoo.exceptions.NoNodeError:
1507 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001508 return reqs
1509
James E. Blaire18d4602017-01-05 11:17:28 -08001510 def getNodes(self):
1511 try:
1512 nodeids = self.client.get_children(self.NODE_ROOT)
1513 except kazoo.exceptions.NoNodeError:
1514 return []
1515 nodes = []
1516 for oid in sorted(nodeids):
1517 path = self.NODE_ROOT + '/' + oid
1518 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001519 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001520 data['_oid'] = oid
1521 try:
1522 lockfiles = self.client.get_children(path + '/lock')
1523 except kazoo.exceptions.NoNodeError:
1524 lockfiles = []
1525 if lockfiles:
1526 data['_lock'] = True
1527 else:
1528 data['_lock'] = False
1529 nodes.append(data)
1530 return nodes
1531
James E. Blaira38c28e2017-01-04 10:33:20 -08001532 def makeNode(self, request_id, node_type):
1533 now = time.time()
1534 path = '/nodepool/nodes/'
1535 data = dict(type=node_type,
1536 provider='test-provider',
1537 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001538 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001539 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001540 public_ipv4='127.0.0.1',
1541 private_ipv4=None,
1542 public_ipv6=None,
1543 allocated_to=request_id,
1544 state='ready',
1545 state_time=now,
1546 created_time=now,
1547 updated_time=now,
1548 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001549 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001550 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001551 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001552 path = self.client.create(path, data,
1553 makepath=True,
1554 sequence=True)
1555 nodeid = path.split("/")[-1]
1556 return nodeid
1557
James E. Blair6ab79e02017-01-06 10:10:17 -08001558 def addFailRequest(self, request):
1559 self.fail_requests.add(request['_oid'])
1560
James E. Blairdce6cea2016-12-20 16:45:32 -08001561 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001562 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001563 return
1564 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001565 oid = request['_oid']
1566 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001567
James E. Blair6ab79e02017-01-06 10:10:17 -08001568 if oid in self.fail_requests:
1569 request['state'] = 'failed'
1570 else:
1571 request['state'] = 'fulfilled'
1572 nodes = []
1573 for node in request['node_types']:
1574 nodeid = self.makeNode(oid, node)
1575 nodes.append(nodeid)
1576 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001577
James E. Blaira38c28e2017-01-04 10:33:20 -08001578 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001579 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001580 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001581 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001582 try:
1583 self.client.set(path, data)
1584 except kazoo.exceptions.NoNodeError:
1585 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001586
1587
James E. Blair498059b2016-12-20 13:50:13 -08001588class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001589 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001590 super(ChrootedKazooFixture, self).__init__()
1591
1592 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1593 if ':' in zk_host:
1594 host, port = zk_host.split(':')
1595 else:
1596 host = zk_host
1597 port = None
1598
1599 self.zookeeper_host = host
1600
1601 if not port:
1602 self.zookeeper_port = 2181
1603 else:
1604 self.zookeeper_port = int(port)
1605
Clark Boylan621ec9a2017-04-07 17:41:33 -07001606 self.test_id = test_id
1607
James E. Blair498059b2016-12-20 13:50:13 -08001608 def _setUp(self):
1609 # Make sure the test chroot paths do not conflict
1610 random_bits = ''.join(random.choice(string.ascii_lowercase +
1611 string.ascii_uppercase)
1612 for x in range(8))
1613
Clark Boylan621ec9a2017-04-07 17:41:33 -07001614 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001615 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1616
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001617 self.addCleanup(self._cleanup)
1618
James E. Blair498059b2016-12-20 13:50:13 -08001619 # Ensure the chroot path exists and clean up any pre-existing znodes.
1620 _tmp_client = kazoo.client.KazooClient(
1621 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1622 _tmp_client.start()
1623
1624 if _tmp_client.exists(self.zookeeper_chroot):
1625 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1626
1627 _tmp_client.ensure_path(self.zookeeper_chroot)
1628 _tmp_client.stop()
1629 _tmp_client.close()
1630
James E. Blair498059b2016-12-20 13:50:13 -08001631 def _cleanup(self):
1632 '''Remove the chroot path.'''
1633 # Need a non-chroot'ed client to remove the chroot path
1634 _tmp_client = kazoo.client.KazooClient(
1635 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1636 _tmp_client.start()
1637 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1638 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001639 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001640
1641
Joshua Heskethd78b4482015-09-14 16:56:34 -06001642class MySQLSchemaFixture(fixtures.Fixture):
1643 def setUp(self):
1644 super(MySQLSchemaFixture, self).setUp()
1645
1646 random_bits = ''.join(random.choice(string.ascii_lowercase +
1647 string.ascii_uppercase)
1648 for x in range(8))
1649 self.name = '%s_%s' % (random_bits, os.getpid())
1650 self.passwd = uuid.uuid4().hex
1651 db = pymysql.connect(host="localhost",
1652 user="openstack_citest",
1653 passwd="openstack_citest",
1654 db="openstack_citest")
1655 cur = db.cursor()
1656 cur.execute("create database %s" % self.name)
1657 cur.execute(
1658 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1659 (self.name, self.name, self.passwd))
1660 cur.execute("flush privileges")
1661
1662 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1663 self.passwd,
1664 self.name)
1665 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1666 self.addCleanup(self.cleanup)
1667
1668 def cleanup(self):
1669 db = pymysql.connect(host="localhost",
1670 user="openstack_citest",
1671 passwd="openstack_citest",
1672 db="openstack_citest")
1673 cur = db.cursor()
1674 cur.execute("drop database %s" % self.name)
1675 cur.execute("drop user '%s'@'localhost'" % self.name)
1676 cur.execute("flush privileges")
1677
1678
Maru Newby3fe5f852015-01-13 04:22:14 +00001679class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001680 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001681 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001682
James E. Blair1c236df2017-02-01 14:07:24 -08001683 def attachLogs(self, *args):
1684 def reader():
1685 self._log_stream.seek(0)
1686 while True:
1687 x = self._log_stream.read(4096)
1688 if not x:
1689 break
1690 yield x.encode('utf8')
1691 content = testtools.content.content_from_reader(
1692 reader,
1693 testtools.content_type.UTF8_TEXT,
1694 False)
1695 self.addDetail('logging', content)
1696
Clark Boylanb640e052014-04-03 16:41:46 -07001697 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001698 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001699 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1700 try:
1701 test_timeout = int(test_timeout)
1702 except ValueError:
1703 # If timeout value is invalid do not set a timeout.
1704 test_timeout = 0
1705 if test_timeout > 0:
1706 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1707
1708 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1709 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1710 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1711 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1712 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1713 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1714 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1715 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1716 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1717 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001718 self._log_stream = StringIO()
1719 self.addOnException(self.attachLogs)
1720 else:
1721 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001722
James E. Blair73b41772017-05-22 13:22:55 -07001723 # NOTE(jeblair): this is temporary extra debugging to try to
1724 # track down a possible leak.
1725 orig_git_repo_init = git.Repo.__init__
1726
1727 def git_repo_init(myself, *args, **kw):
1728 orig_git_repo_init(myself, *args, **kw)
1729 self.log.debug("Created git repo 0x%x %s" %
1730 (id(myself), repr(myself)))
1731
1732 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1733 git_repo_init))
1734
James E. Blair1c236df2017-02-01 14:07:24 -08001735 handler = logging.StreamHandler(self._log_stream)
1736 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1737 '%(levelname)-8s %(message)s')
1738 handler.setFormatter(formatter)
1739
1740 logger = logging.getLogger()
1741 logger.setLevel(logging.DEBUG)
1742 logger.addHandler(handler)
1743
Clark Boylan3410d532017-04-25 12:35:29 -07001744 # Make sure we don't carry old handlers around in process state
1745 # which slows down test runs
1746 self.addCleanup(logger.removeHandler, handler)
1747 self.addCleanup(handler.close)
1748 self.addCleanup(handler.flush)
1749
James E. Blair1c236df2017-02-01 14:07:24 -08001750 # NOTE(notmorgan): Extract logging overrides for specific
1751 # libraries from the OS_LOG_DEFAULTS env and create loggers
1752 # for each. This is used to limit the output during test runs
1753 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001754 log_defaults_from_env = os.environ.get(
1755 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001756 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001757
James E. Blairdce6cea2016-12-20 16:45:32 -08001758 if log_defaults_from_env:
1759 for default in log_defaults_from_env.split(','):
1760 try:
1761 name, level_str = default.split('=', 1)
1762 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001763 logger = logging.getLogger(name)
1764 logger.setLevel(level)
1765 logger.addHandler(handler)
1766 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001767 except ValueError:
1768 # NOTE(notmorgan): Invalid format of the log default,
1769 # skip and don't try and apply a logger for the
1770 # specified module
1771 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001772
Maru Newby3fe5f852015-01-13 04:22:14 +00001773
1774class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001775 """A test case with a functioning Zuul.
1776
1777 The following class variables are used during test setup and can
1778 be overidden by subclasses but are effectively read-only once a
1779 test method starts running:
1780
1781 :cvar str config_file: This points to the main zuul config file
1782 within the fixtures directory. Subclasses may override this
1783 to obtain a different behavior.
1784
1785 :cvar str tenant_config_file: This is the tenant config file
1786 (which specifies from what git repos the configuration should
1787 be loaded). It defaults to the value specified in
1788 `config_file` but can be overidden by subclasses to obtain a
1789 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001790 configuration. See also the :py:func:`simple_layout`
1791 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001792
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001793 :cvar bool create_project_keys: Indicates whether Zuul should
1794 auto-generate keys for each project, or whether the test
1795 infrastructure should insert dummy keys to save time during
1796 startup. Defaults to False.
1797
James E. Blaire7b99a02016-08-05 14:27:34 -07001798 The following are instance variables that are useful within test
1799 methods:
1800
1801 :ivar FakeGerritConnection fake_<connection>:
1802 A :py:class:`~tests.base.FakeGerritConnection` will be
1803 instantiated for each connection present in the config file
1804 and stored here. For instance, `fake_gerrit` will hold the
1805 FakeGerritConnection object for a connection named `gerrit`.
1806
1807 :ivar FakeGearmanServer gearman_server: An instance of
1808 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1809 server that all of the Zuul components in this test use to
1810 communicate with each other.
1811
Paul Belanger174a8272017-03-14 13:20:10 -04001812 :ivar RecordingExecutorServer executor_server: An instance of
1813 :py:class:`~tests.base.RecordingExecutorServer` which is the
1814 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001815
1816 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1817 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001818 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001819 list upon completion.
1820
1821 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1822 objects representing completed builds. They are appended to
1823 the list in the order they complete.
1824
1825 """
1826
James E. Blair83005782015-12-11 14:46:03 -08001827 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001828 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001829 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001830
1831 def _startMerger(self):
1832 self.merge_server = zuul.merger.server.MergeServer(self.config,
1833 self.connections)
1834 self.merge_server.start()
1835
Maru Newby3fe5f852015-01-13 04:22:14 +00001836 def setUp(self):
1837 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001838
1839 self.setupZK()
1840
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001841 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001842 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001843 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1844 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001845 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001846 tmp_root = tempfile.mkdtemp(
1847 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001848 self.test_root = os.path.join(tmp_root, "zuul-test")
1849 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001850 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001851 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001852 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001853
1854 if os.path.exists(self.test_root):
1855 shutil.rmtree(self.test_root)
1856 os.makedirs(self.test_root)
1857 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001858 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001859
1860 # Make per test copy of Configuration.
1861 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001862 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1863 if not os.path.exists(self.private_key_file):
1864 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1865 shutil.copy(src_private_key_file, self.private_key_file)
1866 shutil.copy('{}.pub'.format(src_private_key_file),
1867 '{}.pub'.format(self.private_key_file))
1868 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001869 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001870 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001871 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001872 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001873 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001874 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001875 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001876
Clark Boylanb640e052014-04-03 16:41:46 -07001877 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001878 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1879 # see: https://github.com/jsocol/pystatsd/issues/61
1880 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001881 os.environ['STATSD_PORT'] = str(self.statsd.port)
1882 self.statsd.start()
1883 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001884 reload_module(statsd)
1885 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001886
1887 self.gearman_server = FakeGearmanServer()
1888
1889 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001890 self.log.info("Gearman server on port %s" %
1891 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001892
James E. Blaire511d2f2016-12-08 15:22:26 -08001893 gerritsource.GerritSource.replication_timeout = 1.5
1894 gerritsource.GerritSource.replication_retry_interval = 0.5
1895 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001896
Joshua Hesketh352264b2015-08-11 23:42:08 +10001897 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001898
Jan Hruban7083edd2015-08-21 14:00:54 +02001899 self.webapp = zuul.webapp.WebApp(
1900 self.sched, port=0, listen_address='127.0.0.1')
1901
Jan Hruban6b71aff2015-10-22 16:58:08 +02001902 self.event_queues = [
1903 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001904 self.sched.trigger_event_queue,
1905 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001906 ]
1907
James E. Blairfef78942016-03-11 16:28:56 -08001908 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001909 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001910
Paul Belanger174a8272017-03-14 13:20:10 -04001911 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001912 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001913 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001914 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001915 _test_root=self.test_root,
1916 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001917 self.executor_server.start()
1918 self.history = self.executor_server.build_history
1919 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001920
Paul Belanger174a8272017-03-14 13:20:10 -04001921 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001922 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001923 self.merge_client = zuul.merger.client.MergeClient(
1924 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001925 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001926 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001927 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001928
James E. Blair0d5a36e2017-02-21 10:53:44 -05001929 self.fake_nodepool = FakeNodepool(
1930 self.zk_chroot_fixture.zookeeper_host,
1931 self.zk_chroot_fixture.zookeeper_port,
1932 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001933
Paul Belanger174a8272017-03-14 13:20:10 -04001934 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001935 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001936 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001937 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001938
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001939 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001940
1941 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001942 self.webapp.start()
1943 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001944 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001945 # Cleanups are run in reverse order
1946 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001947 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001948 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001949
James E. Blairb9c0d772017-03-03 14:34:49 -08001950 self.sched.reconfigure(self.config)
1951 self.sched.resume()
1952
Tobias Henkel7df274b2017-05-26 17:41:11 +02001953 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001954 # Set up gerrit related fakes
1955 # Set a changes database so multiple FakeGerrit's can report back to
1956 # a virtual canonical database given by the configured hostname
1957 self.gerrit_changes_dbs = {}
1958
1959 def getGerritConnection(driver, name, config):
1960 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1961 con = FakeGerritConnection(driver, name, config,
1962 changes_db=db,
1963 upstream_root=self.upstream_root)
1964 self.event_queues.append(con.event_queue)
1965 setattr(self, 'fake_' + name, con)
1966 return con
1967
1968 self.useFixture(fixtures.MonkeyPatch(
1969 'zuul.driver.gerrit.GerritDriver.getConnection',
1970 getGerritConnection))
1971
Gregory Haynes4fc12542015-04-22 20:38:06 -07001972 def getGithubConnection(driver, name, config):
1973 con = FakeGithubConnection(driver, name, config,
1974 upstream_root=self.upstream_root)
1975 setattr(self, 'fake_' + name, con)
1976 return con
1977
1978 self.useFixture(fixtures.MonkeyPatch(
1979 'zuul.driver.github.GithubDriver.getConnection',
1980 getGithubConnection))
1981
James E. Blaire511d2f2016-12-08 15:22:26 -08001982 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001983 # TODO(jhesketh): This should come from lib.connections for better
1984 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001985 # Register connections from the config
1986 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001987
Joshua Hesketh352264b2015-08-11 23:42:08 +10001988 def FakeSMTPFactory(*args, **kw):
1989 args = [self.smtp_messages] + list(args)
1990 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001991
Joshua Hesketh352264b2015-08-11 23:42:08 +10001992 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001993
James E. Blaire511d2f2016-12-08 15:22:26 -08001994 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001995 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02001996 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001997
James E. Blair83005782015-12-11 14:46:03 -08001998 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001999 # This creates the per-test configuration object. It can be
2000 # overriden by subclasses, but should not need to be since it
2001 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002002 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002003 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002004
2005 if not self.setupSimpleLayout():
2006 if hasattr(self, 'tenant_config_file'):
2007 self.config.set('zuul', 'tenant_config',
2008 self.tenant_config_file)
2009 git_path = os.path.join(
2010 os.path.dirname(
2011 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2012 'git')
2013 if os.path.exists(git_path):
2014 for reponame in os.listdir(git_path):
2015 project = reponame.replace('_', '/')
2016 self.copyDirToRepo(project,
2017 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002018 self.setupAllProjectKeys()
2019
James E. Blair06cc3922017-04-19 10:08:10 -07002020 def setupSimpleLayout(self):
2021 # If the test method has been decorated with a simple_layout,
2022 # use that instead of the class tenant_config_file. Set up a
2023 # single config-project with the specified layout, and
2024 # initialize repos for all of the 'project' entries which
2025 # appear in the layout.
2026 test_name = self.id().split('.')[-1]
2027 test = getattr(self, test_name)
2028 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002029 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002030 else:
2031 return False
2032
James E. Blairb70e55a2017-04-19 12:57:02 -07002033 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002034 path = os.path.join(FIXTURE_DIR, path)
2035 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002036 data = f.read()
2037 layout = yaml.safe_load(data)
2038 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002039 untrusted_projects = []
2040 for item in layout:
2041 if 'project' in item:
2042 name = item['project']['name']
2043 untrusted_projects.append(name)
2044 self.init_repo(name)
2045 self.addCommitToRepo(name, 'initial commit',
2046 files={'README': ''},
2047 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002048 if 'job' in item:
2049 jobname = item['job']['name']
2050 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002051
2052 root = os.path.join(self.test_root, "config")
2053 if not os.path.exists(root):
2054 os.makedirs(root)
2055 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2056 config = [{'tenant':
2057 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002058 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002059 {'config-projects': ['common-config'],
2060 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002061 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002062 f.close()
2063 self.config.set('zuul', 'tenant_config',
2064 os.path.join(FIXTURE_DIR, f.name))
2065
2066 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002067 self.addCommitToRepo('common-config', 'add content from fixture',
2068 files, branch='master', tag='init')
2069
2070 return True
2071
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002072 def setupAllProjectKeys(self):
2073 if self.create_project_keys:
2074 return
2075
2076 path = self.config.get('zuul', 'tenant_config')
2077 with open(os.path.join(FIXTURE_DIR, path)) as f:
2078 tenant_config = yaml.safe_load(f.read())
2079 for tenant in tenant_config:
2080 sources = tenant['tenant']['source']
2081 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002082 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002083 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002084 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002085 self.setupProjectKeys(source, project)
2086
2087 def setupProjectKeys(self, source, project):
2088 # Make sure we set up an RSA key for the project so that we
2089 # don't spend time generating one:
2090
2091 key_root = os.path.join(self.state_root, 'keys')
2092 if not os.path.isdir(key_root):
2093 os.mkdir(key_root, 0o700)
2094 private_key_file = os.path.join(key_root, source, project + '.pem')
2095 private_key_dir = os.path.dirname(private_key_file)
2096 self.log.debug("Installing test keys for project %s at %s" % (
2097 project, private_key_file))
2098 if not os.path.isdir(private_key_dir):
2099 os.makedirs(private_key_dir)
2100 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2101 with open(private_key_file, 'w') as o:
2102 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002103
James E. Blair498059b2016-12-20 13:50:13 -08002104 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002105 self.zk_chroot_fixture = self.useFixture(
2106 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002107 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002108 self.zk_chroot_fixture.zookeeper_host,
2109 self.zk_chroot_fixture.zookeeper_port,
2110 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002111
James E. Blair96c6bf82016-01-15 16:20:40 -08002112 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002113 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002114
2115 files = {}
2116 for (dirpath, dirnames, filenames) in os.walk(source_path):
2117 for filename in filenames:
2118 test_tree_filepath = os.path.join(dirpath, filename)
2119 common_path = os.path.commonprefix([test_tree_filepath,
2120 source_path])
2121 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2122 with open(test_tree_filepath, 'r') as f:
2123 content = f.read()
2124 files[relative_filepath] = content
2125 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002126 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002127
James E. Blaire18d4602017-01-05 11:17:28 -08002128 def assertNodepoolState(self):
2129 # Make sure that there are no pending requests
2130
2131 requests = self.fake_nodepool.getNodeRequests()
2132 self.assertEqual(len(requests), 0)
2133
2134 nodes = self.fake_nodepool.getNodes()
2135 for node in nodes:
2136 self.assertFalse(node['_lock'], "Node %s is locked" %
2137 (node['_oid'],))
2138
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002139 def assertNoGeneratedKeys(self):
2140 # Make sure that Zuul did not generate any project keys
2141 # (unless it was supposed to).
2142
2143 if self.create_project_keys:
2144 return
2145
2146 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2147 test_key = i.read()
2148
2149 key_root = os.path.join(self.state_root, 'keys')
2150 for root, dirname, files in os.walk(key_root):
2151 for fn in files:
2152 with open(os.path.join(root, fn)) as f:
2153 self.assertEqual(test_key, f.read())
2154
Clark Boylanb640e052014-04-03 16:41:46 -07002155 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002156 self.log.debug("Assert final state")
2157 # Make sure no jobs are running
2158 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002159 # Make sure that git.Repo objects have been garbage collected.
2160 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002161 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002162 gc.collect()
2163 for obj in gc.get_objects():
2164 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002165 self.log.debug("Leaked git repo object: 0x%x %s" %
2166 (id(obj), repr(obj)))
2167 for ref in gc.get_referrers(obj):
2168 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002169 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002170 if repos:
2171 for obj in gc.garbage:
2172 self.log.debug(" Garbage %s" % (repr(obj)))
2173 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002174 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002175 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002176 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002177 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002178 for tenant in self.sched.abide.tenants.values():
2179 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002180 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002181 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002182
2183 def shutdown(self):
2184 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002185 self.executor_server.hold_jobs_in_build = False
2186 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002187 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002188 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002189 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002190 self.sched.stop()
2191 self.sched.join()
2192 self.statsd.stop()
2193 self.statsd.join()
2194 self.webapp.stop()
2195 self.webapp.join()
2196 self.rpc.stop()
2197 self.rpc.join()
2198 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002199 self.fake_nodepool.stop()
2200 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002201 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002202 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002203 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002204 # Further the pydevd threads also need to be whitelisted so debugging
2205 # e.g. in PyCharm is possible without breaking shutdown.
2206 whitelist = ['executor-watchdog',
2207 'pydevd.CommandThread',
2208 'pydevd.Reader',
2209 'pydevd.Writer',
2210 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002211 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002212 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002213 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002214 log_str = ""
2215 for thread_id, stack_frame in sys._current_frames().items():
2216 log_str += "Thread: %s\n" % thread_id
2217 log_str += "".join(traceback.format_stack(stack_frame))
2218 self.log.debug(log_str)
2219 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002220
James E. Blaira002b032017-04-18 10:35:48 -07002221 def assertCleanShutdown(self):
2222 pass
2223
James E. Blairc4ba97a2017-04-19 16:26:24 -07002224 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002225 parts = project.split('/')
2226 path = os.path.join(self.upstream_root, *parts[:-1])
2227 if not os.path.exists(path):
2228 os.makedirs(path)
2229 path = os.path.join(self.upstream_root, project)
2230 repo = git.Repo.init(path)
2231
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002232 with repo.config_writer() as config_writer:
2233 config_writer.set_value('user', 'email', 'user@example.com')
2234 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002235
Clark Boylanb640e052014-04-03 16:41:46 -07002236 repo.index.commit('initial commit')
2237 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002238 if tag:
2239 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002240
James E. Blair97d902e2014-08-21 13:25:56 -07002241 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002242 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002243 repo.git.clean('-x', '-f', '-d')
2244
James E. Blair97d902e2014-08-21 13:25:56 -07002245 def create_branch(self, project, branch):
2246 path = os.path.join(self.upstream_root, project)
2247 repo = git.Repo.init(path)
2248 fn = os.path.join(path, 'README')
2249
2250 branch_head = repo.create_head(branch)
2251 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002252 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002253 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002254 f.close()
2255 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002256 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002257
James E. Blair97d902e2014-08-21 13:25:56 -07002258 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002259 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002260 repo.git.clean('-x', '-f', '-d')
2261
Sachi King9f16d522016-03-16 12:20:45 +11002262 def create_commit(self, project):
2263 path = os.path.join(self.upstream_root, project)
2264 repo = git.Repo(path)
2265 repo.head.reference = repo.heads['master']
2266 file_name = os.path.join(path, 'README')
2267 with open(file_name, 'a') as f:
2268 f.write('creating fake commit\n')
2269 repo.index.add([file_name])
2270 commit = repo.index.commit('Creating a fake commit')
2271 return commit.hexsha
2272
James E. Blairf4a5f022017-04-18 14:01:10 -07002273 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002274 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002275 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002276 while len(self.builds):
2277 self.release(self.builds[0])
2278 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002279 i += 1
2280 if count is not None and i >= count:
2281 break
James E. Blairb8c16472015-05-05 14:55:26 -07002282
Clark Boylanb640e052014-04-03 16:41:46 -07002283 def release(self, job):
2284 if isinstance(job, FakeBuild):
2285 job.release()
2286 else:
2287 job.waiting = False
2288 self.log.debug("Queued job %s released" % job.unique)
2289 self.gearman_server.wakeConnections()
2290
2291 def getParameter(self, job, name):
2292 if isinstance(job, FakeBuild):
2293 return job.parameters[name]
2294 else:
2295 parameters = json.loads(job.arguments)
2296 return parameters[name]
2297
Clark Boylanb640e052014-04-03 16:41:46 -07002298 def haveAllBuildsReported(self):
2299 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002300 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002301 return False
2302 # Find out if every build that the worker has completed has been
2303 # reported back to Zuul. If it hasn't then that means a Gearman
2304 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002305 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002306 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002307 if not zbuild:
2308 # It has already been reported
2309 continue
2310 # It hasn't been reported yet.
2311 return False
2312 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002313 worker = self.executor_server.executor_worker
2314 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002315 if connection.state == 'GRAB_WAIT':
2316 return False
2317 return True
2318
2319 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002320 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002321 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002322 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002323 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002324 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002325 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002326 for j in conn.related_jobs.values():
2327 if j.unique == build.uuid:
2328 client_job = j
2329 break
2330 if not client_job:
2331 self.log.debug("%s is not known to the gearman client" %
2332 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002333 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002334 if not client_job.handle:
2335 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002336 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002337 server_job = self.gearman_server.jobs.get(client_job.handle)
2338 if not server_job:
2339 self.log.debug("%s is not known to the gearman server" %
2340 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002341 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002342 if not hasattr(server_job, 'waiting'):
2343 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002344 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002345 if server_job.waiting:
2346 continue
James E. Blair17302972016-08-10 16:11:42 -07002347 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002348 self.log.debug("%s has not reported start" % build)
2349 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002350 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002351 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002352 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002353 if worker_build:
2354 if worker_build.isWaiting():
2355 continue
2356 else:
2357 self.log.debug("%s is running" % worker_build)
2358 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002359 else:
James E. Blair962220f2016-08-03 11:22:38 -07002360 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002361 return False
James E. Blaira002b032017-04-18 10:35:48 -07002362 for (build_uuid, job_worker) in \
2363 self.executor_server.job_workers.items():
2364 if build_uuid not in seen_builds:
2365 self.log.debug("%s is not finalized" % build_uuid)
2366 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002367 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002368
James E. Blairdce6cea2016-12-20 16:45:32 -08002369 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002370 if self.fake_nodepool.paused:
2371 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002372 if self.sched.nodepool.requests:
2373 return False
2374 return True
2375
Jan Hruban6b71aff2015-10-22 16:58:08 +02002376 def eventQueuesEmpty(self):
2377 for queue in self.event_queues:
2378 yield queue.empty()
2379
2380 def eventQueuesJoin(self):
2381 for queue in self.event_queues:
2382 queue.join()
2383
Clark Boylanb640e052014-04-03 16:41:46 -07002384 def waitUntilSettled(self):
2385 self.log.debug("Waiting until settled...")
2386 start = time.time()
2387 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002388 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002389 self.log.error("Timeout waiting for Zuul to settle")
2390 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002391 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002392 self.log.error(" %s: %s" % (queue, queue.empty()))
2393 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002394 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002395 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002396 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002397 self.log.error("All requests completed: %s" %
2398 (self.areAllNodeRequestsComplete(),))
2399 self.log.error("Merge client jobs: %s" %
2400 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002401 raise Exception("Timeout waiting for Zuul to settle")
2402 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002403
Paul Belanger174a8272017-03-14 13:20:10 -04002404 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002405 # have all build states propogated to zuul?
2406 if self.haveAllBuildsReported():
2407 # Join ensures that the queue is empty _and_ events have been
2408 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002409 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002410 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002411 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002412 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002413 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002414 self.areAllNodeRequestsComplete() and
2415 all(self.eventQueuesEmpty())):
2416 # The queue empty check is placed at the end to
2417 # ensure that if a component adds an event between
2418 # when locked the run handler and checked that the
2419 # components were stable, we don't erroneously
2420 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002421 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002422 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002423 self.log.debug("...settled.")
2424 return
2425 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002426 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002427 self.sched.wake_event.wait(0.1)
2428
2429 def countJobResults(self, jobs, result):
2430 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002431 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002432
Monty Taylor0d926122017-05-24 08:07:56 -05002433 def getBuildByName(self, name):
2434 for build in self.builds:
2435 if build.name == name:
2436 return build
2437 raise Exception("Unable to find build %s" % name)
2438
James E. Blair96c6bf82016-01-15 16:20:40 -08002439 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002440 for job in self.history:
2441 if (job.name == name and
2442 (project is None or
2443 job.parameters['ZUUL_PROJECT'] == project)):
2444 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002445 raise Exception("Unable to find job %s in history" % name)
2446
2447 def assertEmptyQueues(self):
2448 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002449 for tenant in self.sched.abide.tenants.values():
2450 for pipeline in tenant.layout.pipelines.values():
2451 for queue in pipeline.queues:
2452 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002453 print('pipeline %s queue %s contents %s' % (
2454 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002455 self.assertEqual(len(queue.queue), 0,
2456 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002457
2458 def assertReportedStat(self, key, value=None, kind=None):
2459 start = time.time()
2460 while time.time() < (start + 5):
2461 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002462 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002463 if key == k:
2464 if value is None and kind is None:
2465 return
2466 elif value:
2467 if value == v:
2468 return
2469 elif kind:
2470 if v.endswith('|' + kind):
2471 return
2472 time.sleep(0.1)
2473
Clark Boylanb640e052014-04-03 16:41:46 -07002474 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002475
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002476 def assertBuilds(self, builds):
2477 """Assert that the running builds are as described.
2478
2479 The list of running builds is examined and must match exactly
2480 the list of builds described by the input.
2481
2482 :arg list builds: A list of dictionaries. Each item in the
2483 list must match the corresponding build in the build
2484 history, and each element of the dictionary must match the
2485 corresponding attribute of the build.
2486
2487 """
James E. Blair3158e282016-08-19 09:34:11 -07002488 try:
2489 self.assertEqual(len(self.builds), len(builds))
2490 for i, d in enumerate(builds):
2491 for k, v in d.items():
2492 self.assertEqual(
2493 getattr(self.builds[i], k), v,
2494 "Element %i in builds does not match" % (i,))
2495 except Exception:
2496 for build in self.builds:
2497 self.log.error("Running build: %s" % build)
2498 else:
2499 self.log.error("No running builds")
2500 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002501
James E. Blairb536ecc2016-08-31 10:11:42 -07002502 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002503 """Assert that the completed builds are as described.
2504
2505 The list of completed builds is examined and must match
2506 exactly the list of builds described by the input.
2507
2508 :arg list history: A list of dictionaries. Each item in the
2509 list must match the corresponding build in the build
2510 history, and each element of the dictionary must match the
2511 corresponding attribute of the build.
2512
James E. Blairb536ecc2016-08-31 10:11:42 -07002513 :arg bool ordered: If true, the history must match the order
2514 supplied, if false, the builds are permitted to have
2515 arrived in any order.
2516
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002517 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002518 def matches(history_item, item):
2519 for k, v in item.items():
2520 if getattr(history_item, k) != v:
2521 return False
2522 return True
James E. Blair3158e282016-08-19 09:34:11 -07002523 try:
2524 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002525 if ordered:
2526 for i, d in enumerate(history):
2527 if not matches(self.history[i], d):
2528 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002529 "Element %i in history does not match %s" %
2530 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002531 else:
2532 unseen = self.history[:]
2533 for i, d in enumerate(history):
2534 found = False
2535 for unseen_item in unseen:
2536 if matches(unseen_item, d):
2537 found = True
2538 unseen.remove(unseen_item)
2539 break
2540 if not found:
2541 raise Exception("No match found for element %i "
2542 "in history" % (i,))
2543 if unseen:
2544 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002545 except Exception:
2546 for build in self.history:
2547 self.log.error("Completed build: %s" % build)
2548 else:
2549 self.log.error("No completed builds")
2550 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002551
James E. Blair6ac368c2016-12-22 18:07:20 -08002552 def printHistory(self):
2553 """Log the build history.
2554
2555 This can be useful during tests to summarize what jobs have
2556 completed.
2557
2558 """
2559 self.log.debug("Build history:")
2560 for build in self.history:
2561 self.log.debug(build)
2562
James E. Blair59fdbac2015-12-07 17:08:06 -08002563 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002564 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2565
James E. Blair9ea70072017-04-19 16:05:30 -07002566 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002567 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002568 if not os.path.exists(root):
2569 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002570 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2571 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002572- tenant:
2573 name: openstack
2574 source:
2575 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002576 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002577 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002578 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002579 - org/project
2580 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002581 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002582 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002583 self.config.set('zuul', 'tenant_config',
2584 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002585 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002586
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002587 def addCommitToRepo(self, project, message, files,
2588 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002589 path = os.path.join(self.upstream_root, project)
2590 repo = git.Repo(path)
2591 repo.head.reference = branch
2592 zuul.merger.merger.reset_repo_to_head(repo)
2593 for fn, content in files.items():
2594 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002595 try:
2596 os.makedirs(os.path.dirname(fn))
2597 except OSError:
2598 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002599 with open(fn, 'w') as f:
2600 f.write(content)
2601 repo.index.add([fn])
2602 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002603 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002604 repo.heads[branch].commit = commit
2605 repo.head.reference = branch
2606 repo.git.clean('-x', '-f', '-d')
2607 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002608 if tag:
2609 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002610 return before
2611
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002612 def commitConfigUpdate(self, project_name, source_name):
2613 """Commit an update to zuul.yaml
2614
2615 This overwrites the zuul.yaml in the specificed project with
2616 the contents specified.
2617
2618 :arg str project_name: The name of the project containing
2619 zuul.yaml (e.g., common-config)
2620
2621 :arg str source_name: The path to the file (underneath the
2622 test fixture directory) whose contents should be used to
2623 replace zuul.yaml.
2624 """
2625
2626 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002627 files = {}
2628 with open(source_path, 'r') as f:
2629 data = f.read()
2630 layout = yaml.safe_load(data)
2631 files['zuul.yaml'] = data
2632 for item in layout:
2633 if 'job' in item:
2634 jobname = item['job']['name']
2635 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002636 before = self.addCommitToRepo(
2637 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002638 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002639 return before
2640
James E. Blair7fc8daa2016-08-08 15:37:15 -07002641 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002642
James E. Blair7fc8daa2016-08-08 15:37:15 -07002643 """Inject a Fake (Gerrit) event.
2644
2645 This method accepts a JSON-encoded event and simulates Zuul
2646 having received it from Gerrit. It could (and should)
2647 eventually apply to any connection type, but is currently only
2648 used with Gerrit connections. The name of the connection is
2649 used to look up the corresponding server, and the event is
2650 simulated as having been received by all Zuul connections
2651 attached to that server. So if two Gerrit connections in Zuul
2652 are connected to the same Gerrit server, and you invoke this
2653 method specifying the name of one of them, the event will be
2654 received by both.
2655
2656 .. note::
2657
2658 "self.fake_gerrit.addEvent" calls should be migrated to
2659 this method.
2660
2661 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002662 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002663 :arg str event: The JSON-encoded event.
2664
2665 """
2666 specified_conn = self.connections.connections[connection]
2667 for conn in self.connections.connections.values():
2668 if (isinstance(conn, specified_conn.__class__) and
2669 specified_conn.server == conn.server):
2670 conn.addEvent(event)
2671
James E. Blaird8af5422017-05-24 13:59:40 -07002672 def getUpstreamRepos(self, projects):
2673 """Return upstream git repo objects for the listed projects
2674
2675 :arg list projects: A list of strings, each the canonical name
2676 of a project.
2677
2678 :returns: A dictionary of {name: repo} for every listed
2679 project.
2680 :rtype: dict
2681
2682 """
2683
2684 repos = {}
2685 for project in projects:
2686 # FIXME(jeblair): the upstream root does not yet have a
2687 # hostname component; that needs to be added, and this
2688 # line removed:
2689 tmp_project_name = '/'.join(project.split('/')[1:])
2690 path = os.path.join(self.upstream_root, tmp_project_name)
2691 repo = git.Repo(path)
2692 repos[project] = repo
2693 return repos
2694
James E. Blair3f876d52016-07-22 13:07:14 -07002695
2696class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002697 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002698 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002699
Joshua Heskethd78b4482015-09-14 16:56:34 -06002700
2701class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002702 def setup_config(self):
2703 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002704 for section_name in self.config.sections():
2705 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2706 section_name, re.I)
2707 if not con_match:
2708 continue
2709
2710 if self.config.get(section_name, 'driver') == 'sql':
2711 f = MySQLSchemaFixture()
2712 self.useFixture(f)
2713 if (self.config.get(section_name, 'dburi') ==
2714 '$MYSQL_FIXTURE_DBURI$'):
2715 self.config.set(section_name, 'dburi', f.dburi)