blob: a8b899567b57a4e21a6795d72c12ca45f3c024b1 [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 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200952 }
953 }
954 return data
955
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800956 def getPullBySha(self, sha):
957 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
958 if len(prs) > 1:
959 raise Exception('Multiple pulls found with head sha: %s' % sha)
960 pr = prs[0]
961 return self.getPull(pr.project, pr.number)
962
Jan Hruban570d01c2016-03-10 21:51:32 +0100963 def getPullFileNames(self, project, number):
964 pr = self.pull_requests[number - 1]
965 return pr.files
966
Jesse Keatingae4cd272017-01-30 17:10:44 -0800967 def _getPullReviews(self, owner, project, number):
968 pr = self.pull_requests[number - 1]
969 return pr.reviews
970
Jan Hruban3b415922016-02-03 13:10:22 +0100971 def getUser(self, login):
972 data = {
973 'username': login,
974 'name': 'Github User',
975 'email': 'github.user@example.com'
976 }
977 return data
978
Jesse Keatingae4cd272017-01-30 17:10:44 -0800979 def getRepoPermission(self, project, login):
980 owner, proj = project.split('/')
981 for pr in self.pull_requests:
982 pr_owner, pr_project = pr.project.split('/')
983 if (pr_owner == owner and proj == pr_project):
984 if login in pr.writers:
985 return 'write'
986 else:
987 return 'read'
988
Gregory Haynes4fc12542015-04-22 20:38:06 -0700989 def getGitUrl(self, project):
990 return os.path.join(self.upstream_root, str(project))
991
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200992 def real_getGitUrl(self, project):
993 return super(FakeGithubConnection, self).getGitUrl(project)
994
Gregory Haynes4fc12542015-04-22 20:38:06 -0700995 def getProjectBranches(self, project):
996 """Masks getProjectBranches since we don't have a real github"""
997
998 # just returns master for now
999 return ['master']
1000
Jan Hrubane252a732017-01-03 15:03:09 +01001001 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -07001002 pull_request = self.pull_requests[pr_number - 1]
1003 pull_request.addComment(message)
1004
Jan Hruban3b415922016-02-03 13:10:22 +01001005 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +01001006 pull_request = self.pull_requests[pr_number - 1]
1007 if self.merge_failure:
1008 raise Exception('Pull request was not merged')
1009 if self.merge_not_allowed_count > 0:
1010 self.merge_not_allowed_count -= 1
1011 raise MergeFailure('Merge was not successful due to mergeability'
1012 ' conflict')
1013 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001014 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001015
Jesse Keatingd96e5882017-01-19 13:55:50 -08001016 def getCommitStatuses(self, project, sha):
1017 owner, proj = project.split('/')
1018 for pr in self.pull_requests:
1019 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -07001020 # This is somewhat risky, if the same commit exists in multiple
1021 # PRs, we might grab the wrong one that doesn't have a status
1022 # that is expected to be there. Maybe re-work this so that there
1023 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001024 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001025 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001026 return pr.statuses[sha]
1027
Jan Hrubane252a732017-01-03 15:03:09 +01001028 def setCommitStatus(self, project, sha, state,
1029 url='', description='', context=''):
1030 owner, proj = project.split('/')
1031 for pr in self.pull_requests:
1032 pr_owner, pr_project = pr.project.split('/')
1033 if (pr_owner == owner and pr_project == proj and
1034 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001035 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001036
Jan Hruban16ad31f2015-11-07 14:39:07 +01001037 def labelPull(self, project, pr_number, label):
1038 pull_request = self.pull_requests[pr_number - 1]
1039 pull_request.addLabel(label)
1040
1041 def unlabelPull(self, project, pr_number, label):
1042 pull_request = self.pull_requests[pr_number - 1]
1043 pull_request.removeLabel(label)
1044
Gregory Haynes4fc12542015-04-22 20:38:06 -07001045
Clark Boylanb640e052014-04-03 16:41:46 -07001046class BuildHistory(object):
1047 def __init__(self, **kw):
1048 self.__dict__.update(kw)
1049
1050 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001051 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1052 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001053
1054
Clark Boylanb640e052014-04-03 16:41:46 -07001055class FakeStatsd(threading.Thread):
1056 def __init__(self):
1057 threading.Thread.__init__(self)
1058 self.daemon = True
1059 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1060 self.sock.bind(('', 0))
1061 self.port = self.sock.getsockname()[1]
1062 self.wake_read, self.wake_write = os.pipe()
1063 self.stats = []
1064
1065 def run(self):
1066 while True:
1067 poll = select.poll()
1068 poll.register(self.sock, select.POLLIN)
1069 poll.register(self.wake_read, select.POLLIN)
1070 ret = poll.poll()
1071 for (fd, event) in ret:
1072 if fd == self.sock.fileno():
1073 data = self.sock.recvfrom(1024)
1074 if not data:
1075 return
1076 self.stats.append(data[0])
1077 if fd == self.wake_read:
1078 return
1079
1080 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001081 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001082
1083
James E. Blaire1767bc2016-08-02 10:00:27 -07001084class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001085 log = logging.getLogger("zuul.test")
1086
Paul Belanger174a8272017-03-14 13:20:10 -04001087 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001088 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001089 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001090 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001091 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001092 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001093 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001094 # TODOv3(jeblair): self.node is really "the label of the node
1095 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001096 # keep using it like this, or we may end up exposing more of
1097 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001098 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001099 self.node = None
1100 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001101 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001102 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001103 self.pipeline = self.parameters['ZUUL_PIPELINE']
1104 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001105 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001106 self.wait_condition = threading.Condition()
1107 self.waiting = False
1108 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001109 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001110 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001111 self.changes = None
1112 if 'ZUUL_CHANGE_IDS' in self.parameters:
1113 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001114
James E. Blair3158e282016-08-19 09:34:11 -07001115 def __repr__(self):
1116 waiting = ''
1117 if self.waiting:
1118 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001119 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1120 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001121
Clark Boylanb640e052014-04-03 16:41:46 -07001122 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001123 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001124 self.wait_condition.acquire()
1125 self.wait_condition.notify()
1126 self.waiting = False
1127 self.log.debug("Build %s released" % self.unique)
1128 self.wait_condition.release()
1129
1130 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001131 """Return whether this build is being held.
1132
1133 :returns: Whether the build is being held.
1134 :rtype: bool
1135 """
1136
Clark Boylanb640e052014-04-03 16:41:46 -07001137 self.wait_condition.acquire()
1138 if self.waiting:
1139 ret = True
1140 else:
1141 ret = False
1142 self.wait_condition.release()
1143 return ret
1144
1145 def _wait(self):
1146 self.wait_condition.acquire()
1147 self.waiting = True
1148 self.log.debug("Build %s waiting" % self.unique)
1149 self.wait_condition.wait()
1150 self.wait_condition.release()
1151
1152 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.log.debug('Running build %s' % self.unique)
1154
Paul Belanger174a8272017-03-14 13:20:10 -04001155 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001156 self.log.debug('Holding build %s' % self.unique)
1157 self._wait()
1158 self.log.debug("Build %s continuing" % self.unique)
1159
James E. Blair412fba82017-01-26 15:00:50 -08001160 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001161 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001162 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001163 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001164 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001165 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001166 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001167
James E. Blaire1767bc2016-08-02 10:00:27 -07001168 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001169
James E. Blaira5dba232016-08-08 15:53:24 -07001170 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001171 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001172 for change in changes:
1173 if self.hasChanges(change):
1174 return True
1175 return False
1176
James E. Blaire7b99a02016-08-05 14:27:34 -07001177 def hasChanges(self, *changes):
1178 """Return whether this build has certain changes in its git repos.
1179
1180 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001181 are expected to be present (in order) in the git repository of
1182 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001183
1184 :returns: Whether the build has the indicated changes.
1185 :rtype: bool
1186
1187 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001188 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001189 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001190 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001191 try:
1192 repo = git.Repo(path)
1193 except NoSuchPathError as e:
1194 self.log.debug('%s' % e)
1195 return False
1196 ref = self.parameters['ZUUL_REF']
1197 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1198 commit_message = '%s-1' % change.subject
1199 self.log.debug("Checking if build %s has changes; commit_message "
1200 "%s; repo_messages %s" % (self, commit_message,
1201 repo_messages))
1202 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001203 self.log.debug(" messages do not match")
1204 return False
1205 self.log.debug(" OK")
1206 return True
1207
James E. Blaird8af5422017-05-24 13:59:40 -07001208 def getWorkspaceRepos(self, projects):
1209 """Return workspace git repo objects for the listed projects
1210
1211 :arg list projects: A list of strings, each the canonical name
1212 of a project.
1213
1214 :returns: A dictionary of {name: repo} for every listed
1215 project.
1216 :rtype: dict
1217
1218 """
1219
1220 repos = {}
1221 for project in projects:
1222 path = os.path.join(self.jobdir.src_root, project)
1223 repo = git.Repo(path)
1224 repos[project] = repo
1225 return repos
1226
Clark Boylanb640e052014-04-03 16:41:46 -07001227
Paul Belanger174a8272017-03-14 13:20:10 -04001228class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1229 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001230
Paul Belanger174a8272017-03-14 13:20:10 -04001231 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001232 they will report that they have started but then pause until
1233 released before reporting completion. This attribute may be
1234 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001235 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001236 be explicitly released.
1237
1238 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001239 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001240 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001241 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001242 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001243 self.hold_jobs_in_build = False
1244 self.lock = threading.Lock()
1245 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001246 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001247 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001248 self.job_builds = {}
Monty Taylorde8242c2017-02-23 20:29:53 -06001249 self.hostname = 'zl.example.com'
James E. Blairf5dbd002015-12-23 15:26:17 -08001250
James E. Blaira5dba232016-08-08 15:53:24 -07001251 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001252 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001253
1254 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001255 :arg Change change: The :py:class:`~tests.base.FakeChange`
1256 instance which should cause the job to fail. This job
1257 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001258
1259 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001260 l = self.fail_tests.get(name, [])
1261 l.append(change)
1262 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001263
James E. Blair962220f2016-08-03 11:22:38 -07001264 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001265 """Release a held build.
1266
1267 :arg str regex: A regular expression which, if supplied, will
1268 cause only builds with matching names to be released. If
1269 not supplied, all builds will be released.
1270
1271 """
James E. Blair962220f2016-08-03 11:22:38 -07001272 builds = self.running_builds[:]
1273 self.log.debug("Releasing build %s (%s)" % (regex,
1274 len(self.running_builds)))
1275 for build in builds:
1276 if not regex or re.match(regex, build.name):
1277 self.log.debug("Releasing build %s" %
1278 (build.parameters['ZUUL_UUID']))
1279 build.release()
1280 else:
1281 self.log.debug("Not releasing build %s" %
1282 (build.parameters['ZUUL_UUID']))
1283 self.log.debug("Done releasing builds %s (%s)" %
1284 (regex, len(self.running_builds)))
1285
Paul Belanger174a8272017-03-14 13:20:10 -04001286 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001287 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001289 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001290 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001291 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001292 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001293 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001294 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1295 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001296
1297 def stopJob(self, job):
1298 self.log.debug("handle stop")
1299 parameters = json.loads(job.arguments)
1300 uuid = parameters['uuid']
1301 for build in self.running_builds:
1302 if build.unique == uuid:
1303 build.aborted = True
1304 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001305 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001306
James E. Blaira002b032017-04-18 10:35:48 -07001307 def stop(self):
1308 for build in self.running_builds:
1309 build.release()
1310 super(RecordingExecutorServer, self).stop()
1311
Joshua Hesketh50c21782016-10-13 21:34:14 +11001312
Paul Belanger174a8272017-03-14 13:20:10 -04001313class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001314 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001315 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001316 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001317 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001318 if not commit: # merge conflict
1319 self.recordResult('MERGER_FAILURE')
1320 return commit
1321
1322 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001323 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001324 self.executor_server.lock.acquire()
1325 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001326 BuildHistory(name=build.name, result=result, changes=build.changes,
1327 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001328 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001329 pipeline=build.parameters['ZUUL_PIPELINE'])
1330 )
Paul Belanger174a8272017-03-14 13:20:10 -04001331 self.executor_server.running_builds.remove(build)
1332 del self.executor_server.job_builds[self.job.unique]
1333 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001334
1335 def runPlaybooks(self, args):
1336 build = self.executor_server.job_builds[self.job.unique]
1337 build.jobdir = self.jobdir
1338
1339 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1340 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001341 return result
1342
Monty Taylore6562aa2017-02-20 07:37:39 -05001343 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001344 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001345
Paul Belanger174a8272017-03-14 13:20:10 -04001346 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001347 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001348 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001349 else:
1350 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001351 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001352
James E. Blairad8dca02017-02-21 11:48:32 -05001353 def getHostList(self, args):
1354 self.log.debug("hostlist")
1355 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001356 for host in hosts:
1357 host['host_vars']['ansible_connection'] = 'local'
1358
1359 hosts.append(dict(
1360 name='localhost',
1361 host_vars=dict(ansible_connection='local'),
1362 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001363 return hosts
1364
James E. Blairf5dbd002015-12-23 15:26:17 -08001365
Clark Boylanb640e052014-04-03 16:41:46 -07001366class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001367 """A Gearman server for use in tests.
1368
1369 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1370 added to the queue but will not be distributed to workers
1371 until released. This attribute may be changed at any time and
1372 will take effect for subsequently enqueued jobs, but
1373 previously held jobs will still need to be explicitly
1374 released.
1375
1376 """
1377
Clark Boylanb640e052014-04-03 16:41:46 -07001378 def __init__(self):
1379 self.hold_jobs_in_queue = False
1380 super(FakeGearmanServer, self).__init__(0)
1381
1382 def getJobForConnection(self, connection, peek=False):
1383 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1384 for job in queue:
1385 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001386 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001387 job.waiting = self.hold_jobs_in_queue
1388 else:
1389 job.waiting = False
1390 if job.waiting:
1391 continue
1392 if job.name in connection.functions:
1393 if not peek:
1394 queue.remove(job)
1395 connection.related_jobs[job.handle] = job
1396 job.worker_connection = connection
1397 job.running = True
1398 return job
1399 return None
1400
1401 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001402 """Release a held job.
1403
1404 :arg str regex: A regular expression which, if supplied, will
1405 cause only jobs with matching names to be released. If
1406 not supplied, all jobs will be released.
1407 """
Clark Boylanb640e052014-04-03 16:41:46 -07001408 released = False
1409 qlen = (len(self.high_queue) + len(self.normal_queue) +
1410 len(self.low_queue))
1411 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1412 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001413 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001414 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001415 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001416 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001417 self.log.debug("releasing queued job %s" %
1418 job.unique)
1419 job.waiting = False
1420 released = True
1421 else:
1422 self.log.debug("not releasing queued job %s" %
1423 job.unique)
1424 if released:
1425 self.wakeConnections()
1426 qlen = (len(self.high_queue) + len(self.normal_queue) +
1427 len(self.low_queue))
1428 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1429
1430
1431class FakeSMTP(object):
1432 log = logging.getLogger('zuul.FakeSMTP')
1433
1434 def __init__(self, messages, server, port):
1435 self.server = server
1436 self.port = port
1437 self.messages = messages
1438
1439 def sendmail(self, from_email, to_email, msg):
1440 self.log.info("Sending email from %s, to %s, with msg %s" % (
1441 from_email, to_email, msg))
1442
1443 headers = msg.split('\n\n', 1)[0]
1444 body = msg.split('\n\n', 1)[1]
1445
1446 self.messages.append(dict(
1447 from_email=from_email,
1448 to_email=to_email,
1449 msg=msg,
1450 headers=headers,
1451 body=body,
1452 ))
1453
1454 return True
1455
1456 def quit(self):
1457 return True
1458
1459
James E. Blairdce6cea2016-12-20 16:45:32 -08001460class FakeNodepool(object):
1461 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001462 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001463
1464 log = logging.getLogger("zuul.test.FakeNodepool")
1465
1466 def __init__(self, host, port, chroot):
1467 self.client = kazoo.client.KazooClient(
1468 hosts='%s:%s%s' % (host, port, chroot))
1469 self.client.start()
1470 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001471 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001472 self.thread = threading.Thread(target=self.run)
1473 self.thread.daemon = True
1474 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001475 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001476
1477 def stop(self):
1478 self._running = False
1479 self.thread.join()
1480 self.client.stop()
1481 self.client.close()
1482
1483 def run(self):
1484 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001485 try:
1486 self._run()
1487 except Exception:
1488 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001489 time.sleep(0.1)
1490
1491 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001492 if self.paused:
1493 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001494 for req in self.getNodeRequests():
1495 self.fulfillRequest(req)
1496
1497 def getNodeRequests(self):
1498 try:
1499 reqids = self.client.get_children(self.REQUEST_ROOT)
1500 except kazoo.exceptions.NoNodeError:
1501 return []
1502 reqs = []
1503 for oid in sorted(reqids):
1504 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001505 try:
1506 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001507 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001508 data['_oid'] = oid
1509 reqs.append(data)
1510 except kazoo.exceptions.NoNodeError:
1511 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001512 return reqs
1513
James E. Blaire18d4602017-01-05 11:17:28 -08001514 def getNodes(self):
1515 try:
1516 nodeids = self.client.get_children(self.NODE_ROOT)
1517 except kazoo.exceptions.NoNodeError:
1518 return []
1519 nodes = []
1520 for oid in sorted(nodeids):
1521 path = self.NODE_ROOT + '/' + oid
1522 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001523 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001524 data['_oid'] = oid
1525 try:
1526 lockfiles = self.client.get_children(path + '/lock')
1527 except kazoo.exceptions.NoNodeError:
1528 lockfiles = []
1529 if lockfiles:
1530 data['_lock'] = True
1531 else:
1532 data['_lock'] = False
1533 nodes.append(data)
1534 return nodes
1535
James E. Blaira38c28e2017-01-04 10:33:20 -08001536 def makeNode(self, request_id, node_type):
1537 now = time.time()
1538 path = '/nodepool/nodes/'
1539 data = dict(type=node_type,
1540 provider='test-provider',
1541 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001542 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001543 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001544 public_ipv4='127.0.0.1',
1545 private_ipv4=None,
1546 public_ipv6=None,
1547 allocated_to=request_id,
1548 state='ready',
1549 state_time=now,
1550 created_time=now,
1551 updated_time=now,
1552 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001553 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001554 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001555 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001556 path = self.client.create(path, data,
1557 makepath=True,
1558 sequence=True)
1559 nodeid = path.split("/")[-1]
1560 return nodeid
1561
James E. Blair6ab79e02017-01-06 10:10:17 -08001562 def addFailRequest(self, request):
1563 self.fail_requests.add(request['_oid'])
1564
James E. Blairdce6cea2016-12-20 16:45:32 -08001565 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001566 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001567 return
1568 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001569 oid = request['_oid']
1570 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001571
James E. Blair6ab79e02017-01-06 10:10:17 -08001572 if oid in self.fail_requests:
1573 request['state'] = 'failed'
1574 else:
1575 request['state'] = 'fulfilled'
1576 nodes = []
1577 for node in request['node_types']:
1578 nodeid = self.makeNode(oid, node)
1579 nodes.append(nodeid)
1580 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001581
James E. Blaira38c28e2017-01-04 10:33:20 -08001582 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001583 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001584 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001585 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001586 try:
1587 self.client.set(path, data)
1588 except kazoo.exceptions.NoNodeError:
1589 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001590
1591
James E. Blair498059b2016-12-20 13:50:13 -08001592class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001593 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001594 super(ChrootedKazooFixture, self).__init__()
1595
1596 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1597 if ':' in zk_host:
1598 host, port = zk_host.split(':')
1599 else:
1600 host = zk_host
1601 port = None
1602
1603 self.zookeeper_host = host
1604
1605 if not port:
1606 self.zookeeper_port = 2181
1607 else:
1608 self.zookeeper_port = int(port)
1609
Clark Boylan621ec9a2017-04-07 17:41:33 -07001610 self.test_id = test_id
1611
James E. Blair498059b2016-12-20 13:50:13 -08001612 def _setUp(self):
1613 # Make sure the test chroot paths do not conflict
1614 random_bits = ''.join(random.choice(string.ascii_lowercase +
1615 string.ascii_uppercase)
1616 for x in range(8))
1617
Clark Boylan621ec9a2017-04-07 17:41:33 -07001618 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001619 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1620
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001621 self.addCleanup(self._cleanup)
1622
James E. Blair498059b2016-12-20 13:50:13 -08001623 # Ensure the chroot path exists and clean up any pre-existing znodes.
1624 _tmp_client = kazoo.client.KazooClient(
1625 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1626 _tmp_client.start()
1627
1628 if _tmp_client.exists(self.zookeeper_chroot):
1629 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1630
1631 _tmp_client.ensure_path(self.zookeeper_chroot)
1632 _tmp_client.stop()
1633 _tmp_client.close()
1634
James E. Blair498059b2016-12-20 13:50:13 -08001635 def _cleanup(self):
1636 '''Remove the chroot path.'''
1637 # Need a non-chroot'ed client to remove the chroot path
1638 _tmp_client = kazoo.client.KazooClient(
1639 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1640 _tmp_client.start()
1641 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1642 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001643 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001644
1645
Joshua Heskethd78b4482015-09-14 16:56:34 -06001646class MySQLSchemaFixture(fixtures.Fixture):
1647 def setUp(self):
1648 super(MySQLSchemaFixture, self).setUp()
1649
1650 random_bits = ''.join(random.choice(string.ascii_lowercase +
1651 string.ascii_uppercase)
1652 for x in range(8))
1653 self.name = '%s_%s' % (random_bits, os.getpid())
1654 self.passwd = uuid.uuid4().hex
1655 db = pymysql.connect(host="localhost",
1656 user="openstack_citest",
1657 passwd="openstack_citest",
1658 db="openstack_citest")
1659 cur = db.cursor()
1660 cur.execute("create database %s" % self.name)
1661 cur.execute(
1662 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1663 (self.name, self.name, self.passwd))
1664 cur.execute("flush privileges")
1665
1666 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1667 self.passwd,
1668 self.name)
1669 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1670 self.addCleanup(self.cleanup)
1671
1672 def cleanup(self):
1673 db = pymysql.connect(host="localhost",
1674 user="openstack_citest",
1675 passwd="openstack_citest",
1676 db="openstack_citest")
1677 cur = db.cursor()
1678 cur.execute("drop database %s" % self.name)
1679 cur.execute("drop user '%s'@'localhost'" % self.name)
1680 cur.execute("flush privileges")
1681
1682
Maru Newby3fe5f852015-01-13 04:22:14 +00001683class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001684 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001685 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001686
James E. Blair1c236df2017-02-01 14:07:24 -08001687 def attachLogs(self, *args):
1688 def reader():
1689 self._log_stream.seek(0)
1690 while True:
1691 x = self._log_stream.read(4096)
1692 if not x:
1693 break
1694 yield x.encode('utf8')
1695 content = testtools.content.content_from_reader(
1696 reader,
1697 testtools.content_type.UTF8_TEXT,
1698 False)
1699 self.addDetail('logging', content)
1700
Clark Boylanb640e052014-04-03 16:41:46 -07001701 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001702 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001703 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1704 try:
1705 test_timeout = int(test_timeout)
1706 except ValueError:
1707 # If timeout value is invalid do not set a timeout.
1708 test_timeout = 0
1709 if test_timeout > 0:
1710 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1711
1712 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1713 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1714 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1715 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1716 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1717 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1718 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1719 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1720 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1721 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001722 self._log_stream = StringIO()
1723 self.addOnException(self.attachLogs)
1724 else:
1725 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001726
James E. Blair73b41772017-05-22 13:22:55 -07001727 # NOTE(jeblair): this is temporary extra debugging to try to
1728 # track down a possible leak.
1729 orig_git_repo_init = git.Repo.__init__
1730
1731 def git_repo_init(myself, *args, **kw):
1732 orig_git_repo_init(myself, *args, **kw)
1733 self.log.debug("Created git repo 0x%x %s" %
1734 (id(myself), repr(myself)))
1735
1736 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1737 git_repo_init))
1738
James E. Blair1c236df2017-02-01 14:07:24 -08001739 handler = logging.StreamHandler(self._log_stream)
1740 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1741 '%(levelname)-8s %(message)s')
1742 handler.setFormatter(formatter)
1743
1744 logger = logging.getLogger()
1745 logger.setLevel(logging.DEBUG)
1746 logger.addHandler(handler)
1747
Clark Boylan3410d532017-04-25 12:35:29 -07001748 # Make sure we don't carry old handlers around in process state
1749 # which slows down test runs
1750 self.addCleanup(logger.removeHandler, handler)
1751 self.addCleanup(handler.close)
1752 self.addCleanup(handler.flush)
1753
James E. Blair1c236df2017-02-01 14:07:24 -08001754 # NOTE(notmorgan): Extract logging overrides for specific
1755 # libraries from the OS_LOG_DEFAULTS env and create loggers
1756 # for each. This is used to limit the output during test runs
1757 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001758 log_defaults_from_env = os.environ.get(
1759 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001760 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001761
James E. Blairdce6cea2016-12-20 16:45:32 -08001762 if log_defaults_from_env:
1763 for default in log_defaults_from_env.split(','):
1764 try:
1765 name, level_str = default.split('=', 1)
1766 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001767 logger = logging.getLogger(name)
1768 logger.setLevel(level)
1769 logger.addHandler(handler)
1770 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001771 except ValueError:
1772 # NOTE(notmorgan): Invalid format of the log default,
1773 # skip and don't try and apply a logger for the
1774 # specified module
1775 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001776
Maru Newby3fe5f852015-01-13 04:22:14 +00001777
1778class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001779 """A test case with a functioning Zuul.
1780
1781 The following class variables are used during test setup and can
1782 be overidden by subclasses but are effectively read-only once a
1783 test method starts running:
1784
1785 :cvar str config_file: This points to the main zuul config file
1786 within the fixtures directory. Subclasses may override this
1787 to obtain a different behavior.
1788
1789 :cvar str tenant_config_file: This is the tenant config file
1790 (which specifies from what git repos the configuration should
1791 be loaded). It defaults to the value specified in
1792 `config_file` but can be overidden by subclasses to obtain a
1793 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001794 configuration. See also the :py:func:`simple_layout`
1795 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001796
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001797 :cvar bool create_project_keys: Indicates whether Zuul should
1798 auto-generate keys for each project, or whether the test
1799 infrastructure should insert dummy keys to save time during
1800 startup. Defaults to False.
1801
James E. Blaire7b99a02016-08-05 14:27:34 -07001802 The following are instance variables that are useful within test
1803 methods:
1804
1805 :ivar FakeGerritConnection fake_<connection>:
1806 A :py:class:`~tests.base.FakeGerritConnection` will be
1807 instantiated for each connection present in the config file
1808 and stored here. For instance, `fake_gerrit` will hold the
1809 FakeGerritConnection object for a connection named `gerrit`.
1810
1811 :ivar FakeGearmanServer gearman_server: An instance of
1812 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1813 server that all of the Zuul components in this test use to
1814 communicate with each other.
1815
Paul Belanger174a8272017-03-14 13:20:10 -04001816 :ivar RecordingExecutorServer executor_server: An instance of
1817 :py:class:`~tests.base.RecordingExecutorServer` which is the
1818 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001819
1820 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1821 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001822 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001823 list upon completion.
1824
1825 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1826 objects representing completed builds. They are appended to
1827 the list in the order they complete.
1828
1829 """
1830
James E. Blair83005782015-12-11 14:46:03 -08001831 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001832 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001833 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001834
1835 def _startMerger(self):
1836 self.merge_server = zuul.merger.server.MergeServer(self.config,
1837 self.connections)
1838 self.merge_server.start()
1839
Maru Newby3fe5f852015-01-13 04:22:14 +00001840 def setUp(self):
1841 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001842
1843 self.setupZK()
1844
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001845 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001846 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001847 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1848 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001849 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001850 tmp_root = tempfile.mkdtemp(
1851 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001852 self.test_root = os.path.join(tmp_root, "zuul-test")
1853 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001854 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001855 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001856 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001857
1858 if os.path.exists(self.test_root):
1859 shutil.rmtree(self.test_root)
1860 os.makedirs(self.test_root)
1861 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001862 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001863
1864 # Make per test copy of Configuration.
1865 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001866 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1867 if not os.path.exists(self.private_key_file):
1868 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1869 shutil.copy(src_private_key_file, self.private_key_file)
1870 shutil.copy('{}.pub'.format(src_private_key_file),
1871 '{}.pub'.format(self.private_key_file))
1872 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001873 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001874 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001875 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001876 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001877 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001878 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001879 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001880
Clark Boylanb640e052014-04-03 16:41:46 -07001881 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001882 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1883 # see: https://github.com/jsocol/pystatsd/issues/61
1884 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001885 os.environ['STATSD_PORT'] = str(self.statsd.port)
1886 self.statsd.start()
1887 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001888 reload_module(statsd)
1889 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001890
1891 self.gearman_server = FakeGearmanServer()
1892
1893 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001894 self.log.info("Gearman server on port %s" %
1895 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001896
James E. Blaire511d2f2016-12-08 15:22:26 -08001897 gerritsource.GerritSource.replication_timeout = 1.5
1898 gerritsource.GerritSource.replication_retry_interval = 0.5
1899 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001900
Joshua Hesketh352264b2015-08-11 23:42:08 +10001901 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001902
Jan Hruban7083edd2015-08-21 14:00:54 +02001903 self.webapp = zuul.webapp.WebApp(
1904 self.sched, port=0, listen_address='127.0.0.1')
1905
Jan Hruban6b71aff2015-10-22 16:58:08 +02001906 self.event_queues = [
1907 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001908 self.sched.trigger_event_queue,
1909 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001910 ]
1911
James E. Blairfef78942016-03-11 16:28:56 -08001912 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001913 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001914
Paul Belanger174a8272017-03-14 13:20:10 -04001915 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001916 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001917 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001918 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001919 _test_root=self.test_root,
1920 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001921 self.executor_server.start()
1922 self.history = self.executor_server.build_history
1923 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001924
Paul Belanger174a8272017-03-14 13:20:10 -04001925 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001926 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001927 self.merge_client = zuul.merger.client.MergeClient(
1928 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001929 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001930 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001931 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001932
James E. Blair0d5a36e2017-02-21 10:53:44 -05001933 self.fake_nodepool = FakeNodepool(
1934 self.zk_chroot_fixture.zookeeper_host,
1935 self.zk_chroot_fixture.zookeeper_port,
1936 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001937
Paul Belanger174a8272017-03-14 13:20:10 -04001938 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001939 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001940 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001941 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001942
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001943 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001944
1945 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001946 self.webapp.start()
1947 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001948 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001949 # Cleanups are run in reverse order
1950 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001951 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001952 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001953
James E. Blairb9c0d772017-03-03 14:34:49 -08001954 self.sched.reconfigure(self.config)
1955 self.sched.resume()
1956
Tobias Henkel7df274b2017-05-26 17:41:11 +02001957 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001958 # Set up gerrit related fakes
1959 # Set a changes database so multiple FakeGerrit's can report back to
1960 # a virtual canonical database given by the configured hostname
1961 self.gerrit_changes_dbs = {}
1962
1963 def getGerritConnection(driver, name, config):
1964 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1965 con = FakeGerritConnection(driver, name, config,
1966 changes_db=db,
1967 upstream_root=self.upstream_root)
1968 self.event_queues.append(con.event_queue)
1969 setattr(self, 'fake_' + name, con)
1970 return con
1971
1972 self.useFixture(fixtures.MonkeyPatch(
1973 'zuul.driver.gerrit.GerritDriver.getConnection',
1974 getGerritConnection))
1975
Gregory Haynes4fc12542015-04-22 20:38:06 -07001976 def getGithubConnection(driver, name, config):
1977 con = FakeGithubConnection(driver, name, config,
1978 upstream_root=self.upstream_root)
1979 setattr(self, 'fake_' + name, con)
1980 return con
1981
1982 self.useFixture(fixtures.MonkeyPatch(
1983 'zuul.driver.github.GithubDriver.getConnection',
1984 getGithubConnection))
1985
James E. Blaire511d2f2016-12-08 15:22:26 -08001986 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001987 # TODO(jhesketh): This should come from lib.connections for better
1988 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001989 # Register connections from the config
1990 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001991
Joshua Hesketh352264b2015-08-11 23:42:08 +10001992 def FakeSMTPFactory(*args, **kw):
1993 args = [self.smtp_messages] + list(args)
1994 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001995
Joshua Hesketh352264b2015-08-11 23:42:08 +10001996 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001997
James E. Blaire511d2f2016-12-08 15:22:26 -08001998 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001999 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002000 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002001
James E. Blair83005782015-12-11 14:46:03 -08002002 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002003 # This creates the per-test configuration object. It can be
2004 # overriden by subclasses, but should not need to be since it
2005 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002006 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002007 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002008
2009 if not self.setupSimpleLayout():
2010 if hasattr(self, 'tenant_config_file'):
2011 self.config.set('zuul', 'tenant_config',
2012 self.tenant_config_file)
2013 git_path = os.path.join(
2014 os.path.dirname(
2015 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2016 'git')
2017 if os.path.exists(git_path):
2018 for reponame in os.listdir(git_path):
2019 project = reponame.replace('_', '/')
2020 self.copyDirToRepo(project,
2021 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002022 self.setupAllProjectKeys()
2023
James E. Blair06cc3922017-04-19 10:08:10 -07002024 def setupSimpleLayout(self):
2025 # If the test method has been decorated with a simple_layout,
2026 # use that instead of the class tenant_config_file. Set up a
2027 # single config-project with the specified layout, and
2028 # initialize repos for all of the 'project' entries which
2029 # appear in the layout.
2030 test_name = self.id().split('.')[-1]
2031 test = getattr(self, test_name)
2032 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002033 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002034 else:
2035 return False
2036
James E. Blairb70e55a2017-04-19 12:57:02 -07002037 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002038 path = os.path.join(FIXTURE_DIR, path)
2039 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002040 data = f.read()
2041 layout = yaml.safe_load(data)
2042 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002043 untrusted_projects = []
2044 for item in layout:
2045 if 'project' in item:
2046 name = item['project']['name']
2047 untrusted_projects.append(name)
2048 self.init_repo(name)
2049 self.addCommitToRepo(name, 'initial commit',
2050 files={'README': ''},
2051 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002052 if 'job' in item:
2053 jobname = item['job']['name']
2054 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002055
2056 root = os.path.join(self.test_root, "config")
2057 if not os.path.exists(root):
2058 os.makedirs(root)
2059 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2060 config = [{'tenant':
2061 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002062 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002063 {'config-projects': ['common-config'],
2064 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002065 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002066 f.close()
2067 self.config.set('zuul', 'tenant_config',
2068 os.path.join(FIXTURE_DIR, f.name))
2069
2070 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002071 self.addCommitToRepo('common-config', 'add content from fixture',
2072 files, branch='master', tag='init')
2073
2074 return True
2075
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002076 def setupAllProjectKeys(self):
2077 if self.create_project_keys:
2078 return
2079
2080 path = self.config.get('zuul', 'tenant_config')
2081 with open(os.path.join(FIXTURE_DIR, path)) as f:
2082 tenant_config = yaml.safe_load(f.read())
2083 for tenant in tenant_config:
2084 sources = tenant['tenant']['source']
2085 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002086 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002087 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002088 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002089 self.setupProjectKeys(source, project)
2090
2091 def setupProjectKeys(self, source, project):
2092 # Make sure we set up an RSA key for the project so that we
2093 # don't spend time generating one:
2094
2095 key_root = os.path.join(self.state_root, 'keys')
2096 if not os.path.isdir(key_root):
2097 os.mkdir(key_root, 0o700)
2098 private_key_file = os.path.join(key_root, source, project + '.pem')
2099 private_key_dir = os.path.dirname(private_key_file)
2100 self.log.debug("Installing test keys for project %s at %s" % (
2101 project, private_key_file))
2102 if not os.path.isdir(private_key_dir):
2103 os.makedirs(private_key_dir)
2104 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2105 with open(private_key_file, 'w') as o:
2106 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002107
James E. Blair498059b2016-12-20 13:50:13 -08002108 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002109 self.zk_chroot_fixture = self.useFixture(
2110 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002111 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002112 self.zk_chroot_fixture.zookeeper_host,
2113 self.zk_chroot_fixture.zookeeper_port,
2114 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002115
James E. Blair96c6bf82016-01-15 16:20:40 -08002116 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002117 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002118
2119 files = {}
2120 for (dirpath, dirnames, filenames) in os.walk(source_path):
2121 for filename in filenames:
2122 test_tree_filepath = os.path.join(dirpath, filename)
2123 common_path = os.path.commonprefix([test_tree_filepath,
2124 source_path])
2125 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2126 with open(test_tree_filepath, 'r') as f:
2127 content = f.read()
2128 files[relative_filepath] = content
2129 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002130 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002131
James E. Blaire18d4602017-01-05 11:17:28 -08002132 def assertNodepoolState(self):
2133 # Make sure that there are no pending requests
2134
2135 requests = self.fake_nodepool.getNodeRequests()
2136 self.assertEqual(len(requests), 0)
2137
2138 nodes = self.fake_nodepool.getNodes()
2139 for node in nodes:
2140 self.assertFalse(node['_lock'], "Node %s is locked" %
2141 (node['_oid'],))
2142
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002143 def assertNoGeneratedKeys(self):
2144 # Make sure that Zuul did not generate any project keys
2145 # (unless it was supposed to).
2146
2147 if self.create_project_keys:
2148 return
2149
2150 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2151 test_key = i.read()
2152
2153 key_root = os.path.join(self.state_root, 'keys')
2154 for root, dirname, files in os.walk(key_root):
2155 for fn in files:
2156 with open(os.path.join(root, fn)) as f:
2157 self.assertEqual(test_key, f.read())
2158
Clark Boylanb640e052014-04-03 16:41:46 -07002159 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002160 self.log.debug("Assert final state")
2161 # Make sure no jobs are running
2162 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002163 # Make sure that git.Repo objects have been garbage collected.
2164 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002165 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002166 gc.collect()
2167 for obj in gc.get_objects():
2168 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002169 self.log.debug("Leaked git repo object: 0x%x %s" %
2170 (id(obj), repr(obj)))
2171 for ref in gc.get_referrers(obj):
2172 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002173 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002174 if repos:
2175 for obj in gc.garbage:
2176 self.log.debug(" Garbage %s" % (repr(obj)))
2177 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002178 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002179 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002180 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002181 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002182 for tenant in self.sched.abide.tenants.values():
2183 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002184 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002185 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002186
2187 def shutdown(self):
2188 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002189 self.executor_server.hold_jobs_in_build = False
2190 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002191 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002192 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002193 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002194 self.sched.stop()
2195 self.sched.join()
2196 self.statsd.stop()
2197 self.statsd.join()
2198 self.webapp.stop()
2199 self.webapp.join()
2200 self.rpc.stop()
2201 self.rpc.join()
2202 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002203 self.fake_nodepool.stop()
2204 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002205 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002206 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002207 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002208 # Further the pydevd threads also need to be whitelisted so debugging
2209 # e.g. in PyCharm is possible without breaking shutdown.
2210 whitelist = ['executor-watchdog',
2211 'pydevd.CommandThread',
2212 'pydevd.Reader',
2213 'pydevd.Writer',
2214 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002215 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002216 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002217 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002218 log_str = ""
2219 for thread_id, stack_frame in sys._current_frames().items():
2220 log_str += "Thread: %s\n" % thread_id
2221 log_str += "".join(traceback.format_stack(stack_frame))
2222 self.log.debug(log_str)
2223 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002224
James E. Blaira002b032017-04-18 10:35:48 -07002225 def assertCleanShutdown(self):
2226 pass
2227
James E. Blairc4ba97a2017-04-19 16:26:24 -07002228 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002229 parts = project.split('/')
2230 path = os.path.join(self.upstream_root, *parts[:-1])
2231 if not os.path.exists(path):
2232 os.makedirs(path)
2233 path = os.path.join(self.upstream_root, project)
2234 repo = git.Repo.init(path)
2235
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002236 with repo.config_writer() as config_writer:
2237 config_writer.set_value('user', 'email', 'user@example.com')
2238 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002239
Clark Boylanb640e052014-04-03 16:41:46 -07002240 repo.index.commit('initial commit')
2241 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002242 if tag:
2243 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002244
James E. Blair97d902e2014-08-21 13:25:56 -07002245 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002246 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002247 repo.git.clean('-x', '-f', '-d')
2248
James E. Blair97d902e2014-08-21 13:25:56 -07002249 def create_branch(self, project, branch):
2250 path = os.path.join(self.upstream_root, project)
2251 repo = git.Repo.init(path)
2252 fn = os.path.join(path, 'README')
2253
2254 branch_head = repo.create_head(branch)
2255 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002256 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002257 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002258 f.close()
2259 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002260 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002261
James E. Blair97d902e2014-08-21 13:25:56 -07002262 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002263 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002264 repo.git.clean('-x', '-f', '-d')
2265
Sachi King9f16d522016-03-16 12:20:45 +11002266 def create_commit(self, project):
2267 path = os.path.join(self.upstream_root, project)
2268 repo = git.Repo(path)
2269 repo.head.reference = repo.heads['master']
2270 file_name = os.path.join(path, 'README')
2271 with open(file_name, 'a') as f:
2272 f.write('creating fake commit\n')
2273 repo.index.add([file_name])
2274 commit = repo.index.commit('Creating a fake commit')
2275 return commit.hexsha
2276
James E. Blairf4a5f022017-04-18 14:01:10 -07002277 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002278 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002279 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002280 while len(self.builds):
2281 self.release(self.builds[0])
2282 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002283 i += 1
2284 if count is not None and i >= count:
2285 break
James E. Blairb8c16472015-05-05 14:55:26 -07002286
Clark Boylanb640e052014-04-03 16:41:46 -07002287 def release(self, job):
2288 if isinstance(job, FakeBuild):
2289 job.release()
2290 else:
2291 job.waiting = False
2292 self.log.debug("Queued job %s released" % job.unique)
2293 self.gearman_server.wakeConnections()
2294
2295 def getParameter(self, job, name):
2296 if isinstance(job, FakeBuild):
2297 return job.parameters[name]
2298 else:
2299 parameters = json.loads(job.arguments)
2300 return parameters[name]
2301
Clark Boylanb640e052014-04-03 16:41:46 -07002302 def haveAllBuildsReported(self):
2303 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002304 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002305 return False
2306 # Find out if every build that the worker has completed has been
2307 # reported back to Zuul. If it hasn't then that means a Gearman
2308 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002309 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002310 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002311 if not zbuild:
2312 # It has already been reported
2313 continue
2314 # It hasn't been reported yet.
2315 return False
2316 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002317 worker = self.executor_server.executor_worker
2318 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002319 if connection.state == 'GRAB_WAIT':
2320 return False
2321 return True
2322
2323 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002324 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002325 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002326 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002327 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002328 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002329 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002330 for j in conn.related_jobs.values():
2331 if j.unique == build.uuid:
2332 client_job = j
2333 break
2334 if not client_job:
2335 self.log.debug("%s is not known to the gearman client" %
2336 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002337 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002338 if not client_job.handle:
2339 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002340 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002341 server_job = self.gearman_server.jobs.get(client_job.handle)
2342 if not server_job:
2343 self.log.debug("%s is not known to the gearman server" %
2344 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002345 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002346 if not hasattr(server_job, 'waiting'):
2347 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002348 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002349 if server_job.waiting:
2350 continue
James E. Blair17302972016-08-10 16:11:42 -07002351 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002352 self.log.debug("%s has not reported start" % build)
2353 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002354 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002355 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002356 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002357 if worker_build:
2358 if worker_build.isWaiting():
2359 continue
2360 else:
2361 self.log.debug("%s is running" % worker_build)
2362 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002363 else:
James E. Blair962220f2016-08-03 11:22:38 -07002364 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002365 return False
James E. Blaira002b032017-04-18 10:35:48 -07002366 for (build_uuid, job_worker) in \
2367 self.executor_server.job_workers.items():
2368 if build_uuid not in seen_builds:
2369 self.log.debug("%s is not finalized" % build_uuid)
2370 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002371 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002372
James E. Blairdce6cea2016-12-20 16:45:32 -08002373 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002374 if self.fake_nodepool.paused:
2375 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002376 if self.sched.nodepool.requests:
2377 return False
2378 return True
2379
Jan Hruban6b71aff2015-10-22 16:58:08 +02002380 def eventQueuesEmpty(self):
2381 for queue in self.event_queues:
2382 yield queue.empty()
2383
2384 def eventQueuesJoin(self):
2385 for queue in self.event_queues:
2386 queue.join()
2387
Clark Boylanb640e052014-04-03 16:41:46 -07002388 def waitUntilSettled(self):
2389 self.log.debug("Waiting until settled...")
2390 start = time.time()
2391 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002392 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002393 self.log.error("Timeout waiting for Zuul to settle")
2394 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002395 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002396 self.log.error(" %s: %s" % (queue, queue.empty()))
2397 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002398 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002399 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002400 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002401 self.log.error("All requests completed: %s" %
2402 (self.areAllNodeRequestsComplete(),))
2403 self.log.error("Merge client jobs: %s" %
2404 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002405 raise Exception("Timeout waiting for Zuul to settle")
2406 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002407
Paul Belanger174a8272017-03-14 13:20:10 -04002408 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002409 # have all build states propogated to zuul?
2410 if self.haveAllBuildsReported():
2411 # Join ensures that the queue is empty _and_ events have been
2412 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002413 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002414 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002415 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002416 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002417 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002418 self.areAllNodeRequestsComplete() and
2419 all(self.eventQueuesEmpty())):
2420 # The queue empty check is placed at the end to
2421 # ensure that if a component adds an event between
2422 # when locked the run handler and checked that the
2423 # components were stable, we don't erroneously
2424 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002425 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.log.debug("...settled.")
2428 return
2429 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002430 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002431 self.sched.wake_event.wait(0.1)
2432
2433 def countJobResults(self, jobs, result):
2434 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002435 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002436
Monty Taylor0d926122017-05-24 08:07:56 -05002437 def getBuildByName(self, name):
2438 for build in self.builds:
2439 if build.name == name:
2440 return build
2441 raise Exception("Unable to find build %s" % name)
2442
James E. Blair96c6bf82016-01-15 16:20:40 -08002443 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002444 for job in self.history:
2445 if (job.name == name and
2446 (project is None or
2447 job.parameters['ZUUL_PROJECT'] == project)):
2448 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002449 raise Exception("Unable to find job %s in history" % name)
2450
2451 def assertEmptyQueues(self):
2452 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002453 for tenant in self.sched.abide.tenants.values():
2454 for pipeline in tenant.layout.pipelines.values():
2455 for queue in pipeline.queues:
2456 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002457 print('pipeline %s queue %s contents %s' % (
2458 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002459 self.assertEqual(len(queue.queue), 0,
2460 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002461
2462 def assertReportedStat(self, key, value=None, kind=None):
2463 start = time.time()
2464 while time.time() < (start + 5):
2465 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002466 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002467 if key == k:
2468 if value is None and kind is None:
2469 return
2470 elif value:
2471 if value == v:
2472 return
2473 elif kind:
2474 if v.endswith('|' + kind):
2475 return
2476 time.sleep(0.1)
2477
Clark Boylanb640e052014-04-03 16:41:46 -07002478 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002479
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002480 def assertBuilds(self, builds):
2481 """Assert that the running builds are as described.
2482
2483 The list of running builds is examined and must match exactly
2484 the list of builds described by the input.
2485
2486 :arg list builds: A list of dictionaries. Each item in the
2487 list must match the corresponding build in the build
2488 history, and each element of the dictionary must match the
2489 corresponding attribute of the build.
2490
2491 """
James E. Blair3158e282016-08-19 09:34:11 -07002492 try:
2493 self.assertEqual(len(self.builds), len(builds))
2494 for i, d in enumerate(builds):
2495 for k, v in d.items():
2496 self.assertEqual(
2497 getattr(self.builds[i], k), v,
2498 "Element %i in builds does not match" % (i,))
2499 except Exception:
2500 for build in self.builds:
2501 self.log.error("Running build: %s" % build)
2502 else:
2503 self.log.error("No running builds")
2504 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002505
James E. Blairb536ecc2016-08-31 10:11:42 -07002506 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002507 """Assert that the completed builds are as described.
2508
2509 The list of completed builds is examined and must match
2510 exactly the list of builds described by the input.
2511
2512 :arg list history: A list of dictionaries. Each item in the
2513 list must match the corresponding build in the build
2514 history, and each element of the dictionary must match the
2515 corresponding attribute of the build.
2516
James E. Blairb536ecc2016-08-31 10:11:42 -07002517 :arg bool ordered: If true, the history must match the order
2518 supplied, if false, the builds are permitted to have
2519 arrived in any order.
2520
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002521 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002522 def matches(history_item, item):
2523 for k, v in item.items():
2524 if getattr(history_item, k) != v:
2525 return False
2526 return True
James E. Blair3158e282016-08-19 09:34:11 -07002527 try:
2528 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002529 if ordered:
2530 for i, d in enumerate(history):
2531 if not matches(self.history[i], d):
2532 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002533 "Element %i in history does not match %s" %
2534 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002535 else:
2536 unseen = self.history[:]
2537 for i, d in enumerate(history):
2538 found = False
2539 for unseen_item in unseen:
2540 if matches(unseen_item, d):
2541 found = True
2542 unseen.remove(unseen_item)
2543 break
2544 if not found:
2545 raise Exception("No match found for element %i "
2546 "in history" % (i,))
2547 if unseen:
2548 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002549 except Exception:
2550 for build in self.history:
2551 self.log.error("Completed build: %s" % build)
2552 else:
2553 self.log.error("No completed builds")
2554 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002555
James E. Blair6ac368c2016-12-22 18:07:20 -08002556 def printHistory(self):
2557 """Log the build history.
2558
2559 This can be useful during tests to summarize what jobs have
2560 completed.
2561
2562 """
2563 self.log.debug("Build history:")
2564 for build in self.history:
2565 self.log.debug(build)
2566
James E. Blair59fdbac2015-12-07 17:08:06 -08002567 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002568 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2569
James E. Blair9ea70072017-04-19 16:05:30 -07002570 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002571 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002572 if not os.path.exists(root):
2573 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002574 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2575 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002576- tenant:
2577 name: openstack
2578 source:
2579 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002580 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002581 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002582 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002583 - org/project
2584 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002585 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002586 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002587 self.config.set('zuul', 'tenant_config',
2588 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002589 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002590
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002591 def addCommitToRepo(self, project, message, files,
2592 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002593 path = os.path.join(self.upstream_root, project)
2594 repo = git.Repo(path)
2595 repo.head.reference = branch
2596 zuul.merger.merger.reset_repo_to_head(repo)
2597 for fn, content in files.items():
2598 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002599 try:
2600 os.makedirs(os.path.dirname(fn))
2601 except OSError:
2602 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002603 with open(fn, 'w') as f:
2604 f.write(content)
2605 repo.index.add([fn])
2606 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002607 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002608 repo.heads[branch].commit = commit
2609 repo.head.reference = branch
2610 repo.git.clean('-x', '-f', '-d')
2611 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002612 if tag:
2613 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002614 return before
2615
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002616 def commitConfigUpdate(self, project_name, source_name):
2617 """Commit an update to zuul.yaml
2618
2619 This overwrites the zuul.yaml in the specificed project with
2620 the contents specified.
2621
2622 :arg str project_name: The name of the project containing
2623 zuul.yaml (e.g., common-config)
2624
2625 :arg str source_name: The path to the file (underneath the
2626 test fixture directory) whose contents should be used to
2627 replace zuul.yaml.
2628 """
2629
2630 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002631 files = {}
2632 with open(source_path, 'r') as f:
2633 data = f.read()
2634 layout = yaml.safe_load(data)
2635 files['zuul.yaml'] = data
2636 for item in layout:
2637 if 'job' in item:
2638 jobname = item['job']['name']
2639 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002640 before = self.addCommitToRepo(
2641 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002642 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002643 return before
2644
James E. Blair7fc8daa2016-08-08 15:37:15 -07002645 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002646
James E. Blair7fc8daa2016-08-08 15:37:15 -07002647 """Inject a Fake (Gerrit) event.
2648
2649 This method accepts a JSON-encoded event and simulates Zuul
2650 having received it from Gerrit. It could (and should)
2651 eventually apply to any connection type, but is currently only
2652 used with Gerrit connections. The name of the connection is
2653 used to look up the corresponding server, and the event is
2654 simulated as having been received by all Zuul connections
2655 attached to that server. So if two Gerrit connections in Zuul
2656 are connected to the same Gerrit server, and you invoke this
2657 method specifying the name of one of them, the event will be
2658 received by both.
2659
2660 .. note::
2661
2662 "self.fake_gerrit.addEvent" calls should be migrated to
2663 this method.
2664
2665 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002666 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002667 :arg str event: The JSON-encoded event.
2668
2669 """
2670 specified_conn = self.connections.connections[connection]
2671 for conn in self.connections.connections.values():
2672 if (isinstance(conn, specified_conn.__class__) and
2673 specified_conn.server == conn.server):
2674 conn.addEvent(event)
2675
James E. Blaird8af5422017-05-24 13:59:40 -07002676 def getUpstreamRepos(self, projects):
2677 """Return upstream git repo objects for the listed projects
2678
2679 :arg list projects: A list of strings, each the canonical name
2680 of a project.
2681
2682 :returns: A dictionary of {name: repo} for every listed
2683 project.
2684 :rtype: dict
2685
2686 """
2687
2688 repos = {}
2689 for project in projects:
2690 # FIXME(jeblair): the upstream root does not yet have a
2691 # hostname component; that needs to be added, and this
2692 # line removed:
2693 tmp_project_name = '/'.join(project.split('/')[1:])
2694 path = os.path.join(self.upstream_root, tmp_project_name)
2695 repo = git.Repo(path)
2696 repos[project] = repo
2697 return repos
2698
James E. Blair3f876d52016-07-22 13:07:14 -07002699
2700class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002701 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002702 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002703
Joshua Heskethd78b4482015-09-14 16:56:34 -06002704
2705class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002706 def setup_config(self):
2707 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002708 for section_name in self.config.sections():
2709 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2710 section_name, re.I)
2711 if not con_match:
2712 continue
2713
2714 if self.config.get(section_name, 'driver') == 'sql':
2715 f = MySQLSchemaFixture()
2716 self.useFixture(f)
2717 if (self.config.get(section_name, 'dburi') ==
2718 '$MYSQL_FIXTURE_DBURI$'):
2719 self.config.set(section_name, 'dburi', f.dburi)